APP-iOS3rd / PJ3T2_Mymory

๋ฉ‹์Ÿ์ด์‚ฌ์ž์ฒ˜๋Ÿผ iOS ์•ฑ์Šค์ฟจ 3๊ธฐ ํŒ€ ํ”„๋กœ์ ํŠธ
10 stars 3 forks source link

Refactoring/MyPage : MyPage, OtherUser Profile ๋ถ„๋ฆฌ ์ž‘์—… ๐Ÿ™†๐Ÿป #187

Closed jeonguk29 closed 7 months ago

jeonguk29 commented 7 months ago

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๋ฅผ ์žฌํ™œ์šฉ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. (ํ”„๋กœํ† ์ฝœ ์ง€ํ–ฅ ํŒจ๋Ÿฌ๋‹ค์ž„์˜ ์žฅ์  ํ™œ์šฉ์„ ์œ„ํ•˜์—ฌ)


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)

        }
    }
}

๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”

๋ณ€๊ฒฝ ์‚ฌํ•ญ ์Šคํฌ๋ฆฐ์ƒท ํ˜น์€ ํ™”๋ฉด ๋…นํ™”

์Šคํฌ๋ฆฐ์ƒท image

https://github.com/APP-iOS3rd/PJ3T2_Mymory/assets/54401641/45165739-eab6-40ec-a0ea-d63303ac0f82

๊ธฐํƒ€ ์–ธ๊ธ‰ํ•ด์•ผ ํ•  ์‚ฌํ•ญ๋“ค