From 19eac8eecd1773327edf392d423ccef5cabc9d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=ED=98=84=EA=B7=9C?= <48830320+leemhyungyu@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:52:17 +0900 Subject: [PATCH] =?UTF-8?q?[Feature/#328]=20=EC=9E=90=EA=B8=B0=20=EC=86=8C?= =?UTF-8?q?=EA=B0=9C=20=EC=9B=B9=EB=B7=B0=20=EC=97=B0=EA=B2=B0=20(#348)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BaseWebViewType 자기소개 웹 뷰 추가 * feat: 자기소개 및 프로필 사진 업로드 완료 브릿지 추가 * feat: 자기소개 웹 뷰 연결 * feat: 자기소개 작성 안한 사용자 도착한 보틀 볼 수 있도록 수정 --- .../Sources/BottleWebViewAction.swift | 9 ++ .../Interface/Sources/BaseWebViewType.swift | 6 + .../IntroductionSetupFeature.swift | 119 +++--------------- .../IntroductionSetupView.swift | 101 ++++----------- .../Sources/Root/SandBeachRootFeature.swift | 7 -- .../SandBeach/SandBeachFeatureInterface.swift | 55 ++++---- 6 files changed, 76 insertions(+), 221 deletions(-) diff --git a/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift b/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift index eb555528..33de7944 100644 --- a/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift +++ b/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift @@ -50,6 +50,10 @@ public enum BottleWebViewAction: Equatable { /// 프로필 사진 수정 완료 case profileImageDidChanged + // MARK: - Introduction Setup + /// 자기소개 & 프로필 사진 등록 완료 + case introductionDidCompleted + public init?( type: String, message: String? = nil, @@ -146,6 +150,11 @@ public enum BottleWebViewAction: Equatable { case "onProfileImageEditComplete": self = .profileImageDidChanged + + // MARK: - Introduction Setup + + case "onIntroductionComplete": + self = .introductionDidCompleted default: return nil diff --git a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift index bd98a38c..366169b0 100644 --- a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift +++ b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift @@ -28,6 +28,7 @@ public enum BottleWebViewType { case bottleArrival case editProfile case goodFeeling + case introductionSetup case openURL(url: String) var path: String { @@ -44,6 +45,8 @@ public enum BottleWebViewType { return "profile/edit" case .goodFeeling: return "bottles/sents" + case .introductionSetup: + return "/intro/create" case .openURL: return "" } @@ -69,6 +72,9 @@ public enum BottleWebViewType { case .goodFeeling: return makeUrlWithToken(path) + case .introductionSetup: + return makeUrlWithToken(path) + case let .openURL(url): return URL(string: url)! } diff --git a/Projects/Feature/ProfileSetup/Interface/Sources/IntroductionSetup/IntroductionSetupFeature.swift b/Projects/Feature/ProfileSetup/Interface/Sources/IntroductionSetup/IntroductionSetupFeature.swift index 380f3bb9..d78eda3b 100644 --- a/Projects/Feature/ProfileSetup/Interface/Sources/IntroductionSetup/IntroductionSetupFeature.swift +++ b/Projects/Feature/ProfileSetup/Interface/Sources/IntroductionSetup/IntroductionSetupFeature.swift @@ -9,9 +9,12 @@ import Foundation import DomainProfileInterface import DomainProfile -import SharedDesignSystem + +import CoreToastInterface import CoreLoggerInterface +import CoreToastInterface + import ComposableArchitecture @Reducer @@ -24,52 +27,16 @@ public struct IntroductionSetupFeature { @ObservableState public struct State: Equatable { - public var introductionText: String - public var textFieldState: TextFieldState - public var keywordItem: [ClipItem] - public var isNextButtonDisable: Bool - public var maxLength: Int - public var isLoading: Bool - - public init( - introductionText: String = "", - textFieldState: TextFieldState = .enabled, - keywordItem: [ClipItem] = [], - isNextButtonDisable: Bool = true, - maxLength: Int = 50, - isLoading: Bool = false - ) { - self.introductionText = introductionText - self.textFieldState = textFieldState - self.keywordItem = keywordItem - self.isNextButtonDisable = isNextButtonDisable - self.maxLength = maxLength - self.isLoading = isLoading - } + public init() {} } - public enum Action: BindableAction { - // View Life Cycle - case onLoad - - // User Action - case texFieldDidFocused(isFocused: Bool) - case profileSelectDidFatched(ProfileSelect) - case nextButtonDidTapped - case onTapGesture - case backButtonDidTapped - - // Delegate - case delegate(Delegate) - case binding(BindingAction) - - public enum Delegate { - case nextButtonDidTapped(introductionText: String) - } + public enum Action { + // Web Bridge + case closeWebView + case presentToastDidRequired(message: String) } public var body: some ReducerOf { - BindingReducer() reducer } } @@ -77,75 +44,19 @@ public struct IntroductionSetupFeature { extension IntroductionSetupFeature { public init() { @Dependency(\.dismiss) var dismiss + @Dependency(\.toastClient) var toastClient + let reducer = Reduce { state, action in @Dependency(\.profileClient) var profileClient switch action { - case .onLoad: - state.isLoading = true - return .run { send in - let profileSelect = try await profileClient.fetchProfileSelect() - await send(.profileSelectDidFatched(profileSelect)) - } - case let .texFieldDidFocused(isFocused): - state.textFieldState = isFocused ? .focused : .active - return .none - case .profileSelectDidFatched(let profileSelect): - // TODO: 코드 개선 - // TODO: 없으면 ClipItem nil로 - state.keywordItem = [ - ClipItem( - title: "내 키워드를 참고해보세요", - list: [profileSelect.job, profileSelect.mbti, "\(profileSelect.region.city) \(profileSelect.region.state)", "\(profileSelect.height)", profileSelect.smoke, profileSelect.alcohol] - ), - - ClipItem( - title: "나의 성격은", - list: profileSelect.keyword - ), - - ClipItem( - title: "내가 푹 빠진 취미는", - list: (profileSelect.interset.culture ?? []) - + (profileSelect.interset.entertainment ?? []) - + (profileSelect.interset.sports ?? []) - + (profileSelect.interset.etc ?? []) - ) - ] - state.isLoading = false - return .none - case .binding(\.introductionText): - if state.introductionText.count >= state.maxLength { - state.textFieldState = .focused - state.isNextButtonDisable = false - } else { - state.textFieldState = .error - state.isNextButtonDisable = true - } - return .none - - case .nextButtonDidTapped: - return .run { [introductionText = state.introductionText] send in - Log.debug("nextButtonDidTapped") - await send(.delegate(.nextButtonDidTapped(introductionText: introductionText))) - } - case .onTapGesture: - if state.introductionText.count == 0 { - state.textFieldState = .enabled - } else { - state.textFieldState = .active - } - return .none - - case .backButtonDidTapped: + case .closeWebView: return .run { _ in - await dismiss() + await dismiss() } - case .binding(_): - return .none - - case .delegate: + case let .presentToastDidRequired(message): + toastClient.presentToast(message: message) return .none } } diff --git a/Projects/Feature/ProfileSetup/Interface/Sources/IntroductionSetup/IntroductionSetupView.swift b/Projects/Feature/ProfileSetup/Interface/Sources/IntroductionSetup/IntroductionSetupView.swift index 272f7671..78600e75 100644 --- a/Projects/Feature/ProfileSetup/Interface/Sources/IntroductionSetup/IntroductionSetupView.swift +++ b/Projects/Feature/ProfileSetup/Interface/Sources/IntroductionSetup/IntroductionSetupView.swift @@ -7,9 +7,12 @@ import SwiftUI -import SharedDesignSystem +import FeatureBaseWebViewInterface + import CoreLoggerInterface +import SharedDesignSystem + import ComposableArchitecture public struct IntroductionSetupView: View { @@ -22,88 +25,28 @@ public struct IntroductionSetupView: View { public var body: some View { WithPerceptionTracking { - if store.isLoading { - LoadingIndicator() - } else { - ScrollView { - introductionTitle - introductionTextField - keywordList - nextButton - }.onTapGesture { - store.send(.onTapGesture) - }.setNavigationBar { - makeNaivgationleftButton { - store.send(.backButtonDidTapped) - } + BaseWebView(type: .introductionSetup) { action in + switch action { + case .webViewLoadingDidCompleted: + break + + case .closeWebView: + store.send(.closeWebView) + + case .introductionDidCompleted: + store.send(.closeWebView) + + case let .showTaost(message): + store.send(.presentToastDidRequired(message: message)) + + default: + Log.assertion(message: "not handled action: \(action)") } } } - .onLoad { - store.send(.onLoad) - } .scrollIndicators(.hidden) - .ignoresSafeArea(.all, edges: .bottom) + .ignoresSafeArea(.all, edges: [.bottom, .top]) .toolbar(.hidden, for: .bottomBar) - } -} - -private extension IntroductionSetupView { - var introductionTitle: some View { - TitleView( - pageInfo: PageInfo(nowPage: 1, totalCount: 2), - title: "보틀에 담을\n소개를 작성해 주세요", - caption: "수정이 어려우니 신중하게 작성해주세요" - ) - .padding(.top, .xl) - .padding(.bottom, 32) - .padding(.horizontal, .md) - } - - var introductionTextField: some View { - LinesTextField( - textFieldType: .introduction, - textFieldState: $store.textFieldState, - text: $store.introductionText, - placeHolder: "호기심이 많고 새로운 경험을 즐깁니다. 주말엔 책을 읽거나 맛집을 찾아다니며 여유를 즐기고, 친구들과 소소한 모임으로 에너지를 충전해요.", - errorMessage: "최소 \(store.maxLength)글자 이상 작성해주세요", - textLimit: 300 - ) - .focused($isTextFieldFocused) - .padding(.horizontal, .md) - .padding(.bottom, .sm) - .onChange(of: isTextFieldFocused) { isFocused in - store.send(.texFieldDidFocused(isFocused: isFocused)) - } - .onChange(of: store.textFieldState) { textFieldState in - Log.error(textFieldState) - isTextFieldFocused = textFieldState == .active || textFieldState == .enabled ? false : true - } - } - - var keywordList: some View { - ClipListContainerView( - clipItemList: store.keywordItem - ) - .padding(.horizontal, .md) - .padding(.bottom, 47) - } - - var nextButton: some View { - SolidButton( - title: "다음", - sizeType: .full, - buttonType: .throttle, - action: { store.send(.nextButtonDidTapped) } - ) - .padding(.horizontal, .md) - .padding(.bottom, .xl) - .disabled(store.isNextButtonDisable) - } -} - -extension View { - func endTextEditing() { - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + .toolbar(.hidden, for: .navigationBar) } } diff --git a/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift b/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift index a17bbf52..3008787f 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift @@ -100,13 +100,6 @@ extension SandBeachRootFeature { switch action { - // IntrodctionSetup Delegate - case let .path(.element(id: _, action: - .IntroductionSetup(.delegate(.nextButtonDidTapped(introductionText))))): - state.introduction = introductionText - state.path.append(.ProfileImageUpload(ProfileImageUploadFeature.State())) - return .none - // ProfileImageUpload Delegate case let .path(.element(id: _, action: .ProfileImageUpload(.delegate(.doneButtonDidTapped(selectedImageData))))): diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift index eadd391a..f7f4e9a4 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift @@ -100,43 +100,36 @@ extension SandBeachFeature { return .run { send in async let _ = authClient.checkUpdateVersion() - async let isExsit = try await profileClient.checkExistIntroduction() - // 자기소개 없는 상태 - if try await !isExsit { + let userProfileStatus = try await profileClient.fetchUserProfileSelect() + let userBottleInfo = try await bottleClient.fetchUserBottleInfo() + let newBottlesCount = userBottleInfo.randomBottleCount + let bottlesStorageList = try await bottleClient.fetchBottleStorageList() + let activeBottlesCount = bottlesStorageList.pingPongBottles + .filter { $0.lastStatus != .conversationStopped && $0.lastStatus != .contactSharedByMeOnly }.count + let nextBottleLeftHours = userBottleInfo.nextBottlLeftHours + + if newBottlesCount > 0 { await send(.userStateFetchCompleted( - userState: .noIntroduction, - isDisableButton: true)) + userState: .hasNewBottle(bottleCount: newBottlesCount), + isDisableButton: false)) return } - let userBottleInfo = try await bottleClient.fetchUserBottleInfo() - let newBottlesCount = userBottleInfo.randomBottleCount - // 새로 도착한 보틀이 있는 상태 + if activeBottlesCount > 0 { + await send(.userStateFetchCompleted( + userState: .hasActiveBottle(bottleCount: activeBottlesCount), + isDisableButton: false)) + return + } - if newBottlesCount > 0 { + if userProfileStatus == .empty || userProfileStatus == .doneIntroduction { await send(.userStateFetchCompleted( - userState: .hasNewBottle(bottleCount: newBottlesCount), - isDisableButton: false) - ) - } else { - let bottlesStorageList = try await bottleClient.fetchBottleStorageList() - let activeBottlesCount = bottlesStorageList.pingPongBottles - .filter { $0.lastStatus != .conversationStopped && $0.lastStatus != .contactSharedByMeOnly }.count - - // 자기소개만 작성한 상태 - if activeBottlesCount <= 0 { - // TODO: time 설정 - let nextBottleLeftHours = userBottleInfo.nextBottlLeftHours - await send(.userStateFetchCompleted( - userState: .noBottle(time: nextBottleLeftHours ?? 0), - isDisableButton: false) - ) - } else { // 대화 중인 보틀이 있는 상태 - await send(.userStateFetchCompleted( - userState: .hasActiveBottle(bottleCount: activeBottlesCount), - isDisableButton: false) - ) - } + userState: .noIntroduction, + isDisableButton: true)) + } else if userProfileStatus == .doneProfileImage { + await send(.userStateFetchCompleted( + userState: .noBottle(time: nextBottleLeftHours ?? 0), + isDisableButton: false)) } } catch: { error, send in // TODO: 에러 핸들링