diff --git a/Projects/App/Sources/Data/Constants/Response.swift b/Projects/App/Sources/Data/Constants/Response.swift index 12bb516..ec6f74b 100644 --- a/Projects/App/Sources/Data/Constants/Response.swift +++ b/Projects/App/Sources/Data/Constants/Response.swift @@ -13,3 +13,5 @@ struct Response: Decodable { let status: Bool let data: T? } + +struct NoneDecodeResponse: Decodable { } diff --git a/Projects/App/Sources/Data/DTO/Mypage/MemberBasicInfoResponseDTO.swift b/Projects/App/Sources/Data/DTO/Mypage/MemberBasicInfoResponseDTO.swift new file mode 100644 index 0000000..d03a1a1 --- /dev/null +++ b/Projects/App/Sources/Data/DTO/Mypage/MemberBasicInfoResponseDTO.swift @@ -0,0 +1,14 @@ +// +// MemberBasicInfoResponseDTO.swift +// App +// +// Created by 박서연 on 2024/08/29. +// Copyright © 2024 iOS. All rights reserved. +// + +import Foundation + +struct MemberBasicInfoResponseDTO: Decodable { + var nickname: String? + var rivewCnt: Int? +} diff --git a/Projects/App/Sources/Data/EndPoint/EndPoint.swift b/Projects/App/Sources/Data/EndPoint/EndPoint.swift index df45837..e666e22 100644 --- a/Projects/App/Sources/Data/EndPoint/EndPoint.swift +++ b/Projects/App/Sources/Data/EndPoint/EndPoint.swift @@ -18,7 +18,6 @@ struct APIEndPoint { case join = "/api/v1/auth/join" case refreshToken = "/api/v1/auth/refresh" case logout = "/api/v1/auth/logout" - case revoke = "/api/v1/auth/reovke" // ??미정 } enum Home: String { @@ -35,6 +34,17 @@ struct APIEndPoint { case zeroTagList = "/api/app/v1/filter/zero-category" } + enum Review: String { + case changeNickname = "/api/app/v1/member/nickname" + case modifierReview = "/api/app/v1/member" + case deleteReview = "/api/app/v1/review" + } + + enum Mypage: String { + case userInfo = "/api/app/v1/member" + case userReviewList = "/api/app/v1/review/member" + } + enum Detail: String { case detail = "/api/app/v1/product/detail" } @@ -58,6 +68,16 @@ struct APIEndPoint { let base = baseURL + endPoint.rawValue return build(url: base, parameters: parameters) } + + static func url(for endPoint: Review, with parameters: [String: Any]? = nil) -> String { + let base = baseURL + endPoint.rawValue + return build(url: base, parameters: parameters) + } + + static func url(for endPoint: Mypage, with parameters: [String: Any]? = nil) -> String { + let base = baseURL + endPoint.rawValue + return build(url: base, parameters: parameters) + } } extension APIEndPoint { diff --git a/Projects/App/Sources/Data/Mapper/Mypage/MypageMapper.swift b/Projects/App/Sources/Data/Mapper/Mypage/MypageMapper.swift new file mode 100644 index 0000000..569f03c --- /dev/null +++ b/Projects/App/Sources/Data/Mapper/Mypage/MypageMapper.swift @@ -0,0 +1,18 @@ +// +// MypageMapper.swift +// App +// +// Created by 박서연 on 2024/08/29. +// Copyright © 2024 iOS. All rights reserved. +// + +import Foundation + +class MypageMapper { + static func toMemberBasicInfo(response: MemberBasicInfoResponseDTO) -> MemberBasicInfoResult { + return MemberBasicInfoResult( + nickname: response.nickname ?? "", + rivewCnt: response.rivewCnt ?? 0 + ) + } +} diff --git a/Projects/App/Sources/Data/Repository/Mypage/MypageRepository.swift b/Projects/App/Sources/Data/Repository/Mypage/MypageRepository.swift new file mode 100644 index 0000000..371adf0 --- /dev/null +++ b/Projects/App/Sources/Data/Repository/Mypage/MypageRepository.swift @@ -0,0 +1,79 @@ +// +// MypageRepository.swift +// App +// +// Created by 박서연 on 2024/08/30. +// Copyright © 2024 iOS. All rights reserved. +// + +import Combine + +final class MypageRepository: MypageRepositoryProtocol { + + private let apiService: ApiService + + init(apiService: ApiService) { + self.apiService = apiService + } + + func getUserBasicInfo() -> Future { + return Future { promise in + Task { + let response: Result = await self.apiService.request( + httpMethod: .get, + endPoint: APIEndPoint.url(for: .userInfo), + header: AccountStorage.shared.accessToken + ) + + switch response { + case .success(let success): + let mappedResult = MypageMapper.toMemberBasicInfo(response: success) + promise(.success(mappedResult)) + case .failure(let failure): + debugPrint("Mypage UserInfo failed \(failure.localizedDescription)") + promise(.failure(NetworkError.badRequest)) + } + } + } + } + + func logout() -> Future { + return Future { promise in + Task { + let response: Result = await self.apiService.request( + httpMethod: .delete, + endPoint: APIEndPoint.url(for: .logout), + header: AccountStorage.shared.accessToken + ) + + switch response { + case .success(let success): + promise(.success(true)) + case .failure(let failure): + debugPrint("Failure to Logout!! \(failure.localizedDescription)") + promise(.failure(NetworkError.badRequest)) + } + } + } + } + + func revoke() -> Future { + return Future { promise in + Task { + let response: Result = await self.apiService.request( + httpMethod: .delete, + endPoint: APIEndPoint.url(for: .signIn), + header: AccountStorage.shared.accessToken + ) + + switch response { + case .success(let success): + promise(.success(true)) + case .failure(let failure): + debugPrint("Failure to Revoke!! \(failure.localizedDescription)") + promise(.failure(NetworkError.badRequest)) + } + } + } + } +} diff --git a/Projects/App/Sources/Data/Service/ApiService.swift b/Projects/App/Sources/Data/Service/ApiService.swift index abeddb9..1a5248a 100644 --- a/Projects/App/Sources/Data/Service/ApiService.swift +++ b/Projects/App/Sources/Data/Service/ApiService.swift @@ -196,6 +196,11 @@ final class ApiService { do { let result = try JSONDecoder().decode(Response.self, from: data) + + if T.self == Void.self { + return .success(() as! T) + } + debugPrint("🚨🚨 <<>> 🚨🚨 \(result)") guard let data = result.data else { diff --git a/Projects/App/Sources/Domain/Entity/Mypage/MemberBasicInfoResult.swift b/Projects/App/Sources/Domain/Entity/Mypage/MemberBasicInfoResult.swift new file mode 100644 index 0000000..f2b02f9 --- /dev/null +++ b/Projects/App/Sources/Domain/Entity/Mypage/MemberBasicInfoResult.swift @@ -0,0 +1,14 @@ +// +// MemberBasicInfoResult.swift +// App +// +// Created by 박서연 on 2024/08/29. +// Copyright © 2024 iOS. All rights reserved. +// + +import Foundation + +struct MemberBasicInfoResult { + var nickname: String + var rivewCnt: Int +} diff --git a/Projects/App/Sources/Domain/Repository/MypageRepositoryProtocol.swift b/Projects/App/Sources/Domain/Repository/MypageRepositoryProtocol.swift new file mode 100644 index 0000000..b07a7ba --- /dev/null +++ b/Projects/App/Sources/Domain/Repository/MypageRepositoryProtocol.swift @@ -0,0 +1,15 @@ +// +// MypageRepositoryProtocol.swift +// App +// +// Created by 박서연 on 2024/08/30. +// Copyright © 2024 iOS. All rights reserved. +// + +import Combine + +protocol MypageRepositoryProtocol { + func getUserBasicInfo() -> Future + func logout() -> Future + func revoke() -> Future +} diff --git a/Projects/App/Sources/Domain/Usecase/MypageUsecase.swift b/Projects/App/Sources/Domain/Usecase/MypageUsecase.swift new file mode 100644 index 0000000..da4126c --- /dev/null +++ b/Projects/App/Sources/Domain/Usecase/MypageUsecase.swift @@ -0,0 +1,25 @@ +// +// MypageUsecase.swift +// App +// +// Created by 박서연 on 2024/08/30. +// Copyright © 2024 iOS. All rights reserved. +// + +import Combine + +struct MypageUsecase { + let mypageRepoProtocol: MypageRepositoryProtocol + + func getUserBasicInfo() -> Future { + return mypageRepoProtocol.getUserBasicInfo() + } + + func logout() -> Future { + return mypageRepoProtocol.logout() + } + + func revoke() -> Future { + return mypageRepoProtocol.revoke() + } +} diff --git a/Projects/App/Sources/Presentation/Categoery/Main/CategoeryMain.swift b/Projects/App/Sources/Presentation/Categoery/Main/CategoeryMain.swift index d93c7d9..ddd89d2 100644 --- a/Projects/App/Sources/Presentation/Categoery/Main/CategoeryMain.swift +++ b/Projects/App/Sources/Presentation/Categoery/Main/CategoeryMain.swift @@ -24,7 +24,6 @@ struct CategoryMainView: View { .tapTitle { // 전체 필터로 이동 viewModel.send(action: .tapCategoryTitle(d1Category)) viewModel.send(action: .getBrandNameForCafe(d1Category)) - print("main 🏪🏪🏪 \(viewModel.entirCode)") router.navigateTo(.categoryFilter( viewModel.filteredTitle, diff --git a/Projects/App/Sources/Presentation/Categoery/Main/CategoryViewModel.swift b/Projects/App/Sources/Presentation/Categoery/Main/CategoryViewModel.swift index 34365bd..96890fa 100644 --- a/Projects/App/Sources/Presentation/Categoery/Main/CategoryViewModel.swift +++ b/Projects/App/Sources/Presentation/Categoery/Main/CategoryViewModel.swift @@ -82,7 +82,6 @@ extension CategoryViewModel { case .getBrandNameForCafe(let d1Category): self.brandFilter = d1Category.d2Category.filter { $0.d2CategoryName != "전체" }.map { $0.d2CategoryName } - print("🩵🩵🩵🩵brandFilterbrandFilter: \(self.brandFilter)🩵🩵🩵🩵") } } } diff --git a/Projects/App/Sources/Presentation/Mypage/Component/UserInfoView.swift b/Projects/App/Sources/Presentation/Mypage/Component/UserInfoView.swift index 1484cae..2fe91a7 100644 --- a/Projects/App/Sources/Presentation/Mypage/Component/UserInfoView.swift +++ b/Projects/App/Sources/Presentation/Mypage/Component/UserInfoView.swift @@ -10,14 +10,17 @@ import SwiftUI import DesignSystem struct UserInfoView: View { + @ObservedObject var viewModel: MypageViewModel let reviewCount: Int = 1 var nickname: (() -> Void)? var action: (() -> Void)? init( + viewModel: MypageViewModel, nickname: (() -> Void)? = nil, action: (() -> Void)? = nil ) { + self.viewModel = viewModel self.nickname = nickname self.action = action } @@ -25,7 +28,7 @@ struct UserInfoView: View { var body: some View { VStack(spacing: 30) { HStack { - ZSText("닉네임닉네임닉네임닉네임", fontType: .subtitle1) + ZSText(viewModel.userInfo.nickname, fontType: .subtitle1) Spacer() ZSText("닉네임 변경", fontType: .body3, color: Color.neutral600) .padding(.init(top: 6,leading: 10, bottom: 6, trailing: 10)) @@ -36,12 +39,12 @@ struct UserInfoView: View { } } - Text(reviewCount == 0 ? "아직 작성한 리뷰가 없어요" : "작성한 리뷰 (reviewCount)") + Text(viewModel.userInfo.rivewCnt == 0 ? "아직 작성한 리뷰가 없어요" : "작성한 리뷰 (reviewCount)") .applyFont(font: .subtitle2) - .foregroundStyle(reviewCount == 0 ? Color.neutral800 : Color.white) + .foregroundStyle(viewModel.userInfo.rivewCnt == 0 ? Color.neutral800 : Color.white) .padding(.vertical, 16) .frame(maxWidth: .infinity) - .background(reviewCount == 0 ? Color.negative.opacity(0.1) : Color.primaryFF6972) + .background(viewModel.userInfo.rivewCnt == 0 ? Color.primaryFF6972.opacity(0.1) : Color.primaryFF6972) .clipShape(RoundedRectangle(cornerRadius: 8)) .onTapGesture { action?() @@ -66,5 +69,5 @@ extension UserInfoView { } #Preview { - UserInfoView() + UserInfoView(viewModel: MypageViewModel(mypageUseCase: MypageUsecase(mypageRepoProtocol: MypageRepository(apiService: ApiService())))) } diff --git a/Projects/App/Sources/Presentation/Mypage/Main/MypageInfoView.swift b/Projects/App/Sources/Presentation/Mypage/Main/MypageInfoView.swift new file mode 100644 index 0000000..4a5b947 --- /dev/null +++ b/Projects/App/Sources/Presentation/Mypage/Main/MypageInfoView.swift @@ -0,0 +1,53 @@ +// +// MypageInfoView.swift +// App +// +// Created by 박서연 on 2024/08/30. +// Copyright © 2024 iOS. All rights reserved. +// + +import SwiftUI +import DesignSystem + +struct MypageInfoView: View { + var body: some View { + VStack { + ForEach(MypageCenter.allCases, id: \.self) { center in + Text(center.rawValue) + .applyFont(font: .body3) + .foregroundStyle(Color.neutral300) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 10) + .padding(.top, 20) + + ForEach(center.type, id: \.self) { type in + HStack { + Text(type) + .applyFont(font: .body2) + .foregroundStyle(Color.neutral900) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + + if type == MypageCenter.service.type.last! { + Text("앱 버전1.201.23") + .applyFont(font: .body2) + .foregroundStyle(Color.neutral500) + } else { + ZerosomeAsset.ic_arrow_after + .resizable() + .frame(width: 24, height: 24) + } + + } + .onTapGesture { + print("case 별로 이동 처리 추가 예정") + } + } + .padding(.bottom, 10) + DivideRectangle(height: 1, color: Color.neutral100) + } + } + .padding(.horizontal, 22) + } +} diff --git a/Projects/App/Sources/Presentation/Mypage/Main/MypageMainView.swift b/Projects/App/Sources/Presentation/Mypage/Main/MypageMainView.swift index eb35a93..42c40fd 100644 --- a/Projects/App/Sources/Presentation/Mypage/Main/MypageMainView.swift +++ b/Projects/App/Sources/Presentation/Mypage/Main/MypageMainView.swift @@ -6,20 +6,104 @@ // Copyright © 2024 iOS. All rights reserved. // -import SwiftUI +import Combine import DesignSystem +import SwiftUI + +class MypageViewModel: ObservableObject { + + enum Action { + case getUserBasicInfo + case logout + case revoke + } + + private let mypageUseCase: MypageUsecase + private var cancellables = Set() + + init(mypageUseCase: MypageUsecase) { + self.mypageUseCase = mypageUseCase + } + + @EnvironmentObject var authViewModel: AuthViewModel + @Published var userInfo: MemberBasicInfoResult = .init(nickname: "", rivewCnt: 0) + @Published var logoutResult: Bool = false + @Published var revokeResult: Bool = false + + func send(_ action: Action) { + switch action { + case .getUserBasicInfo: + mypageUseCase.getUserBasicInfo() + .sink { completion in + switch completion { + case .finished: + break + case .failure(let failure): + debugPrint("GetUserBasicInfo Failed \(failure.localizedDescription)") + } + } receiveValue: { [weak self] data in + self?.userInfo = data + } + .store(in: &cancellables) + + case .logout: + print("로그아웃") + mypageUseCase.logout() + .sink { completion in + switch completion { + case .finished: + break + case .failure(let failure): + debugPrint("Failed to logout \(failure.localizedDescription)") + } + } receiveValue: { result in + if result { + self.logoutResult = true + self.authViewModel.authenticationState = .initial + } else { + self.logoutResult = false + } + } + .store(in: &cancellables) + + + case .revoke: + print("회원탈퇴") + mypageUseCase.revoke() + .sink { completion in + switch completion { + case .finished: + break + case .failure(let failure): + debugPrint("Failed to revoke \(failure.localizedDescription)") + } + } receiveValue: { result in + if result { + self.revokeResult = true + self.authViewModel.authenticationState = .initial + } else { + self.revokeResult = false + } + } + .store(in: &cancellables) + + } + } + +} struct MypageMainView: View { @EnvironmentObject var router: Router + @ObservedObject var viewModel: MypageViewModel var body: some View { ScrollView { - UserInfoView() + UserInfoView(viewModel: viewModel) .tapAction { router.navigateTo(.mypageReviewList) } .tapNickname { - router.navigateTo(.mypgaeNickname) + router.navigateTo(.mypgaeNickname(viewModel.userInfo.nickname)) } .padding(.init(top: 24,leading: 0,bottom: 30,trailing: 0)) @@ -31,12 +115,12 @@ struct MypageMainView: View { HStack { MypageButton(title: "로그아웃") .tap { - print("로그아웃") + viewModel.send(.logout) } MypageButton(title: "회원탈퇴") .tap { - print("회원 탈퇴") + viewModel.send(.revoke) } } .padding(.horizontal, 22) @@ -45,50 +129,9 @@ struct MypageMainView: View { } .ZSnavigationTitle("마이페이지") .scrollIndicators(.hidden) - } -} - - -struct MypageInfoView: View { - var body: some View { - VStack { - ForEach(MypageCenter.allCases, id: \.self) { center in - Text(center.rawValue) - .applyFont(font: .body3) - .foregroundStyle(Color.neutral300) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.bottom, 10) - .padding(.top, 20) - - ForEach(center.type, id: \.self) { type in - HStack { - Text(type) - .applyFont(font: .body2) - .foregroundStyle(Color.neutral900) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - - if type == MypageCenter.service.type.last! { - Text("앱 버전1.201.23") - .applyFont(font: .body2) - .foregroundStyle(Color.neutral500) - } else { - ZerosomeAsset.ic_arrow_after - .resizable() - .frame(width: 24, height: 24) - } - - } - .onTapGesture { - print("case 별로 이동 처리 추가 예정") - } - } - .padding(.bottom, 10) - DivideRectangle(height: 1, color: Color.neutral100) - } + .onAppear { + } - .padding(.horizontal, 22) } } @@ -101,11 +144,11 @@ enum MypageCenter: String, CaseIterable { case .customCenter: return ["공지사항", "FAQ", "1:1 문의"] case .service: - return ["설정", "약관 및 정책", "앱 버전 정보"] + return ["서비스 이용약관", "개인정보 처리방침", "앱 버전 정보"] } } } #Preview { - MypageMainView() + MypageMainView(viewModel: MypageViewModel(mypageUseCase: MypageUsecase(mypageRepoProtocol: MypageRepository(apiService: ApiService())))) } diff --git a/Projects/App/Sources/Presentation/Tabbar+Navigation/Router/Router.swift b/Projects/App/Sources/Presentation/Tabbar+Navigation/Router/Router.swift index ef71277..60085b5 100644 --- a/Projects/App/Sources/Presentation/Tabbar+Navigation/Router/Router.swift +++ b/Projects/App/Sources/Presentation/Tabbar+Navigation/Router/Router.swift @@ -21,7 +21,7 @@ final class Router: ObservableObject { case creatReview(ReviewEntity) // proudct it, name, brand case mypageReviewList case myReivew - case mypgaeNickname + case mypgaeNickname(String) // nickname case report } @@ -58,7 +58,7 @@ final class Router: ObservableObject { case .myReivew: MyReivewView() - case .mypgaeNickname: + case .mypgaeNickname(let nickname): ChangeNicknameView() case .report: diff --git a/Projects/App/Sources/Presentation/Tabbar+Navigation/Tabbar/Tabbar.swift b/Projects/App/Sources/Presentation/Tabbar+Navigation/Tabbar/Tabbar.swift index 2a7ae1b..a815ff4 100644 --- a/Projects/App/Sources/Presentation/Tabbar+Navigation/Tabbar/Tabbar.swift +++ b/Projects/App/Sources/Presentation/Tabbar+Navigation/Tabbar/Tabbar.swift @@ -28,7 +28,10 @@ enum Tabbar: CaseIterable { CategoryMainView(viewModel: viewModel) case .mypage: - MypageMainView() + let mypageRepo = MypageRepository(apiService: apiService) + let mypageUsecase = MypageUsecase(mypageRepoProtocol: mypageRepo) + let viewModel = MypageViewModel(mypageUseCase: mypageUsecase) + MypageMainView(viewModel: viewModel) } }