Closed jeonguk29 closed 7 months ago
PR ๋ ๋ฆด ๋ ์ฒดํฌ ๋ฆฌ์คํธ
์ด๋ค ์ข ๋ฅ์ PR์ธ๊ฐ์?
Issue Number: #186 #164
์ด PR์ ๋ํด ๊ฐ๋ตํ๊ฒ ์๊ฐํด์ฃผ์ธ์!
๊ฐ๊ฐ์ View๋ ์๋ง์ ViewModel์ ๊ฐ์ง๊ณ ์์ผ๋ฉฐ ๊ธฐ์กด ViewModel์ ๋ถ๊ธฐ ์ฒ๋ฆฌํ์ฌ ๋๊ฒจ์ฃผ๋ ๋ถ๋ถ์ ๊ฐ๊ฐ์ View๊ฐ ๊ฐ์ง๊ณ ์๋ ์๋ง์ ViewModel์ ๋๊ธฐ๋๋ก ๊ตฌํ๋์์ต๋๋ค. MyPageView๋ MenuTabBar๋ฅผ ํ์ฉํ์ฌ MemoList๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ถ๋ถ๊ณผ Memo Image Marker๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฐ๊ฐ์ View๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค. ์ต๋ํ ๊ฐ๊ฐ์ ViewModel์ ๋๊ธฐ๋๋ก ํ์ฌ ProfileMemoList, ProfileMemoListCell๋ฅผ ์ฌํ์ฉ ํ์์ต๋๋ค. (ํ๋กํ ์ฝ ์งํฅ ํจ๋ฌ๋ค์์ ์ฅ์ ํ์ฉ์ ์ํ์ฌ)
struct MyPageView: View { @State var selected: Int = 2 @State private var presentLoginAlert = false @State private var presentLoginView = false @ObservedObject var authViewModel: AuthService = .shared @ObservedObject var mypageViewModel: MypageViewModel = .init() @State var selectedIndex = 0 var body: some View { ZStack(alignment: .top) { Color.bgColor.edgesIgnoringSafeArea(.top) ScrollView(.vertical, showsIndicators: false) { VStack(alignment: .leading) { // ๋ก๊ทธ์ธ ๋์๋ค๋ฉด ๋ก์ง ์คํ if let currentUser = authViewModel.currentUser, let userId = UserDefaults.standard.string(forKey: "userId") { let isCurrentUser = authViewModel.userSession?.uid == userId MypageTopView() // ํ๋์ฉ ์ถ๊ฐํด์ ํญ ์ถ๊ฐ, spacin......g, horizontalInset ๋์ด๋๋ฉด ๊ฐ ์์ ํ์ MenuTabBar(menus: [MenuTabModel(index: 0, image: "list.bullet.below.rectangle"), MenuTabModel(index: 1, image: "newspaper")], selectedIndex: $selectedIndex, fullWidth: UIScreen.main.bounds.width, spacing: 50, horizontalInset: 91.5) .padding(.horizontal) switch selectedIndex { case 0: createHeader() ProfileMemoList<MypageViewModel>().environmentObject(mypageViewModel) default: MapImageMarkerView<MypageViewModel>().environmentObject(mypageViewModel) } } else { showLoginPrompt() } } .refreshable { // Refresh logic } .padding(.horizontal, 14) .safeAreaInset(edge: .top) { Color.clear.frame(height: 0).background(Color.bgColor) } .safeAreaInset(edge: .bottom) { Color.clear.frame(height: 0).background(Color.bgColor).border(Color.black) } } } .onAppear { checkLoginStatus() authViewModel.fetchUser() } .alert("๋ก๊ทธ์ธ ํ์ ์ฌ์ฉ ๊ฐ๋ฅํ ๊ธฐ๋ฅ์ ๋๋ค.\n๋ก๊ทธ์ธ ํ์๊ฒ ์ต๋๊น?", isPresented: $presentLoginAlert) { Button("๋ก๊ทธ์ธ ํ๊ธฐ", role: .destructive) { self.presentLoginView = true } Button("๋๋ฌ๋ณด๊ธฐ", role: .cancel) { // Handle '๋๋ฌ๋ณด๊ธฐ' case } } .fullScreenCover(isPresented: $presentLoginView) { LoginView().environmentObject(authViewModel) } .overlay { if LoadingManager.shared.phase == .loading { LoadingView() } } } private func createHeader() -> some View { HStack(alignment: .lastTextBaseline) { Spacer() Button { mypageViewModel.isShowingOptions.toggle() } label: { Image(systemName: "slider.horizontal.3").foregroundStyle(Color.gray).font(.system(size: 24)) } .confirmationDialog("์ ๋ ฌํ๊ณ ์ถ์ ๊ธฐ์ค์ ์ ํํ์ธ์.", isPresented: $mypageViewModel.isShowingOptions) { ForEach(SortedTypeOfMemo.allCases, id: \.id) { type in Button(type.rawValue) { mypageViewModel.sortMemoList(type: type) } } } } .padding(.top, 38) } private func showLoginPrompt() -> some View { VStack(alignment: .center) { Spacer() HStack { Spacer() Text("๋ก๊ทธ์ธ์ด ํ์ํด์!").font(.semibold20) Spacer() } Button { self.presentLoginView = true } label: { Text("๋ก๊ทธ์ธ ํ๋ฌ๊ฐ๊ธฐ") } Spacer() } } private func checkLoginStatus() { Task { if UserDefaults.standard.string(forKey: "userId") != nil { presentLoginAlert = false } else { presentLoginAlert = true } } } }
OtherUserProfileView๋ ์์ฑ์์ ํ๋กํ์ ํ์ธํ ๋ Detail ์ชฝ์์ ๋์ด์ฌ ๋๋ง ์ฌ์ฉํฉ๋๋ค. ๊ธฐ์กด์ MyPage๋ฅผ ๊ณ์ ์์ฑํ์ฌ ๋ฌดํ ๋ก๋ฉ๋๋ ๋ฌธ์ ๊ฐ ์์๋๋ฐ #164 ํด๋น ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ด๊ฐ ์์ฑํ ๋ฉ๋ชจ๋ผ๋ otherUserViewModel๋ฅผ ํ๊ณ ๋ค์ด๊ฐ์ ๋ก์ง๋ค์ด ์คํ๋๋ฉฐ ๋จ์ง View๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํด TopView๋ง ๊ต์ฒดํ์ฌ ํ๋ก์ฐ, ํ๋ก์ ๋์ ์ค์ ํ๋ฉด์ด ๋ณด์ด๋๋ก ๋ถ๊ธฐ ์ฒ๋ฆฌํ์์ต๋๋ค. ๊ทธ ์ธ ๋ก์ง์ ๊ฐ์ต๋๋ค.
enum SortedTypeOfMemo: String, CaseIterable, Identifiable { case last = "์ต์ ์" case like = "์ข์์์" case close = "๊ฐ๊น์ด์" var id: SortedTypeOfMemo { self } } struct OtherUserProfileView: View { @State private var presentLoginAlert = false @State private var presentLoginView = false @ObservedObject var authViewModel: AuthService = .shared @ObservedObject var otherUserViewModel: OtherUserViewModel = .init() @State var selectedIndex = 0 // ์์ฑ์๋ฅผ ํตํด @State๋ฅผ ๋ง๋ค์ ์๋๋ก fromDetail true๋ฉด ์๋๋ฐฉ ํ๋กํ ๊ฐ์ ธ์ค๊ธฐ init(memoCreator: User) { otherUserViewModel.fetchMemoCreatorProfile( memoCreator: memoCreator) } var body: some View { ZStack(alignment: .top) { Color.bgColor.edgesIgnoringSafeArea(.top) ScrollView(.vertical, showsIndicators: false) { VStack(alignment: .leading) { // ๋ก๊ทธ์ธ ๋์๋ค๋ฉด ๋ก์ง ์คํ if let currentUser = authViewModel.currentUser, let userId = UserDefaults.standard.string(forKey: "userId") { let isCurrentUser = authViewModel.userSession?.uid == userId // ์๋๋ฐฉ ํ๋กํ์ ํ์ํ ๋๋ ์ ๋ค๋ฆญ์ ์ฌ์ฉํ์ฌ OtherUserViewModel์ ์ ๋ฌ MyPage๋ฅผ ํ์ํ ๋๋ MypageViewModel ์ ๋ฌ if otherUserViewModel.memoCreator.isCurrentUser == false { OtherUserTopView(memoCreator: $otherUserViewModel.memoCreator, viewModel: otherUserViewModel) createHeader() ProfileMemoList<OtherUserViewModel>().environmentObject(otherUserViewModel) } else { MypageTopView() createHeader() ProfileMemoList<OtherUserViewModel>().environmentObject(otherUserViewModel) } } else { showLoginPrompt() } } }
ํ๋กํ ์ฝ ์งํฅ ํจ๋ฌ๋ค์์ ์ฅ์ ํ์ฉํ์ฌ ๊ฐ ViewModel์์ ์ค๋ณต๋๋ ๋ฉ์๋๋ค์ ๋ชจ์์ต๋๋ค.
import SwiftUI import _PhotosUI_SwiftUI import FirebaseAuth import FirebaseCore import FirebaseFirestore protocol ProfileViewModelProtocol: ObservableObject { var merkerMemoList: [Memo] { get set } var memoList: [Memo] { get set } var selectedFilter: SortedTypeOfMemo { get set } var isShowingOptions: Bool { get set } var isCurrentUserLoginState: Bool { get set } var user: User? { get set } var currentLocation: CLLocation? { get set } var lastDocument: QueryDocumentSnapshot? { get set } func fetchDistanceOfUserAndMemo(myLocation: CLLocationCoordinate2D, memoLocation: Location) -> Double func sortMemoList(type: SortedTypeOfMemo) func fetchUserState() func fetchCurrentUserLoginState() -> Bool func fetchCurrentUserLocation(returnCompletion: @escaping (CLLocation?) -> Void) func pagenate(userID: String) async } extension ProfileViewModelProtocol { func fetchUserState() { guard let _ = UserDefaults.standard.string(forKey: "userId") else { return } } func fetchCurrentUserLoginState() -> Bool { if let _ = Auth.auth().currentUser { return true } return false } // MARK: ํ์ฌ ์ฌ์ฉ์ ์์น(์๋, ๊ฒฝ๋)์ ๋ฉ๋ชจ์ ์์น, ๊ทธ๋ฆฌ๊ณ ์ค์ ํ ๊ฑฐ๋ฆฌ๋ฅผ ํตํด ์ค์ ๋ ๊ฑฐ๋ฆฌ ๋ด ๋ฉ๋ชจ๋ฅผ ํํฐ๋งํ๋ ํจ์(CLLocation์ distance ๋ฉ์๋ ์ฌ์ฉ) func fetchDistanceOfUserAndMemo(myLocation: CLLocationCoordinate2D, memoLocation: Location ) -> Double { // ์ฌ์ฉ์์ ์์น๋ฅผ CLLocation๊ฐ์ฒด๋ก ์์ฑ let location = CLLocationCoordinate2D(latitude: memoLocation.latitude, longitude: memoLocation.longitude) return location.distance(to: myLocation) } // MARK: MemoList ํํฐ๋ง & ์ ๋ ฌํ๋ ๋ฉ์๋์ ๋๋ค func sortMemoList(type: SortedTypeOfMemo) { self.selectedFilter = type switch type { case .last: self.memoList = memoList.sorted { let first = Date(timeIntervalSince1970: $0.date) let second = Date(timeIntervalSince1970: $1.date) // ์๊ฐ๋น๊ต orderedAscending: first๊ฐ second๋ณด๋ค ์ด์ (๋น ๋ฅธ), orderedDescending: first๊ฐ second๋ณด๋ค ์ดํ(๋ฆ์) switch first.compare(second) { case .orderedAscending: return false case .orderedDescending: return true case .orderedSame: return true } } case .like: self.memoList = memoList.sorted { $0.likeCount > $1.likeCount } case .close: self.memoList = memoList.sorted { let first = $0.location.distance(from: currentLocation ?? CLLocation(latitude: 37.5664056, longitude: 126.9778222)) let second = $1.location.distance(from: currentLocation ?? CLLocation(latitude: 37.5664056, longitude: 126.9778222)) return first < second } } } }
๊ฐ๋จํ๊ฒ MapImageMarkerView๋ฅผ ๊ตฌํํ์์ต๋๋ค!
import SwiftUI import MapKit struct MapImageMarkerView<ViewModel: ProfileViewModelProtocol>: View { @EnvironmentObject var viewModel: ViewModel @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) var body: some View { VStack { Map(position: $position) { ForEach($viewModel.memoList, id: \.id) { memo in let location = CLLocationCoordinate2D( latitude: Double(memo.location.latitude.wrappedValue), longitude: Double(memo.location.longitude.wrappedValue) ) Annotation(memo.title.wrappedValue, coordinate: location) { ZStack { RoundedRectangle(cornerRadius: 5) .fill(.background) RoundedRectangle(cornerRadius: 5) .stroke(.secondary, lineWidth: 5) Image(uiImage: UIImage(data: memo.images.first?.wrappedValue ?? Data()) ?? UIImage()) .resizable() .scaledToFit() .frame(width: 50, height: 50) .padding(5) } } } .annotationTitles(.hidden) // ์ ๋ชฉ ๊ฐ์ถ๊ธฐ } .mapControls { // ์ด์ ๋ฒํผ์ ํญํ์ฌ ๋ด ์์น๋ฅผ ํ์ํ ์ ์์ต๋๋ค. ๋ด๊ฐ ์์ง์ผ ๋ ์ง๋ ์นด๋ฉ๋ผ๊ฐ ๋๋ฅผ ๋ฐ๋ผ๋ค๋ ๊ฒ์ ๋๋ค. MapUserLocationButton() // ๋๋ฅด๋ฉด ๋ด ์์น๋ก ๋ฐ๋ก ์ด๋ํจ, ๋ด๊ฐ ์ด๋ํ๋ฉด ์นด๋ฉ๋ผ๋ ์ด๋ํจ MapCompass() MapScaleView() /* mapControls ์ค์ ์ ์ง๋๋ฅผ ํ์ ํ๋ฉด ๋์นจ๋ฐ์ ๋์ฐ๊ณ ํ๋ฉด์ ํ๋ํ๊ฑฐ๋ ์ถ์ํ๋ฉด ์ถ์ ์ ํ์ํจ */ } .frame(height: 400) } } }
์คํฌ๋ฆฐ์ท
https://github.com/APP-iOS3rd/PJ3T2_Mymory/assets/54401641/45165739-eab6-40ec-a0ea-d63303ac0f82
PR ๊ฐ์ด๋๋ผ์ธ
PR Checklist
PR ๋ ๋ฆด ๋ ์ฒดํฌ ๋ฆฌ์คํธ
PR Type
์ด๋ค ์ข ๋ฅ์ PR์ธ๊ฐ์?
์ฐ๊ด๋๋ issue ์ ๋ณด๋ฅผ ์๋ ค์ฃผ์ธ์
Issue Number: #186 #164
PR ์ค๋ช ํ๊ธฐ
์ด PR์ ๋ํด ๊ฐ๋ตํ๊ฒ ์๊ฐํด์ฃผ์ธ์!
์ด๋ป๊ฒ ์๋ํ๋์? code ๊ธฐ๋ฐ์ผ๋ก ์ค๋ช ํด์ฃผ์ธ์
๊ฐ๊ฐ์ View๋ ์๋ง์ ViewModel์ ๊ฐ์ง๊ณ ์์ผ๋ฉฐ ๊ธฐ์กด ViewModel์ ๋ถ๊ธฐ ์ฒ๋ฆฌํ์ฌ ๋๊ฒจ์ฃผ๋ ๋ถ๋ถ์ ๊ฐ๊ฐ์ View๊ฐ ๊ฐ์ง๊ณ ์๋ ์๋ง์ ViewModel์ ๋๊ธฐ๋๋ก ๊ตฌํ๋์์ต๋๋ค. MyPageView๋ MenuTabBar๋ฅผ ํ์ฉํ์ฌ MemoList๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ถ๋ถ๊ณผ Memo Image Marker๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฐ๊ฐ์ View๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค. ์ต๋ํ ๊ฐ๊ฐ์ ViewModel์ ๋๊ธฐ๋๋ก ํ์ฌ ProfileMemoList, ProfileMemoListCell๋ฅผ ์ฌํ์ฉ ํ์์ต๋๋ค. (ํ๋กํ ์ฝ ์งํฅ ํจ๋ฌ๋ค์์ ์ฅ์ ํ์ฉ์ ์ํ์ฌ)
OtherUserProfileView๋ ์์ฑ์์ ํ๋กํ์ ํ์ธํ ๋ Detail ์ชฝ์์ ๋์ด์ฌ ๋๋ง ์ฌ์ฉํฉ๋๋ค. ๊ธฐ์กด์ MyPage๋ฅผ ๊ณ์ ์์ฑํ์ฌ ๋ฌดํ ๋ก๋ฉ๋๋ ๋ฌธ์ ๊ฐ ์์๋๋ฐ #164 ํด๋น ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ด๊ฐ ์์ฑํ ๋ฉ๋ชจ๋ผ๋ otherUserViewModel๋ฅผ ํ๊ณ ๋ค์ด๊ฐ์ ๋ก์ง๋ค์ด ์คํ๋๋ฉฐ ๋จ์ง View๋ฅผ ๊ตฌ๋ถํ๊ธฐ ์ํด TopView๋ง ๊ต์ฒดํ์ฌ ํ๋ก์ฐ, ํ๋ก์ ๋์ ์ค์ ํ๋ฉด์ด ๋ณด์ด๋๋ก ๋ถ๊ธฐ ์ฒ๋ฆฌํ์์ต๋๋ค. ๊ทธ ์ธ ๋ก์ง์ ๊ฐ์ต๋๋ค.
ํ๋กํ ์ฝ ์งํฅ ํจ๋ฌ๋ค์์ ์ฅ์ ํ์ฉํ์ฌ ๊ฐ ViewModel์์ ์ค๋ณต๋๋ ๋ฉ์๋๋ค์ ๋ชจ์์ต๋๋ค.
๊ฐ๋จํ๊ฒ MapImageMarkerView๋ฅผ ๊ตฌํํ์์ต๋๋ค!
๊ฐ๋ฅํ๋ค๋ฉด ์ถ๊ฐํด์ฃผ์ธ์
๋ณ๊ฒฝ ์ฌํญ ์คํฌ๋ฆฐ์ท ํน์ ํ๋ฉด ๋ นํ
์คํฌ๋ฆฐ์ท
https://github.com/APP-iOS3rd/PJ3T2_Mymory/assets/54401641/45165739-eab6-40ec-a0ea-d63303ac0f82
๊ธฐํ ์ธ๊ธํด์ผ ํ ์ฌํญ๋ค