diff --git a/Projects/Core/NetworkProtocol/Sources/Request.swift b/Projects/Core/NetworkProtocol/Sources/Request.swift index e86eb45..4ae1e2b 100644 --- a/Projects/Core/NetworkProtocol/Sources/Request.swift +++ b/Projects/Core/NetworkProtocol/Sources/Request.swift @@ -13,6 +13,7 @@ public enum HTTPMethod: String, Encodable { case get = "GET" case post = "POST" case put = "PUT" + case delete = "DELETE" } /// Query Params Array diff --git a/Projects/DataSource/Sources/User/Delete/DeleteUserRequest.swift b/Projects/DataSource/Sources/User/Delete/DeleteUserRequest.swift new file mode 100644 index 0000000..11c6516 --- /dev/null +++ b/Projects/DataSource/Sources/User/Delete/DeleteUserRequest.swift @@ -0,0 +1,24 @@ +// +// DeleteUserRequest.swift +// DataSource +// +// Created by 박건우 on 2022/08/30. +// Copyright © 2022 kr.mash-up. All rights reserved. +// + +import NetworkProtocol + +public struct DeleteUserRequest: Request { + + public typealias Body = EmptyRequestBody + + public typealias Output = DeleteUserResponse + + public var endpoint: String = "/api/v1/users" + + public var method: HTTPMethod = .delete + + public init(userId: Int) { + endpoint = endpoint + "/\(userId)" + } +} diff --git a/Projects/DataSource/Sources/User/Delete/DeleteUserResponse.swift b/Projects/DataSource/Sources/User/Delete/DeleteUserResponse.swift new file mode 100644 index 0000000..8ec5f2a --- /dev/null +++ b/Projects/DataSource/Sources/User/Delete/DeleteUserResponse.swift @@ -0,0 +1,13 @@ +// +// DeleteUserResponse.swift +// DataSource +// +// Created by 박건우 on 2022/08/30. +// Copyright © 2022 kr.mash-up. All rights reserved. +// + +import NetworkProtocol + +public typealias DeleteUserResponse = BaseResponse + +public struct DeleteUserResponseBody: Response {} diff --git a/Projects/DataSource/Sources/User/UserDataSource.swift b/Projects/DataSource/Sources/User/UserDataSource.swift index 7326ef1..55d35bd 100644 --- a/Projects/DataSource/Sources/User/UserDataSource.swift +++ b/Projects/DataSource/Sources/User/UserDataSource.swift @@ -8,6 +8,7 @@ import Foundation import NetworkProtocol +import Network public enum LocalStorageKey: String { case token @@ -20,11 +21,16 @@ public protocol UserLocalDataSoureceProtocol { func read(key: LocalStorageKey) -> LocalUser? func save(_ data: T, key: LocalStorageKey) func removeAll(key: LocalStorageKey) + func deleteUser(request: DeleteUserRequest) async throws -> DeleteUserResponse } public final class UserLocalDataSourece: UserLocalDataSoureceProtocol { - public init() {} + private let network: NetworkProtocol + + public init(network: NetworkProtocol = Network(session: .shared)) { + self.network = network + } public func saveToken(_ token: String, key: LocalStorageKey) { UserDefaults.standard.set(token, forKey: key.rawValue) @@ -57,4 +63,8 @@ public final class UserLocalDataSourece: UserLocalDataSoureceProtocol { public func removeAll(key: LocalStorageKey) { UserDefaults.standard.removeObject(forKey: key.rawValue) } + + public func deleteUser(request: DeleteUserRequest) async throws -> DeleteUserResponse { + return try await network.send(request) + } } diff --git a/Projects/Features/GuestDetail/Targets/Router/GuestDetailRouter.swift b/Projects/Features/GuestDetail/Targets/Router/GuestDetailRouter.swift index 66d8c3d..8287f94 100644 --- a/Projects/Features/GuestDetail/Targets/Router/GuestDetailRouter.swift +++ b/Projects/Features/GuestDetail/Targets/Router/GuestDetailRouter.swift @@ -41,4 +41,8 @@ public final class GuestDetailRouter: GuestDetailRoutingLogic, GuestDetailDataPa public func routeToMeetingListScene() { viewController?.navigationController?.popToRootViewController(animated: true) } + + public func routeToLoginScene() { + NotificationCenter.default.post(name: Notification.Name("LogoutNotification"), object: nil) + } } diff --git a/Projects/Features/GuestDetail/Targets/RoutingProtocol/GuestDetailRoutingLogic.swift b/Projects/Features/GuestDetail/Targets/RoutingProtocol/GuestDetailRoutingLogic.swift index 105d798..05d6969 100644 --- a/Projects/Features/GuestDetail/Targets/RoutingProtocol/GuestDetailRoutingLogic.swift +++ b/Projects/Features/GuestDetail/Targets/RoutingProtocol/GuestDetailRoutingLogic.swift @@ -15,4 +15,5 @@ public protocol GuestDetailRoutingLogic { func routeToLikeRequestScene(targetId id: Int) func routeToMeetingListScene() + func routeToLoginScene() } diff --git a/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailInteractor.swift b/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailInteractor.swift index a579d50..814c675 100644 --- a/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailInteractor.swift +++ b/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailInteractor.swift @@ -15,6 +15,7 @@ import Models protocol GuestDetailBusinessLogic { func fetchGuest() + func withdraw() } final class GuestDetailInteractor: GuestDetailBusinessLogic, GuestDetailDataStore { @@ -46,6 +47,20 @@ final class GuestDetailInteractor: GuestDetailBusinessLogic, GuestDetailDataStor presenter?.presentGuest(response: .init(guest: selectedGuest)) } + + func withdraw() { + guard let worker = worker else { + return + } + Task { + do { + try await worker.withdraw() + presenter?.presentWithdrawResult() + } catch { + + } + } + } private func fetchChangeMeetingButton() { guard let worker = worker else { diff --git a/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailPresenter.swift b/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailPresenter.swift index ab4d8cf..029414d 100644 --- a/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailPresenter.swift +++ b/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailPresenter.swift @@ -15,6 +15,7 @@ import UIKit protocol GuestDetailPresentationLogic { func presentGuest(response: GuestDetail.GetGuest.Response) func presentMeetingChangeButton(response: GuestDetail.GetMeetingCount.Response) + func presentWithdrawResult() } final class GuestDetailPresenter: GuestDetailPresentationLogic { @@ -50,4 +51,8 @@ final class GuestDetailPresenter: GuestDetailPresentationLogic { let meetings = response.meetings self.viewController?.displayChangeMeetingButton(viewModel: .init(isHidden: meetings.count <= 1)) } + + func presentWithdrawResult() { + self.viewController?.displayWithdrawResult() + } } diff --git a/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailViewController.swift b/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailViewController.swift index e87fedf..f387d13 100644 --- a/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailViewController.swift +++ b/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailViewController.swift @@ -21,6 +21,7 @@ import SnapKit protocol GuestDetailDisplayLogic: AnyObject { func displayGuest(viewModel: GuestDetail.GetGuest.ViewModel) func displayChangeMeetingButton(viewModel: GuestDetail.GetMeetingCount.ViewModel) + func displayWithdrawResult() } struct GuestDetailViewModel { @@ -40,15 +41,6 @@ public final class GuestDetailViewController: UIViewController, GuestDetailDispl // MARK: Object lifecycle - public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - setup() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } // MARK: Setup @@ -234,6 +226,17 @@ public final class GuestDetailViewController: UIViewController, GuestDetailDispl v.title = "WHO I AM" return v }() + + private lazy var withdrawLabel: UILabel = { + let lb = UILabel() + lb.text = "탈퇴하기" + lb.font = .body2() + lb.textColor = Pallete.Light.grey300.color + lb.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapWithdraw))) + lb.isUserInteractionEnabled = true + lb.isHidden = profileDetailType == .guestProfile + return lb + }() var viewModel: GuestDetailViewModel? { didSet { @@ -271,15 +274,20 @@ public final class GuestDetailViewController: UIViewController, GuestDetailDispl case guestProfile } - private var profileDetailType: ProfileDetailType? + private var profileDetailType: ProfileDetailType // MARK: View lifecycle public init(profileDetailType: ProfileDetailType = .guestProfile) { - self.init() self.profileDetailType = profileDetailType + super.init(nibName: nil, bundle: nil) + setup() } - + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + public override func viewDidLoad() { super.viewDidLoad() setUI() @@ -298,7 +306,7 @@ public final class GuestDetailViewController: UIViewController, GuestDetailDispl self.contentView.addSubviews(self.topHeaderStackView) self.contentView.addSubviews(self.addressStackView, self.careerStackView) self.contentView.addSubviews(self.collectionView, self.pageControl) - self.contentView.addSubviews(self.keywordContainerView, self.whoIAmContainerView) + self.contentView.addSubviews(self.keywordContainerView, self.whoIAmContainerView, self.withdrawLabel) self.scrollView.addSubview(self.contentView) self.navigationView.addSubview(self.backButton) self.navigationView.addSubview(self.changeMeetingButton) @@ -371,7 +379,11 @@ public final class GuestDetailViewController: UIViewController, GuestDetailDispl self.whoIAmContainerView.snp.makeConstraints { make in make.top.equalTo(self.keywordContainerView.snp.bottom) make.leading.trailing.equalToSuperview() - make.bottom.equalToSuperview().inset(20) + } + self.withdrawLabel.snp.makeConstraints { make in + make.top.equalTo(self.whoIAmContainerView.snp.bottom) + make.leading.equalToSuperview().inset(40) + make.bottom.equalToSuperview().inset(90) } self.collectionView.snp.makeConstraints { make in make.top.equalTo(careerStackView.snp.bottom).offset(50) @@ -404,6 +416,10 @@ public final class GuestDetailViewController: UIViewController, GuestDetailDispl // } } + + func displayWithdrawResult() { + self.router?.routeToLoginScene() + } } // MARK: Button Tap @@ -462,6 +478,20 @@ extension GuestDetailViewController { func changeMeetingButtonDidTap() { router?.routeToMeetingListScene() } + + @objc func didTapWithdraw() { + let alertViewController = UIAlertController( + title: "탈퇴하기", + message: "현재 계정을 탈퇴합니다.", + preferredStyle: .alert + ) + alertViewController.addAction(UIAlertAction(title: "취소", style: .cancel, handler: nil)) + alertViewController.addAction(UIAlertAction(title: "탈퇴할래요", style: .default, handler: { [weak self] _ in + self?.interactor?.withdraw() + })) + + self.present(alertViewController, animated: true) + } } // MARK: UICollectionViewDataSource diff --git a/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailWorker.swift b/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailWorker.swift index 4e63cda..6827bf8 100644 --- a/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailWorker.swift +++ b/Projects/Features/GuestDetail/Targets/Scene/Sources/GuestDetailWorker.swift @@ -17,6 +17,7 @@ import DataSource protocol GuestDetailWorkerProtocol { func fetchUser() -> User func fetchMeetings() async throws -> [Meeting] + func withdraw() async throws } final class GuestDetailWorker: GuestDetailWorkerProtocol { @@ -33,9 +34,10 @@ final class GuestDetailWorker: GuestDetailWorkerProtocol { } func fetchUser() -> User { - userLocalDataSourece.read(key: .localUser).map { [weak self] localUser in - return self!.convertToUser(localUser) - }! + let defaultUser: User = .init(id: 0, name: "", gender: .male, career: "", birth: Date(), age: 0, address: "", pictures: [], answers: [], keyword: []) + return userLocalDataSourece.read(key: .localUser).map { [weak self] localUser in + return self?.convertToUser(localUser) ?? defaultUser + } ?? defaultUser } func convertToUser(_ user: LocalUser) -> User { @@ -53,6 +55,12 @@ final class GuestDetailWorker: GuestDetailWorkerProtocol { return [] } } + + func withdraw() async throws { + _ = try await userLocalDataSourece.deleteUser(request: .init(userId: fetchUser().id)) + userLocalDataSourece.removeAll(key: .token) + userLocalDataSourece.removeAll(key: .localUser) + } private func convertToMeeting(_ dto: GetMeetingListDTO) -> Meeting { let dateFormatter = DateFormatter() diff --git a/Projects/Features/GuestList/Targets/Scene/Sources/GuestListViewController.swift b/Projects/Features/GuestList/Targets/Scene/Sources/GuestListViewController.swift index 2f7ab16..0ca7796 100644 --- a/Projects/Features/GuestList/Targets/Scene/Sources/GuestListViewController.swift +++ b/Projects/Features/GuestList/Targets/Scene/Sources/GuestListViewController.swift @@ -67,7 +67,7 @@ public class GuestListViewController: UIViewController, GuestListDisplayLogic { let v = UIView() return v }() - + lazy var likeListButton: UIImageView = { let v = UIImageView() v.image = .create(.ic_heart) @@ -102,7 +102,7 @@ public class GuestListViewController: UIViewController, GuestListDisplayLogic { lazy var guestSwipeableView: ZLSwipeableView = { let v = ZLSwipeableView() - v.shouldSwipeView = { _, _, _ in true } + v.onlySwipeTopCard = true return v }() @@ -215,7 +215,6 @@ public class GuestListViewController: UIViewController, GuestListDisplayLogic { self.view.addSubview(self.guestSwipeableView) self.view.addSubview(self.reportButton) self.view.addSubview(self.emptyView) -// self.view.addSubview(self.refreshGuestListButton) self.navigationView.addSubview(self.likeListButton) self.navigationView.addSubview(self.myInfoButton) diff --git a/Projects/Marryting/Sources/AppDelegate.swift b/Projects/Marryting/Sources/AppDelegate.swift index a26a633..6e9a936 100644 --- a/Projects/Marryting/Sources/AppDelegate.swift +++ b/Projects/Marryting/Sources/AppDelegate.swift @@ -24,6 +24,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ) -> Bool { KakaoSDK.initSDK(appKey: "bd614625345b00170f51b167c97e96e9") Font.registerFonts() + registerNotification() window = UIWindow(frame: UIScreen.main.bounds) let navigationController = UINavigationController() navigationController.isNavigationBarHidden = true @@ -40,4 +41,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return false } + + func registerNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(logout(_:)), name: NSNotification.Name("LogoutNotification"), object: nil) + } + + @objc func logout(_ notification: Notification) { + DispatchQueue.main.async { + let loginViewController = LoginViewController() + let navigationController = UINavigationController(rootViewController: loginViewController) + navigationController.isNavigationBarHidden = true + self.window?.rootViewController = navigationController + } + } }