diff --git a/echog/NetworkFeatureKit/Sources/NetworkFeatureKit/User/Builder/UserDeleteNetworkBuilder.swift b/echog/NetworkFeatureKit/Sources/NetworkFeatureKit/User/Builder/UserDeleteNetworkBuilder.swift new file mode 100644 index 0000000..6366141 --- /dev/null +++ b/echog/NetworkFeatureKit/Sources/NetworkFeatureKit/User/Builder/UserDeleteNetworkBuilder.swift @@ -0,0 +1,22 @@ +// +// UserDeleteNetworkBuilder.swift +// NetworkFeatureKit +// +// Created by minsong kim on 2/27/25. +// + +import Foundation +import NetworkKit + +struct UserDeleteNetworkBuilder: NetworkBuilderProtocol { + typealias Response = DefalutDTO + + var baseURL: BaseURLType { .api } + var path: String { "/api/users/delete" } + var queries: [URLQueryItem]? + var method: HTTPMethod { .post } + var parameters: [String : Any] = [:] + var deserializer: any NetworkDeserializable = JSONNetworkDeserializer(decoder: JSONDecoder()) + + var useAuthorization: Bool = true +} diff --git a/echog/NetworkFeatureKit/Sources/NetworkFeatureKit/User/UserNetwork.swift b/echog/NetworkFeatureKit/Sources/NetworkFeatureKit/User/UserNetwork.swift index 0a29d49..a0111c7 100644 --- a/echog/NetworkFeatureKit/Sources/NetworkFeatureKit/User/UserNetwork.swift +++ b/echog/NetworkFeatureKit/Sources/NetworkFeatureKit/User/UserNetwork.swift @@ -38,4 +38,9 @@ public final class UserNetwork: @unchecked Sendable { let builder = UserLogInNetworkBuilder(parameters: ["loginId": email, "password": password]) return try await networkManager.fetchData(builder) } + + public func signOut() async throws -> DefalutDTO { + let builder = UserDeleteNetworkBuilder() + return try await networkManager.fetchData(builder) + } } diff --git a/echog/echog.xcodeproj/project.pbxproj b/echog/echog.xcodeproj/project.pbxproj index 3539c34..5f86828 100644 --- a/echog/echog.xcodeproj/project.pbxproj +++ b/echog/echog.xcodeproj/project.pbxproj @@ -23,6 +23,14 @@ 920470AB2C9D47BC00D547FB /* PopUpProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920470AA2C9D47BC00D547FB /* PopUpProtocol.swift */; }; 92325B642D661C0300F5CCE9 /* NetworkKit in Frameworks */ = {isa = PBXBuildFile; productRef = 92325B632D661C0300F5CCE9 /* NetworkKit */; }; 92325B662D661C0700F5CCE9 /* NetworkFeatureKit in Frameworks */ = {isa = PBXBuildFile; productRef = 92325B652D661C0700F5CCE9 /* NetworkFeatureKit */; }; + 92325BA02D6741B000F5CCE9 /* MyPageReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92325B9F2D6741A800F5CCE9 /* MyPageReducer.swift */; }; + 92325BA22D6741B900F5CCE9 /* MyPageStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92325BA12D6741B500F5CCE9 /* MyPageStore.swift */; }; + 92325BA42D67425700F5CCE9 /* MyPageCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92325BA32D67425100F5CCE9 /* MyPageCoordinator.swift */; }; + 92325FD32D6CB26B00F5CCE9 /* SignOutReasonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92325FD22D6CB25500F5CCE9 /* SignOutReasonViewController.swift */; }; + 92325FD52D6DC1FE00F5CCE9 /* SignOutReasonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92325FD42D6DC1F900F5CCE9 /* SignOutReasonCell.swift */; }; + 92325FD72D6DCDEB00F5CCE9 /* SignOutConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92325FD62D6DCDE100F5CCE9 /* SignOutConfirmViewController.swift */; }; + 92325FD92D6DCE1300F5CCE9 /* SignOutReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92325FD82D6DCE0C00F5CCE9 /* SignOutReason.swift */; }; + 923260012D6EEA4C00F5CCE9 /* TermsCheckViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923260002D6EEA3900F5CCE9 /* TermsCheckViewController.swift */; }; 92365B5F2D06B108001D4A71 /* Pretendard-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 9204707E2C9A760300D547FB /* Pretendard-SemiBold.otf */; }; 92365B602D06B121001D4A71 /* Pretendard-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 920470872C9A7A7300D547FB /* Pretendard-Regular.otf */; }; 92365B612D06B127001D4A71 /* Pretendard-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 9204707F2C9A760F00D547FB /* Pretendard-Medium.otf */; }; @@ -139,6 +147,14 @@ 920470A42C9BBC6B00D547FB /* UIImage+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+.swift"; sourceTree = ""; }; 920470A82C9BC45B00D547FB /* PopUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpView.swift; sourceTree = ""; }; 920470AA2C9D47BC00D547FB /* PopUpProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpProtocol.swift; sourceTree = ""; }; + 92325B9F2D6741A800F5CCE9 /* MyPageReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageReducer.swift; sourceTree = ""; }; + 92325BA12D6741B500F5CCE9 /* MyPageStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageStore.swift; sourceTree = ""; }; + 92325BA32D67425100F5CCE9 /* MyPageCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageCoordinator.swift; sourceTree = ""; }; + 92325FD22D6CB25500F5CCE9 /* SignOutReasonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignOutReasonViewController.swift; sourceTree = ""; }; + 92325FD42D6DC1F900F5CCE9 /* SignOutReasonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignOutReasonCell.swift; sourceTree = ""; }; + 92325FD62D6DCDE100F5CCE9 /* SignOutConfirmViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignOutConfirmViewController.swift; sourceTree = ""; }; + 92325FD82D6DCE0C00F5CCE9 /* SignOutReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignOutReason.swift; sourceTree = ""; }; + 923260002D6EEA3900F5CCE9 /* TermsCheckViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsCheckViewController.swift; sourceTree = ""; }; 92365B572D05D96D001D4A71 /* InformationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InformationViewController.swift; sourceTree = ""; }; 92365B592D05DEC3001D4A71 /* TextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldView.swift; sourceTree = ""; }; 92365B5B2D06A066001D4A71 /* UIControl+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+.swift"; sourceTree = ""; }; @@ -397,6 +413,7 @@ 92365BBE2D1BC1F5001D4A71 /* Cell */ = { isa = PBXGroup; children = ( + 92325FD42D6DC1F900F5CCE9 /* SignOutReasonCell.swift */, 92E697342D5CDF150059D3AE /* VoteCell.swift */, 92365BFE2D2EC565001D4A71 /* MyVoteCell.swift */, 92365BC12D1C16A0001D4A71 /* HeaderView.swift */, @@ -408,7 +425,12 @@ 92365BDF2D1D737A001D4A71 /* MyPage */ = { isa = PBXGroup; children = ( + 92325BA12D6741B500F5CCE9 /* MyPageStore.swift */, + 92325B9F2D6741A800F5CCE9 /* MyPageReducer.swift */, + 92325FD22D6CB25500F5CCE9 /* SignOutReasonViewController.swift */, + 92325FD62D6DCDE100F5CCE9 /* SignOutConfirmViewController.swift */, 92365BE02D1D7387001D4A71 /* MyVoteListViewController.swift */, + 923260002D6EEA3900F5CCE9 /* TermsCheckViewController.swift */, 92365BDB2D1D043A001D4A71 /* MyPageViewController.swift */, ); path = MyPage; @@ -424,6 +446,7 @@ 92DD8F4B2CEC9979001197D0 /* Model */ = { isa = PBXGroup; children = ( + 92325FD82D6DCE0C00F5CCE9 /* SignOutReason.swift */, 92E697322D5CDA5E0059D3AE /* VoteDTO.swift */, 92365BDD2D1D72B7001D4A71 /* MyPage.swift */, ); @@ -477,6 +500,7 @@ 92DFD4022D335448008CFE95 /* Coordinator */ = { isa = PBXGroup; children = ( + 92325BA32D67425100F5CCE9 /* MyPageCoordinator.swift */, 92656BF82D62D10500FE4337 /* PasswordCoordinator.swift */, 92E697532D5DFA5F0059D3AE /* LogInCoordinator.swift */, 92DFD4652D4350D4008CFE95 /* DiaryHomeCoordinator.swift */, @@ -690,8 +714,11 @@ 92365BC02D1BC201001D4A71 /* DiaryCell.swift in Sources */, 92365B9B2D0833DA001D4A71 /* DiaryHomeViewController.swift in Sources */, 92E697332D5CDA7C0059D3AE /* VoteDTO.swift in Sources */, + 92325BA22D6741B900F5CCE9 /* MyPageStore.swift in Sources */, + 92325FD92D6DCE1300F5CCE9 /* SignOutReason.swift in Sources */, 92DFD3F62D3236C8008CFE95 /* UIView+.swift in Sources */, 92365BC22D1C16A4001D4A71 /* HeaderView.swift in Sources */, + 923260012D6EEA4C00F5CCE9 /* TermsCheckViewController.swift in Sources */, 92E697462D5DA8A80059D3AE /* VoteSelectLineView.swift in Sources */, 92E697352D5CDF180059D3AE /* VoteCell.swift in Sources */, 92DFD4C72D531D7C008CFE95 /* OnBoardingReducer.swift in Sources */, @@ -722,10 +749,13 @@ 92365BE12D1D7390001D4A71 /* MyVoteListViewController.swift in Sources */, 92365BFF2D2EC56E001D4A71 /* MyVoteCell.swift in Sources */, 92E6975A2D5E05CE0059D3AE /* PasswordCompleteViewController.swift in Sources */, + 92325BA42D67425700F5CCE9 /* MyPageCoordinator.swift in Sources */, 92E697542D5DFA650059D3AE /* LogInCoordinator.swift in Sources */, + 92325FD52D6DC1FE00F5CCE9 /* SignOutReasonCell.swift in Sources */, 92E697412D5D9D440059D3AE /* ChipLabel.swift in Sources */, 92DFD4C92D532197008CFE95 /* OnBoardingStore.swift in Sources */, 92656BF32D62C7E600FE4337 /* DiaryReducer.swift in Sources */, + 92325FD32D6CB26B00F5CCE9 /* SignOutReasonViewController.swift in Sources */, 92E6976B2D5F23D20059D3AE /* VoteCreationViewController.swift in Sources */, 920470852C9A76F700D547FB /* UIFontMetrics+.swift in Sources */, 92E697442D5DA38C0059D3AE /* VoteView.swift in Sources */, @@ -735,11 +765,13 @@ 92656BF92D62D10C00FE4337 /* PasswordCoordinator.swift in Sources */, 92DFD4662D4350DC008CFE95 /* DiaryHomeCoordinator.swift in Sources */, 92DFD3F82D327653008CFE95 /* InformationLoadingViewController.swift in Sources */, + 92325BA02D6741B000F5CCE9 /* MyPageReducer.swift in Sources */, 92DFD4BD2D52E243008CFE95 /* InformationStore.swift in Sources */, 92DFD3F42D323391008CFE95 /* UIGestureRecognizer+.swift in Sources */, 92DFD4832D450C71008CFE95 /* UILabel+.swift in Sources */, 920470A52C9BBC6B00D547FB /* UIImage+.swift in Sources */, 920470A92C9BC45B00D547FB /* PopUpView.swift in Sources */, + 92325FD72D6DCDEB00F5CCE9 /* SignOutConfirmViewController.swift in Sources */, 92E6976D2D5F5D0C0059D3AE /* SelectionButton.swift in Sources */, 92DFD3FF2D335198008CFE95 /* Coordinator.swift in Sources */, 92656BF52D62C7F100FE4337 /* DiaryStore.swift in Sources */, @@ -924,6 +956,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = BMG3X5CM6G; + EAGER_LINKING = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = echog/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -952,6 +985,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = BMG3X5CM6G; + EAGER_LINKING = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = echog/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; diff --git a/echog/echog/Base.lproj/LaunchScreen.storyboard b/echog/echog/Base.lproj/LaunchScreen.storyboard index 865e932..7d90bb1 100644 --- a/echog/echog/Base.lproj/LaunchScreen.storyboard +++ b/echog/echog/Base.lproj/LaunchScreen.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -11,10 +13,26 @@ - + - + + + + + + + + + + + + + + + + + @@ -22,4 +40,11 @@ + + + + + + + diff --git a/echog/echog/Data/Model/MyPage.swift b/echog/echog/Data/Model/MyPage.swift index 4318fd0..1bbc56a 100644 --- a/echog/echog/Data/Model/MyPage.swift +++ b/echog/echog/Data/Model/MyPage.swift @@ -21,14 +21,17 @@ enum MyPageList: CaseIterable { } } -enum MyPageSignOut: CaseIterable { - case logOut +enum MyPageSignOut: Int, CaseIterable { + case logOut = 0 + case checkTerms case signOut var title: String { switch self { case .logOut: "로그아웃" + case .checkTerms: + "개인정보 처리방침" case .signOut: "회원탈퇴" } @@ -37,9 +40,11 @@ enum MyPageSignOut: CaseIterable { var color: UIColor { switch self { case .logOut: - .black + .slate800 + case .checkTerms: + .slate800 case .signOut: - .red + .red500 } } } diff --git a/echog/echog/Data/Model/SignOutReason.swift b/echog/echog/Data/Model/SignOutReason.swift new file mode 100644 index 0000000..f520654 --- /dev/null +++ b/echog/echog/Data/Model/SignOutReason.swift @@ -0,0 +1,35 @@ +// +// SignOutReason.swift +// echog +// +// Created by minsong kim on 2/25/25. +// + +enum SignOutReason: CaseIterable { + case uncomfortableService + case uncomfortableDiary + case contentsNotSatisfying + case moveToAnotherService + case personalReason + case burdenPrice + case etc + + var title: String { + switch self { + case .uncomfortableService: + "서비스 이용이 불편함" + case .uncomfortableDiary: + "일기 작성 기능이 부족하거나 불편함" + case .contentsNotSatisfying: + "콘텐츠(투표, 커뮤니티)에 대한 불만" + case .moveToAnotherService: + "다른 일기/투표 서비스로 이동 예정" + case .personalReason: + "개인적인 이유로 더 이상 이용하지 않음" + case .burdenPrice: + "광고 또는 유료 결제 정책이 부담됨" + case .etc: + "기타 (직접 입력 가능)" + } + } +} diff --git a/echog/echog/Design System/Cell/SignOutReasonCell.swift b/echog/echog/Design System/Cell/SignOutReasonCell.swift new file mode 100644 index 0000000..3e1db91 --- /dev/null +++ b/echog/echog/Design System/Cell/SignOutReasonCell.swift @@ -0,0 +1,78 @@ +// +// SignOutReasonCell.swift +// echog +// +// Created by minsong kim on 2/25/25. +// + +import UIKit +import SnapKit + +class SignOutReasonCell: UITableViewCell { + static let identifier = "SignOutReasonCell" + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .regularTitle15 + label.textColor = .slate800 + + return label + }() + + private let checkButton = SelectionButton(isSelected: false) + + let reasonTextView: UITextView = { + let textView = UITextView() + textView.font = .regularTitle15 + textView.layer.borderWidth = 1 + textView.layer.borderColor = UIColor.slate100.cgColor + textView.layer.cornerRadius = 8 + textView.backgroundColor = .slate25 + textView.text = "탈퇴사유를 입력해 주세요." + textView.textColor = .textDisabled + + return textView + }() + + private func configureLabels() { + self.addSubview(titleLabel) + self.addSubview(checkButton) + + checkButton.snp.makeConstraints { make in + make.leading.equalToSuperview().inset(16) + make.top.equalToSuperview().inset(20) + make.width.height.equalTo(16) + } + + titleLabel.snp.makeConstraints { make in + make.leading.equalTo(checkButton.snp.trailing).offset(8) + make.centerY.equalTo(checkButton) + make.height.equalTo(22) + } + } + + private func configureTextView() { + self.addSubview(reasonTextView) + + reasonTextView.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(16) + make.leading.equalTo(titleLabel) + make.trailing.equalToSuperview().inset(16) + make.height.equalTo(100) + } + } + + func configureCells(title: String, isTextView: Bool = false) { + titleLabel.text = title + + configureLabels() + + if isTextView { + configureTextView() + } + } + + func selectedCell() { + checkButton.isSelected.toggle() + } +} diff --git a/echog/echog/Presentation/Coordinator/AppCoordinator.swift b/echog/echog/Presentation/Coordinator/AppCoordinator.swift index 3ad1d1f..5b5e9a1 100644 --- a/echog/echog/Presentation/Coordinator/AppCoordinator.swift +++ b/echog/echog/Presentation/Coordinator/AppCoordinator.swift @@ -80,6 +80,10 @@ class AppCoordinator: Coordinator { } func startMyPageCoordinator() { - + let myPageCoordinator = MyPageCoordinator(navigationController: navigationController) + children.removeAll() + myPageCoordinator.parentCoordinator = self + children.append(myPageCoordinator) + myPageCoordinator.start() } } diff --git a/echog/echog/Presentation/Coordinator/MyPageCoordinator.swift b/echog/echog/Presentation/Coordinator/MyPageCoordinator.swift new file mode 100644 index 0000000..6d41d8e --- /dev/null +++ b/echog/echog/Presentation/Coordinator/MyPageCoordinator.swift @@ -0,0 +1,67 @@ +// +// MyPageCoordinator.swift +// echog +// +// Created by minsong kim on 2/20/25. +// + +import UIKit + +protocol MyPageNavigation: AnyObject { + func goToDiaryViewController() +} + +class MyPageCoordinator: Coordinator { + var parentCoordinator: Coordinator? + var children: [Coordinator] = [] + var navigationController: UINavigationController + private var reducer = MyPageReducer() + private lazy var store = MyPageStore(reducer: reducer) + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + reducer.delegate = self + } + + func start() { + pushMyPageViewController() + } +} + +extension MyPageCoordinator: MyPageNavigation { + func pushMyPageViewController() { + let myPageViewController = MyPageViewController(store: store) + navigationController.pushViewController(myPageViewController, animated: false) + } + + func pushSignOutReasonViewController() { + let signOutResonViewController = SignOutReasonViewController(store: store) + navigationController.pushViewController(signOutResonViewController, animated: true) + } + + func pushSignOutConfirmViewController() { + let signOutConfirmViewController = SignOutConfirmViewController(store: store) + navigationController.pushViewController(signOutConfirmViewController, animated: true) + } + + func pushTermsViewController() { + let termsViewController = TermsCheckViewController(store: store) + navigationController.pushViewController(termsViewController, animated: false) + } + + func goToDiaryViewController() { + let appCoordinator = parentCoordinator as? AppCoordinator + appCoordinator?.startDiaryCoordinator() + appCoordinator?.childDidFinish(self) + } + + func goToLogInViewController() { + let appCoordinator = parentCoordinator as? AppCoordinator + appCoordinator?.startLoginCoordinator() + appCoordinator?.childDidFinish(self) + } + + func popViewController() { + navigationController.popViewController(animated: false) + } +} diff --git a/echog/echog/Presentation/Information/InformationViewController.swift b/echog/echog/Presentation/Information/InformationViewController.swift index 6f9ee5f..6d36863 100644 --- a/echog/echog/Presentation/Information/InformationViewController.swift +++ b/echog/echog/Presentation/Information/InformationViewController.swift @@ -258,7 +258,7 @@ class InformationViewController: UIViewController, View, ToastProtocol, BottomSh nextButton.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(20) - make.bottom.equalTo(view.safeAreaLayoutGuide.snp.top).offset(-20) + make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-20) make.height.equalTo(50) } } diff --git a/echog/echog/Presentation/LogIn/LogInViewController.swift b/echog/echog/Presentation/LogIn/LogInViewController.swift index ec96bf8..4f09340 100644 --- a/echog/echog/Presentation/LogIn/LogInViewController.swift +++ b/echog/echog/Presentation/LogIn/LogInViewController.swift @@ -238,7 +238,7 @@ extension LogInViewController { extension LogInViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { if textField == emailTextField.mainTextField { - emailTextField.mainTextField.becomeFirstResponder() + passwordTextField.mainTextField.becomeFirstResponder() } else { textField.resignFirstResponder() } diff --git a/echog/echog/Presentation/MyPage/MyPageReducer.swift b/echog/echog/Presentation/MyPage/MyPageReducer.swift new file mode 100644 index 0000000..88b574b --- /dev/null +++ b/echog/echog/Presentation/MyPage/MyPageReducer.swift @@ -0,0 +1,87 @@ +// +// MyPageReducer.swift +// echog +// +// Created by minsong kim on 2/20/25. +// + +import Combine +import Foundation +import NetworkFeatureKit +import KeyChainModule + +struct MyPageReducer: ReducerProtocol { + + enum Intent { + case selectMyPage(IndexPath) + case goToNextSignOutPage + case popPage + case signOut + case goBackDiaryHome + } + + enum Mutation { + case signOutFailure + } + + struct State { + var isSignOutSuccess: TryState = .notYet + } + + var initialState = State() + + weak var delegate: MyPageCoordinator? + + func mutate(action: Intent) -> AnyPublisher? { + switch action { + case .selectMyPage(let indexPath): + let page = MyPageSignOut(rawValue: indexPath.row) + + if page == .logOut { + KeyChain.delete(key: .accessToken) + KeyChain.delete(key: .refreshToken) + delegate?.goToLogInViewController() + } else if page == .checkTerms { + delegate?.pushTermsViewController() + } else if page == .signOut { + delegate?.pushSignOutReasonViewController() + } + + return nil + case .goToNextSignOutPage: + delegate?.pushSignOutConfirmViewController() + return nil + case .popPage: + delegate?.popViewController() + return nil + case .signOut: + return Future { promise in + Task { @MainActor in + do { + _ = try await UserNetwork.shared.signOut() + KeyChain.delete(key: .accessToken) + KeyChain.delete(key: .refreshToken) + delegate?.goToLogInViewController() + } catch { + promise(.success(.signOutFailure)) + } + } + } + .eraseToAnyPublisher() + case .goBackDiaryHome: + delegate?.goToDiaryViewController() + return nil + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + + switch mutation { + case .signOutFailure: + newState.isSignOutSuccess = .failure + } + + return newState + } +} diff --git a/echog/echog/Presentation/MyPage/MyPageStore.swift b/echog/echog/Presentation/MyPage/MyPageStore.swift new file mode 100644 index 0000000..4a3f5a5 --- /dev/null +++ b/echog/echog/Presentation/MyPage/MyPageStore.swift @@ -0,0 +1,35 @@ +// +// MyPageStore.swift +// echog +// +// Created by minsong kim on 2/20/25. +// + +import Combine +import Foundation + +final class MyPageStore: StoreProtocol { + typealias Reducer = MyPageReducer + + var cancellables = Set() + @Published var state: MyPageReducer.State + var reducer: MyPageReducer + + init(reducer: MyPageReducer) { + self.reducer = reducer + self.state = reducer.initialState + } + + func dispatch(_ intent: MyPageReducer.Intent) { + let mutationPublisher = reducer.mutate(action: intent) + + mutationPublisher? + .sink { [weak self] mutation in + guard let self else { return } + + let newState = self.reducer.reduce(state: self.state, mutation: mutation) + self.state = newState + } + .store(in: &cancellables) + } +} diff --git a/echog/echog/Presentation/MyPage/MyPageViewController.swift b/echog/echog/Presentation/MyPage/MyPageViewController.swift index e942785..7a316c2 100644 --- a/echog/echog/Presentation/MyPage/MyPageViewController.swift +++ b/echog/echog/Presentation/MyPage/MyPageViewController.swift @@ -5,10 +5,14 @@ // Created by minsong kim on 12/26/24. // +import Combine import UIKit import SnapKit -class MyPageViewController: UIViewController { +class MyPageViewController: UIViewController, View { + var store: MyPageStore + private var cancellables = Set() + private let titleLabel: UILabel = { let label = UILabel() label.font = .semiboldHeadline24 @@ -34,12 +38,32 @@ class MyPageViewController: UIViewController { return table }() + required init(store: MyPageStore) { + self.store = store + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground configureBar() configureTableView() + + bind() + } + + private func bind() { + closeButton.publisher(for: .touchUpInside) + .sink { [weak self] in + self?.store.dispatch(.goBackDiaryHome) + } + .store(in: &cancellables) } private func configureBar() { @@ -76,22 +100,22 @@ class MyPageViewController: UIViewController { extension MyPageViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - 2 + 3 } - func numberOfSections(in tableView: UITableView) -> Int { - 2 - } +// func numberOfSections(in tableView: UITableView) -> Int { +// 2 +// } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell(style: .default, reuseIdentifier: "MyPageListCell") var content = cell.defaultContentConfiguration() - if indexPath.section == 0 { - content.text = MyPageList.allCases[indexPath.item].title - } else { +// if indexPath.section == 0 { +// content.text = MyPageList.allCases[indexPath.item].title +// } else { content.text = MyPageSignOut.allCases[indexPath.item].title content.textProperties.color = MyPageSignOut.allCases[indexPath.item].color - } +// } content.textProperties.font = .mediumTitle15 cell.contentConfiguration = content @@ -113,10 +137,14 @@ extension MyPageViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 60 } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + store.dispatch(.selectMyPage(indexPath)) + } } -//#Preview { -// let vc = MyPageViewController() -// -// return vc -//} +#Preview { + let vc = MyPageViewController(store: MyPageStore(reducer: MyPageReducer())) + + return vc +} diff --git a/echog/echog/Presentation/MyPage/SignOutConfirmViewController.swift b/echog/echog/Presentation/MyPage/SignOutConfirmViewController.swift new file mode 100644 index 0000000..955614d --- /dev/null +++ b/echog/echog/Presentation/MyPage/SignOutConfirmViewController.swift @@ -0,0 +1,245 @@ +// +// SignOutConfirmViewController.swift +// echog +// +// Created by minsong kim on 2/25/25. +// + +import Combine +import UIKit +import SnapKit + +class SignOutConfirmViewController: UIViewController, ToastProtocol { + var window: UIWindow? = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first + + var store: MyPageStore + private var cancellables = Set() + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .semiboldLargetitle17 + label.textColor = .black + label.text = "회원탈퇴" + + return label + }() + + private let backButton: UIButton = { + var configuration = UIButton.Configuration.plain() + configuration.image = UIImage(systemName: "arrow.backward") + configuration.baseForegroundColor = .slate800 + + let button = UIButton(configuration: configuration) + + return button + }() + + private let separatorLine: UIView = { + let view = UIView() + view.backgroundColor = .slate100 + + return view + }() + + private let separatorSecondLine: UIView = { + let view = UIView() + view.backgroundColor = .slate100 + + return view + }() + + private let guideTitleLabel: UILabel = { + let label = UILabel() + label.font = .semiboldTitle13 + label.text = "탈퇴 안내" + label.textColor = .slate600 + + return label + }() + + private let guideLabel: UILabel = { + let label = UILabel() + label.font = .regularTitle15 + label.textColor = .slate600 + label.numberOfLines = 2 + label.setTextWithLineSpacing("탈퇴 후에는 내 일기를 더 이상 보관하거나 볼 수 없어요. \n회원 탈퇴를 신청하려면 아래 문장을 입력해주세요.", font: .regularTitle15, lineSpacing: 12, alinment: .left, color: .slate600) + + return label + }() + + private let textTitleLabel: UILabel = { + let label = UILabel() + label.font = .semiboldTitle13 + label.text = "문구" + label.textColor = .slate600 + + return label + }() + + private let confirmTextLabel: UILabel = { + let label = UILabel() + label.font = .regularTitle15 + label.textColor = .slate600 + label.numberOfLines = 2 + label.setTextWithLineSpacing("탈퇴를 신청하며 더 이상 내 일기를 작성하거나\n투표에 참여할 수 없음을 확인했습니다.", font: .mediumTitle15, lineSpacing: 12, alinment: .left, color: .countBlue) + + return label + }() + + private let signOutTextView: UITextView = { + let textView = UITextView() + textView.font = .regularTitle15 + textView.layer.borderWidth = 1 + textView.layer.borderColor = UIColor.slate100.cgColor + textView.layer.cornerRadius = 8 + textView.text = "탈퇴를 신청하며 더 이상 내 일기를 작성하거나 투표에 참여할 수 없음을 확인했습니다." + textView.textColor = .textDisabled + + return textView + }() + + private let signOutButton = MainButton(title: "탈퇴하기") + + required init(store: MyPageStore) { + self.store = store + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + + configureBar() + configureGuideLabel() + configureConfirmLabel() + configureButton() + + bind() + } + + private func setUpBind() { + store.$state + .receive(on: DispatchQueue.main) + .sink { [weak self] newState in + self?.render(newState) + } + .store(in: &cancellables) + } + + private func render(_ state: MyPageReducer.State) { + if state.isSignOutSuccess == .failure { + showToast(icon: .colorXmark, message: "탈퇴에 실패했어요.") + } + } + + private func bind() { + backButton.publisher(for: .touchUpInside) + .sink { [weak self] in + self?.store.dispatch(.popPage) + } + .store(in: &cancellables) + + signOutButton.publisher(for: .touchUpInside) + .sink { [weak self] in + self?.store.dispatch(.signOut) + } + .store(in: &cancellables) + } + + private func configureBar() { + view.addSubview(titleLabel) + view.addSubview(backButton) + + backButton.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide) + make.leading.equalToSuperview().inset(8) + } + + titleLabel.snp.makeConstraints { make in + make.leading.equalTo(backButton.snp.trailing).inset(8) + make.centerY.equalTo(backButton.snp.centerY) + } + } + + private func configureGuideLabel() { + view.addSubview(separatorLine) + view.addSubview(guideTitleLabel) + view.addSubview(guideLabel) + + separatorLine.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.height.equalTo(0.5) + make.top.equalTo(titleLabel.snp.bottom).offset(16) + } + + guideTitleLabel.snp.makeConstraints { make in + make.top.equalTo(separatorLine.snp.bottom).offset(16) + make.leading.equalToSuperview().inset(20) + } + + guideLabel.snp.makeConstraints { make in + make.top.equalTo(guideTitleLabel.snp.bottom).offset(16) + make.leading.equalTo(guideTitleLabel) + } + } + + private func configureConfirmLabel() { + view.addSubview(separatorSecondLine) + view.addSubview(textTitleLabel) + view.addSubview(confirmTextLabel) + view.addSubview(signOutTextView) + + signOutTextView.delegate = self + + separatorSecondLine.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.height.equalTo(0.5) + make.top.equalTo(guideLabel.snp.bottom).offset(16) + } + + textTitleLabel.snp.makeConstraints { make in + make.top.equalTo(separatorSecondLine.snp.bottom).offset(16) + make.leading.equalToSuperview().inset(20) + } + + confirmTextLabel.snp.makeConstraints { make in + make.top.equalTo(textTitleLabel.snp.bottom).offset(16) + make.leading.equalTo(guideTitleLabel) + } + + signOutTextView.snp.makeConstraints { make in + make.top.equalTo(confirmTextLabel.snp.bottom).offset(16) + make.leading.equalTo(confirmTextLabel) + make.trailing.equalToSuperview().inset(20) + make.height.equalTo(60) + } + } + + private func configureButton() { + view.addSubview(signOutButton) + + signOutButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(20) + make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-20) + make.height.equalTo(50) + } + } +} + +extension SignOutConfirmViewController: UITextViewDelegate { + func textViewDidBeginEditing(_ textView: UITextView) { + textView.text = nil + textView.textColor = .slate800 + } +} + +//#Preview { +// let vc = SignOutConfirmViewController(store: MyPageStore(reducer: MyPageReducer())) +// +// return vc +//} diff --git a/echog/echog/Presentation/MyPage/SignOutReasonViewController.swift b/echog/echog/Presentation/MyPage/SignOutReasonViewController.swift new file mode 100644 index 0000000..d436451 --- /dev/null +++ b/echog/echog/Presentation/MyPage/SignOutReasonViewController.swift @@ -0,0 +1,183 @@ +// +// SignOutReasonViewController.swift +// echog +// +// Created by minsong kim on 2/24/25. +// + +import Combine +import UIKit +import SnapKit + +class SignOutReasonViewController: UIViewController { + var store: MyPageStore + private var cancellables = Set() + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .semiboldLargetitle17 + label.textColor = .black + label.text = "회원탈퇴" + + return label + }() + + private let backButton: UIButton = { + var configuration = UIButton.Configuration.plain() + configuration.image = UIImage(systemName: "arrow.backward") + configuration.baseForegroundColor = .slate800 + + let button = UIButton(configuration: configuration) + + return button + }() + + private let tableView: UITableView = { + let table = UITableView() + table.backgroundColor = .clear + table.separatorStyle = .none + + return table + }() + + private let nextButton = MainButton(title: "다음") + + required init(store: MyPageStore) { + self.store = store + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + + configureBar() + configureTableView() + configureButton() + + bind() + } + + private func bind() { + backButton.publisher(for: .touchUpInside) + .sink { [weak self] in + self?.store.dispatch(.popPage) + } + .store(in: &cancellables) + + nextButton.publisher(for: .touchUpInside) + .sink { [weak self] in + self?.store.dispatch(.goToNextSignOutPage) + } + .store(in: &cancellables) + } + + private func configureBar() { + view.addSubview(titleLabel) + view.addSubview(backButton) + + backButton.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide) + make.leading.equalToSuperview().inset(8) + } + + titleLabel.snp.makeConstraints { make in + make.leading.equalTo(backButton.snp.trailing).inset(8) + make.centerY.equalTo(backButton.snp.centerY) + } + } + + private func configureTableView() { + tableView.dataSource = self + tableView.delegate = self + tableView.register(SignOutReasonCell.self, forCellReuseIdentifier: SignOutReasonCell.identifier) + tableView.separatorStyle = .singleLine + tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + tableView.separatorColor = .slate100 + tableView.allowsMultipleSelection = false + + view.addSubview(tableView) + + tableView.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(16) + make.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview() + } + } + + private func configureButton() { + view.addSubview(nextButton) + + nextButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(20) + make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).offset(-20) + make.height.equalTo(50) + } + } +} + +extension SignOutReasonViewController: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + SignOutReason.allCases.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: SignOutReasonCell.identifier, for: indexPath) as? SignOutReasonCell else { + return UITableViewCell() + } + + if indexPath.item == SignOutReason.allCases.count - 1 { + cell.configureCells(title: SignOutReason.allCases[indexPath.item].title, isTextView: true) + cell.reasonTextView.delegate = self + } else { + cell.configureCells(title: SignOutReason.allCases[indexPath.item].title) + } + + return cell + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let line = UIView(frame: CGRect(x: 0, y:0, width: tableView.frame.width, height: 0.5)) + line.backgroundColor = .slate100 + + return line + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + 0.5 + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if indexPath.item == SignOutReason.allCases.count - 1 { + 172 + } else { + 56 + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let cell = tableView.cellForRow(at: indexPath) as? SignOutReasonCell else { + return + } + + cell.selectedCell() + } +} + +extension SignOutReasonViewController: UITextViewDelegate { + func textViewDidBeginEditing(_ textView: UITextView) { + textView.text = nil + textView.textColor = .slate800 + } +} + +#Preview { + let vc = SignOutReasonViewController(store: MyPageStore(reducer: MyPageReducer())) + + return vc +} diff --git a/echog/echog/Presentation/MyPage/TermsCheckViewController.swift b/echog/echog/Presentation/MyPage/TermsCheckViewController.swift new file mode 100644 index 0000000..6f4c913 --- /dev/null +++ b/echog/echog/Presentation/MyPage/TermsCheckViewController.swift @@ -0,0 +1,112 @@ +// +// TermsCheckViewController.swift +// echog +// +// Created by minsong kim on 2/26/25. +// + +import Combine +import UIKit +import WebKit + +class TermsCheckViewController: UIViewController { + var store: MyPageStore + private var cancellables = Set() + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .semiboldLargetitle17 + label.textColor = .black + label.text = "개인정보 처리방침" + + return label + }() + + private let backButton: UIButton = { + var configuration = UIButton.Configuration.plain() + configuration.image = UIImage(systemName: "arrow.backward") + configuration.baseForegroundColor = .slate800 + + let button = UIButton(configuration: configuration) + + return button + }() + + private let webView: WKWebView = { + let configuration = WKWebViewConfiguration() + let view = WKWebView(frame: .zero, configuration: configuration) + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + required init(store: MyPageStore) { + self.store = store + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + + configureBar() + configureView() + loadWebView() + + bind() + } + + private func bind() { + backButton.publisher(for: .touchUpInside) + .sink { [weak self] in + self?.store.dispatch(.popPage) + } + .store(in: &cancellables) + } + + private func configureBar() { + view.addSubview(titleLabel) + view.addSubview(backButton) + + backButton.snp.makeConstraints { make in + make.top.equalTo(view.safeAreaLayoutGuide) + make.leading.equalToSuperview().inset(8) + } + + titleLabel.snp.makeConstraints { make in + make.leading.equalTo(backButton.snp.trailing).inset(8) + make.centerY.equalTo(backButton.snp.centerY) + } + } + + private func configureView() { + view.addSubview(webView) + + webView.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(8) + make.leading.trailing.bottom.equalToSuperview() + } + } + + private func loadWebView() { + //웹 링크 띄우기 + let link = "https://marchens.notion.site/echog-terms?pvs=4" + guard let url = URL(string: link) else { + return + } + let request = URLRequest(url: url) + + webView.load(request) + } +} + +//#Preview { +// let vc = TermsCheckViewController() +// +// return vc +//} diff --git a/echog/echog/Presentation/OnBoarding/OnBoardingReducer.swift b/echog/echog/Presentation/OnBoarding/OnBoardingReducer.swift index 5a53f2b..11736cb 100644 --- a/echog/echog/Presentation/OnBoarding/OnBoardingReducer.swift +++ b/echog/echog/Presentation/OnBoarding/OnBoardingReducer.swift @@ -29,7 +29,7 @@ struct OnBoardingReducer: ReducerProtocol { weak var delegate: OnBoardingCoordinator? - let initialState: State = State(page: 0, image: UIImage(resource: .logo), title: "", isStartButton: false) + let initialState: State = State(page: 1, image: UIImage(resource: .diary), title: "나의 일기를 작성하고", isStartButton: false) func mutate(action: Intent) -> AnyPublisher? { switch action {