From 2b4169b44a286d18f9bd0de97ef17c822a4edcf3 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 14 Sep 2024 16:04:52 +0900 Subject: [PATCH 01/90] =?UTF-8?q?feat:=20BottleAlert=20Modifier=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Alert/BottleAlert.swift | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift diff --git a/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift new file mode 100644 index 00000000..b0753349 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift @@ -0,0 +1,74 @@ +// +// BottleAlert.swift +// SharedDesignSystem +// +// Created by 임현규 on 9/14/24. +// + +import SwiftUI +import ComposableArchitecture + +private struct BottleAlertView: View where A: View, M: View { + private let title: Text + private var isPresented: Binding + private let presenting: T? + private let actions: (T) -> A + private let message: (T) -> M + + public init( + _ title: Text, + isPresented: Binding, + presenting: T?, + @ViewBuilder actions: @escaping (T) -> A, + @ViewBuilder message: @escaping (T) -> M + ) { + self.title = title + self.isPresented = isPresented + self.presenting = presenting + self.actions = actions + self.message = message + } + + var body: some View { + EmptyView() + } +} + +extension View { + public func BottleAlert(_ item: Binding, Action>?>) -> some View { + + let store = item.wrappedValue + let alertState = store?.withState { $0 } + + return BottleAlertView( + (alertState?.title).map { Text($0).font(to: .wantedSans(.subTitle1)) } + ?? Text(verbatim: ""), + isPresented: item.isPresent(), + presenting: alertState, + actions: { alertState in + ForEach(alertState.buttons) { button in + Button(role: button.role.map(ButtonRole.init)) { + switch button.action.type { + case let .send(action): + if let action { + store?.send(action) + } + case let .animatedSend(action, animation): + if let action { + store?.send(action, animation: animation) + } + } + } label: { + Text(button.label) + .font(to: .wantedSans(.body)) + + } + } + }, + message: { + $0.message.map(Text.init) + .font(to: .wantedSans(.body)) + } + ) + } +} From aea352275af8a3e2994df93333e9120aa04df746 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 14 Sep 2024 18:21:23 +0900 Subject: [PATCH 02/90] =?UTF-8?q?feat:=20ButtonAppearanceType=20cancel=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Button/SolidButton/SolidButton.swift | 2 ++ .../Components/Button/SolidButton/SolidButtonStyle.swift | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButton.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButton.swift index 3720a565..b3fae586 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButton.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButton.swift @@ -95,6 +95,8 @@ extension SolidButton { EmptyView() case .generalSignIn: EmptyView() + case .cancel: + EmptyView() } } diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButtonStyle.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButtonStyle.swift index 3dd064e5..1b7268ce 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButtonStyle.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButtonStyle.swift @@ -12,6 +12,7 @@ public enum ButtonAppearanceType { case kakao case apple case generalSignIn + case cancel } struct SolidButtonStyle: ButtonStyle { @@ -91,9 +92,10 @@ private extension SolidButtonStyle { return ColorToken.container(.kakao).color case .apple: return ColorToken.container(.primary).color - case .generalSignIn: return Color.white + case .cancel: + return ColorToken.container(.disableSecondary).color } } @@ -115,6 +117,9 @@ private extension SolidButtonStyle { case .generalSignIn: return ColorToken.text(.primary).color + + case .cancel: + return ColorToken.text(.enablePrimary).color } } } From 9c1169db609cd4cf33231cb74b51be7e988749b0 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 14 Sep 2024 18:52:19 +0900 Subject: [PATCH 03/90] =?UTF-8?q?feat:=20BottleAlertView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Alert/BottleAlertView.swift | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift diff --git a/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift new file mode 100644 index 00000000..4fb99925 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift @@ -0,0 +1,84 @@ +// +// BottleAlertView.swift +// SharedDesignSystem +// +// Created by 임현규 on 9/14/24. +// + +import SwiftUI + +struct BottleAlertView: View where A: View, M: View { + private let title: Text + private var isPresented: Binding + private let presenting: T? + private let actions: (T) -> A + private let message: (T) -> M + + public init( + _ title: Text, + isPresented: Binding, + presenting: T?, + @ViewBuilder actions: @escaping (T) -> A, + @ViewBuilder message: @escaping (T) -> M + ) { + self.title = title + self.isPresented = isPresented + self.presenting = presenting + self.actions = actions + self.message = message + } + + var body: some View { + if isPresented.wrappedValue { + ZStack { + Color.black + .opacity(0.5) + .edgesIgnoringSafeArea(.all) + + VStack(spacing: 0) { + alertImage + title + .padding(.bottom, 4) + messageView + actionsView + } + .padding(.horizontal, .md) + .frame(maxWidth: 300) + .background(to: ColorToken.container(.primary)) + .cornerRadius(12) + } + } + } +} + +private extension BottleAlertView { + var alertImage: some View { + BottleImageView(type: .local(bottleImageSystem: .icom(.siren))) + .foregroundStyle(to: ColorToken.icon(.primary)) + .padding(.top, .lg) + .padding(.bottom, .xs) + } + + @ViewBuilder + var messageView: some View { + if let data = presenting { + message(data) + .multilineTextAlignment(.center) + .padding(.bottom, .sm) + + } else { + EmptyView() + } + } + + @ViewBuilder + var actionsView: some View { + if let data = presenting { + actions(data) + .padding(.top, .md) + .padding(.bottom, .md) + } else { + EmptyView() + } + } +} From 9c27729dd1b7d563839c76bc6419bf2a28bf40cf Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 14 Sep 2024 18:53:03 +0900 Subject: [PATCH 04/90] =?UTF-8?q?feat:=20BottleAlert=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=EC=86=8D=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Alert/BottleAlert.swift | 97 +++++++++---------- 1 file changed, 44 insertions(+), 53 deletions(-) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift index b0753349..d3efecfc 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift @@ -8,67 +8,58 @@ import SwiftUI import ComposableArchitecture -private struct BottleAlertView: View where A: View, M: View { - private let title: Text - private var isPresented: Binding - private let presenting: T? - private let actions: (T) -> A - private let message: (T) -> M - - public init( - _ title: Text, - isPresented: Binding, - presenting: T?, - @ViewBuilder actions: @escaping (T) -> A, - @ViewBuilder message: @escaping (T) -> M - ) { - self.title = title - self.isPresented = isPresented - self.presenting = presenting - self.actions = actions - self.message = message - } - - var body: some View { - EmptyView() - } -} - extension View { public func BottleAlert(_ item: Binding, Action>?>) -> some View { let store = item.wrappedValue let alertState = store?.withState { $0 } + let isPresented = Binding( + get: { item.wrappedValue != nil }, + set: { newValue in + if !newValue { + item.wrappedValue = nil + } + } + ) - return BottleAlertView( - (alertState?.title).map { Text($0).font(to: .wantedSans(.subTitle1)) } - ?? Text(verbatim: ""), - isPresented: item.isPresent(), - presenting: alertState, - actions: { alertState in - ForEach(alertState.buttons) { button in - Button(role: button.role.map(ButtonRole.init)) { - switch button.action.type { - case let .send(action): - if let action { - store?.send(action) - } - case let .animatedSend(action, animation): - if let action { - store?.send(action, animation: animation) - } + return ZStack { + self + BottleAlertView( + (alertState?.title).map { Text($0).font(to: .wantedSans(.subTitle1)) } + ?? Text(verbatim: ""), + isPresented: isPresented, + presenting: alertState, + actions: { alertState in + HStack(spacing: .sm) { + ForEach(alertState.buttons) { button in + Text(button.label) + .font(to: .wantedSans(.body)) + .asButton { + switch button.action.type { + case let .send(action): + if let action { + store?.send(action) + } + case let .animatedSend(action, animation): + if let action { + store?.send(action, animation: animation) + } + } + } + .buttonStyle( + SolidButtonStyle( + sizeType: .small, + buttonApperance: button.role == .cancel ? .cancel : .solid + ) + ) } - } label: { - Text(button.label) - .font(to: .wantedSans(.body)) - } + }, + message: { + $0.message.map(Text.init) + .font(to: .wantedSans(.body)) } - }, - message: { - $0.message.map(Text.init) - .font(to: .wantedSans(.body)) - } - ) + ) + } } } From d59261868e56087c42b0437a2d3119928b4d62b7 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Fri, 20 Sep 2024 19:06:31 +0900 Subject: [PATCH 05/90] =?UTF-8?q?chore:=20warning=20Image=20Assets=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icon/icon_warning.imageset/Contents.json | 21 +++++++++++++++++++ .../icon_warning.imageset/icon_warning.svg | 8 +++++++ .../Image/BottleImageSystem+Icon.swift | 4 ++++ 3 files changed, 33 insertions(+) create mode 100644 Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_warning.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_warning.imageset/icon_warning.svg diff --git a/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_warning.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_warning.imageset/Contents.json new file mode 100644 index 00000000..832f1037 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_warning.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "icon_warning.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_warning.imageset/icon_warning.svg b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_warning.imageset/icon_warning.svg new file mode 100644 index 00000000..1dbb08bd --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_warning.imageset/icon_warning.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift b/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift index 999d320f..6abc53c8 100644 --- a/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift +++ b/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift @@ -24,6 +24,7 @@ public extension Image.BottleImageSystem { case bottleStorage case myPage case appleLogo + case warning } } @@ -65,6 +66,9 @@ public extension Image.BottleImageSystem.Icon { case .appleLogo: return SharedDesignSystemAsset.Images.iconAppleLogo.swiftUIImage + + case .warning: + return SharedDesignSystemAsset.Images.iconWarning.swiftUIImage } } } From 743bf5211b5a5529ecfb443564edf6b0a667176b Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Fri, 20 Sep 2024 19:07:24 +0900 Subject: [PATCH 06/90] =?UTF-8?q?style:=20BottleAlert=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSystem/Sources/Components/Alert/BottleAlert.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift index d3efecfc..e93ccd67 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlert.swift @@ -9,7 +9,7 @@ import SwiftUI import ComposableArchitecture extension View { - public func BottleAlert(_ item: Binding, Action>?>) -> some View { + public func bottleAlert(_ item: Binding, Action>?>) -> some View { let store = item.wrappedValue let alertState = store?.withState { $0 } From 3e71208058b873fd44bdd1898b005a99f8bd1250 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Fri, 20 Sep 2024 19:17:48 +0900 Subject: [PATCH 07/90] =?UTF-8?q?feat:=20BottleAlert=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Components/Alert/BottleAlertView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift index 4fb99925..80228c04 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift @@ -38,7 +38,7 @@ struct BottleAlertView: View where A: View, M: View { VStack(spacing: 0) { alertImage title - .padding(.bottom, 4) + .padding(.bottom, 7) messageView actionsView } @@ -53,7 +53,7 @@ struct BottleAlertView: View where A: View, M: View { private extension BottleAlertView { var alertImage: some View { - BottleImageView(type: .local(bottleImageSystem: .icom(.siren))) + BottleImageView(type: .local(bottleImageSystem: .icom(.warning))) .foregroundStyle(to: ColorToken.icon(.primary)) .padding(.top, .lg) .padding(.bottom, .xs) @@ -65,7 +65,7 @@ private extension BottleAlertView { message(data) .multilineTextAlignment(.center) .padding(.bottom, .sm) - + .lineSpacing(5) } else { EmptyView() } From f76b40955675bb2f1b99784799a5110df1149fe2 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Mon, 2 Sep 2024 20:16:22 +0900 Subject: [PATCH 08/90] =?UTF-8?q?feat:=20ArrowListView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/List/ArrowListView.swift | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift new file mode 100644 index 00000000..bfd99622 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift @@ -0,0 +1,65 @@ +// +// ArrowListView.swift +// DesignSystemExample +// +// Created by 임현규 on 9/2/24. +// + +import SwiftUI + +public struct ArrowListView: View { + public let title: String + public let subTitle: String? + + public init( + title: String, + subTitle: String? = nil + ) { + self.title = title + self.subTitle = subTitle + } + + public var body: some View { + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 8) { + titleView + subTitleView + } + Spacer() + rightArrowImage + } + } +} + +// MARK: - Views +private extension ArrowListView { + var titleView: some View { + WantedSansStyleText( + title, + style: .subTitle2, + color: .secondary + ) + } + + @ViewBuilder + var subTitleView: some View { + if let subTitle = subTitle { + WantedSansStyleText( + subTitle, + style: .caption, + color: .tertiary + ) + } else { + EmptyView() + } + } + + var rightArrowImage: some View { + BottleImageView( + type: .local( + bottleImageSystem: .icom(.right) + ) + ) + .foregroundStyle(to: ColorToken.icon(.primary)) + } +} From 36721feacbe893e8c5fa4876190e9a18fcf2243c Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Mon, 2 Sep 2024 20:51:58 +0900 Subject: [PATCH 09/90] =?UTF-8?q?feat:=20BottleToggle=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Toggle/BottleToggle.swift | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 Projects/Shared/DesignSystem/Sources/Components/Toggle/BottleToggle.swift diff --git a/Projects/Shared/DesignSystem/Sources/Components/Toggle/BottleToggle.swift b/Projects/Shared/DesignSystem/Sources/Components/Toggle/BottleToggle.swift new file mode 100644 index 00000000..c5dd1bf8 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/Toggle/BottleToggle.swift @@ -0,0 +1,45 @@ +// +// BottleToggle.swift +// SharedDesignSystem +// +// Created by 임현규 on 9/2/24. +// + +import SwiftUI + +public struct BottleToggle: View { + @Binding private var isOn: Bool + + public init(isOn: Binding) { + self._isOn = isOn + } + + public var body: some View { + Toggle("", isOn: $isOn) + .toggleStyle(BottleToggleStyle()) + } +} + +// MARK: - ToggleStyle +private struct BottleToggleStyle: ToggleStyle { + private let width: CGFloat = 44 + private let height: CGFloat = 26 + + func makeBody(configuration: Configuration) -> some View { + ZStack(alignment: configuration.isOn ? .trailing : .leading) { + RoundedRectangle(cornerRadius: 100) + .frame(width: width, height: height) + .foregroundStyle(to: configuration.isOn ? ColorToken.container(.pressed) : ColorToken.icon(.disabled)) + + RoundedRectangle(cornerRadius: width / 2) + .frame(width: (width / 2), height: (width / 2)) + .padding(2) + .foregroundStyle(to: ColorToken.container(.primary)) + .onTapGesture { + withAnimation { + configuration.$isOn.wrappedValue.toggle() + } + } + } + } +} From 5b44fdda014e787701a354caf286c399c3383416 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Mon, 2 Sep 2024 20:52:56 +0900 Subject: [PATCH 10/90] =?UTF-8?q?feat:=20ToggleListView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/List/ToggleListView.swift | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 Projects/Shared/DesignSystem/Sources/Components/List/ToggleListView.swift diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ToggleListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ToggleListView.swift new file mode 100644 index 00000000..47f52aa7 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ToggleListView.swift @@ -0,0 +1,63 @@ +// +// ToggleListView.swift +// SharedDesignSystem +// +// Created by 임현규 on 9/2/24. +// + +import SwiftUI + +public struct ToggleListView: View { + private let title: String + private let subTitle: String? + @Binding private var isOn: Bool + + public init( + title: String, + subTitle: String? = nil, + isOn: Binding + ) { + self.title = title + self.subTitle = subTitle + self._isOn = isOn + } + + public var body: some View { + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 8) { + titleView + subTitleView + } + Spacer() + toggle + } + } +} + +// MARK: - Views +private extension ToggleListView { + var titleView: some View { + WantedSansStyleText( + title, + style: .subTitle2, + color: .secondary + ) + } + + @ViewBuilder + var subTitleView: some View { + if let subTitle = subTitle { + WantedSansStyleText( + subTitle, + style: .caption, + color: .tertiary + ) + } else { + EmptyView() + } + } + + var toggle: some View { + BottleToggle(isOn: $isOn) + } +} From 85cbd64cef5db4310ccef0d0aff0783ac8fc456d Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Mon, 2 Sep 2024 20:59:53 +0900 Subject: [PATCH 11/90] =?UTF-8?q?feat:=20ListContainerView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/List/ArrowListView.swift | 34 ++--------- .../Components/List/ListContainerView.swift | 59 +++++++++++++++++++ .../Components/List/ToggleListView.swift | 33 ++--------- 3 files changed, 68 insertions(+), 58 deletions(-) create mode 100644 Projects/Shared/DesignSystem/Sources/Components/List/ListContainerView.swift diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift index bfd99622..08862686 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift @@ -20,40 +20,16 @@ public struct ArrowListView: View { } public var body: some View { - HStack(spacing: 0) { - VStack(alignment: .leading, spacing: 8) { - titleView - subTitleView - } - Spacer() - rightArrowImage - } + ListContainerView( + title: title, + subTitle: subTitle, + content: rightArrowImage + ) } } // MARK: - Views private extension ArrowListView { - var titleView: some View { - WantedSansStyleText( - title, - style: .subTitle2, - color: .secondary - ) - } - - @ViewBuilder - var subTitleView: some View { - if let subTitle = subTitle { - WantedSansStyleText( - subTitle, - style: .caption, - color: .tertiary - ) - } else { - EmptyView() - } - } - var rightArrowImage: some View { BottleImageView( type: .local( diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ListContainerView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ListContainerView.swift new file mode 100644 index 00000000..cd397707 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ListContainerView.swift @@ -0,0 +1,59 @@ +// +// ListContainerView.swift +// SharedDesignSystem +// +// Created by 임현규 on 9/2/24. +// + +import SwiftUI + +struct ListContainerView: View { + private let title: String + private let subTitle: String? + private let content: Content + + init( + title: String, + subTitle: String? = nil, + content: Content + ) { + self.title = title + self.subTitle = subTitle + self.content = content + } + + var body: some View { + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 8) { + titleView + subTitleView + } + Spacer() + content + } + } +} + +// MARK: - Views +private extension ListContainerView { + var titleView: some View { + WantedSansStyleText( + title, + style: .subTitle2, + color: .secondary + ) + } + + @ViewBuilder + var subTitleView: some View { + if let subTitle = subTitle { + WantedSansStyleText( + subTitle, + style: .caption, + color: .tertiary + ) + } else { + EmptyView() + } + } +} diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ToggleListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ToggleListView.swift index 47f52aa7..ba285fe0 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/List/ToggleListView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ToggleListView.swift @@ -23,40 +23,15 @@ public struct ToggleListView: View { } public var body: some View { - HStack(spacing: 0) { - VStack(alignment: .leading, spacing: 8) { - titleView - subTitleView - } - Spacer() - toggle - } + ListContainerView( + title: title, + subTitle: subTitle, + content: toggle) } } // MARK: - Views private extension ToggleListView { - var titleView: some View { - WantedSansStyleText( - title, - style: .subTitle2, - color: .secondary - ) - } - - @ViewBuilder - var subTitleView: some View { - if let subTitle = subTitle { - WantedSansStyleText( - subTitle, - style: .caption, - color: .tertiary - ) - } else { - EmptyView() - } - } - var toggle: some View { BottleToggle(isOn: $isOn) } From c211110b5184a842a61570b92bda519309be882e Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Mon, 2 Sep 2024 21:05:39 +0900 Subject: [PATCH 12/90] =?UTF-8?q?feat:=20ButtonListView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/List/ButtonListView.swift | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift new file mode 100644 index 00000000..9eb1f394 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift @@ -0,0 +1,46 @@ +// +// ButtonListView.swift +// SharedDesignSystem +// +// Created by 임현규 on 9/2/24. +// + +import SwiftUI + +public struct ButtonListView: View { + private let title: String + private let subTitle: String? + private let buttonTitle: String + private let action: () -> Void + + public init( + title: String, + subTitle: String? = nil, + buttonTitle: String, + action: @escaping () -> Void + ) { + self.title = title + self.subTitle = subTitle + self.buttonTitle = buttonTitle + self.action = action + } + + public var body: some View { + ListContainerView( + title: title, + subTitle: subTitle, + content: button) + } +} + +// MARK: - Views +public extension ButtonListView { + var button: some View { + OutlinedStyleButton( + .small(contentType: .text), + title: buttonTitle, + buttonType: .throttle, + action: action + ) + } +} From d987dddf410c89bd4811407f25faf209fbd5c039 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Mon, 2 Sep 2024 21:21:27 +0900 Subject: [PATCH 13/90] =?UTF-8?q?feat:=20ListView=20=EB=8D=B0=EB=AA=A8=20?= =?UTF-8?q?=EC=95=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSystemExampleView.swift | 4 ++ .../SubViews/ListTest/ListTestView.swift | 38 +++++++++++++++++++ .../Components/List/ButtonListView.swift | 3 +- .../Components/List/ListContainerView.swift | 2 +- 4 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift diff --git a/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift b/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift index 5b83ef92..9b851cc9 100644 --- a/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift +++ b/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift @@ -329,6 +329,10 @@ struct ListSection: View { destination: BottleStorageList(), label: { Text("Bottle Storage List") } ) + + NavigationLink( + destination: ListTestView(), + label: { Text("Lists View")}) }, header: { Text("List") diff --git a/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift b/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift new file mode 100644 index 00000000..287e28f1 --- /dev/null +++ b/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift @@ -0,0 +1,38 @@ +// +// ListTestView.swift +// DesignSystemExample +// +// Created by 임현규 on 9/2/24. +// + +import SwiftUI + +import SharedDesignSystem + +struct ListTestView: View { + @State private var isOn: Bool = false + private let title = "title" + private let subTitle = "subTitle" + private let buttonTitle = "업데이트" + + var body: some View { + VStack(spacing: .md) { + ArrowListView(title: title) + + ArrowListView(title: title, subTitle: subTitle) + + ToggleListView(title: title, isOn: $isOn) + + ToggleListView(title: title, subTitle: subTitle, isOn: $isOn) + + ButtonListView(title: title, buttonTitle: buttonTitle) { + print("first ButtonListView Button DidTapped") + } + + ButtonListView(title: title, subTitle: subTitle, buttonTitle: buttonTitle) { + print("second ButtonListView Button DidTapped") + } + } + .padding(.horizontal, .md) + } +} diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift index 9eb1f394..1e10d8e3 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift @@ -29,7 +29,8 @@ public struct ButtonListView: View { ListContainerView( title: title, subTitle: subTitle, - content: button) + content: button + ) } } diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ListContainerView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ListContainerView.swift index cd397707..38190de2 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/List/ListContainerView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ListContainerView.swift @@ -24,7 +24,7 @@ struct ListContainerView: View { var body: some View { HStack(spacing: 0) { - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: .xs) { titleView subTitleView } From cc8a9eb98375306c954eb3c8a0a04f6399b4fbf8 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Tue, 17 Sep 2024 14:42:21 +0900 Subject: [PATCH 14/90] =?UTF-8?q?feat:=20ArrowListView=20arrowButton=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EC=A6=88=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DesignSystem/Sources/Components/List/ArrowListView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift index 08862686..71f8bd43 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift @@ -37,5 +37,7 @@ private extension ArrowListView { ) ) .foregroundStyle(to: ColorToken.icon(.primary)) + .frame(width: 24) + .frame(height: 24) } } From b86b435ddb4d8037f95eb171baad9688c41881e6 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Tue, 17 Sep 2024 14:53:19 +0900 Subject: [PATCH 15/90] =?UTF-8?q?feat:=20=ED=99=94=EC=82=B4=ED=91=9C=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=EC=8B=9C=20action=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/SubViews/ListTest/ListTestView.swift | 8 ++++++-- .../Sources/Components/List/ArrowListView.swift | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift b/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift index 287e28f1..178e503c 100644 --- a/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift +++ b/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift @@ -17,9 +17,13 @@ struct ListTestView: View { var body: some View { VStack(spacing: .md) { - ArrowListView(title: title) + ArrowListView(title: title) { + print("first ArrowListView Button DidTapped") + } - ArrowListView(title: title, subTitle: subTitle) + ArrowListView(title: title, subTitle: subTitle) { + print("second ArrowListView Button DidTapped") + } ToggleListView(title: title, isOn: $isOn) diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift index 71f8bd43..1415146d 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift @@ -8,15 +8,18 @@ import SwiftUI public struct ArrowListView: View { - public let title: String - public let subTitle: String? + private let title: String + private let subTitle: String? + private let action: () -> Void public init( title: String, - subTitle: String? = nil + subTitle: String? = nil, + action: @escaping () -> Void ) { self.title = title self.subTitle = subTitle + self.action = action } public var body: some View { @@ -39,5 +42,6 @@ private extension ArrowListView { .foregroundStyle(to: ColorToken.icon(.primary)) .frame(width: 24) .frame(height: 24) + .asThrottleButton(action: action) } } From ffff94b29e3792ce22069c07e97009a6c3566235 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Fri, 20 Sep 2024 19:23:38 +0900 Subject: [PATCH 16/90] =?UTF-8?q?Revert=20"feat:=20=ED=99=94=EC=82=B4?= =?UTF-8?q?=ED=91=9C=20=ED=81=B4=EB=A6=AD=EC=8B=9C=20action=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 19a527792be7f7d4027fa31dff47be80f0ceb7bc. --- .../Sources/SubViews/ListTest/ListTestView.swift | 8 ++------ .../Sources/Components/List/ArrowListView.swift | 10 +++------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift b/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift index 178e503c..287e28f1 100644 --- a/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift +++ b/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift @@ -17,13 +17,9 @@ struct ListTestView: View { var body: some View { VStack(spacing: .md) { - ArrowListView(title: title) { - print("first ArrowListView Button DidTapped") - } + ArrowListView(title: title) - ArrowListView(title: title, subTitle: subTitle) { - print("second ArrowListView Button DidTapped") - } + ArrowListView(title: title, subTitle: subTitle) ToggleListView(title: title, isOn: $isOn) diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift index 1415146d..71f8bd43 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift @@ -8,18 +8,15 @@ import SwiftUI public struct ArrowListView: View { - private let title: String - private let subTitle: String? - private let action: () -> Void + public let title: String + public let subTitle: String? public init( title: String, - subTitle: String? = nil, - action: @escaping () -> Void + subTitle: String? = nil ) { self.title = title self.subTitle = subTitle - self.action = action } public var body: some View { @@ -42,6 +39,5 @@ private extension ArrowListView { .foregroundStyle(to: ColorToken.icon(.primary)) .frame(width: 24) .frame(height: 24) - .asThrottleButton(action: action) } } From 7f9a4f3cd8a3883c45cd3484e94d64b237b2491e Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Fri, 20 Sep 2024 19:29:46 +0900 Subject: [PATCH 17/90] =?UTF-8?q?feat:=20TextListView=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Title과 SubTitle만 있는 ListView --- .../SubViews/ListTest/ListTestView.swift | 4 +++ .../Components/List/TextListView.swift | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 Projects/Shared/DesignSystem/Sources/Components/List/TextListView.swift diff --git a/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift b/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift index 287e28f1..ac06f464 100644 --- a/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift +++ b/Projects/Shared/DesignSystem/Example/Sources/SubViews/ListTest/ListTestView.swift @@ -32,6 +32,10 @@ struct ListTestView: View { ButtonListView(title: title, subTitle: subTitle, buttonTitle: buttonTitle) { print("second ButtonListView Button DidTapped") } + + TextListView(title: title) + + TextListView(title: title, subTitle: subTitle) } .padding(.horizontal, .md) } diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/TextListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/TextListView.swift new file mode 100644 index 00000000..d8acb4c8 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/List/TextListView.swift @@ -0,0 +1,29 @@ +// +// TextListView.swift +// SharedDesignSystem +// +// Created by 임현규 on 9/20/24. +// + +import SwiftUI + +public struct TextListView: View { + public let title: String + public let subTitle: String? + + public init( + title: String, + subTitle: String? = nil + ) { + self.title = title + self.subTitle = subTitle + } + + public var body: some View { + ListContainerView( + title: title, + subTitle: subTitle, + content: EmptyView() + ) + } +} From c8e2995f54dfd975afecf39601127b037cb2ea86 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Mon, 16 Sep 2024 01:11:01 +0900 Subject: [PATCH 18/90] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B3=80=EA=B2=BD=EB=90=9C=20View=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/MyPage/MyPageView.swift | 89 +++++++++++-------- 1 file changed, 53 insertions(+), 36 deletions(-) diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift index 74fb50f4..0b375670 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift @@ -27,17 +27,22 @@ public struct MyPageView: View { Spacer() .frame(height: 52.0) userProfile - myIntroduction - myKeywords + profileEditList - HStack(spacing: 0) { - Spacer() - logoutButton - Spacer() - withdrawalButton - Spacer() + VStack(spacing: .lg) { + blockPhoneNumberList + pushSettingList + accountSettingList + Divider() + appVersionList + contactList + Divider() + termsOfServiceList + privacyPolicyList } - .padding(.bottom, .xl) + .padding(.horizontal, .md) + .padding(.vertical, .xl) + .overlay(roundedRectangle) } .padding(.horizontal, .md) } @@ -83,40 +88,52 @@ private extension MyPageView { .padding(.bottom, .xl) } - @ViewBuilder - var myIntroduction: some View { - if store.introduction.answer == "" { - EmptyView() - } else { - LettetCardView(title: "내가 쓴 편지" , letterContent: store.introduction.answer) - .padding(.bottom, .sm) - } + var roundedRectangle: some View { + RoundedRectangle(cornerRadius: BottleRadiusType.xl.value) + .strokeBorder( + ColorToken.border(.primary).color, + lineWidth: 1 + ) } - var myKeywords: some View { - ClipListContainerView(clipItemList: store.keywordItem) + var profileEditList: some View { + ArrowListView(title: "프로필 수정") + .padding(.horizontal, .md) + .padding(.vertical, .xl) + .overlay(roundedRectangle) .padding(.bottom, .md) } - var logoutButton: some View { - WantedSansStyleText( - "로그아웃", - style: .subTitle2, - color: .enableSecondary + var blockPhoneNumberList: some View { + ButtonListView( + title: "연락처 차단", + subTitle: "연락처 속 0명을 차단했어요", + buttonTitle: "업데이트", + action: {} ) - .asThrottleButton { - store.send(.logOutButtonDidTapped) - } } - var withdrawalButton: some View { - WantedSansStyleText( - "탈퇴하기", - style: .subTitle2, - color: .enableSecondary - ) - .asThrottleButton { - store.send(.withdrawalButtonDidTapped) - } + var pushSettingList: some View { + ArrowListView(title: "알림 설정") + } + + var accountSettingList: some View { + ArrowListView(title: "계정 관리") + } + + var appVersionList: some View { + ArrowListView(title: "앱 버전", subTitle: "0.0.0") + } + + var contactList: some View { + ArrowListView(title: "1:1 문의") + } + + var termsOfServiceList: some View { + ArrowListView(title: "보틀 이용 약관") + } + + var privacyPolicyList: some View { + ArrowListView(title: "개인정보처리방침") } } From 3fa8fd710b44c44092e65aa5dd0791e488109f3f Mon Sep 17 00:00:00 2001 From: JongHoon Date: Sat, 21 Sep 2024 21:23:10 +0900 Subject: [PATCH 19/90] =?UTF-8?q?[Feature/#239]=20=EA=B0=95=EC=A0=9C=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20(#250)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [chore]: 불필요한 return 삭제 * chore: needUpdateAppVersion -> invalidAppVersion 명명 수정 * feat: URLHandler 모듈 생성 및 적용 --- .../ProjectDescriptionHelpers/Modules.swift | 1 + .../Interface/Sources/BottleURLType.swift | 19 +++++++++ .../Interface/Sources/URLHandler.swift | 19 +++++++++ Projects/Core/URLHandler/Project.swift | 40 +++++++++++++++++++ Projects/Core/URLHandler/Sources/Source.swift | 1 + .../Testing/Sources/URLHandlerTesting.swift | 1 + .../Tests/Sources/URLHandlerTest.swift | 11 +++++ .../Auth/Interface/Sources/AuthClient.swift | 2 +- Projects/Domain/Auth/Sources/AuthClient.swift | 2 +- .../Error/Interface/Sources/DomainError.swift | 2 +- .../SandBeach/SandBeachFeatureInterface.swift | 7 ++-- .../Sources/SplashView/SplashFeature.swift | 7 ++-- 12 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift create mode 100644 Projects/Core/URLHandler/Interface/Sources/URLHandler.swift create mode 100644 Projects/Core/URLHandler/Project.swift create mode 100644 Projects/Core/URLHandler/Sources/Source.swift create mode 100644 Projects/Core/URLHandler/Testing/Sources/URLHandlerTesting.swift create mode 100644 Projects/Core/URLHandler/Tests/Sources/URLHandlerTest.swift diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift index 218eada7..9e2502eb 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift @@ -64,6 +64,7 @@ public extension ModulePath { public extension ModulePath { enum Core: String, CaseIterable { + case URLHandler case Toast case KeyChainStore case WebView diff --git a/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift b/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift new file mode 100644 index 00000000..48851688 --- /dev/null +++ b/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift @@ -0,0 +1,19 @@ +// +// BottleURLType.swift +// CoreURLHandlerInterface +// +// Created by JongHoon on 9/20/24. +// + +import Foundation + +public enum BottleURLType { + case bottleAppStore + + public var url: URL { + switch self { + case .bottleAppStore: + return URL(string: Bundle.main.infoDictionary?["APP_STORE_URL"] as? String ?? "")! + } + } +} diff --git a/Projects/Core/URLHandler/Interface/Sources/URLHandler.swift b/Projects/Core/URLHandler/Interface/Sources/URLHandler.swift new file mode 100644 index 00000000..f02cbc96 --- /dev/null +++ b/Projects/Core/URLHandler/Interface/Sources/URLHandler.swift @@ -0,0 +1,19 @@ +// +// URLHandler.swift +// CoreURLHandlerInterface +// +// Created by JongHoon on 9/20/24. +// + +import UIKit + +public final class URLHandler { + + public static let shared = URLHandler() + + private init() { } + + public func openURL(urlType: BottleURLType) { + UIApplication.shared.open(urlType.url) + } +} diff --git a/Projects/Core/URLHandler/Project.swift b/Projects/Core/URLHandler/Project.swift new file mode 100644 index 00000000..12cd19c6 --- /dev/null +++ b/Projects/Core/URLHandler/Project.swift @@ -0,0 +1,40 @@ +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: ModulePath.Core.name+ModulePath.Core.URLHandler.rawValue, + targets: [ + .core( + interface: .URLHandler, + factory: .init() + ), + .core( + implements: .URLHandler, + factory: .init( + dependencies: [ + .core(interface: .URLHandler) + ] + ) + ), + + .core( + testing: .URLHandler, + factory: .init( + dependencies: [ + .core(interface: .URLHandler) + ] + ) + ), + .core( + tests: .URLHandler, + factory: .init( + dependencies: [ + .core(testing: .URLHandler), + .core(implements: .URLHandler) + ] + ) + ), + + ] +) diff --git a/Projects/Core/URLHandler/Sources/Source.swift b/Projects/Core/URLHandler/Sources/Source.swift new file mode 100644 index 00000000..b1853ce6 --- /dev/null +++ b/Projects/Core/URLHandler/Sources/Source.swift @@ -0,0 +1 @@ +// This is for Tuist diff --git a/Projects/Core/URLHandler/Testing/Sources/URLHandlerTesting.swift b/Projects/Core/URLHandler/Testing/Sources/URLHandlerTesting.swift new file mode 100644 index 00000000..b1853ce6 --- /dev/null +++ b/Projects/Core/URLHandler/Testing/Sources/URLHandlerTesting.swift @@ -0,0 +1 @@ +// This is for Tuist diff --git a/Projects/Core/URLHandler/Tests/Sources/URLHandlerTest.swift b/Projects/Core/URLHandler/Tests/Sources/URLHandlerTest.swift new file mode 100644 index 00000000..9c7ef9e7 --- /dev/null +++ b/Projects/Core/URLHandler/Tests/Sources/URLHandlerTest.swift @@ -0,0 +1,11 @@ +import XCTest + +final class URLHandlerTests: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func testExample() { + XCTAssertEqual(1, 1) + } +} diff --git a/Projects/Domain/Auth/Interface/Sources/AuthClient.swift b/Projects/Domain/Auth/Interface/Sources/AuthClient.swift index 81d65931..3f268530 100644 --- a/Projects/Domain/Auth/Interface/Sources/AuthClient.swift +++ b/Projects/Domain/Auth/Interface/Sources/AuthClient.swift @@ -93,7 +93,7 @@ public struct AuthClient { } public func checkUpdateVersion() async throws { - return try await checkUpdateVersion() + try await checkUpdateVersion() } } diff --git a/Projects/Domain/Auth/Sources/AuthClient.swift b/Projects/Domain/Auth/Sources/AuthClient.swift index 86c0ebf3..c6c69323 100644 --- a/Projects/Domain/Auth/Sources/AuthClient.swift +++ b/Projects/Domain/Auth/Sources/AuthClient.swift @@ -106,7 +106,7 @@ extension AuthClient: DependencyKey { guard minimumBuildNumber <= buildNumber else { - throw DomainError.AuthError.needUpdateAppVersion + throw DomainError.AuthError.invalidAppVersion } } ) diff --git a/Projects/Domain/Error/Interface/Sources/DomainError.swift b/Projects/Domain/Error/Interface/Sources/DomainError.swift index c3803c98..5a2111a0 100644 --- a/Projects/Domain/Error/Interface/Sources/DomainError.swift +++ b/Projects/Domain/Error/Interface/Sources/DomainError.swift @@ -9,7 +9,7 @@ import Foundation public enum DomainError: Error { public enum AuthError: Error { - case needUpdateAppVersion + case invalidAppVersion } case unknown(_ message: String? = nil) diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift index 95ac0b99..715d3da5 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift @@ -6,7 +6,6 @@ // import Foundation -import UIKit import DomainProfile import DomainBottle @@ -14,6 +13,7 @@ import DomainAuth import DomainErrorInterface import CoreLoggerInterface +import CoreURLHandlerInterface import SharedDesignSystem import SharedUtilInterface @@ -141,7 +141,7 @@ extension SandBeachFeature { Log.error(error) if let authError = error as? DomainError.AuthError { switch authError { - case .needUpdateAppVersion: + case .invalidAppVersion: await send(.needUpdateAppVersionErrorOccured) } } @@ -184,8 +184,7 @@ extension SandBeachFeature { } case .updateAppVersion: - let appStoreURL = URL(string: Bundle.main.infoDictionary?["APP_STORE_URL"] as? String ?? "")! - UIApplication.shared.open(appStoreURL) + URLHandler.shared.openURL(urlType: .bottleAppStore) return .run { send in await send(.needUpdateAppVersionErrorOccured) } diff --git a/Projects/Feature/Sources/SplashView/SplashFeature.swift b/Projects/Feature/Sources/SplashView/SplashFeature.swift index 60ed9240..6f009b86 100644 --- a/Projects/Feature/Sources/SplashView/SplashFeature.swift +++ b/Projects/Feature/Sources/SplashView/SplashFeature.swift @@ -6,9 +6,9 @@ // import Foundation -import UIKit import CoreLoggerInterface +import CoreURLHandlerInterface import DomainAuthInterface import DomainErrorInterface @@ -66,7 +66,7 @@ public struct SplashFeature { // TODO: Error handling if let authError = error as? DomainError.AuthError { switch authError { - case .needUpdateAppVersion: + case .invalidAppVersion: await send(.needUpdateAppVersionErrorOccured) } } @@ -92,8 +92,7 @@ public struct SplashFeature { } case .updateAppVersion: - let appStoreURL = URL(string: Bundle.main.infoDictionary?["APP_STORE_URL"] as? String ?? "")! - UIApplication.shared.open(appStoreURL) + URLHandler.shared.openURL(urlType: .bottleAppStore) return .run { send in await send(.needUpdateAppVersionErrorOccured) } From cfc1a972eddcb921641404c22a3749fa623eb769 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 13:23:45 +0900 Subject: [PATCH 20/90] =?UTF-8?q?feat:=20AlertSettingFeature=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AlertSetting/AlertSettingFeature.swift | 23 +++++++++++++ .../AlertSettingFeatureInterface.swift | 32 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift create mode 100644 Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift new file mode 100644 index 00000000..f3d9f1dc --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift @@ -0,0 +1,23 @@ +// +// AlertSettingFeature.swift +// FeatureMyPageInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +import ComposableArchitecture + +extension AlertSettingFeature { + public init() { + let reducer = Reduce { state, action in + switch action { + case .onLoad: + return .none + } + } + + self.init(reducer: reducer) + } +} diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift new file mode 100644 index 00000000..18b1cba7 --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift @@ -0,0 +1,32 @@ +// +// AlertSettingFeatureInterface.swift +// FeatureMyPageInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +import ComposableArchitecture + +@Reducer +public struct AlertSettingFeature { + private let reducer: Reduce + + public init(reducer: Reduce) { + self.reducer = reducer + } + + @ObservableState + public struct State { + + } + + public enum Action { + case onLoad + } + + public var body: some ReducerOf { + reducer + } +} From 6272eb42edbdfafd3248267e5cfc4e56ec8bfe2d Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 13:24:01 +0900 Subject: [PATCH 21/90] =?UTF-8?q?feat:=20AlertSettingView=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AlertSetting/AlertSettingView.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift new file mode 100644 index 00000000..05c39714 --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift @@ -0,0 +1,24 @@ +// +// AlertSettingView.swift +// FeatureMyPageInterface +// +// Created by 임현규 on 9/21/24. +// + +import SwiftUI + +import SharedDesignSystem + +import ComposableArchitecture + +public struct AlertSettingView: View { + private let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + EmptyView() + } +} From 856f899707c3e6144572de87f7b6f5c831f7be8c Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 13:28:57 +0900 Subject: [PATCH 22/90] =?UTF-8?q?feat:=20MyPageRoot=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/MyPageRootFeature.swift | 38 +++++++++++++++++++ .../Sources/MyPageRootFeatureInterface.swift | 21 ++++++++++ .../Interface/Sources/MyPageRootView.swift | 18 +++++++++ 3 files changed, 77 insertions(+) create mode 100644 Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift create mode 100644 Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift create mode 100644 Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift new file mode 100644 index 00000000..4a3d5410 --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift @@ -0,0 +1,38 @@ +// +// MyPageRootFeature.swift +// FeatureMyPageInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +import ComposableArchitecture + +@Reducer +public struct MyPageRootFeature { + private let reducer: Reduce + + public init(reducer: Reduce) { + self.reducer = reducer + } + + + public enum Path { + + } + + public struct State { + + } + + public enum Action { + + } + + public var body: some ReducerOf { + reducer + } +} + + diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift new file mode 100644 index 00000000..12e23547 --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift @@ -0,0 +1,21 @@ +// +// MyPageRootFeatureInterface.swift +// FeatureMyPageInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +import ComposableArchitecture + +extension MyPageRootFeature { + public init() { + let reducer = Reduce { state, action in + return .none + } + + self.init(reducer: reducer) + } +} + diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift new file mode 100644 index 00000000..615b65fc --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift @@ -0,0 +1,18 @@ +// +// MyPageRootView.swift +// FeatureMyPageInterface +// +// Created by 임현규 on 9/21/24. +// + +import SwiftUI + +struct MyPageRootView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + MyPageRootView() +} From 280da4770a11d78eba7cde06894cc32f0fc1e68c Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 14:37:13 +0900 Subject: [PATCH 23/90] =?UTF-8?q?feat:=20MainTab=20MyPageRootView=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AlertSettingFeatureInterface.swift | 2 +- .../Interface/Sources/MyPageRootFeature.swift | 38 ++++++++++++++++--- .../Sources/MyPageRootFeatureInterface.swift | 13 ++++++- .../Interface/Sources/MyPageRootView.swift | 38 +++++++++++++++---- .../Feature/Sources/TabView/MainTabView.swift | 2 +- .../Sources/TabView/MainTabViewFeature.swift | 14 +++---- 6 files changed, 83 insertions(+), 24 deletions(-) diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift index 18b1cba7..758481f1 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift @@ -18,7 +18,7 @@ public struct AlertSettingFeature { } @ObservableState - public struct State { + public struct State: Equatable { } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift index 4a3d5410..664e8a2a 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift @@ -7,6 +7,8 @@ import Foundation +import FeatureTabBarInterface + import ComposableArchitecture @Reducer @@ -17,22 +19,46 @@ public struct MyPageRootFeature { self.reducer = reducer } - + @Reducer(state: .equatable) public enum Path { - + case AlertSetting(AlertSettingFeature) } - public struct State { + @ObservableState + public struct State: Equatable { + var path = StackState() + public var myPage: MyPageFeature.State + public init( + path: StackState = StackState(), + myPage: MyPageFeature.State = .init() + ) { + self.path = path + self.myPage = myPage + } } public enum Action { - + case path(StackAction) + case myPage(MyPageFeature.Action) + case delegate(Delegate) + case selectedTabDidChanged(selectedTab: TabType) + case userProfileUpdateDidRequest + + public enum Delegate { + case withdrawalButtonDidTapped + case withdrawalDidCompleted + case logoutDidCompleted + case selectedTabDidChanged(TabType) + } } public var body: some ReducerOf { + Scope(state: \.myPage, action: \.myPage) { + MyPageFeature() + } + reducer + .forEach(\.path, action: \.path) } } - - diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift index 12e23547..6bf4a2c3 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift @@ -12,10 +12,19 @@ import ComposableArchitecture extension MyPageRootFeature { public init() { let reducer = Reduce { state, action in - return .none + switch action { + + case .userProfileUpdateDidRequest: + return .send(.myPage(.userProfileUpdateDidRequest)) + + case let .selectedTabDidChanged(selectedTab): + return .send(.delegate(.selectedTabDidChanged(selectedTab))) + + default: + return .none + } } self.init(reducer: reducer) } } - diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift index 615b65fc..9433cd65 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift @@ -7,12 +7,36 @@ import SwiftUI -struct MyPageRootView: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} +import FeatureTabBarInterface -#Preview { - MyPageRootView() +import ComposableArchitecture + +public struct MyPageRootView: View { + @Perception.Bindable private var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithPerceptionTracking { + NavigationStack(path: $store.scope(state: \.path, action: \.path)) { + MyPageView(store: store.scope(state: \.myPage, action: \.myPage)) + .setTabBar(selectedTab: .myPage) { selectedTab in + store.send(.selectedTabDidChanged(selectedTab: selectedTab)) + } + } destination: { store in + WithPerceptionTracking { + switch store.state { + case .AlertSetting: + if let store = store.scope( + state: \.AlertSetting, + action: \.AlertSetting) { + AlertSettingView(store: store) + } + } + } + } + } + } } diff --git a/Projects/Feature/Sources/TabView/MainTabView.swift b/Projects/Feature/Sources/TabView/MainTabView.swift index dcb03162..32787aa8 100644 --- a/Projects/Feature/Sources/TabView/MainTabView.swift +++ b/Projects/Feature/Sources/TabView/MainTabView.swift @@ -34,7 +34,7 @@ public struct MainTabView: View { .tag(TabType.bottleStorage) .toolbar(.hidden, for: .tabBar) - MyPageView(store: store.scope(state: \.myPage, action: \.myPage)) + MyPageRootView(store: store.scope(state: \.myPageRoot, action: \.myPageRoot)) .tag(TabType.myPage) .toolbar(.hidden, for: .tabBar) } diff --git a/Projects/Feature/Sources/TabView/MainTabViewFeature.swift b/Projects/Feature/Sources/TabView/MainTabViewFeature.swift index 92124157..ecbe49c7 100644 --- a/Projects/Feature/Sources/TabView/MainTabViewFeature.swift +++ b/Projects/Feature/Sources/TabView/MainTabViewFeature.swift @@ -24,13 +24,13 @@ public struct MainTabViewFeature { public struct State: Equatable { var sandBeachRoot: SandBeachRootFeature.State var bottleStorage: BottleStorageFeature.State - var myPage: MyPageFeature.State + var myPageRoot: MyPageRootFeature.State var selectedTab: TabType var isLoading: Bool public init() { self.sandBeachRoot = .init() self.bottleStorage = .init() - self.myPage = .init() + self.myPageRoot = .init() self.selectedTab = .sandBeach self.isLoading = false } @@ -39,7 +39,7 @@ public struct MainTabViewFeature { public enum Action: BindableAction { case sandBeachRoot(SandBeachRootFeature.Action) case bottleStorage(BottleStorageFeature.Action) - case myPage(MyPageFeature.Action) + case myPageRoot(MyPageRootFeature.Action) case selectedTabChanged(TabType) case binding(BindingAction) @@ -60,8 +60,8 @@ public struct MainTabViewFeature { Scope(state: \.bottleStorage, action: \.bottleStorage) { BottleStorageFeature() } - Scope(state: \.myPage, action: \.myPage) { - MyPageFeature() + Scope(state: \.myPageRoot, action: \.myPageRoot) { + MyPageRootFeature() } Reduce(feature) } @@ -83,7 +83,7 @@ public struct MainTabViewFeature { case let .selectedTabDidChanged(selectedTab): state.selectedTab = selectedTab case .profileSetUpDidCompleted: - return .send(.myPage(.userProfileUpdateDidRequest)) + return .send(.myPageRoot(.userProfileUpdateDidRequest)) } return .none @@ -96,7 +96,7 @@ public struct MainTabViewFeature { return .none // MyPage Delegate - case let .myPage(.delegate(delegate)): + case let .myPageRoot(.delegate(delegate)): switch delegate { case .logoutDidCompleted: return .send(.delegate(.logoutDidCompleted)) From 9c5a7d26a34015aca9a331d60368ce906146e7eb Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 14:41:46 +0900 Subject: [PATCH 24/90] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20AlertSettingView?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/MyPage/MyPageFeature.swift | 4 ++++ .../Sources/MyPage/MyPageFeatureInterface.swift | 3 ++- .../MyPage/Interface/Sources/MyPage/MyPageView.swift | 1 + .../Sources/MyPageRootFeatureInterface.swift | 11 +++++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift index 430bfb83..a39d39c9 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift @@ -113,6 +113,10 @@ extension MyPageFeature { let userProfile = try await profileClient.fetchUserProfile() await send(.userProfileDidFetched(userProfile)) } + + case .alertSettingListDidTapped: + return .send(.delegate(.alertSettingListDidTapped)) + default: return .none } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift index cfd4f513..e8801d98 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift @@ -50,7 +50,7 @@ public struct MyPageFeature { case withdrawalButtonDidTapped case withdrawalDidCompleted case selectedTabDidChanged(TabType) - + case alertSettingListDidTapped case delegate(Delegate) public enum Delegate { @@ -58,6 +58,7 @@ public struct MyPageFeature { case withdrawalDidCompleted case logoutDidCompleted case selectedTabDidChanged(TabType) + case alertSettingListDidTapped } case alert(Alert) diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift index 0b375670..99f54b3c 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift @@ -115,6 +115,7 @@ private extension MyPageView { var pushSettingList: some View { ArrowListView(title: "알림 설정") + .asThrottleButton(action: { store.send(.alertSettingListDidTapped)}) } var accountSettingList: some View { diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift index 6bf4a2c3..a22d5cdf 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift @@ -20,6 +20,17 @@ extension MyPageRootFeature { case let .selectedTabDidChanged(selectedTab): return .send(.delegate(.selectedTabDidChanged(selectedTab))) + // MyPage Delegate + case let .myPage(delegate): + switch delegate { + case .alertSettingListDidTapped: + state.path.append(.AlertSetting(.init())) + return .none + + default: + return .none + } + default: return .none } From bcdda5ab88dc766fb65f6bc0a21153f021b0b0f4 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 14:57:22 +0900 Subject: [PATCH 25/90] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20View=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AlertSetting/AlertSettingView.swift | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift index 05c39714..15d6a087 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift @@ -19,6 +19,65 @@ public struct AlertSettingView: View { } public var body: some View { - EmptyView() + VStack(spacing: 0) { + VStack(spacing: .lg) { + randomBottleSetting + arrivalBottleSetting + pingpongSetting + Divider() + marketingSetting + } + .padding(.horizontal, .md) + .padding(.vertical, .xl) + .overlay(roundedRectangle) + .padding(.top, 32) + Spacer() + } + .padding(.horizontal, .lg) + .setNavigationBar { + makeNaivgationleftButton { + print("BackButtonDidTapped") + } + } + } +} + +private extension AlertSettingView { + var roundedRectangle: some View { + RoundedRectangle(cornerRadius: BottleRadiusType.xl.value) + .strokeBorder( + ColorToken.border(.primary).color, + lineWidth: 1 + ) + } + + var randomBottleSetting: some View { + ToggleListView( + title: "떠나니는 보틀 알림", + subTitle: "매일 랜덤으로 추천되는 보틀 안내", + isOn: .constant(true)) + } + + var arrivalBottleSetting: some View { + ToggleListView( + title: "호감 도착 안내", + subTitle: "내가 받은 호감 안내", + isOn: .constant(true) + ) + } + + var pingpongSetting: some View { + ToggleListView( + title: "대화 알림", + subTitle: "가치관 문답 시작 · 진행 · 중단 , 매칭 안내", + isOn: .constant(true) + ) + } + + var marketingSetting: some View { + ToggleListView( + title: "마케팅 수신 동의", + isOn: .constant(true) + ) } } From b59e98d7cbc806b26a8637509c0128aaba0d082b Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 15:50:48 +0900 Subject: [PATCH 26/90] =?UTF-8?q?feat:=20AlertType=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../User/Interface/Sources/Entity/AlertType.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Projects/Domain/User/Interface/Sources/Entity/AlertType.swift diff --git a/Projects/Domain/User/Interface/Sources/Entity/AlertType.swift b/Projects/Domain/User/Interface/Sources/Entity/AlertType.swift new file mode 100644 index 00000000..e57387d3 --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/Entity/AlertType.swift @@ -0,0 +1,15 @@ +// +// AlertType.swift +// DomainUserInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +public enum AlertType: String, Encodable { + case randomBottle = "DAILY_RANDOM" + case ArrivalBottle = "RECEIVE_LIKE" + case pingpong = "PINGPONG" + case marketing = "MARKETING" +} From e3e562aa9e68f5ab121867115fc9fbb9787dacd7 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 15:51:02 +0900 Subject: [PATCH 27/90] =?UTF-8?q?feat:=20AlertStateRequestDTO=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/Request/AlertStateRequestDTO.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Projects/Domain/User/Interface/Sources/DTO/Request/AlertStateRequestDTO.swift diff --git a/Projects/Domain/User/Interface/Sources/DTO/Request/AlertStateRequestDTO.swift b/Projects/Domain/User/Interface/Sources/DTO/Request/AlertStateRequestDTO.swift new file mode 100644 index 00000000..0d6bc6ee --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/DTO/Request/AlertStateRequestDTO.swift @@ -0,0 +1,18 @@ +// +// AlertStateRequestDTO.swift +// DomainUserInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +public struct AlertStateRequestDTO: Encodable { + public let alimyType: AlertType.RawValue + public let enabled: Bool + + public init(alertType: AlertType, enabled: Bool) { + self.alimyType = alertType.rawValue + self.enabled = enabled + } +} From f0883e59ba836aa7faead96d50c1adb2579e4dd8 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 15:51:12 +0900 Subject: [PATCH 28/90] =?UTF-8?q?feat:=20UserAPI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../User/Interface/Sources/API/UserAPI.swift | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Projects/Domain/User/Interface/Sources/API/UserAPI.swift diff --git a/Projects/Domain/User/Interface/Sources/API/UserAPI.swift b/Projects/Domain/User/Interface/Sources/API/UserAPI.swift new file mode 100644 index 00000000..db5cdc18 --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/API/UserAPI.swift @@ -0,0 +1,46 @@ +// +// UserAPI.swift +// DomainUserInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +import CoreNetworkInterface + +import Moya + +public enum UserAPI { + case fetchAlertState + case updateAlertState(reqeustData: AlertStateRequestDTO) +} + +extension UserAPI: BaseTargetType { + public var path: String { + switch self { + case .fetchAlertState: + return "api/v1/user/alimy" + case .updateAlertState: + return "api/v1/user/alimy" + } + } + + public var method: Moya.Method { + switch self { + case .fetchAlertState: + return .get + case .updateAlertState: + return .post + } + } + + public var task: Moya.Task { + switch self { + case .fetchAlertState: + return .requestPlain + case .updateAlertState(let requestData): + return .requestJSONEncodable(requestData) + } + } +} From 059eae0e35268410ff141c122ff170c126773ad5 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 16:02:27 +0900 Subject: [PATCH 29/90] =?UTF-8?q?feat:=20AlertState=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/Entity/AlertState.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Projects/Domain/User/Interface/Sources/Entity/AlertState.swift diff --git a/Projects/Domain/User/Interface/Sources/Entity/AlertState.swift b/Projects/Domain/User/Interface/Sources/Entity/AlertState.swift new file mode 100644 index 00000000..75dc2bc8 --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/Entity/AlertState.swift @@ -0,0 +1,21 @@ +// +// AlertState.swift +// DomainUserInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +public struct AlertState { + private let alertType: AlertType + private let enabled: Bool + + public init( + alertType: AlertType, + enabled: Bool + ) { + self.alertType = alertType + self.enabled = enabled + } +} From 16da510c6f01d0852128c080d3e576d906a8c5bf Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 16:02:39 +0900 Subject: [PATCH 30/90] =?UTF-8?q?feat:=20AlertStateResponseDTO=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/Response/AlertStateReponseDTO.swift | 28 +++++++++++++++++++ .../Interface/Sources/Entity/AlertType.swift | 3 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Projects/Domain/User/Interface/Sources/DTO/Response/AlertStateReponseDTO.swift diff --git a/Projects/Domain/User/Interface/Sources/DTO/Response/AlertStateReponseDTO.swift b/Projects/Domain/User/Interface/Sources/DTO/Response/AlertStateReponseDTO.swift new file mode 100644 index 00000000..14498cb3 --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/DTO/Response/AlertStateReponseDTO.swift @@ -0,0 +1,28 @@ +// +// AlertStateReponseDTO.swift +// DomainUserInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +public struct AlertStateReponseDTO: Decodable { + public let alertStateList: [AlertStateDTO] + + public struct AlertStateDTO: Decodable { + let alertType: String + let enabled: Bool + + public func toDomain() -> AlertState { + return .init( + alertType: AlertType(rawValue: alertType) ?? .none, + enabled: enabled + ) + } + } + + public func toDomain() -> [AlertState] { + return alertStateList.map { $0.toDomain() } + } +} diff --git a/Projects/Domain/User/Interface/Sources/Entity/AlertType.swift b/Projects/Domain/User/Interface/Sources/Entity/AlertType.swift index e57387d3..2c236d1d 100644 --- a/Projects/Domain/User/Interface/Sources/Entity/AlertType.swift +++ b/Projects/Domain/User/Interface/Sources/Entity/AlertType.swift @@ -7,7 +7,8 @@ import Foundation -public enum AlertType: String, Encodable { +public enum AlertType: String, Codable { + case none = "NONE" case randomBottle = "DAILY_RANDOM" case ArrivalBottle = "RECEIVE_LIKE" case pingpong = "PINGPONG" From 56a1ace9cb8d5869099860e65b4d89b8b01e2515 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 16:38:54 +0900 Subject: [PATCH 31/90] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=A7=84=EC=9E=85=20=EC=8B=9C=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EB=A1=9C=EB=B6=80=ED=84=B0=20=EB=B0=9B=EC=95=84=EC=98=A8=20?= =?UTF-8?q?=EA=B0=92=EC=9C=BC=EB=A1=9C=20=ED=99=94=EB=A9=B4=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/Response/AlertStateReponseDTO.swift | 24 +++----- .../Interface/Sources/Entity/AlertState.swift | 6 +- .../User/Interface/Sources/UserClient.swift | 17 +++++- Projects/Domain/User/Sources/UserClient.swift | 15 +++++ .../AlertSetting/AlertSettingFeature.swift | 41 +++++++++++++ .../AlertSettingFeatureInterface.swift | 25 +++++++- .../AlertSetting/AlertSettingView.swift | 57 ++++++++++--------- 7 files changed, 138 insertions(+), 47 deletions(-) diff --git a/Projects/Domain/User/Interface/Sources/DTO/Response/AlertStateReponseDTO.swift b/Projects/Domain/User/Interface/Sources/DTO/Response/AlertStateReponseDTO.swift index 14498cb3..d3f66baa 100644 --- a/Projects/Domain/User/Interface/Sources/DTO/Response/AlertStateReponseDTO.swift +++ b/Projects/Domain/User/Interface/Sources/DTO/Response/AlertStateReponseDTO.swift @@ -7,22 +7,14 @@ import Foundation -public struct AlertStateReponseDTO: Decodable { - public let alertStateList: [AlertStateDTO] +public struct AlertStateResponseDTO: Decodable { + let alimyType: String + let enabled: Bool - public struct AlertStateDTO: Decodable { - let alertType: String - let enabled: Bool - - public func toDomain() -> AlertState { - return .init( - alertType: AlertType(rawValue: alertType) ?? .none, - enabled: enabled - ) - } - } - - public func toDomain() -> [AlertState] { - return alertStateList.map { $0.toDomain() } + public func toDomain() -> UserAlertState { + return .init( + alertType: AlertType(rawValue: alimyType) ?? .none, + enabled: enabled + ) } } diff --git a/Projects/Domain/User/Interface/Sources/Entity/AlertState.swift b/Projects/Domain/User/Interface/Sources/Entity/AlertState.swift index 75dc2bc8..d05ead1c 100644 --- a/Projects/Domain/User/Interface/Sources/Entity/AlertState.swift +++ b/Projects/Domain/User/Interface/Sources/Entity/AlertState.swift @@ -7,9 +7,9 @@ import Foundation -public struct AlertState { - private let alertType: AlertType - private let enabled: Bool +public struct UserAlertState { + public let alertType: AlertType + public let enabled: Bool public init( alertType: AlertType, diff --git a/Projects/Domain/User/Interface/Sources/UserClient.swift b/Projects/Domain/User/Interface/Sources/UserClient.swift index dadc0abd..b54308c3 100644 --- a/Projects/Domain/User/Interface/Sources/UserClient.swift +++ b/Projects/Domain/User/Interface/Sources/UserClient.swift @@ -14,6 +14,9 @@ public struct UserClient { private let updateLoginState: (Bool) -> Void private let updateDeleteState: (Bool) -> Void private let updateFcmToken: (String) -> Void + private let _fetchAlertState: () async throws -> [UserAlertState] + private let updateAlertState: (UserAlertState) async throws -> Void + public init( isLoggedIn: @escaping () -> Bool, @@ -21,7 +24,9 @@ public struct UserClient { fetchFcmToken: @escaping () -> String?, updateLoginState: @escaping (Bool) -> Void, updateDeleteState: @escaping (Bool) -> Void, - updateFcmToken: @escaping (String) -> Void + updateFcmToken: @escaping (String) -> Void, + fetchAlertState: @escaping () async throws -> [UserAlertState], + updateAlertState: @escaping (UserAlertState) async throws -> Void ) { self._isLoggedIn = isLoggedIn self._isAppDeleted = isAppDeleted @@ -29,6 +34,8 @@ public struct UserClient { self.updateLoginState = updateLoginState self.updateDeleteState = updateDeleteState self.updateFcmToken = updateFcmToken + self._fetchAlertState = fetchAlertState + self.updateAlertState = updateAlertState } public func isLoggedIn() -> Bool { @@ -54,4 +61,12 @@ public struct UserClient { public func updateFcmToken(fcmToken: String) { updateFcmToken(fcmToken) } + + public func fetchAlertState() async throws -> [UserAlertState] { + try await _fetchAlertState() + } + + public func updateAlertState(alertState: UserAlertState) async throws { + try await updateAlertState(alertState) + } } diff --git a/Projects/Domain/User/Sources/UserClient.swift b/Projects/Domain/User/Sources/UserClient.swift index 6df5fad8..b14bfaa7 100644 --- a/Projects/Domain/User/Sources/UserClient.swift +++ b/Projects/Domain/User/Sources/UserClient.swift @@ -10,13 +10,17 @@ import Foundation import DomainUserInterface import CoreKeyChainStore +import CoreNetwork import ComposableArchitecture +import Moya extension UserClient: DependencyKey { static public var liveValue: UserClient = .live() static func live() -> UserClient { + @Dependency(\.network) var networkManager + return .init( isLoggedIn: { return UserDefaults.standard.bool(forKey: "loginState") @@ -40,6 +44,17 @@ extension UserClient: DependencyKey { updateFcmToken: { fcmToken in UserDefaults.standard.set(fcmToken, forKey: "fcmToken") + }, + + fetchAlertState: { + let responseData = try await networkManager.reqeust(api: .apiType(UserAPI.fetchAlertState), dto: [AlertStateResponseDTO].self) + return responseData.map { $0.toDomain() } + + }, + + updateAlertState: { alertState in + let requestData = AlertStateRequestDTO(alertType: alertState.alertType, enabled: alertState.enabled) + try await networkManager.reqeust(api: .apiType(UserAPI.updateAlertState(reqeustData: requestData))) } ) } diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift index f3d9f1dc..eb204b30 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift @@ -7,13 +7,54 @@ import Foundation +import DomainUser + import ComposableArchitecture extension AlertSettingFeature { public init() { + @Dependency(\.userClient) var userClient + let reducer = Reduce { state, action in switch action { case .onLoad: + return .run { send in + let alertStateList = try await userClient.fetchAlertState() + + for alertState in alertStateList { + let isOn = alertState.enabled + switch alertState.alertType { + case .randomBottle: + await send(.randomBottleToggleDidFetched(isOn: isOn)) + case .ArrivalBottle: + await send(.arrivalBottleToggleDidFetched(isOn: isOn)) + case .pingpong: + await send(.pingpongToggleDidFetched(isOn: isOn)) + case .marketing: + await send(.marketingToggleDidFetched(isOn: isOn)) + default: + break + } + } + } + + case let .randomBottleToggleDidFetched(isOn): + state.isOnRandomBottleToggle = isOn + return .none + + case let .arrivalBottleToggleDidFetched(isOn): + state.isOnArrivalBottleToggle = isOn + return .none + + case let .pingpongToggleDidFetched(isOn): + state.isOnPingPongToggle = isOn + return .none + + case let .marketingToggleDidFetched(isOn): + state.isOnMarketingToggle = isOn + return .none + + default: return .none } } diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift index 758481f1..43f17762 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift @@ -19,14 +19,37 @@ public struct AlertSettingFeature { @ObservableState public struct State: Equatable { + public var isOnRandomBottleToggle: Bool + public var isOnArrivalBottleToggle: Bool + public var isOnPingPongToggle: Bool + public var isOnMarketingToggle: Bool + public init( + isOnRandomBottleToggle: Bool = false, + isOnArrivalBottleToggle: Bool = false, + isOnPingPongToggle: Bool = false, + isOnMarketingToggle: Bool = false + ) { + self.isOnRandomBottleToggle = isOnRandomBottleToggle + self.isOnArrivalBottleToggle = isOnArrivalBottleToggle + self.isOnPingPongToggle = isOnPingPongToggle + self.isOnMarketingToggle = isOnMarketingToggle + } } - public enum Action { + public enum Action: BindableAction { case onLoad + + case randomBottleToggleDidFetched(isOn: Bool) + case arrivalBottleToggleDidFetched(isOn: Bool) + case pingpongToggleDidFetched(isOn: Bool) + case marketingToggleDidFetched(isOn: Bool) + + case binding(BindingAction) } public var body: some ReducerOf { + BindingReducer() reducer } } diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift index 15d6a087..e87baf98 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift @@ -12,32 +12,36 @@ import SharedDesignSystem import ComposableArchitecture public struct AlertSettingView: View { - private let store: StoreOf + @Perception.Bindable private var store: StoreOf public init(store: StoreOf) { self.store = store } public var body: some View { - VStack(spacing: 0) { - VStack(spacing: .lg) { - randomBottleSetting - arrivalBottleSetting - pingpongSetting - Divider() - marketingSetting + WithPerceptionTracking { + + VStack(spacing: 0) { + VStack(spacing: .lg) { + randomBottleToggle + arrivalBottleToggle + pingpongToggle + Divider() + marketingToggle + } + .padding(.horizontal, .md) + .padding(.vertical, .xl) + .overlay(roundedRectangle) + .padding(.top, 32) + Spacer() } - .padding(.horizontal, .md) - .padding(.vertical, .xl) - .overlay(roundedRectangle) - .padding(.top, 32) - Spacer() - } - .padding(.horizontal, .lg) - .setNavigationBar { - makeNaivgationleftButton { - print("BackButtonDidTapped") + .padding(.horizontal, .lg) + .setNavigationBar { + makeNaivgationleftButton { + print("BackButtonDidTapped") + } } + .onLoad { store.send(.onLoad) } } } } @@ -51,33 +55,34 @@ private extension AlertSettingView { ) } - var randomBottleSetting: some View { + var randomBottleToggle: some View { ToggleListView( title: "떠나니는 보틀 알림", subTitle: "매일 랜덤으로 추천되는 보틀 안내", - isOn: .constant(true)) + isOn: $store.isOnRandomBottleToggle + ) } - var arrivalBottleSetting: some View { + var arrivalBottleToggle: some View { ToggleListView( title: "호감 도착 안내", subTitle: "내가 받은 호감 안내", - isOn: .constant(true) + isOn: $store.isOnArrivalBottleToggle ) } - var pingpongSetting: some View { + var pingpongToggle: some View { ToggleListView( title: "대화 알림", subTitle: "가치관 문답 시작 · 진행 · 중단 , 매칭 안내", - isOn: .constant(true) + isOn: $store.isOnPingPongToggle ) } - var marketingSetting: some View { + var marketingToggle: some View { ToggleListView( title: "마케팅 수신 동의", - isOn: .constant(true) + isOn: $store.isOnMarketingToggle ) } } From 141e01f213f9c9acce330dab1d22664d017ebd57 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 17:07:47 +0900 Subject: [PATCH 32/90] =?UTF-8?q?feat:=20=EA=B0=81=20=ED=86=A0=EA=B8=80=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20API=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=94=94=EB=B0=94=EC=9A=B4=EC=8A=A4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/Entity/AlertType.swift | 2 +- .../AlertSetting/AlertSettingFeature.swift | 50 ++++++++++++++++++- .../AlertSettingFeatureInterface.swift | 13 +++++ .../AlertSetting/AlertSettingView.swift | 4 +- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/Projects/Domain/User/Interface/Sources/Entity/AlertType.swift b/Projects/Domain/User/Interface/Sources/Entity/AlertType.swift index 2c236d1d..2c0b956d 100644 --- a/Projects/Domain/User/Interface/Sources/Entity/AlertType.swift +++ b/Projects/Domain/User/Interface/Sources/Entity/AlertType.swift @@ -10,7 +10,7 @@ import Foundation public enum AlertType: String, Codable { case none = "NONE" case randomBottle = "DAILY_RANDOM" - case ArrivalBottle = "RECEIVE_LIKE" + case arrivalBottle = "RECEIVE_LIKE" case pingpong = "PINGPONG" case marketing = "MARKETING" } diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift index eb204b30..71617f7c 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift @@ -8,12 +8,14 @@ import Foundation import DomainUser +import DomainUserInterface import ComposableArchitecture extension AlertSettingFeature { public init() { @Dependency(\.userClient) var userClient + @Dependency(\.dismiss) var dismiss let reducer = Reduce { state, action in switch action { @@ -26,7 +28,7 @@ extension AlertSettingFeature { switch alertState.alertType { case .randomBottle: await send(.randomBottleToggleDidFetched(isOn: isOn)) - case .ArrivalBottle: + case .arrivalBottle: await send(.arrivalBottleToggleDidFetched(isOn: isOn)) case .pingpong: await send(.pingpongToggleDidFetched(isOn: isOn)) @@ -54,6 +56,52 @@ extension AlertSettingFeature { state.isOnMarketingToggle = isOn return .none + case .backButtonDidTapped: + return .run { _ in + await dismiss() + } + + case .binding(\.isOnRandomBottleToggle): + return .run { [isOn = state.isOnRandomBottleToggle] send in + await send(.toggleDidChanged(alertState: .init(alertType: .randomBottle, enabled: isOn))) + } + .debounce( + id: ID.randomBottle, + for: 1.0, + scheduler: DispatchQueue.main) + + case .binding(\.isOnArrivalBottleToggle): + return .run { [isOn = state.isOnArrivalBottleToggle] send in + await send(.toggleDidChanged(alertState: .init(alertType: .arrivalBottle, enabled: isOn))) + } + .debounce( + id: ID.arrivalBottle, + for: 1.0, + scheduler: DispatchQueue.main) + + case .binding(\.isOnPingPongToggle): + return .run { [isOn = state.isOnPingPongToggle] send in + await send(.toggleDidChanged(alertState: .init(alertType: .pingpong, enabled: isOn))) + } + .debounce( + id: ID.pingping, + for: 1.0, + scheduler: DispatchQueue.main) + + case .binding(\.isOnMarketingToggle): + return .run { [isOn = state.isOnMarketingToggle] send in + await send(.toggleDidChanged(alertState: .init(alertType: .marketing, enabled: isOn))) + } + .debounce( + id: ID.marketing, + for: 1.0, + scheduler: DispatchQueue.main) + + case let .toggleDidChanged(alertState): + return .run { send in + try await userClient.updateAlertState(alertState: alertState) + } + default: return .none } diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift index 43f17762..3f4ae4c0 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift @@ -7,6 +7,8 @@ import Foundation +import DomainUserInterface + import ComposableArchitecture @Reducer @@ -45,9 +47,20 @@ public struct AlertSettingFeature { case pingpongToggleDidFetched(isOn: Bool) case marketingToggleDidFetched(isOn: Bool) + // UserAction + case toggleDidChanged(alertState: UserAlertState) + case backButtonDidTapped + case binding(BindingAction) } + enum ID: Hashable { + case randomBottle + case arrivalBottle + case pingping + case marketing + } + public var body: some ReducerOf { BindingReducer() reducer diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift index e87baf98..d9c9840b 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift @@ -37,9 +37,7 @@ public struct AlertSettingView: View { } .padding(.horizontal, .lg) .setNavigationBar { - makeNaivgationleftButton { - print("BackButtonDidTapped") - } + makeNaivgationleftButton { store.send(.backButtonDidTapped) } } .onLoad { store.send(.onLoad) } } From f58ebaa0784b8e04cd9d55802da83d5e86e5911f Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 17:35:28 +0900 Subject: [PATCH 33/90] =?UTF-8?q?feat:=20AccountSettingFeature=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountSettingFeature.swift | 56 +++++++++++++++++++ .../AccountSettingFeatureInterface.swift | 53 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift create mode 100644 Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift diff --git a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift new file mode 100644 index 00000000..8ca08294 --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift @@ -0,0 +1,56 @@ +// +// AccountSettingFeature.swift +// FeatureMyPageInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +import DomainUser +import DomainUserInterface + +import ComposableArchitecture + +extension AccountSettingFeature { + public init() { + @Dependency(\.userClient) var userClient + @Dependency(\.dismiss) var dismiss + + let reducer = Reduce { state, action in + switch action { + case .onLoad: + return .none + + case let .matchingToggleDidFetched(isOn): + state.isOnMatchingToggle = isOn + return .none + + case .backButtonDidTapped: + return .run { _ in + await dismiss() + } + + case .binding(\.isOnMatchingToggle): + return .run { [isOn = state.isOnMatchingToggle] send in + await send(.toggleDidChanged(alertState: .init(alertType: .randomBottle, enabled: isOn))) + } + .debounce( + id: ID.matcingToggle, + for: 1.0, + scheduler: DispatchQueue.main) + + case let .toggleDidChanged(alertState): + return .run { send in + try await userClient.updateAlertState(alertState: alertState) + } + + default: + return .none + } + + + } + self.init(reducer: reducer) + } +} diff --git a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift new file mode 100644 index 00000000..067a52dc --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift @@ -0,0 +1,53 @@ +// +// AccountSettingFeatureInterface.swift +// FeatureMyPageInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +import DomainUserInterface + +import ComposableArchitecture + +@Reducer +public struct AccountSettingFeature { + private let reducer: Reduce + + public init(reducer: Reduce) { + self.reducer = reducer + } + + @ObservableState + public struct State: Equatable { + public var isOnMatchingToggle: Bool + + public init( + isOnMatchingToggle: Bool = false + ) { + self.isOnMatchingToggle = isOnMatchingToggle + } + } + + public enum Action: BindableAction { + case onLoad + + case matchingToggleDidFetched(isOn: Bool) + + // UserAction + case toggleDidChanged(alertState: UserAlertState) + case backButtonDidTapped + + case binding(BindingAction) + } + + enum ID: Hashable { + case matcingToggle + } + + public var body: some ReducerOf { + BindingReducer() + reducer + } +} From 9caf4db1e09653a82f00c28324c897e51f3d3a47 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 17:35:41 +0900 Subject: [PATCH 34/90] =?UTF-8?q?feat:=20AccountSettingView=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountSetting/AccountSettingView.swift | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingView.swift diff --git a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingView.swift b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingView.swift new file mode 100644 index 00000000..7818afb1 --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingView.swift @@ -0,0 +1,69 @@ +// +// AccountSettingView.swift +// FeatureMyPageInterface +// +// Created by 임현규 on 9/21/24. +// + +import SwiftUI + +import SharedDesignSystem + +import ComposableArchitecture + +public struct AccountSettingView: View { + @Perception.Bindable private var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithPerceptionTracking { + + VStack(spacing: 0) { + VStack(spacing: .lg) { + matchingToggle + logoutList + withdrawList + } + .padding(.horizontal, .md) + .padding(.vertical, .xl) + .overlay(roundedRectangle) + .padding(.top, 32) + Spacer() + } + .padding(.horizontal, .lg) + .setNavigationBar { + makeNaivgationleftButton { store.send(.backButtonDidTapped) } + } + .onLoad { store.send(.onLoad) } + } + } +} + +private extension AccountSettingView { + var roundedRectangle: some View { + RoundedRectangle(cornerRadius: BottleRadiusType.xl.value) + .strokeBorder( + ColorToken.border(.primary).color, + lineWidth: 1 + ) + } + + var matchingToggle: some View { + ToggleListView( + title: "매칭 활성화", + subTitle: "비활성화 시 다른 사람을 추천 받을 수 없고\n회원님도 다른 사람에게 추천되지 않아요", + isOn: $store.isOnMatchingToggle + ) + } + + var logoutList: some View { + ArrowListView(title: "로그아웃") + } + + var withdrawList: some View { + ArrowListView(title: "탈퇴하기") + } +} From c48cec434ff236b0e8ecb7386b8fd716ba5a1179 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 17:35:59 +0900 Subject: [PATCH 35/90] =?UTF-8?q?feat:=20=EA=B3=84=EC=A0=95=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MyPage/Interface/Sources/MyPage/MyPageFeature.swift | 2 ++ .../Interface/Sources/MyPage/MyPageFeatureInterface.swift | 3 +++ .../MyPage/Interface/Sources/MyPage/MyPageView.swift | 1 + .../MyPage/Interface/Sources/MyPageRootFeature.swift | 1 + .../Interface/Sources/MyPageRootFeatureInterface.swift | 4 ++++ .../Feature/MyPage/Interface/Sources/MyPageRootView.swift | 6 ++++++ 6 files changed, 17 insertions(+) diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift index a39d39c9..55970d74 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift @@ -117,6 +117,8 @@ extension MyPageFeature { case .alertSettingListDidTapped: return .send(.delegate(.alertSettingListDidTapped)) + case .accountSettingListDidTapped: + return .send(.delegate(.accountSettingListDidTapped)) default: return .none } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift index e8801d98..04eb6f82 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift @@ -51,6 +51,8 @@ public struct MyPageFeature { case withdrawalDidCompleted case selectedTabDidChanged(TabType) case alertSettingListDidTapped + case accountSettingListDidTapped + case delegate(Delegate) public enum Delegate { @@ -59,6 +61,7 @@ public struct MyPageFeature { case logoutDidCompleted case selectedTabDidChanged(TabType) case alertSettingListDidTapped + case accountSettingListDidTapped } case alert(Alert) diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift index 99f54b3c..92cc1691 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift @@ -120,6 +120,7 @@ private extension MyPageView { var accountSettingList: some View { ArrowListView(title: "계정 관리") + .asThrottleButton(action: { store.send(.accountSettingListDidTapped) }) } var appVersionList: some View { diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift index 664e8a2a..4ae57b4d 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift @@ -22,6 +22,7 @@ public struct MyPageRootFeature { @Reducer(state: .equatable) public enum Path { case AlertSetting(AlertSettingFeature) + case AccountSetting(AccountSettingFeature) } @ObservableState diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift index a22d5cdf..734cd367 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift @@ -27,6 +27,10 @@ extension MyPageRootFeature { state.path.append(.AlertSetting(.init())) return .none + case .accountSettingListDidTapped: + state.path.append(.AccountSetting(.init())) + return .none + default: return .none } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift index 9433cd65..ee414a44 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift @@ -34,6 +34,12 @@ public struct MyPageRootView: View { action: \.AlertSetting) { AlertSettingView(store: store) } + case .AccountSetting: + if let store = store.scope( + state: \.AccountSetting, + action: \.AccountSetting) { + AccountSettingView(store: store) + } } } } From 311c5a072054df0bbb609af960456e43af9fd00b Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 18:13:41 +0900 Subject: [PATCH 36/90] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83,=20=ED=83=88=ED=87=B4=ED=95=98=EA=B8=B0=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountSettingFeature.swift | 59 ++++++++++++++++++- .../AccountSettingFeatureInterface.swift | 33 ++++++++++- .../AccountSetting/AccountSettingView.swift | 3 + .../Sources/MyPageRootFeatureInterface.swift | 14 +++++ 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift index 8ca08294..8f101d27 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift @@ -8,13 +8,17 @@ import Foundation import DomainUser +import DomainAuth import DomainUserInterface +import CoreKeyChainStore + import ComposableArchitecture extension AccountSettingFeature { public init() { @Dependency(\.userClient) var userClient + @Dependency(\.authClient) var authClient @Dependency(\.dismiss) var dismiss let reducer = Reduce { state, action in @@ -31,6 +35,28 @@ extension AccountSettingFeature { await dismiss() } + case .logoutButtonDidTapped: + state.destination = .alert(.init( + title: { TextState("로그아웃") }, + actions: { + ButtonState(role: .cancel, action: .confirmLogOut, label: { TextState("로그아웃하기") }) + ButtonState(role: .destructive, action: .dismiss, label: { TextState("취소하기") }) + }, + message: { TextState("정말 로그아웃 하시겠어요?") } + )) + return .none + + case .withdrawalButtonDidTapped: + state.destination = .alert(.init( + title: { TextState("탈퇴하기") }, + actions: { + ButtonState(role: .cancel, action: .confirmWithdrawal, label: { TextState("탈퇴하기") }) + ButtonState(role: .destructive, action: .dismiss, label: { TextState("계속 이용하기") }) + }, + message: { TextState("탈퇴 시 계정 복구가 어려워요.\n정말 탈퇴하시겠어요?") } + )) + return .none + case .binding(\.isOnMatchingToggle): return .run { [isOn = state.isOnMatchingToggle] send in await send(.toggleDidChanged(alertState: .init(alertType: .randomBottle, enabled: isOn))) @@ -45,11 +71,40 @@ extension AccountSettingFeature { try await userClient.updateAlertState(alertState: alertState) } + case let .destination(.presented(.alert(alert))): + switch alert { + case .confirmLogOut: + return .run { send in + try await authClient.logout() + await send(.logoutDidCompleted) + } + + case .confirmWithdrawal: + return .run { send in + await send(.delegate(.withdrawalButtonDidTapped)) + try await authClient.withdraw() + if !KeyChainTokenStore.shared.load(property: .AppleUserID).isEmpty { + // clientSceret 받아오기 + let clientSceret = try await authClient.fetchAppleClientSecret() + KeyChainTokenStore.shared.save(property: .AppleClientSecret, value: clientSceret) + try await authClient.revokeAppleLogin() + } + await send(.withdrawalDidCompleted) + } + + case .dismiss: + return .none + } + + case .logoutDidCompleted: + return .send(.delegate(.logoutDidCompleted)) + + case .withdrawalDidCompleted: + return .send(.delegate(.withdrawalDidCompleted)) + default: return .none } - - } self.init(reducer: reducer) } diff --git a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift index 067a52dc..43eb2f01 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift @@ -22,7 +22,8 @@ public struct AccountSettingFeature { @ObservableState public struct State: Equatable { public var isOnMatchingToggle: Bool - + @Presents var destination: Destination.State? + public init( isOnMatchingToggle: Bool = false ) { @@ -38,8 +39,37 @@ public struct AccountSettingFeature { // UserAction case toggleDidChanged(alertState: UserAlertState) case backButtonDidTapped + case logoutButtonDidTapped + case withdrawalButtonDidTapped + case logoutDidCompleted + case withdrawalDidCompleted + // binding case binding(BindingAction) + + // alert + case alert(Alert) + public enum Alert: Equatable { + case confirmLogOut + case confirmWithdrawal + case dismiss + } + + // delegate + case delegate(Delegate) + + public enum Delegate { + case logoutDidCompleted + case withdrawalDidCompleted + case withdrawalButtonDidTapped + } + + case destination(PresentationAction) + } + + @Reducer(state: .equatable) + public enum Destination { + case alert(AlertState) } enum ID: Hashable { @@ -49,5 +79,6 @@ public struct AccountSettingFeature { public var body: some ReducerOf { BindingReducer() reducer + .ifLet(\.$destination, action: \.destination) } } diff --git a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingView.swift b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingView.swift index 7818afb1..0dde8a29 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingView.swift @@ -38,6 +38,7 @@ public struct AccountSettingView: View { makeNaivgationleftButton { store.send(.backButtonDidTapped) } } .onLoad { store.send(.onLoad) } + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) } } } @@ -61,9 +62,11 @@ private extension AccountSettingView { var logoutList: some View { ArrowListView(title: "로그아웃") + .asThrottleButton(action: { store.send(.logoutButtonDidTapped) }) } var withdrawList: some View { ArrowListView(title: "탈퇴하기") + .asThrottleButton(action: { store.send(.withdrawalButtonDidTapped) }) } } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift index 734cd367..79612a63 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift @@ -35,6 +35,20 @@ extension MyPageRootFeature { return .none } + // AccountSetting Delegate + case let .path(.element(id: _, action: .AccountSetting(.delegate(delegate)))): + switch delegate { + case .logoutDidCompleted: + return .send(.delegate(.logoutDidCompleted)) + + case .withdrawalButtonDidTapped: + return .send(.delegate(.withdrawalButtonDidTapped)) + + case .withdrawalDidCompleted: + return .send(.delegate(.withdrawalDidCompleted)) + } + + default: return .none } From 86991de51673cda95f54e2ae89d571eb10dee5d0 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 18:38:44 +0900 Subject: [PATCH 37/90] =?UTF-8?q?feat:=20=EB=A7=A4=EC=B9=AD=20=ED=86=A0?= =?UTF-8?q?=EA=B8=80=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20API=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/API/ProfileAPI.swift | 7 +++++++ .../DTO/Request/MatchingActivateRequestDTO.swift | 16 ++++++++++++++++ .../Interface/Sources/ProfileClient.swift | 10 ++++++++-- .../Domain/Profile/Sources/ProfileClient.swift | 4 ++++ .../AccountSetting/AccountSettingFeature.swift | 10 +++++----- .../AccountSettingFeatureInterface.swift | 2 +- 6 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 Projects/Domain/Profile/Interface/Sources/DTO/Request/MatchingActivateRequestDTO.swift diff --git a/Projects/Domain/Profile/Interface/Sources/API/ProfileAPI.swift b/Projects/Domain/Profile/Interface/Sources/API/ProfileAPI.swift index 2e8d5367..be5c3f4c 100644 --- a/Projects/Domain/Profile/Interface/Sources/API/ProfileAPI.swift +++ b/Projects/Domain/Profile/Interface/Sources/API/ProfileAPI.swift @@ -17,6 +17,7 @@ public enum ProfileAPI { case checkIntroduction case uploadProfileImage(data: Data) case fetchUserProfileStatus + case updateMachingActivate(requestData: MatchingActivateRequestDTO) } extension ProfileAPI: BaseTargetType { @@ -32,6 +33,8 @@ extension ProfileAPI: BaseTargetType { return "api/v1/profile/images" case .fetchUserProfileStatus: return "api/v1/profile/status" + case .updateMachingActivate: + return "api/v1/profile/activate/matching" } } @@ -47,6 +50,8 @@ extension ProfileAPI: BaseTargetType { return .post case .fetchUserProfileStatus: return .get + case .updateMachingActivate: + return .post } } @@ -69,6 +74,8 @@ extension ProfileAPI: BaseTargetType { return .uploadMultipart([imageData]) case .fetchUserProfileStatus: return .requestPlain + case let .updateMachingActivate(requestData): + return .requestJSONEncodable(requestData) } } } diff --git a/Projects/Domain/Profile/Interface/Sources/DTO/Request/MatchingActivateRequestDTO.swift b/Projects/Domain/Profile/Interface/Sources/DTO/Request/MatchingActivateRequestDTO.swift new file mode 100644 index 00000000..b8e4be19 --- /dev/null +++ b/Projects/Domain/Profile/Interface/Sources/DTO/Request/MatchingActivateRequestDTO.swift @@ -0,0 +1,16 @@ +// +// MatchingActivateRequestDTO.swift +// DomainProfileInterface +// +// Created by 임현규 on 9/21/24. +// + +import Foundation + +public struct MatchingActivateRequestDTO: Encodable { + private let activate: Bool + + public init(activate: Bool) { + self.activate = activate + } +} diff --git a/Projects/Domain/Profile/Interface/Sources/ProfileClient.swift b/Projects/Domain/Profile/Interface/Sources/ProfileClient.swift index 3dcfe4ed..44571bb8 100644 --- a/Projects/Domain/Profile/Interface/Sources/ProfileClient.swift +++ b/Projects/Domain/Profile/Interface/Sources/ProfileClient.swift @@ -15,14 +15,15 @@ public struct ProfileClient { private var uploadProfileImage: (Data) async throws -> Void private var fetchUserProfile: () async throws -> UserProfile private var fetchUserProfileSelect: () async throws -> UserProfileStatus - + private var updateMatchingActivate: (Bool) async throws -> Void public init( checkExistIntroduction: @escaping () async throws -> Bool, registerIntroduction: @escaping (String) async throws -> Void, fetchProfileSelect: @escaping () async throws -> ProfileSelect, uploadProfileImage: @escaping (Data) async throws -> Void, fetchUserProfile: @escaping () async throws -> UserProfile, - fetchUserProfileSelect: @escaping () async throws -> UserProfileStatus + fetchUserProfileSelect: @escaping () async throws -> UserProfileStatus, + updateMatchingActivate: @escaping (Bool) async throws -> Void ) { self.checkExistIntroduction = checkExistIntroduction self.registerIntroduction = registerIntroduction @@ -30,6 +31,7 @@ public struct ProfileClient { self.uploadProfileImage = uploadProfileImage self.fetchUserProfile = fetchUserProfile self.fetchUserProfileSelect = fetchUserProfileSelect + self.updateMatchingActivate = updateMatchingActivate } public func checkExistIntroduction() async throws -> Bool { @@ -55,5 +57,9 @@ public struct ProfileClient { public func fetchUserProfileSelect() async throws -> UserProfileStatus { try await fetchUserProfileSelect() } + + public func updateMatcingActivate(isActive: Bool) async throws { + try await updateMatchingActivate(isActive) + } } diff --git a/Projects/Domain/Profile/Sources/ProfileClient.swift b/Projects/Domain/Profile/Sources/ProfileClient.swift index d6265d3f..d0c36466 100644 --- a/Projects/Domain/Profile/Sources/ProfileClient.swift +++ b/Projects/Domain/Profile/Sources/ProfileClient.swift @@ -46,6 +46,10 @@ extension ProfileClient: DependencyKey { let responseData = try await networkManager.reqeust(api: .apiType(ProfileAPI.fetchUserProfileStatus), dto: ProfileStatusResponseDTO.self) let userStatus = responseData.toDomain() return userStatus + }, + updateMatchingActivate: { isActive in + let requestData = MatchingActivateRequestDTO(activate: isActive) + try await networkManager.reqeust(api: .apiType(ProfileAPI.updateMachingActivate(requestData: requestData))) } ) } diff --git a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift index 8f101d27..27372a6a 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift @@ -7,8 +7,8 @@ import Foundation -import DomainUser import DomainAuth +import DomainProfile import DomainUserInterface import CoreKeyChainStore @@ -17,7 +17,7 @@ import ComposableArchitecture extension AccountSettingFeature { public init() { - @Dependency(\.userClient) var userClient + @Dependency(\.profileClient) var profileClient @Dependency(\.authClient) var authClient @Dependency(\.dismiss) var dismiss @@ -59,16 +59,16 @@ extension AccountSettingFeature { case .binding(\.isOnMatchingToggle): return .run { [isOn = state.isOnMatchingToggle] send in - await send(.toggleDidChanged(alertState: .init(alertType: .randomBottle, enabled: isOn))) + await send(.matchingToggleDidChanged(isOn: isOn)) } .debounce( id: ID.matcingToggle, for: 1.0, scheduler: DispatchQueue.main) - case let .toggleDidChanged(alertState): + case let .matchingToggleDidChanged(isOn): return .run { send in - try await userClient.updateAlertState(alertState: alertState) + try await profileClient.updateMatcingActivate(isActive: isOn) } case let .destination(.presented(.alert(alert))): diff --git a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift index 43eb2f01..adb9473b 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeatureInterface.swift @@ -37,7 +37,7 @@ public struct AccountSettingFeature { case matchingToggleDidFetched(isOn: Bool) // UserAction - case toggleDidChanged(alertState: UserAlertState) + case matchingToggleDidChanged(isOn: Bool) case backButtonDidTapped case logoutButtonDidTapped case withdrawalButtonDidTapped From 882e7ebbe01d26f33b756510919167795049e438 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sat, 21 Sep 2024 20:26:14 +0900 Subject: [PATCH 38/90] =?UTF-8?q?feat:=20=EA=B3=84=EC=A0=95=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=A7=84=EC=9E=85=20=EC=8B=9C=20=EB=A7=A4=EC=B9=AD?= =?UTF-8?q?=20=ED=99=9C=EC=84=B1=ED=99=94=20=EC=97=AC=EB=B6=80=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/DTO/Response/ProfileResponseDTO.swift | 9 +++++++-- .../Sources/AccountSetting/AccountSettingFeature.swift | 8 +++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Projects/Domain/Profile/Interface/Sources/DTO/Response/ProfileResponseDTO.swift b/Projects/Domain/Profile/Interface/Sources/DTO/Response/ProfileResponseDTO.swift index 240ef5e0..578d5973 100644 --- a/Projects/Domain/Profile/Interface/Sources/DTO/Response/ProfileResponseDTO.swift +++ b/Projects/Domain/Profile/Interface/Sources/DTO/Response/ProfileResponseDTO.swift @@ -12,6 +12,7 @@ public struct ProfileResponseDTO: Decodable { public let userName: String? public let imageUrl: String? public let age: Int? + public let isMatchActivated: Bool? public let introduction: [IntroductionDTO]? public let profileSelect: ProfileSelectDTO? @@ -77,7 +78,9 @@ public struct ProfileResponseDTO: Decodable { userInfo: UserInfo( userAge: age ?? -1, userImageURL: imageUrl ?? "", - userName: userName ?? ""), + userName: userName ?? "", + isActiveMatching: isMatchActivated ?? false + ), introduction: Introduction(answer: introduction?.first?.answer ?? "", question: introduction?.first?.question ?? ""), profileSelect: profileSelect?.toDomain() ?? ProfileSelect( mbti: "", @@ -105,11 +108,13 @@ public struct UserInfo: Equatable { public let userAge: Int public let userImageURL: String public let userName: String + public let isActiveMatching: Bool - public init(userAge: Int, userImageURL: String, userName: String) { + public init(userAge: Int, userImageURL: String, userName: String, isActiveMatching: Bool = false) { self.userAge = userAge self.userImageURL = userImageURL self.userName = userName + self.isActiveMatching = isActiveMatching } } diff --git a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift index 27372a6a..b3ad4753 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift @@ -24,8 +24,10 @@ extension AccountSettingFeature { let reducer = Reduce { state, action in switch action { case .onLoad: - return .none - + return .run { send in + let profile = try await profileClient.fetchUserProfile() + await send(.matchingToggleDidFetched(isOn: profile.userInfo.isActiveMatching)) + } case let .matchingToggleDidFetched(isOn): state.isOnMatchingToggle = isOn return .none @@ -63,7 +65,7 @@ extension AccountSettingFeature { } .debounce( id: ID.matcingToggle, - for: 1.0, + for: 0.5, scheduler: DispatchQueue.main) case let .matchingToggleDidChanged(isOn): From 632ce68131c5b0cb380a8171915fa7f14e32bcc3 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Sun, 22 Sep 2024 16:39:11 +0900 Subject: [PATCH 39/90] =?UTF-8?q?[Feature/#251]=20=EC=97=B0=EB=9D=BD?= =?UTF-8?q?=EC=B2=98=20=EC=B0=A8=EB=8B=A8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#256)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 연락처 차단 기능 구현 * refactor: BlockContactRequestDTO의 blockContacts 프로퍼티 접근제한자 수정 * fix: 누락된 의존성 주입 * refactor: 연락처 trim 로직 개선 --- Projects/Core/Network/Project.swift | 3 +- .../User/Interface/Sources/API/UserAPI.swift | 7 +++ .../DTO/Request/BlockContactRequestDTO.swift | 14 +++++ .../User/Interface/Sources/UserClient.swift | 17 +++++- .../User/Interface/Sources/UserError.swift | 13 +++++ Projects/Domain/User/Sources/UserClient.swift | 37 ++++++++++++- .../Sources/MyPage/MyPageFeature.swift | 55 +++++++++++++++++++ .../MyPage/MyPageFeatureInterface.swift | 9 +++ .../Interface/Sources/MyPage/MyPageView.swift | 13 ++++- .../DesignSystemThirdPartyLib/Project.swift | 3 +- .../InfoPlist+Templates.swift | 2 + 11 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 Projects/Domain/User/Interface/Sources/DTO/Request/BlockContactRequestDTO.swift create mode 100644 Projects/Domain/User/Interface/Sources/UserError.swift diff --git a/Projects/Core/Network/Project.swift b/Projects/Core/Network/Project.swift index 91468fd7..cc98e573 100644 --- a/Projects/Core/Network/Project.swift +++ b/Projects/Core/Network/Project.swift @@ -18,7 +18,8 @@ let project = Project.makeModule( factory: .init( dependencies: [ .core(interface: .Network), - .core(interface: .Logger) + .core(interface: .Logger), + .core(implements: .KeyChainStore) ] ) ), diff --git a/Projects/Domain/User/Interface/Sources/API/UserAPI.swift b/Projects/Domain/User/Interface/Sources/API/UserAPI.swift index db5cdc18..59a2206d 100644 --- a/Projects/Domain/User/Interface/Sources/API/UserAPI.swift +++ b/Projects/Domain/User/Interface/Sources/API/UserAPI.swift @@ -14,6 +14,7 @@ import Moya public enum UserAPI { case fetchAlertState case updateAlertState(reqeustData: AlertStateRequestDTO) + case updateBlockContacts(blockContactRequestDTO: BlockContactRequestDTO) } extension UserAPI: BaseTargetType { @@ -23,6 +24,8 @@ extension UserAPI: BaseTargetType { return "api/v1/user/alimy" case .updateAlertState: return "api/v1/user/alimy" + case .updateBlockContacts: + return "api/v1/user/block/contact-list" } } @@ -32,6 +35,8 @@ extension UserAPI: BaseTargetType { return .get case .updateAlertState: return .post + case .updateBlockContacts: + return .post } } @@ -41,6 +46,8 @@ extension UserAPI: BaseTargetType { return .requestPlain case .updateAlertState(let requestData): return .requestJSONEncodable(requestData) + case let .updateBlockContacts(blockContactRequestDTO): + return .requestJSONEncodable(blockContactRequestDTO) } } } diff --git a/Projects/Domain/User/Interface/Sources/DTO/Request/BlockContactRequestDTO.swift b/Projects/Domain/User/Interface/Sources/DTO/Request/BlockContactRequestDTO.swift new file mode 100644 index 00000000..3a3a2a1f --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/DTO/Request/BlockContactRequestDTO.swift @@ -0,0 +1,14 @@ +// +// BlockContactRequestDTO.swift +// DomainUserInterface +// +// Created by JongHoon on 9/22/24. +// + +public struct BlockContactRequestDTO: Encodable { + private let blockContacts: [String] + + public init(blockContacts: [String]) { + self.blockContacts = blockContacts + } +} diff --git a/Projects/Domain/User/Interface/Sources/UserClient.swift b/Projects/Domain/User/Interface/Sources/UserClient.swift index b54308c3..e80d44c8 100644 --- a/Projects/Domain/User/Interface/Sources/UserClient.swift +++ b/Projects/Domain/User/Interface/Sources/UserClient.swift @@ -16,7 +16,8 @@ public struct UserClient { private let updateFcmToken: (String) -> Void private let _fetchAlertState: () async throws -> [UserAlertState] private let updateAlertState: (UserAlertState) async throws -> Void - + private let fetchContacts: () async throws -> [String] + private let updateBlockContacts: ([String]) async throws -> Void public init( isLoggedIn: @escaping () -> Bool, @@ -26,7 +27,9 @@ public struct UserClient { updateDeleteState: @escaping (Bool) -> Void, updateFcmToken: @escaping (String) -> Void, fetchAlertState: @escaping () async throws -> [UserAlertState], - updateAlertState: @escaping (UserAlertState) async throws -> Void + updateAlertState: @escaping (UserAlertState) async throws -> Void, + fetchContacts: @escaping () async throws -> [String], + updateBlockContacts: @escaping ([String]) async throws -> Void ) { self._isLoggedIn = isLoggedIn self._isAppDeleted = isAppDeleted @@ -36,6 +39,8 @@ public struct UserClient { self.updateFcmToken = updateFcmToken self._fetchAlertState = fetchAlertState self.updateAlertState = updateAlertState + self.fetchContacts = fetchContacts + self.updateBlockContacts = updateBlockContacts } public func isLoggedIn() -> Bool { @@ -69,4 +74,12 @@ public struct UserClient { public func updateAlertState(alertState: UserAlertState) async throws { try await updateAlertState(alertState) } + + public func fetchContacts() async throws -> [String] { + try await fetchContacts() + } + + public func updateBlockContacts(contacts: [String]) async throws { + try await updateBlockContacts(contacts) + } } diff --git a/Projects/Domain/User/Interface/Sources/UserError.swift b/Projects/Domain/User/Interface/Sources/UserError.swift new file mode 100644 index 00000000..789f2d7f --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/UserError.swift @@ -0,0 +1,13 @@ +// +// UserError.swift +// DomainUserInterface +// +// Created by JongHoon on 9/22/24. +// + +import Foundation + +public enum UserError: Error { + case requestContactsAccessAuthorityFailed + case contactsAccessDenied +} diff --git a/Projects/Domain/User/Sources/UserClient.swift b/Projects/Domain/User/Sources/UserClient.swift index b14bfaa7..a818cc4d 100644 --- a/Projects/Domain/User/Sources/UserClient.swift +++ b/Projects/Domain/User/Sources/UserClient.swift @@ -6,6 +6,7 @@ // import Foundation +import Contacts import DomainUserInterface @@ -51,10 +52,44 @@ extension UserClient: DependencyKey { return responseData.map { $0.toDomain() } }, - updateAlertState: { alertState in let requestData = AlertStateRequestDTO(alertType: alertState.alertType, enabled: alertState.enabled) try await networkManager.reqeust(api: .apiType(UserAPI.updateAlertState(reqeustData: requestData))) + }, + fetchContacts: { + let store = CNContactStore() + var contacts: [String] = [] + let keys = [CNContactPhoneNumbersKey] as [CNKeyDescriptor] + + let request = CNContactFetchRequest(keysToFetch: keys) + request.sortOrder = CNContactSortOrder.userDefault + + let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts) + guard authorizationStatus == .authorized || + authorizationStatus == .notDetermined + else { + throw UserError.contactsAccessDenied + } + + let granted = try await store.requestAccess(for: .contacts) + guard granted + else { + throw UserError.requestContactsAccessAuthorityFailed + } + + try store.enumerateContacts(with: request) { contact, _ in + contacts = contact.phoneNumbers + .map { $0.value.stringValue } + .map { $0.replacingOccurrences(of: "+82", with: "0") } + .map { $0.trimmingCharacters(in: .whitespaces) } + .map { $0.filter { $0.isNumber } } + } + + return contacts + }, + updateBlockContacts: { contacts in + let blockContactRequestDTO = BlockContactRequestDTO(blockContacts: contacts) + try await networkManager.reqeust(api: .apiType(UserAPI.updateBlockContacts(blockContactRequestDTO: blockContactRequestDTO))) } ) } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift index 55970d74..3b601714 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift @@ -9,10 +9,14 @@ import Foundation import DomainAuth import DomainProfile +import DomainUserInterface + import CoreKeyChainStore import CoreToastInterface import CoreLoggerInterface + import SharedDesignSystem + import ComposableArchitecture extension MyPageFeature { @@ -20,6 +24,8 @@ extension MyPageFeature { @Dependency(\.authClient) var authClient @Dependency(\.toastClient) var toastClient @Dependency(\.profileClient) var profileClient + @Dependency(\.userClient) var userClient + let reducer = Reduce { state, action in switch action { case .onLoad: @@ -71,6 +77,10 @@ extension MyPageFeature { } await send(.withdrawalDidCompleted) } + + case .dismissAlert: + state.destination = nil + return .none } case .userProfileDidFetched(let userProfile): @@ -114,11 +124,56 @@ extension MyPageFeature { await send(.userProfileDidFetched(userProfile)) } + case .updatePhoneNumberForBlockButtonDidTapped: + return .run { send in + await send(.configureLoadingProgressView(isShow: true)) + let contacts = try await userClient.fetchContacts() + try await userClient.updateBlockContacts(contacts: contacts) + await send(.updatePhoneNumberForBlockCompleted(count: contacts.count)) + await send(.configureLoadingProgressView(isShow: false)) + } catch: { error, send in + await send(.configureLoadingProgressView(isShow: false)) + if let userError = error as? UserError { + switch userError { + case .requestContactsAccessAuthorityFailed: + Log.debug("연락처 접근 요정 거부") + case .contactsAccessDenied: + await send(.contactsAccessDeniedErrorOccurred) + } + } + } + + case let .updatePhoneNumberForBlockCompleted(count): + state.blockedContactsCount = count + return .none + + case .contactsAccessDeniedErrorOccurred: + state.destination = .alert(.init( + title: { + TextState("안내") + }, + actions: { + ButtonState( + action: .dismissAlert, + label: { TextState("확인") } + ) + }, + message: { + TextState("설정 > 개인정보 보호 및 보안 > 연락처에서 '보틀'의 연락처 접근을 허락해 주세요.") + } + )) + return .none + case .alertSettingListDidTapped: return .send(.delegate(.alertSettingListDidTapped)) case .accountSettingListDidTapped: return .send(.delegate(.accountSettingListDidTapped)) + + case let .configureLoadingProgressView(isShow): + state.isShowLoadingProgressView = isShow + return .none + default: return .none } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift index 04eb6f82..f525e64f 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift @@ -27,6 +27,7 @@ public struct MyPageFeature { public var keywordItem: [ClipItem] public var userInfo: UserInfo public var introduction: Introduction + public var blockedContactsCount: Int @Presents var destination: Destination.State? @@ -37,14 +38,19 @@ public struct MyPageFeature { self.keywordItem = keywordItem self.userInfo = .init(userAge: -1, userImageURL: "", userName: "") self.introduction = .init(answer: "", question: "") + self.blockedContactsCount = 0 } } public enum Action: BindableAction { // View Life Cycle case onLoad + case userProfileDidFetched(UserProfile) case userProfileUpdateDidRequest + case updatePhoneNumberForBlockButtonDidTapped + case updatePhoneNumberForBlockCompleted(count: Int) + case contactsAccessDeniedErrorOccurred case logOutButtonDidTapped case logOutDidCompleted case withdrawalButtonDidTapped @@ -53,6 +59,8 @@ public struct MyPageFeature { case alertSettingListDidTapped case accountSettingListDidTapped + case configureLoadingProgressView(isShow: Bool) + case delegate(Delegate) public enum Delegate { @@ -68,6 +76,7 @@ public struct MyPageFeature { public enum Alert: Equatable { case confirmLogOut case confirmWithdrawal + case dismissAlert } // ETC diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift index 92cc1691..adf042e0 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift @@ -6,13 +6,18 @@ // import SwiftUI +import Contacts import FeatureTabBarInterface import FeatureBaseWebViewInterface + +import CoreLoggerInterface + import SharedDesignSystem import ComposableArchitecture + public struct MyPageView: View { @Perception.Bindable private var store: StoreOf @@ -63,7 +68,7 @@ public struct MyPageView: View { } } } - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) } } } @@ -107,9 +112,11 @@ private extension MyPageView { var blockPhoneNumberList: some View { ButtonListView( title: "연락처 차단", - subTitle: "연락처 속 0명을 차단했어요", + subTitle: "연락처 속 \(store.blockedContactsCount)명을 차단했어요", buttonTitle: "업데이트", - action: {} + action: { + store.send(.updatePhoneNumberForBlockButtonDidTapped) + } ) } diff --git a/Projects/Shared/DesignSystemThirdPartyLib/Project.swift b/Projects/Shared/DesignSystemThirdPartyLib/Project.swift index f896a148..21d0701b 100644 --- a/Projects/Shared/DesignSystemThirdPartyLib/Project.swift +++ b/Projects/Shared/DesignSystemThirdPartyLib/Project.swift @@ -10,7 +10,8 @@ let project = Project.makeModule( factory: .init( dependencies: [ .SPM.Kingfisher, - .SPM.Lottie + .SPM.Lottie, + .SPM.ComposableArchitecture ] ) ), diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index d1f8d7f0..e14b5e5d 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -22,6 +22,7 @@ public extension InfoPlist { "UISupportedInterfaceOrientations": [ "UIInterfaceOrientationPortrait" ], + "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다.", "BASE_URL": "$(BASE_URL)", "WEB_VIEW_BASE_URL": "$(WEB_VIEW_BASE_URL)", "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", @@ -47,6 +48,7 @@ public extension InfoPlist { "UISupportedInterfaceOrientations": [ "UIInterfaceOrientationPortrait" ], + "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다.", "BASE_URL": "$(BASE_URL)", "WEB_VIEW_BASE_URL": "$(WEB_VIEW_BASE_URL)", "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", From f456fb443dcdc37cd1f3eae717c38de0e4e5c98c 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: Sun, 22 Sep 2024 21:57:29 +0900 Subject: [PATCH 40/90] =?UTF-8?q?[Feature/#259]=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=201:1=20=EB=AC=B8=EC=9D=98=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#260)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 카카오톡 채널 톡 URL 추가 * feat: 1:1 문의 버튼 클릭 시 카카오톡 채널 톡으로 이동 * style: 줄바꿈 수정 --- .../Core/URLHandler/Interface/Sources/BottleURLType.swift | 3 +++ .../MyPage/Interface/Sources/MyPage/MyPageFeature.swift | 5 +++++ .../Interface/Sources/MyPage/MyPageFeatureInterface.swift | 1 + .../Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift | 1 + Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift | 2 ++ 5 files changed, 12 insertions(+) diff --git a/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift b/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift index 48851688..3fb053db 100644 --- a/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift +++ b/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift @@ -9,11 +9,14 @@ import Foundation public enum BottleURLType { case bottleAppStore + case kakaoChannelTalk public var url: URL { switch self { case .bottleAppStore: return URL(string: Bundle.main.infoDictionary?["APP_STORE_URL"] as? String ?? "")! + case .kakaoChannelTalk: + return URL(string: Bundle.main.infoDictionary?["KAKAO_CHANNEL_TALK_URL"] as? String ?? "")! } } } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift index 3b601714..5881dbb4 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift @@ -14,6 +14,7 @@ import DomainUserInterface import CoreKeyChainStore import CoreToastInterface import CoreLoggerInterface +import CoreURLHandlerInterface import SharedDesignSystem @@ -170,6 +171,10 @@ extension MyPageFeature { case .accountSettingListDidTapped: return .send(.delegate(.accountSettingListDidTapped)) + case .contactListDidTapped: + URLHandler.shared.openURL(urlType: .kakaoChannelTalk) + return .none + case let .configureLoadingProgressView(isShow): state.isShowLoadingProgressView = isShow return .none diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift index f525e64f..3a32ba59 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift @@ -58,6 +58,7 @@ public struct MyPageFeature { case selectedTabDidChanged(TabType) case alertSettingListDidTapped case accountSettingListDidTapped + case contactListDidTapped case configureLoadingProgressView(isShow: Bool) diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift index adf042e0..d72a2e98 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift @@ -136,6 +136,7 @@ private extension MyPageView { var contactList: some View { ArrowListView(title: "1:1 문의") + .asThrottleButton(action: { store.send(.contactListDidTapped) }) } var termsOfServiceList: some View { diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index e14b5e5d..4be2de0a 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -27,6 +27,7 @@ public extension InfoPlist { "WEB_VIEW_BASE_URL": "$(WEB_VIEW_BASE_URL)", "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", "APP_STORE_URL": "$(APP_STORE_URL)", + "KAKAO_CHANNEL_TALK_URL": "$(KAKAO_CHANNEL_TALK_URL)", "LSApplicationQueriesSchemes": ["kakaokompassauth", "kakaotalk"], "CFBundleURLTypes": [ [ @@ -53,6 +54,7 @@ public extension InfoPlist { "WEB_VIEW_BASE_URL": "$(WEB_VIEW_BASE_URL)", "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", "APP_STORE_URL": "$(APP_STORE_URL)", + "KAKAO_CHANNEL_TALK_URL": "$(KAKAO_CHANNEL_TALK_URL)", "LSApplicationQueriesSchemes": ["kakaokompassauth", "kakaotalk"], "CFBundleURLTypes": [ [ From b6a4f16b4ef6c0580bcf91810bbd86d1465b534d 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: Sun, 22 Sep 2024 22:00:12 +0900 Subject: [PATCH 41/90] =?UTF-8?q?feat:=20=EC=9D=B4=EC=9A=A9=EC=95=BD?= =?UTF-8?q?=EA=B4=80,=20=EA=B0=9C=EC=9D=B8=EC=A0=95=EB=B3=B4=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#262)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/MyPage/MyPageFeature.swift | 15 +++++++++++++++ .../Sources/MyPage/MyPageFeatureInterface.swift | 6 ++++++ .../Interface/Sources/MyPage/MyPageView.swift | 8 ++++++++ 3 files changed, 29 insertions(+) diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift index 5881dbb4..f81b9289 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift @@ -171,6 +171,21 @@ extension MyPageFeature { case .accountSettingListDidTapped: return .send(.delegate(.accountSettingListDidTapped)) + case .termsOfServiceListDidTapped: + state.isPresentTerms = true + state.temrsURL = "https://spiral-ogre-a4d.notion.site/240724-e3676639ea864147bb293cfcda40d99f" + return .none + + case .privacyPolicyListDidTapped: + state.isPresentTerms = true + state.temrsURL = "https://spiral-ogre-a4d.notion.site/abb2fd284516408e8c2fc267d07c6421" + return .none + + case .termsWebViewDidDismiss: + state.isPresentTerms = false + state.temrsURL = "" + return .none + case .contactListDidTapped: URLHandler.shared.openURL(urlType: .kakaoChannelTalk) return .none diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift index 3a32ba59..6f38bda5 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift @@ -28,6 +28,8 @@ public struct MyPageFeature { public var userInfo: UserInfo public var introduction: Introduction public var blockedContactsCount: Int + public var isPresentTerms: Bool + public var temrsURL: String? @Presents var destination: Destination.State? @@ -39,6 +41,7 @@ public struct MyPageFeature { self.userInfo = .init(userAge: -1, userImageURL: "", userName: "") self.introduction = .init(answer: "", question: "") self.blockedContactsCount = 0 + self.isPresentTerms = false } } @@ -58,6 +61,9 @@ public struct MyPageFeature { case selectedTabDidChanged(TabType) case alertSettingListDidTapped case accountSettingListDidTapped + case termsOfServiceListDidTapped + case privacyPolicyListDidTapped + case termsWebViewDidDismiss case contactListDidTapped case configureLoadingProgressView(isShow: Bool) diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift index d72a2e98..ef0b54e9 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift @@ -10,6 +10,7 @@ import Contacts import FeatureTabBarInterface import FeatureBaseWebViewInterface +import FeatureGeneralSignUpInterface import CoreLoggerInterface @@ -69,6 +70,11 @@ public struct MyPageView: View { } } .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) + .sheet(isPresented: $store.isPresentTerms) { + store.send(.termsWebViewDidDismiss) + } content: { + TermsWebView(url: store.temrsURL ?? "") + } } } } @@ -141,9 +147,11 @@ private extension MyPageView { var termsOfServiceList: some View { ArrowListView(title: "보틀 이용 약관") + .asThrottleButton(action: { store.send(.termsOfServiceListDidTapped) }) } var privacyPolicyList: some View { ArrowListView(title: "개인정보처리방침") + .asThrottleButton(action: { store.send(.privacyPolicyListDidTapped) }) } } From 9548a32c9668ab357c0fb8d8ab668912a3bf3c66 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Sun, 22 Sep 2024 22:23:57 +0900 Subject: [PATCH 42/90] =?UTF-8?q?[Feature/#257]=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=95=B1=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#258)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 마이페이지 버전 체크 및 업데이트 기능 구현 * feat: 연락처 차단 로직 수정 --------- Co-authored-by: leemhyungyu --- .../ProjectDescriptionHelpers/Modules.swift | 1 + .../Interface/Sources/BottleURLType.swift | 5 ++ .../Interface/Sources/ApplicationClient.swift | 36 ++++++++++ Projects/Domain/Application/Project.swift | 44 +++++++++++++ .../Sources/ApplicationClient.swift | 66 +++++++++++++++++++ .../Testing/Sources/ApplicationTesting.swift | 1 + .../Tests/Sources/ApplicationTest.swift | 11 ++++ Projects/Domain/User/Sources/UserClient.swift | 2 +- .../Sources/MyPage/MyPageFeature.swift | 20 ++++++ .../MyPage/MyPageFeatureInterface.swift | 16 ++++- .../Interface/Sources/MyPage/MyPageView.swift | 13 +++- .../Components/List/ButtonListView.swift | 20 ++++-- .../InfoPlist+Templates.swift | 2 + 13 files changed, 226 insertions(+), 11 deletions(-) create mode 100644 Projects/Domain/Application/Interface/Sources/ApplicationClient.swift create mode 100644 Projects/Domain/Application/Project.swift create mode 100644 Projects/Domain/Application/Sources/ApplicationClient.swift create mode 100644 Projects/Domain/Application/Testing/Sources/ApplicationTesting.swift create mode 100644 Projects/Domain/Application/Tests/Sources/ApplicationTest.swift diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift index 9e2502eb..8f56cd8c 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift @@ -48,6 +48,7 @@ public extension ModulePath { public extension ModulePath { enum Domain: String, CaseIterable { + case Application case Error case User case Report diff --git a/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift b/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift index 3fb053db..f38f82fb 100644 --- a/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift +++ b/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift @@ -9,14 +9,19 @@ import Foundation public enum BottleURLType { case bottleAppStore + case bottleAppLookUp case kakaoChannelTalk public var url: URL { switch self { case .bottleAppStore: return URL(string: Bundle.main.infoDictionary?["APP_STORE_URL"] as? String ?? "")! + case .kakaoChannelTalk: return URL(string: Bundle.main.infoDictionary?["KAKAO_CHANNEL_TALK_URL"] as? String ?? "")! + + case .bottleAppLookUp: + return URL(string: Bundle.main.infoDictionary?["APP_LOOK_UP_URL"] as? String ?? "")! } } } diff --git a/Projects/Domain/Application/Interface/Sources/ApplicationClient.swift b/Projects/Domain/Application/Interface/Sources/ApplicationClient.swift new file mode 100644 index 00000000..05641128 --- /dev/null +++ b/Projects/Domain/Application/Interface/Sources/ApplicationClient.swift @@ -0,0 +1,36 @@ +// +// ApplicationClient.swift +// DomainApplicationInterface +// +// Created by JongHoon on 9/22/24. +// + +import Foundation + +public struct ApplicationClient { + private let _fetchCurrentAppVersion: () -> String + private let fetchLatestAppVersion: () async throws -> String + private let checkNeedApplicationUpdate: () async throws -> Bool + + public init( + fetchCurrentAppVersion: @escaping () -> String, + fetchLatestAppVersion: @escaping () async throws -> String, + checkNeedApplicationUpdate: @escaping () async throws -> Bool + ) { + self._fetchCurrentAppVersion = fetchCurrentAppVersion + self.fetchLatestAppVersion = fetchLatestAppVersion + self.checkNeedApplicationUpdate = checkNeedApplicationUpdate + } + + public func fetchCurrentAppVersion() -> String { + _fetchCurrentAppVersion() + } + + public func fetchLatestAppVersion() async throws -> String { + try await fetchLatestAppVersion() + } + + public func checkNeedApplicationUpdate() async throws -> Bool { + try await checkNeedApplicationUpdate() + } +} diff --git a/Projects/Domain/Application/Project.swift b/Projects/Domain/Application/Project.swift new file mode 100644 index 00000000..0b0d5d6e --- /dev/null +++ b/Projects/Domain/Application/Project.swift @@ -0,0 +1,44 @@ +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: ModulePath.Domain.name+ModulePath.Domain.Application.rawValue, + targets: [ + .domain( + interface: .Application, + factory: .init( + dependencies: [ + .core + ] + ) + ), + .domain( + implements: .Application, + factory: .init( + dependencies: [ + .domain(interface: .Application) + ] + ) + ), + + .domain( + testing: .Application, + factory: .init( + dependencies: [ + .domain(interface: .Application) + ] + ) + ), + .domain( + tests: .Application, + factory: .init( + dependencies: [ + .domain(testing: .Application), + .domain(implements: .Application) + ] + ) + ), + + ] +) diff --git a/Projects/Domain/Application/Sources/ApplicationClient.swift b/Projects/Domain/Application/Sources/ApplicationClient.swift new file mode 100644 index 00000000..a6e0054c --- /dev/null +++ b/Projects/Domain/Application/Sources/ApplicationClient.swift @@ -0,0 +1,66 @@ +// +// ApplicationClient.swift +// DomainApplicationInterface +// +// Created by JongHoon on 9/22/24. +// + +import Foundation + +import DomainApplicationInterface +import DomainErrorInterface + +import CoreURLHandlerInterface +import CoreURLHandler + +import Dependencies + +extension ApplicationClient: DependencyKey { + static public var liveValue: ApplicationClient = .live() + + static func live() -> ApplicationClient { + @Dependency(\.applicationClient) var applicationClient + return .init( + fetchCurrentAppVersion: { + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String + return version + }, + fetchLatestAppVersion: { + let appLookUpURL = BottleURLType.bottleAppLookUp.url + let (data, _) = try await URLSession.shared.data(from: appLookUpURL) + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let results = json["results"] as? [[String: Any]], + let appStoreVersion = results.first?["version"] as? String { + return appStoreVersion + } else { + throw DomainError.unknown("fetch latest app version failed") + } + }, + checkNeedApplicationUpdate: { + let currentAppVersion = applicationClient.fetchCurrentAppVersion() + let latestAppVersion = try await applicationClient.fetchLatestAppVersion() + let currentAppVersionArray = currentAppVersion.split(separator: ".").compactMap { Int($0) } + let latestAppVersionArray = latestAppVersion.split(separator: ".").compactMap { Int($0) } + + let maxLength = max(currentAppVersionArray.count, latestAppVersionArray.count) + + for i in 0.. currentVersion { + return true + } + } + return false + } + ) + } +} + +extension DependencyValues { + public var applicationClient: ApplicationClient { + get { self[ApplicationClient.self] } + set { self[ApplicationClient.self] = newValue } + } +} diff --git a/Projects/Domain/Application/Testing/Sources/ApplicationTesting.swift b/Projects/Domain/Application/Testing/Sources/ApplicationTesting.swift new file mode 100644 index 00000000..b1853ce6 --- /dev/null +++ b/Projects/Domain/Application/Testing/Sources/ApplicationTesting.swift @@ -0,0 +1 @@ +// This is for Tuist diff --git a/Projects/Domain/Application/Tests/Sources/ApplicationTest.swift b/Projects/Domain/Application/Tests/Sources/ApplicationTest.swift new file mode 100644 index 00000000..028a2c6a --- /dev/null +++ b/Projects/Domain/Application/Tests/Sources/ApplicationTest.swift @@ -0,0 +1,11 @@ +import XCTest + +final class ApplicationTests: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func testExample() { + XCTAssertEqual(1, 1) + } +} diff --git a/Projects/Domain/User/Sources/UserClient.swift b/Projects/Domain/User/Sources/UserClient.swift index a818cc4d..303f1feb 100644 --- a/Projects/Domain/User/Sources/UserClient.swift +++ b/Projects/Domain/User/Sources/UserClient.swift @@ -78,7 +78,7 @@ extension UserClient: DependencyKey { } try store.enumerateContacts(with: request) { contact, _ in - contacts = contact.phoneNumbers + contacts += contact.phoneNumbers .map { $0.value.stringValue } .map { $0.replacingOccurrences(of: "+82", with: "0") } .map { $0.trimmingCharacters(in: .whitespaces) } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift index f81b9289..f687cc9e 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift @@ -10,6 +10,7 @@ import Foundation import DomainAuth import DomainProfile import DomainUserInterface +import DomainApplication import CoreKeyChainStore import CoreToastInterface @@ -26,6 +27,7 @@ extension MyPageFeature { @Dependency(\.toastClient) var toastClient @Dependency(\.profileClient) var profileClient @Dependency(\.userClient) var userClient + @Dependency(\.applicationClient) var applicationClient let reducer = Reduce { state, action in switch action { @@ -36,6 +38,20 @@ extension MyPageFeature { await send(.userProfileDidFetched(userProfile)) } + case .onAppear: + return .run { send in + let currentAppVersion = applicationClient.fetchCurrentAppVersion() + let isNeedUpdateApplication = try await applicationClient.checkNeedApplicationUpdate() + await send(.applicationVersionInfoFetched(currentAppVersion: currentAppVersion, isNeedUpdate: isNeedUpdateApplication)) + } catch: { error, send in + Log.error(error) + } + + case let .applicationVersionInfoFetched(currentAppVersion, isNeedUpdate): + state.currentAppVersion = currentAppVersion + state.isShowApplicationUpdateButton = isNeedUpdate + return .none + case .logOutButtonDidTapped: state.destination = .alert(.init( title: { TextState("로그아웃") }, @@ -144,6 +160,10 @@ extension MyPageFeature { } } + case .updateApplicationButtonTapped: + URLHandler.shared.openURL(urlType: .bottleAppStore) + return .none + case let .updatePhoneNumberForBlockCompleted(count): state.blockedContactsCount = count return .none diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift index 6f38bda5..5e3d4f25 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift @@ -7,10 +7,12 @@ import Foundation -import SharedDesignSystem import DomainProfileInterface + import FeatureTabBarInterface +import SharedDesignSystem + import ComposableArchitecture @Reducer @@ -24,10 +26,13 @@ public struct MyPageFeature { @ObservableState public struct State: Equatable { var isShowLoadingProgressView: Bool + public var keywordItem: [ClipItem] public var userInfo: UserInfo public var introduction: Introduction public var blockedContactsCount: Int + public var currentAppVersion: String? + public var isShowApplicationUpdateButton: Bool public var isPresentTerms: Bool public var temrsURL: String? @@ -41,6 +46,7 @@ public struct MyPageFeature { self.userInfo = .init(userAge: -1, userImageURL: "", userName: "") self.introduction = .init(answer: "", question: "") self.blockedContactsCount = 0 + self.isShowApplicationUpdateButton = false self.isPresentTerms = false } } @@ -48,12 +54,11 @@ public struct MyPageFeature { public enum Action: BindableAction { // View Life Cycle case onLoad + case onAppear case userProfileDidFetched(UserProfile) case userProfileUpdateDidRequest case updatePhoneNumberForBlockButtonDidTapped - case updatePhoneNumberForBlockCompleted(count: Int) - case contactsAccessDeniedErrorOccurred case logOutButtonDidTapped case logOutDidCompleted case withdrawalButtonDidTapped @@ -61,6 +66,11 @@ public struct MyPageFeature { case selectedTabDidChanged(TabType) case alertSettingListDidTapped case accountSettingListDidTapped + case updateApplicationButtonTapped + + case updatePhoneNumberForBlockCompleted(count: Int) + case contactsAccessDeniedErrorOccurred + case applicationVersionInfoFetched(currentAppVersion: String, isNeedUpdate: Bool) case termsOfServiceListDidTapped case privacyPolicyListDidTapped case termsWebViewDidDismiss diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift index ef0b54e9..b16065e0 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift @@ -62,6 +62,9 @@ public struct MyPageView: View { .onLoad { store.send(.onLoad) } + .task { + store.send(.onAppear) + } .overlay { if store.isShowLoadingProgressView { WithPerceptionTracking { @@ -137,7 +140,15 @@ private extension MyPageView { } var appVersionList: some View { - ArrowListView(title: "앱 버전", subTitle: "0.0.0") + ButtonListView( + title: "앱 버전", + subTitle: "\(store.currentAppVersion ?? "0.0.0")", + buttonTitle: "업데이트", + isShowButton: store.isShowApplicationUpdateButton, + action: { + store.send(.updateApplicationButtonTapped) + } + ) } var contactList: some View { diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift index 1e10d8e3..848a99e4 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ButtonListView.swift @@ -11,17 +11,20 @@ public struct ButtonListView: View { private let title: String private let subTitle: String? private let buttonTitle: String + private let isShowButton: Bool private let action: () -> Void public init( title: String, subTitle: String? = nil, buttonTitle: String, + isShowButton: Bool = true, action: @escaping () -> Void ) { self.title = title self.subTitle = subTitle self.buttonTitle = buttonTitle + self.isShowButton = isShowButton self.action = action } @@ -36,12 +39,17 @@ public struct ButtonListView: View { // MARK: - Views public extension ButtonListView { + @ViewBuilder var button: some View { - OutlinedStyleButton( - .small(contentType: .text), - title: buttonTitle, - buttonType: .throttle, - action: action - ) + if isShowButton { + OutlinedStyleButton( + .small(contentType: .text), + title: buttonTitle, + buttonType: .throttle, + action: action + ) + } else { + EmptyView() + } } } diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index 4be2de0a..ab33dae3 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -27,6 +27,7 @@ public extension InfoPlist { "WEB_VIEW_BASE_URL": "$(WEB_VIEW_BASE_URL)", "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", "APP_STORE_URL": "$(APP_STORE_URL)", + "APP_LOOK_UP_URL": "$(APP_LOOK_UP_URL)", "KAKAO_CHANNEL_TALK_URL": "$(KAKAO_CHANNEL_TALK_URL)", "LSApplicationQueriesSchemes": ["kakaokompassauth", "kakaotalk"], "CFBundleURLTypes": [ @@ -55,6 +56,7 @@ public extension InfoPlist { "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", "APP_STORE_URL": "$(APP_STORE_URL)", "KAKAO_CHANNEL_TALK_URL": "$(KAKAO_CHANNEL_TALK_URL)", + "APP_LOOK_UP_URL": "$(APP_LOOK_UP_URL)", "LSApplicationQueriesSchemes": ["kakaokompassauth", "kakaotalk"], "CFBundleURLTypes": [ [ From 89ea3f5ebc3a9bae90632fde782fd8771272973b Mon Sep 17 00:00:00 2001 From: JongHoon Date: Mon, 23 Sep 2024 00:31:34 +0900 Subject: [PATCH 43/90] =?UTF-8?q?[feature/#264]=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=9B=B9=EB=B7=B0=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?(#270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/BaseWebView.swift | 1 + .../Interface/Sources/BaseWebViewType.swift | 37 +++++++++---- .../MyPage/Example/Sources/AppView.swift | 6 +-- .../EditProfile/EditProfileFeature.swift | 37 +++++++++++++ .../EditProfileFeatureInterface.swift | 47 +++++++++++++++++ .../Sources/EditProfile/ProfileEditView.swift | 52 +++++++++++++++++++ .../Sources/MyPage/MyPageFeature.swift | 3 ++ .../MyPage/MyPageFeatureInterface.swift | 2 + .../Interface/Sources/MyPage/MyPageView.swift | 3 ++ .../Interface/Sources/MyPageRootFeature.swift | 5 +- .../Sources/MyPageRootFeatureInterface.swift | 18 +++++-- .../Interface/Sources/MyPageRootView.swift | 19 ++++--- Projects/Feature/MyPage/Project.swift | 3 +- 13 files changed, 206 insertions(+), 27 deletions(-) create mode 100644 Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeature.swift create mode 100644 Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeatureInterface.swift create mode 100644 Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift diff --git a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebView.swift b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebView.swift index 16543ec7..6cbfbcb6 100644 --- a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebView.swift +++ b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebView.swift @@ -10,6 +10,7 @@ import WebKit import CoreLoggerInterface import CoreWebViewInterface + import DomainWebView import ComposableArchitecture diff --git a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift index 376ba1d4..60136dbf 100644 --- a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift +++ b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift @@ -11,33 +11,48 @@ import CoreWebViewInterface import CoreKeyChainStoreInterface import CoreKeyChainStore -public enum BottleWebViewType: String { +public enum BottleWebViewType { private var baseURL: String { (Bundle.main.infoDictionary?["WEB_VIEW_BASE_URL"] as? String) ?? "" } - case createProfile = "create-profile" - case myPage = "my" - case signUp = "signup" + case createProfile + case signUp case login case bottles + case editProfile + + var path: String { + switch self { + case .createProfile: + return "create-profile" + case .signUp: + return "signup" + case .login: + return "login" + case .bottles: + return "bottles" + case .editProfile: + return "profile/edit" + } + } public var url: URL { switch self { case .createProfile: - return makeUrlWithToken(rawValue) - - case .myPage: - return makeUrlWithToken(rawValue) + return makeUrlWithToken(path) case .signUp: - return URL(string: baseURL + "/" + rawValue)! + return URL(string: baseURL + "/" + path)! case .login: - return URL(string: baseURL + "/" + rawValue)! + return URL(string: baseURL + "/" + path)! case .bottles: - return makeUrlWithToken(rawValue) + return makeUrlWithToken(path) + + case .editProfile: + return makeUrlWithToken(path) } } diff --git a/Projects/Feature/MyPage/Example/Sources/AppView.swift b/Projects/Feature/MyPage/Example/Sources/AppView.swift index cc918e49..698e8164 100644 --- a/Projects/Feature/MyPage/Example/Sources/AppView.swift +++ b/Projects/Feature/MyPage/Example/Sources/AppView.swift @@ -9,9 +9,9 @@ import ComposableArchitecture struct AppView: App { var body: some Scene { WindowGroup { - MyPageView(store: Store( - initialState: MyPageFeature.State(), - reducer: { MyPageFeature() } + MyPageRootView(store: Store( + initialState: MyPageRootFeature.State(), + reducer: { MyPageRootFeature() } )) } } diff --git a/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeature.swift b/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeature.swift new file mode 100644 index 00000000..e711cc73 --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeature.swift @@ -0,0 +1,37 @@ +// +// EditProfileFeature.swift +// FeatureMyPage +// +// Created by JongHoon on 9/22/24. +// + +import Foundation + +import CoreToastInterface + +import ComposableArchitecture + +extension EditProfileFeature { + public init() { + @Dependency(\.toastClient) var toastClient + + let reducer = Reduce { state, action in + switch action { + case .initialLoadingCompleted: + state.isLoading = false + return .none + + case let .presentToast(message): + toastClient.presentToast(message: message) + return .none + + case .backButtonDidTapped: + return .send(.delegate(.closeEditProfileView)) + + default: + return .none + } + } + self.init(reducer: reducer) + } +} diff --git a/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeatureInterface.swift new file mode 100644 index 00000000..43b47e82 --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeatureInterface.swift @@ -0,0 +1,47 @@ +// +// EditProfileFeatureInterface.swift +// FeatureMyPage +// +// Created by JongHoon on 9/22/24. +// + +import Foundation + +import ComposableArchitecture + +@Reducer +public struct EditProfileFeature { + private let reducer: Reduce + + public init(reducer: Reduce) { + self.reducer = reducer + } + + @ObservableState + public struct State: Equatable { + var isLoading: Bool + + init() { + self.isLoading = true + } + } + + public enum Action: BindableAction { + case initialLoadingCompleted + case presentToast(message: String) + case backButtonDidTapped + case delegate(Delegate) + + public enum Delegate { + case closeEditProfileView + } + + // binding + case binding(BindingAction) + } + + public var body: some ReducerOf { + BindingReducer() + reducer + } +} diff --git a/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift b/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift new file mode 100644 index 00000000..af132360 --- /dev/null +++ b/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift @@ -0,0 +1,52 @@ +// +// ProfileEditView.swift +// FeatureMyPage +// +// Created by JongHoon on 9/22/24. +// + +import SwiftUI + +import FeatureBaseWebViewInterface + +import CoreLoggerInterface + +import SharedDesignSystem + +import ComposableArchitecture + +public struct ProfileEditView: View { + @Perception.Bindable private var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithPerceptionTracking { + BaseWebView(type: .editProfile) { action in + switch action { + case .webViewLoadingDidCompleted: + store.send(.initialLoadingCompleted) + + case let .showTaost(message): + store.send(.presentToast(message: message)) + + case .closeWebView: + store.send(.backButtonDidTapped) + + default: + Log.assertion(message: "\(action) - not handled action") + } + } + } + .navigationBarBackButtonHidden() + .overlay { + WithPerceptionTracking { + if store.isLoading { + LoadingIndicator() + } + } + } + } +} diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift index f687cc9e..f1b9a57a 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift @@ -185,6 +185,9 @@ extension MyPageFeature { )) return .none + case .profileEditListDidTapped: + return .send(.delegate(.profileEditListDidTapped)) + case .alertSettingListDidTapped: return .send(.delegate(.alertSettingListDidTapped)) diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift index 5e3d4f25..247426fb 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift @@ -64,6 +64,7 @@ public struct MyPageFeature { case withdrawalButtonDidTapped case withdrawalDidCompleted case selectedTabDidChanged(TabType) + case profileEditListDidTapped case alertSettingListDidTapped case accountSettingListDidTapped case updateApplicationButtonTapped @@ -85,6 +86,7 @@ public struct MyPageFeature { case withdrawalDidCompleted case logoutDidCompleted case selectedTabDidChanged(TabType) + case profileEditListDidTapped case alertSettingListDidTapped case accountSettingListDidTapped } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift index b16065e0..933780b5 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift @@ -116,6 +116,9 @@ private extension MyPageView { .padding(.vertical, .xl) .overlay(roundedRectangle) .padding(.bottom, .md) + .asThrottleButton { + store.send(.profileEditListDidTapped) + } } var blockPhoneNumberList: some View { diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift index 4ae57b4d..ff28ef53 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeature.swift @@ -21,8 +21,9 @@ public struct MyPageRootFeature { @Reducer(state: .equatable) public enum Path { - case AlertSetting(AlertSettingFeature) - case AccountSetting(AccountSettingFeature) + case alertSetting(AlertSettingFeature) + case accountSetting(AccountSettingFeature) + case editProfile(EditProfileFeature) } @ObservableState diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift index 79612a63..a7d175b3 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift @@ -24,11 +24,15 @@ extension MyPageRootFeature { case let .myPage(delegate): switch delegate { case .alertSettingListDidTapped: - state.path.append(.AlertSetting(.init())) + state.path.append(.alertSetting(.init())) return .none case .accountSettingListDidTapped: - state.path.append(.AccountSetting(.init())) + state.path.append(.accountSetting(.init())) + return .none + + case .profileEditListDidTapped: + state.path.append(.editProfile(.init())) return .none default: @@ -36,7 +40,7 @@ extension MyPageRootFeature { } // AccountSetting Delegate - case let .path(.element(id: _, action: .AccountSetting(.delegate(delegate)))): + case let .path(.element(id: _, action: .accountSetting(.delegate(delegate)))): switch delegate { case .logoutDidCompleted: return .send(.delegate(.logoutDidCompleted)) @@ -47,7 +51,13 @@ extension MyPageRootFeature { case .withdrawalDidCompleted: return .send(.delegate(.withdrawalDidCompleted)) } - + + case let .path(.element(id: _, action: .editProfile(.delegate(delegate)))): + switch delegate { + case .closeEditProfileView: + _ = state.path.popLast() + return .none + } default: return .none diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift index ee414a44..c9fd1793 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift @@ -28,18 +28,25 @@ public struct MyPageRootView: View { } destination: { store in WithPerceptionTracking { switch store.state { - case .AlertSetting: + case .alertSetting: if let store = store.scope( - state: \.AlertSetting, - action: \.AlertSetting) { + state: \.alertSetting, + action: \.alertSetting) { AlertSettingView(store: store) } - case .AccountSetting: + case .accountSetting: if let store = store.scope( - state: \.AccountSetting, - action: \.AccountSetting) { + state: \.accountSetting, + action: \.accountSetting) { AccountSettingView(store: store) } + case .editProfile: + if let store = store.scope( + state: \.editProfile, + action: \.editProfile + ) { + ProfileEditView(store: store) + } } } } diff --git a/Projects/Feature/MyPage/Project.swift b/Projects/Feature/MyPage/Project.swift index 8398d1f4..f49c46e1 100644 --- a/Projects/Feature/MyPage/Project.swift +++ b/Projects/Feature/MyPage/Project.swift @@ -11,7 +11,8 @@ let project = Project.makeModule( dependencies: [ .domain, .feature(interface: .BaseWebView), - .feature(interface: .TabBar) + .feature(interface: .TabBar), + .feature(interface: .GeneralSignUp) ] ) ), From 737a6da126d72a4cd8530caa6c69fce3b128f72c 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: Mon, 23 Sep 2024 00:32:23 +0900 Subject: [PATCH 44/90] =?UTF-8?q?[Feature/#263]=20=EC=97=B0=EB=9D=BD?= =?UTF-8?q?=EC=B2=98=20=EC=A0=91=EA=B7=BC=20=EA=B6=8C=ED=95=9C=20=ED=97=88?= =?UTF-8?q?=EC=9A=A9=EC=95=88=ED=95=A8=20=EC=8B=9C=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20(#265)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BottleURLType 설정 URLScheme 추가 * feat: 연락처 접근 권한 미허용시 설정으로 이동 --- .../Core/URLHandler/Interface/Sources/BottleURLType.swift | 4 ++++ .../MyPage/Interface/Sources/MyPage/MyPageFeature.swift | 5 +++-- .../Interface/Sources/MyPage/MyPageFeatureInterface.swift | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift b/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift index f38f82fb..d568ae38 100644 --- a/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift +++ b/Projects/Core/URLHandler/Interface/Sources/BottleURLType.swift @@ -11,6 +11,7 @@ public enum BottleURLType { case bottleAppStore case bottleAppLookUp case kakaoChannelTalk + case setting public var url: URL { switch self { @@ -22,6 +23,9 @@ public enum BottleURLType { case .bottleAppLookUp: return URL(string: Bundle.main.infoDictionary?["APP_LOOK_UP_URL"] as? String ?? "")! + + case .setting: + return URL(string: "App-prefs:root=General")! } } } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift index f1b9a57a..2775e3be 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift @@ -95,8 +95,9 @@ extension MyPageFeature { await send(.withdrawalDidCompleted) } - case .dismissAlert: + case .dismissContactsAlert: state.destination = nil + URLHandler.shared.openURL(urlType: .setting) return .none } @@ -175,7 +176,7 @@ extension MyPageFeature { }, actions: { ButtonState( - action: .dismissAlert, + action: .dismissContactsAlert, label: { TextState("확인") } ) }, diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift index 247426fb..c7f41226 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift @@ -95,7 +95,7 @@ public struct MyPageFeature { public enum Alert: Equatable { case confirmLogOut case confirmWithdrawal - case dismissAlert + case dismissContactsAlert } // ETC From d3c2872ef5738aa8a53b01a402f86bcd8e098b48 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: Mon, 23 Sep 2024 00:32:53 +0900 Subject: [PATCH 45/90] =?UTF-8?q?[Feature/#266]=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A7=84=EC=9E=85=20=EC=8B=9C=20?= =?UTF-8?q?=EC=97=B0=EB=9D=BD=EC=B2=98=20=EC=B0=A8=EB=8B=A8=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EB=8F=99=EA=B8=B0=ED=99=94=20(#267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: UserInfo blockedContactsCount 추가 * feat: 마이페이지 onLoad시 차단된 전화번호수 동기화 --- .../Sources/DTO/Response/ProfileResponseDTO.swift | 10 +++++++--- .../Interface/Sources/MyPage/MyPageFeature.swift | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Projects/Domain/Profile/Interface/Sources/DTO/Response/ProfileResponseDTO.swift b/Projects/Domain/Profile/Interface/Sources/DTO/Response/ProfileResponseDTO.swift index 578d5973..c5a599f9 100644 --- a/Projects/Domain/Profile/Interface/Sources/DTO/Response/ProfileResponseDTO.swift +++ b/Projects/Domain/Profile/Interface/Sources/DTO/Response/ProfileResponseDTO.swift @@ -13,6 +13,7 @@ public struct ProfileResponseDTO: Decodable { public let imageUrl: String? public let age: Int? public let isMatchActivated: Bool? + public let blockedUserCount: Int? public let introduction: [IntroductionDTO]? public let profileSelect: ProfileSelectDTO? @@ -79,7 +80,8 @@ public struct ProfileResponseDTO: Decodable { userAge: age ?? -1, userImageURL: imageUrl ?? "", userName: userName ?? "", - isActiveMatching: isMatchActivated ?? false + isActiveMatching: isMatchActivated ?? false, + blockedContactsCount: blockedUserCount ?? 0 ), introduction: Introduction(answer: introduction?.first?.answer ?? "", question: introduction?.first?.question ?? ""), profileSelect: profileSelect?.toDomain() ?? ProfileSelect( @@ -109,12 +111,14 @@ public struct UserInfo: Equatable { public let userImageURL: String public let userName: String public let isActiveMatching: Bool - - public init(userAge: Int, userImageURL: String, userName: String, isActiveMatching: Bool = false) { + public let blockedContactsCount: Int + + public init(userAge: Int, userImageURL: String, userName: String, isActiveMatching: Bool = false, blockedContactsCount: Int = 0) { self.userAge = userAge self.userImageURL = userImageURL self.userName = userName self.isActiveMatching = isActiveMatching + self.blockedContactsCount = blockedContactsCount } } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift index 2775e3be..f8b46363 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift @@ -107,6 +107,7 @@ extension MyPageFeature { let introduction = userProfile.introduction Log.debug(userProfile) + state.blockedContactsCount = userInfo.blockedContactsCount state.keywordItem = [ ClipItem( From 97e6980dc519e9600d94443d43e0d5bca52ebe4f 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: Mon, 23 Sep 2024 00:39:31 +0900 Subject: [PATCH 46/90] =?UTF-8?q?[Feature/#268]=20=EC=97=B0=EB=9D=BD?= =?UTF-8?q?=EC=B2=98=20=EC=B0=A8=EB=8B=A8=20=EC=99=84=EB=A3=8C=20=EC=8B=9C?= =?UTF-8?q?=20Toast=20=EB=9D=84=EC=9A=B0=EA=B8=B0=20(#269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 연락처 차단 업데이트 시 Toast 띄우기 * feat: 연락처 차단 업데이트 클릭 시 Alert 추가 --- .../Sources/MyPage/MyPageFeature.swift | 27 ++++++++++++++++--- .../MyPage/MyPageFeatureInterface.swift | 4 ++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift index f8b46363..fbfca7fe 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeature.swift @@ -95,10 +95,21 @@ extension MyPageFeature { await send(.withdrawalDidCompleted) } + case .dismissAlert: + state.destination = nil + return .send(.configureLoadingProgressView(isShow: false)) + case .dismissContactsAlert: state.destination = nil URLHandler.shared.openURL(urlType: .setting) return .none + + case let .confirmBlockContacts(contacts): + return .run { send in + try await userClient.updateBlockContacts(contacts: contacts) + await send(.updatePhoneNumberForBlockCompleted(count: contacts.count)) + await send(.configureLoadingProgressView(isShow: false)) + } } case .userProfileDidFetched(let userProfile): @@ -147,9 +158,7 @@ extension MyPageFeature { return .run { send in await send(.configureLoadingProgressView(isShow: true)) let contacts = try await userClient.fetchContacts() - try await userClient.updateBlockContacts(contacts: contacts) - await send(.updatePhoneNumberForBlockCompleted(count: contacts.count)) - await send(.configureLoadingProgressView(isShow: false)) + await send(.contactsDidReceived(contacts: contacts)) } catch: { error, send in await send(.configureLoadingProgressView(isShow: false)) if let userError = error as? UserError { @@ -161,12 +170,24 @@ extension MyPageFeature { } } } + + case let .contactsDidReceived(contacts): + let count = contacts.count + state.destination = .alert(.init( + title: { TextState("연락처 차단") }, + actions: { + ButtonState(role: .cancel, action: .dismissAlert, label: { TextState("취소하기")}) + ButtonState(role: .destructive, action: .confirmBlockContacts(contacts: contacts), label: { TextState("차단하기")}) + }, + message: { TextState("주소록에 있는 \(count)개의\n전화번호를 차단할까요?") })) + return .none case .updateApplicationButtonTapped: URLHandler.shared.openURL(urlType: .bottleAppStore) return .none case let .updatePhoneNumberForBlockCompleted(count): + toastClient.presentToast(message: "차단이 완료됐어요") state.blockedContactsCount = count return .none diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift index c7f41226..c1e1b8a1 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageFeatureInterface.swift @@ -76,7 +76,7 @@ public struct MyPageFeature { case privacyPolicyListDidTapped case termsWebViewDidDismiss case contactListDidTapped - + case contactsDidReceived(contacts: [String]) case configureLoadingProgressView(isShow: Bool) case delegate(Delegate) @@ -95,6 +95,8 @@ public struct MyPageFeature { public enum Alert: Equatable { case confirmLogOut case confirmWithdrawal + case confirmBlockContacts(contacts: [String]) + case dismissAlert case dismissContactsAlert } From 1524b1a08684e96ffffba387093cea0693ca0a4b 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, 26 Sep 2024 16:10:48 +0900 Subject: [PATCH 47/90] Release/1.0.8 -> develop (#285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 빌드 넘버 1.0.8 (29) * chore: DomainAuth 의존성 추가 * feat: ProfileEditView bottom ignoreSafeArea 추가 * [Feature/#277] 웹뷰 os type, 버전 파라미터 추가 (#279) * [Feature/#278] 웹뷰 상단 safe area 무시하도록 수정 (#284) * [Fix/#280] 로그인화면 백그라운드 이미지 비율 수정 (#281) * [Fix/#282] 로그인 화면 로그인 버튼 vstack 하단 마진 수정 (#283) * [Feature/#232] 커스텀 alert 적용 (#272) * feat: SplashView bottleAlert 적용 * feat: PingPongDetailView bottleAlert 적용 * feat: ReportUserView bottleAlert 적용 * feat: SandBeachView bottleAlert 적용 * feat: 탈퇴하기 Alert message 수정 (#274) * [Feature/#275] 알림 권한 미허용 시 alert 추가 (#276) * feat: AppDelegate 푸시 수신 상태 Notification 등록 * feat: UserClient 푸시 알림 허용 상태 로직 추가 * feat: 푸시 알림 허용 상태에 따른 알림설정 화면 로직 구현 * feat: UserClient 푸쉬알림허용상태 Publisher 구현 * feat: 푸쉬알림허용상태에 따른 로직 변경 * feat: 토글 버튼 binding 코드 개선 - 코드리뷰 반영 * feat: UserClient UserDefaultKeys enum 추가 * feat: 오탈자 수정 - pushNotificationSubject -> pushNotificationAllowStatusSubject * chore: 빌드 넘버 1.0.8 (30) --------- Co-authored-by: JongHoon --- Projects/App/Sources/AppDelegate.swift | 42 +++++- Projects/Domain/Auth/Project.swift | 3 +- .../User/Interface/Sources/UserClient.swift | 22 ++++ Projects/Domain/User/Sources/UserClient.swift | 29 ++++- .../Interface/Sources/BaseWebViewType.swift | 11 +- .../Interface/Sources/BottleArrivalView.swift | 2 +- .../PingPongDetailFeature.swift | 48 ++++++- .../PingPongDetailFeatureInterface.swift | 20 ++- .../PingPongDetail/PingPongDetailView.swift | 1 + .../Introduction/IntroductionFeature.swift | 23 +--- .../IntroductionFeatureInterface.swift | 24 +--- .../Introduction/IntroductionView.swift | 2 - .../QuestionAndAnswerFeature.swift | 24 +--- .../QuestionAndAnswerFeatureInterface.swift | 21 +-- .../QuestionAndAnswerView.swift | 1 - .../Interface/Sources/GeneralSignUpView.swift | 2 +- .../GeneralLogIn/GeneralLogInView.swift | 2 +- .../Sources/Login/AppleLoginView.swift | 34 ++--- .../Interface/Sources/Login/LoginView.swift | 38 +++--- .../AccountSettingFeature.swift | 2 +- .../AlertSetting/AlertSettingFeature.swift | 121 +++++++++++++----- .../AlertSettingFeatureInterface.swift | 27 +++- .../AlertSetting/AlertSettingView.swift | 1 + .../Sources/EditProfile/ProfileEditView.swift | 1 + .../Sources/Onboarding/OnboardingView.swift | 2 +- .../Interface/Sources/ReportUserFeature.swift | 32 +++-- .../Sources/ReportUserFeatureInterface.swift | 1 + .../Interface/Sources/ReportUserView.swift | 2 +- .../Sources/SandBeach/SandBeachView.swift | 2 +- .../Sources/App/AppDelegateFeature.swift | 9 ++ .../Sources/SplashView/SplashView.swift | 2 +- .../InfoPlist+Templates.swift | 8 +- 32 files changed, 365 insertions(+), 194 deletions(-) diff --git a/Projects/App/Sources/AppDelegate.swift b/Projects/App/Sources/AppDelegate.swift index 1b193461..870f4114 100644 --- a/Projects/App/Sources/AppDelegate.swift +++ b/Projects/App/Sources/AppDelegate.swift @@ -28,14 +28,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, MessagingDelegate { UIApplication.shared.registerForRemoteNotifications() UNUserNotificationCenter.current().delegate = self Messaging.messaging().delegate = self - + setNotification() application.registerForRemoteNotifications() - store.send(.appDelegate(.didFinishLunching)) return true } } +// MARK: - UNUserNotificationCenterDelegate extension AppDelegate: UNUserNotificationCenterDelegate { func messaging( _ messaging: Messaging, @@ -62,3 +62,41 @@ extension AppDelegate: UNUserNotificationCenterDelegate { return [.badge, .sound, .banner, .list] } } + +// MARK: - objc funcs +private extension AppDelegate { + @objc func checkPushNotificationStatus() { + UNUserNotificationCenter.current() + .getNotificationSettings { [weak self] permission in + guard let self = self else { return } + DispatchQueue.main.async { + switch permission.authorizationStatus { + case .notDetermined: + self.store.send(.appDelegate(.pushNotificationAllowStatusDidChanged(isAllow: true))) + case .denied: + self.store.send(.appDelegate(.pushNotificationAllowStatusDidChanged(isAllow: false))) + case .authorized: + self.store.send(.appDelegate(.pushNotificationAllowStatusDidChanged(isAllow: true))) + case .provisional: + self.store.send(.appDelegate(.pushNotificationAllowStatusDidChanged(isAllow: false))) + case .ephemeral: + self.store.send(.appDelegate(.pushNotificationAllowStatusDidChanged(isAllow: true))) + @unknown default: + Log.error("Unknow Notification Status") + } + } + } + } +} + +// MARK: - Private Methods +private extension AppDelegate { + func setNotification() { + NotificationCenter.default.addObserver( + self, + selector: #selector(checkPushNotificationStatus), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } +} diff --git a/Projects/Domain/Auth/Project.swift b/Projects/Domain/Auth/Project.swift index ff0338f1..6d3339ba 100644 --- a/Projects/Domain/Auth/Project.swift +++ b/Projects/Domain/Auth/Project.swift @@ -18,7 +18,8 @@ let project = Project.makeModule( factory: .init( dependencies: [ .domain(interface: .Auth), - .domain(interface: .Error) + .domain(interface: .Error), + .domain(implements: .User) ] ) ), diff --git a/Projects/Domain/User/Interface/Sources/UserClient.swift b/Projects/Domain/User/Interface/Sources/UserClient.swift index e80d44c8..34fd1927 100644 --- a/Projects/Domain/User/Interface/Sources/UserClient.swift +++ b/Projects/Domain/User/Interface/Sources/UserClient.swift @@ -7,6 +7,8 @@ import Foundation +import Combine + public struct UserClient { private let _isLoggedIn: () -> Bool private let _isAppDeleted: () -> Bool @@ -14,10 +16,17 @@ public struct UserClient { private let updateLoginState: (Bool) -> Void private let updateDeleteState: (Bool) -> Void private let updateFcmToken: (String) -> Void + private let updatePushNotificationAllowStatus: (Bool) -> Void private let _fetchAlertState: () async throws -> [UserAlertState] + private let _fetchPushNotificationAllowStatus: () -> Bool private let updateAlertState: (UserAlertState) async throws -> Void private let fetchContacts: () async throws -> [String] private let updateBlockContacts: ([String]) async throws -> Void + private let pushNotificationAllowStatusSubject = CurrentValueSubject(true) + + public var pushNotificationAllowStatusPublisher: AnyPublisher { + return pushNotificationAllowStatusSubject.eraseToAnyPublisher() + } public init( isLoggedIn: @escaping () -> Bool, @@ -26,7 +35,9 @@ public struct UserClient { updateLoginState: @escaping (Bool) -> Void, updateDeleteState: @escaping (Bool) -> Void, updateFcmToken: @escaping (String) -> Void, + updatePushNotificationAllowStatus: @escaping (Bool) -> Void, fetchAlertState: @escaping () async throws -> [UserAlertState], + fetchPushNotificationAllowStatus: @escaping () -> Bool, updateAlertState: @escaping (UserAlertState) async throws -> Void, fetchContacts: @escaping () async throws -> [String], updateBlockContacts: @escaping ([String]) async throws -> Void @@ -37,7 +48,9 @@ public struct UserClient { self.updateLoginState = updateLoginState self.updateDeleteState = updateDeleteState self.updateFcmToken = updateFcmToken + self.updatePushNotificationAllowStatus = updatePushNotificationAllowStatus self._fetchAlertState = fetchAlertState + self._fetchPushNotificationAllowStatus = fetchPushNotificationAllowStatus self.updateAlertState = updateAlertState self.fetchContacts = fetchContacts self.updateBlockContacts = updateBlockContacts @@ -67,10 +80,19 @@ public struct UserClient { updateFcmToken(fcmToken) } + public func updatePushNotificationAllowStatus(isAllow: Bool) { + pushNotificationAllowStatusSubject.send(isAllow) + updatePushNotificationAllowStatus(isAllow) + } + public func fetchAlertState() async throws -> [UserAlertState] { try await _fetchAlertState() } + public func fetchPushNotificationAllowStatus() -> Bool { + _fetchPushNotificationAllowStatus() + } + public func updateAlertState(alertState: UserAlertState) async throws { try await updateAlertState(alertState) } diff --git a/Projects/Domain/User/Sources/UserClient.swift b/Projects/Domain/User/Sources/UserClient.swift index 303f1feb..c46472f1 100644 --- a/Projects/Domain/User/Sources/UserClient.swift +++ b/Projects/Domain/User/Sources/UserClient.swift @@ -17,6 +17,13 @@ import ComposableArchitecture import Moya extension UserClient: DependencyKey { + private enum UserDefaultsKeys: String { + case loginState + case deleteState + case fcmToken + case alertAllowState + } + static public var liveValue: UserClient = .live() static func live() -> UserClient { @@ -24,34 +31,42 @@ extension UserClient: DependencyKey { return .init( isLoggedIn: { - return UserDefaults.standard.bool(forKey: "loginState") + return UserDefaults.standard.bool(forKey: UserDefaultsKeys.loginState.rawValue) }, isAppDeleted: { - return !UserDefaults.standard.bool(forKey: "deleteState") + return !UserDefaults.standard.bool(forKey: UserDefaultsKeys.deleteState.rawValue) }, fetchFcmToken: { - return UserDefaults.standard.string(forKey: "fcmToken") + return UserDefaults.standard.string(forKey: UserDefaultsKeys.fcmToken.rawValue) }, updateLoginState: { isLoggedIn in - UserDefaults.standard.set(isLoggedIn, forKey: "loginState") + UserDefaults.standard.set(isLoggedIn, forKey: UserDefaultsKeys.loginState.rawValue) }, updateDeleteState: { isDelete in - UserDefaults.standard.set(!isDelete, forKey: "deleteState") + UserDefaults.standard.set(!isDelete, forKey: UserDefaultsKeys.deleteState.rawValue) }, updateFcmToken: { fcmToken in - UserDefaults.standard.set(fcmToken, forKey: "fcmToken") + UserDefaults.standard.set(fcmToken, forKey: UserDefaultsKeys.fcmToken.rawValue) + }, + + updatePushNotificationAllowStatus: { isAllow in + UserDefaults.standard.set(isAllow, forKey: UserDefaultsKeys.alertAllowState.rawValue) }, fetchAlertState: { let responseData = try await networkManager.reqeust(api: .apiType(UserAPI.fetchAlertState), dto: [AlertStateResponseDTO].self) return responseData.map { $0.toDomain() } - }, + + fetchPushNotificationAllowStatus: { + return UserDefaults.standard.bool(forKey: UserDefaultsKeys.alertAllowState.rawValue) + }, + updateAlertState: { alertState in let requestData = AlertStateRequestDTO(alertType: alertState.alertType, enabled: alertState.enabled) try await networkManager.reqeust(api: .apiType(UserAPI.updateAlertState(reqeustData: requestData))) diff --git a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift index 60136dbf..11d53307 100644 --- a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift +++ b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift @@ -7,10 +7,15 @@ import Foundation +import DomainApplicationInterface +import DomainApplication + import CoreWebViewInterface import CoreKeyChainStoreInterface import CoreKeyChainStore +import Dependencies + public enum BottleWebViewType { private var baseURL: String { (Bundle.main.infoDictionary?["WEB_VIEW_BASE_URL"] as? String) ?? "" @@ -67,11 +72,15 @@ public enum BottleWebViewType { // MARK: - private methods private extension BottleWebViewType { func makeUrlWithToken(_ path: String) -> URL { + @Dependency(\.applicationClient) var applicationClient + var components = URLComponents(string: baseURL) components?.path = "/\(path)" components?.queryItems = [ URLQueryItem(name: "accessToken", value: KeyChainTokenStore.shared.load(property: .accessToken)), - URLQueryItem(name: "refreshToken", value: KeyChainTokenStore.shared.load(property: .refreshToken)) + URLQueryItem(name: "refreshToken", value: KeyChainTokenStore.shared.load(property: .refreshToken)), + URLQueryItem(name: "device", value: "ios"), + URLQueryItem(name: "version", value: applicationClient.fetchCurrentAppVersion()) ] return (components?.url)! diff --git a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalView.swift b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalView.swift index 18e02dc9..1b002cf9 100644 --- a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalView.swift +++ b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalView.swift @@ -43,7 +43,7 @@ public struct BottleArrivalView: View { LoadingIndicator() } } - .ignoresSafeArea(.all, edges: .bottom) + .ignoresSafeArea(.all, edges: [.top, .bottom]) .toolbar(.hidden, for: .navigationBar) .toolbar(.hidden, for: .bottomBar) } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift index b24ca8dd..9ee14df5 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift @@ -7,10 +7,11 @@ import Foundation -import CoreLoggerInterface import FeatureReportInterface import DomainBottle +import CoreLoggerInterface + import ComposableArchitecture extension PingPongDetailFeature { @@ -42,12 +43,46 @@ extension PingPongDetailFeature { imageURL: imageURL ?? "", userID: userId ?? -1, userName: userName, userAge: userAge ?? -1) return .send(.delegate(.reportButtonDidTapped(userReportProfile))) + case .stopTalkAlertDidRequired: + state.destination = .alert(.init( + title: { TextState("중단하기") }, + actions: { + ButtonState( + role: .cancel, + action: .dismiss, + label: { TextState("계속하기")}) + + ButtonState( + role: .destructive, + action: .confirmStopTalk, + label: { TextState("중단하기") }) + }, + message: { TextState("중단 시 모든 핑퐁 내용이 사라져요. 정말 중단하시겠어요?") } + )) + return .none + + // Destination + case let .destination(.presented(.alert(alert))): + switch alert { + case .confirmStopTalk: + return .run { [bottleID = state.bottleID] send in + try await bottleClient.stopTalk(bottleID: bottleID) + await send(.delegate(.popToRootDidRequired)) + } + + case .dismiss: + state.destination = nil + return .none + } + + // Introduction Delegate case let .introduction(.delegate(delegate)): switch delegate { - case .popToRootDidRequired: - return .send(.delegate(.popToRootDidRequired)) + case .stopTaskButtonTapped: + return .send(.stopTalkAlertDidRequired) } - + + // QuestionAndAnswer Delegate case let .questionAndAnswer(.delegate(delegate)): switch delegate { case .reloadPingPongRequired: @@ -56,8 +91,11 @@ extension PingPongDetailFeature { return .send(.delegate(.popToRootDidRequired)) case .refreshPingPong: return fetchPingPong(state: &state) + case .stopTaskButtonDidTapped: + return .send(.stopTalkAlertDidRequired) } - + + // Matching Delegate case let .matching(.delegate(delegate)): switch delegate { case .otherBottleButtonDidTapped: diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeatureInterface.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeatureInterface.swift index 45dfb790..5938b3e1 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeatureInterface.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeatureInterface.swift @@ -43,6 +43,8 @@ public struct PingPongDetailFeature { var matching: MatchingFeature.State var selectedTab: PingPongDetailViewTabType + @Presents var destination: Destination.State? + public init( bottleID: Int, isRead: Bool, @@ -67,7 +69,7 @@ public struct PingPongDetailFeature { case pingPongDidFetched(_: BottlePingPong) case backButtonDidTapped case reportButtonDidTapped - + case stopTalkAlertDidRequired // Delegate case delegate(Delegate) @@ -83,6 +85,13 @@ public struct PingPongDetailFeature { case questionAndAnswer(QuestionAndAnswerFeature.Action) case matching(MatchingFeature.Action) case binding(BindingAction) + case destination(PresentationAction) + // Alert + case alert(Alert) + public enum Alert: Equatable { + case confirmStopTalk + case dismiss + } } public var body: some ReducerOf { @@ -97,6 +106,15 @@ public struct PingPongDetailFeature { MatchingFeature() } reducer + .ifLet(\.$destination, action: \.destination) } } +// MARK: - Destination + +extension PingPongDetailFeature { + @Reducer(state: .equatable) + public enum Destination { + case alert(AlertState) + } +} diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift index 55cce1f8..9da0026a 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift @@ -55,6 +55,7 @@ public struct PingPongDetailView: View { } ) .ignoresSafeArea(.all, edges: .bottom) + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) } } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeature.swift index 3a88ad0d..1ba3a024 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeature.swift @@ -33,26 +33,7 @@ extension IntroductionFeature { return .none case .stopTaskButtonTapped: - state.destination = .alert(.init( - title: { TextState("중단하기") }, - actions: { - ButtonState( - role: .destructive, - action: .confirmStopTalk, - label: { TextState("중단하기") }) - }, - message: { TextState("중단 시 모든 핑퐁 내용이 사라져요. 정말 중단하시겠어요?") } - )) - return .none - - case let .destination(.presented(.alert(alert))): - switch alert { - case .confirmStopTalk: - return .run { [bottleID = state.bottleID] send in - try await bottleClient.stopTalk(bottleID: bottleID) - await send(.delegate(.popToRootDidRequired)) - } - } + return .send(.delegate(.stopTaskButtonTapped)) case .refreshPingPongDidRequired: return .run { [bottleID = state.bottleID] send in @@ -62,7 +43,7 @@ extension IntroductionFeature { await send(.introductionFetched(pingPong.introduction ?? [])) } - case .binding, .alert: + case .binding: return .none default: diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeatureInterface.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeatureInterface.swift index 52be5fb3..17e4d8df 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeatureInterface.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionFeatureInterface.swift @@ -68,9 +68,7 @@ public struct IntroductionFeature { + (interest?.etc ?? []) + (interest?.sports ?? []) } - - @Presents var destination: Destination.State? - + public init (bottleID: Int) { self.bottleID = bottleID } @@ -88,32 +86,14 @@ public struct IntroductionFeature { // ETC. case binding(BindingAction) - case destination(PresentationAction) - - case alert(Alert) - public enum Alert: Equatable { - case confirmStopTalk - } - case delegate(Delegate) public enum Delegate { - case popToRootDidRequired + case stopTaskButtonTapped } } public var body: some ReducerOf { reducer - .ifLet(\.$destination, action: \.destination) } } - -// MARK: - Destination - -extension IntroductionFeature { - @Reducer(state: .equatable) - public enum Destination { - case alert(AlertState) - } -} - diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift index 16d1f144..9744ee5a 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift @@ -81,9 +81,7 @@ public struct IntroductionView: View { .padding(.top, 32.0) } .scrollIndicators(.hidden) - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .background(to: ColorToken.background(.primary)) - } } } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift index 5a4dc213..a03ed829 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift @@ -83,30 +83,10 @@ extension QuestionAndAnswerFeature { } case .stopTalkButtonDidTapped: - state.destination = .alert(.init( - title: { TextState("중단하기") }, - actions: { - ButtonState( - role: .destructive, - action: .confirmStopTalk, - label: { TextState("중단하기") }) - }, - message: { TextState("중단 시 모든 핑퐁 내용이 사라져요. 정말 중단하시겠어요?") } - )) - return .none + return .send(.delegate(.stopTaskButtonDidTapped)) case .refreshDidPulled: return .send(.delegate(.refreshPingPong)) - - case let .destination(.presented(.alert(alert))): - switch alert { - case .confirmStopTalk: - state.isShowLoadingIndicator = true - return .run { [bottleID = state.bottleID] send in - try await bottleClient.stopTalk(bottleID: bottleID) - await send(.delegate(.popToRootDidRequired)) - } - } case .binding(\.firstLetterTextFieldContent): if state.firstLetterTextFieldContent.count >= 50 { @@ -132,7 +112,7 @@ extension QuestionAndAnswerFeature { } return .none - case .binding, .destination, .alert: + case .binding: return .none default: diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift index f4144743..770ba42a 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift @@ -114,9 +114,7 @@ public struct QuestionAndAnswerFeature { } var finalSelectIsSelctedYesButton: Bool var finalSelectIsSelctedNoButton: Bool - - @Presents var destination: Destination.State? - + public init(bottleID: Int) { self.bottleID = bottleID self.isShowLoadingIndicator = false @@ -181,12 +179,6 @@ public struct QuestionAndAnswerFeature { // ETC. case binding(BindingAction) - case destination(PresentationAction) - - case alert(Alert) - public enum Alert: Equatable { - case confirmStopTalk - } case delegate(Delegate) @@ -194,21 +186,12 @@ public struct QuestionAndAnswerFeature { case reloadPingPongRequired case popToRootDidRequired case refreshPingPong + case stopTaskButtonDidTapped } } public var body: some ReducerOf { BindingReducer() reducer - .ifLet(\.$destination, action: \.destination) - } -} - -// MARK: - Destination - -extension QuestionAndAnswerFeature { - @Reducer(state: .equatable) - public enum Destination { - case alert(AlertState) } } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift index 22c45bfe..24270272 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift @@ -134,7 +134,6 @@ public struct QuestionAndAnswerView: View { LoadingIndicator() } } - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .background(to: ColorToken.background(.primary)) .toolbar(.hidden, for: .bottomBar) diff --git a/Projects/Feature/GeneralSignUp/Interface/Sources/GeneralSignUpView.swift b/Projects/Feature/GeneralSignUp/Interface/Sources/GeneralSignUpView.swift index 6cb6f38c..a396f737 100644 --- a/Projects/Feature/GeneralSignUp/Interface/Sources/GeneralSignUpView.swift +++ b/Projects/Feature/GeneralSignUp/Interface/Sources/GeneralSignUpView.swift @@ -54,7 +54,7 @@ public struct GeneralSignUpView: View { } } .toolbar(.hidden, for: .navigationBar) - .ignoresSafeArea(.all, edges: .bottom) + .ignoresSafeArea(.all, edges: [.top, .bottom]) .sheet(isPresented: $store.isPresentTerms) { TermsWebView(url: store.termsURL ?? "") } diff --git a/Projects/Feature/Login/Interface/Sources/GeneralLogIn/GeneralLogInView.swift b/Projects/Feature/Login/Interface/Sources/GeneralLogIn/GeneralLogInView.swift index 7b50c6ac..04af2723 100644 --- a/Projects/Feature/Login/Interface/Sources/GeneralLogIn/GeneralLogInView.swift +++ b/Projects/Feature/Login/Interface/Sources/GeneralLogIn/GeneralLogInView.swift @@ -51,7 +51,7 @@ public struct GeneralLogInView: View { LoadingIndicator() } } - .ignoresSafeArea(.all, edges: .bottom) + .ignoresSafeArea(.all, edges: [.top, .bottom]) .toolbar(.hidden, for: .navigationBar) } } diff --git a/Projects/Feature/Login/Interface/Sources/Login/AppleLoginView.swift b/Projects/Feature/Login/Interface/Sources/Login/AppleLoginView.swift index e1ace40d..d79754b4 100644 --- a/Projects/Feature/Login/Interface/Sources/Login/AppleLoginView.swift +++ b/Projects/Feature/Login/Interface/Sources/Login/AppleLoginView.swift @@ -19,31 +19,33 @@ public struct AppleLoginView: View { } public var body: some View { - VStack(spacing: 0) { - Spacer() - .frame(height: 52) - whiteLogo - .padding(.top, 52) - .padding(.bottom, .xl) - mainText + ZStack(alignment: .bottom) { + VStack(spacing: 0) { + Spacer() + .frame(height: 52) + whiteLogo + .padding(.top, 52) + .padding(.bottom, .xl) + mainText - Spacer() + Spacer() + } + .frame(maxWidth: .infinity) + .background { + BottleImageView( + type: .local(bottleImageSystem: .illustraition(.loginBackground)) + ) + } + .edgesIgnoringSafeArea([.top, .bottom]) signInWithAppleButton - .padding(.bottom, 30.0) - - } - .background { - BottleImageView( - type: .local(bottleImageSystem: .illustraition(.loginBackground)) - ) + .padding(.bottom, 16.0) } .setNavigationBar { makeNaivgationleftButton() { store.send(.backButtonDidTapped) } } - .edgesIgnoringSafeArea([.top, .bottom]) } } diff --git a/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift b/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift index 3b585f70..56d322f6 100644 --- a/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift +++ b/Projects/Feature/Login/Interface/Sources/Login/LoginView.swift @@ -28,29 +28,33 @@ public struct LoginView: View { public var body: some View { WithPerceptionTracking { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { - VStack(spacing: 0) { - Spacer() - .frame(height: 52) - whiteLogo - .padding(.top, 52) - .padding(.bottom, .xl) - - mainText - - Spacer() + ZStack(alignment: .bottom) { + VStack(spacing: 0) { + Spacer() + .frame(height: 52) + whiteLogo + .padding(.top, 52) + .padding(.bottom, .xl) + + mainText + + Spacer() + } + .frame(maxWidth: .infinity) + .background { + BottleImageView( + type: .local(bottleImageSystem: .illustraition(.loginBackground)) + ) + .scaledToFill() + } + .edgesIgnoringSafeArea([.top, .bottom]) VStack(spacing: 30.0) { signInWithKakaoButton snsLoginButton } - .padding(.bottom, 30.0) - } - .background { - BottleImageView( - type: .local(bottleImageSystem: .illustraition(.loginBackground)) - ) + .padding(.bottom, 16.0) } - .edgesIgnoringSafeArea([.top, .bottom]) .sheet( isPresented: $store.isPresentTermView, content: { diff --git a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift index b3ad4753..bc4aed2e 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AccountSetting/AccountSettingFeature.swift @@ -55,7 +55,7 @@ extension AccountSettingFeature { ButtonState(role: .cancel, action: .confirmWithdrawal, label: { TextState("탈퇴하기") }) ButtonState(role: .destructive, action: .dismiss, label: { TextState("계속 이용하기") }) }, - message: { TextState("탈퇴 시 계정 복구가 어려워요.\n정말 탈퇴하시겠어요?") } + message: { TextState("탈퇴 시 48시간 동안 재가입이 불가능하며 계정 복구가 어려워요.\n정말 탈퇴하시겠어요?") } )) return .none diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift index 71617f7c..4a719871 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift @@ -6,10 +6,14 @@ // import Foundation +import Combine import DomainUser import DomainUserInterface +import CoreURLHandlerInterface +import CoreLoggerInterface + import ComposableArchitecture extension AlertSettingFeature { @@ -20,11 +24,24 @@ extension AlertSettingFeature { let reducer = Reduce { state, action in switch action { case .onLoad: - return .run { send in + return Effect.publisher { + userClient.pushNotificationAllowStatusPublisher + .receive(on: DispatchQueue.main) + .map { isAllow in + .pushNotificationAllowed(isAllow: isAllow) + } + } + .cancellable(id: "PushNotificationPublisher", cancelInFlight: true) + + case .alertStateFetchDidRequest: + updatePushNotificationAllowStatus(state: &state) + + return .run { [state = state] send in + let isAllow = state.isAllowPushNotification let alertStateList = try await userClient.fetchAlertState() for alertState in alertStateList { - let isOn = alertState.enabled + let isOn = isAllow ? alertState.enabled : false switch alertState.alertType { case .randomBottle: await send(.randomBottleToggleDidFetched(isOn: isOn)) @@ -39,7 +56,20 @@ extension AlertSettingFeature { } } } - + + case let .pushNotificationAllowed(isAllow): + state.isAllowPushNotification = isAllow + if isAllow { + return .send(.alertStateFetchDidRequest) + } else { + return .merge( + .send(.randomBottleToggleDidFetched(isOn: false)), + .send(.pingpongToggleDidFetched(isOn: false)), + .send(.arrivalBottleToggleDidFetched(isOn: false)), + .send(.marketingToggleDidFetched(isOn: false)) + ) + } + case let .randomBottleToggleDidFetched(isOn): state.isOnRandomBottleToggle = isOn return .none @@ -62,49 +92,70 @@ extension AlertSettingFeature { } case .binding(\.isOnRandomBottleToggle): - return .run { [isOn = state.isOnRandomBottleToggle] send in - await send(.toggleDidChanged(alertState: .init(alertType: .randomBottle, enabled: isOn))) - } - .debounce( - id: ID.randomBottle, - for: 1.0, - scheduler: DispatchQueue.main) + let isOn = state.isOnRandomBottleToggle + return .send(.toggleDidChanged( + alertState: .init(alertType: .randomBottle, enabled: isOn), + id: .randomBottle)) case .binding(\.isOnArrivalBottleToggle): - return .run { [isOn = state.isOnArrivalBottleToggle] send in - await send(.toggleDidChanged(alertState: .init(alertType: .arrivalBottle, enabled: isOn))) - } - .debounce( - id: ID.arrivalBottle, - for: 1.0, - scheduler: DispatchQueue.main) + let isOn = state.isOnArrivalBottleToggle + return .send(.toggleDidChanged( + alertState: .init(alertType: .arrivalBottle, enabled: isOn), + id: .arrivalBottle)) case .binding(\.isOnPingPongToggle): - return .run { [isOn = state.isOnPingPongToggle] send in - await send(.toggleDidChanged(alertState: .init(alertType: .pingpong, enabled: isOn))) - } - .debounce( - id: ID.pingping, - for: 1.0, - scheduler: DispatchQueue.main) + let isOn = state.isOnPingPongToggle + return .send(.toggleDidChanged( + alertState: .init(alertType: .pingpong, enabled: isOn), + id: .pingping)) case .binding(\.isOnMarketingToggle): - return .run { [isOn = state.isOnMarketingToggle] send in - await send(.toggleDidChanged(alertState: .init(alertType: .marketing, enabled: isOn))) - } - .debounce( - id: ID.marketing, - for: 1.0, - scheduler: DispatchQueue.main) - - case let .toggleDidChanged(alertState): - return .run { send in - try await userClient.updateAlertState(alertState: alertState) + let isOn = state.isOnMarketingToggle + return .send(.toggleDidChanged( + alertState: .init(alertType: .marketing, enabled: isOn), + id: .marketing)) + + case let .toggleDidChanged(alertState, id): + updatePushNotificationAllowStatus(state: &state) + + if state.isAllowPushNotification { + return .run { send in + try await userClient.updateAlertState(alertState: alertState) + } + .debounce( + id: id, + for: 0.5, + scheduler: DispatchQueue.main) + } else { + return .send(.pushNotificationAlertDidRequired) + } + + case .pushNotificationAlertDidRequired: + state.destination = .alert(.init( + title: { TextState("알림 권한 안내")}, + actions: { ButtonState( + role: .destructive, + action: .confirmPushNotification, + label: { TextState("설정하러 가기") }) }, + message: { TextState("설정 > '보틀' > 알림에서 알림을 허용해주세요.")})) + + return .none + + case let .destination(.presented(.alert(alert))): + switch alert { + case .confirmPushNotification: + URLHandler.shared.openURL(urlType: .setting) + return .none } default: return .none } + + func updatePushNotificationAllowStatus(state: inout State) { + let isAllow = userClient.fetchPushNotificationAllowStatus() + state.isAllowPushNotification = isAllow + } } self.init(reducer: reducer) diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift index 3f4ae4c0..0f554493 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeatureInterface.swift @@ -6,6 +6,7 @@ // import Foundation +import Combine import DomainUserInterface @@ -18,20 +19,30 @@ public struct AlertSettingFeature { public init(reducer: Reduce) { self.reducer = reducer } + // Desination + @Reducer(state: .equatable) + public enum Destination { + case alert(AlertState) + } @ObservableState public struct State: Equatable { + var isAllowPushNotification: Bool public var isOnRandomBottleToggle: Bool public var isOnArrivalBottleToggle: Bool public var isOnPingPongToggle: Bool public var isOnMarketingToggle: Bool + @Presents var destination: Destination.State? + public init( + isAllowPushNotification: Bool = false, isOnRandomBottleToggle: Bool = false, isOnArrivalBottleToggle: Bool = false, isOnPingPongToggle: Bool = false, isOnMarketingToggle: Bool = false ) { + self.isAllowPushNotification = isAllowPushNotification self.isOnRandomBottleToggle = isOnRandomBottleToggle self.isOnArrivalBottleToggle = isOnArrivalBottleToggle self.isOnPingPongToggle = isOnPingPongToggle @@ -46,15 +57,26 @@ public struct AlertSettingFeature { case arrivalBottleToggleDidFetched(isOn: Bool) case pingpongToggleDidFetched(isOn: Bool) case marketingToggleDidFetched(isOn: Bool) + case pushNotificationAlertDidRequired + case pushNotificationAllowed(isAllow: Bool) + case alertStateFetchDidRequest // UserAction - case toggleDidChanged(alertState: UserAlertState) + case toggleDidChanged(alertState: UserAlertState, id: ID) case backButtonDidTapped + // ETC case binding(BindingAction) + case destination(PresentationAction) + + // Alert + case alert(Alert) + public enum Alert: Equatable { + case confirmPushNotification + } } - enum ID: Hashable { + public enum ID: Hashable { case randomBottle case arrivalBottle case pingping @@ -64,5 +86,6 @@ public struct AlertSettingFeature { public var body: some ReducerOf { BindingReducer() reducer + .ifLet(\.$destination, action: \.destination) } } diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift index d9c9840b..d0428bea 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingView.swift @@ -39,6 +39,7 @@ public struct AlertSettingView: View { .setNavigationBar { makeNaivgationleftButton { store.send(.backButtonDidTapped) } } + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .onLoad { store.send(.onLoad) } } } diff --git a/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift b/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift index af132360..7addfb6b 100644 --- a/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift @@ -48,5 +48,6 @@ public struct ProfileEditView: View { } } } + .ignoresSafeArea(.all, edges: [.bottom, .top]) } } diff --git a/Projects/Feature/Onboarding/Interface/Sources/Onboarding/OnboardingView.swift b/Projects/Feature/Onboarding/Interface/Sources/Onboarding/OnboardingView.swift index fb869298..6191dcde 100644 --- a/Projects/Feature/Onboarding/Interface/Sources/Onboarding/OnboardingView.swift +++ b/Projects/Feature/Onboarding/Interface/Sources/Onboarding/OnboardingView.swift @@ -43,7 +43,7 @@ public struct OnboardingView: View { } } ) - .ignoresSafeArea(.all, edges: .bottom) + .ignoresSafeArea(.all, edges: [.top, .bottom]) .toolbar(.hidden, for: .navigationBar) .overlay { if store.isShowLoadingProgressView { diff --git a/Projects/Feature/Report/Interface/Sources/ReportUserFeature.swift b/Projects/Feature/Report/Interface/Sources/ReportUserFeature.swift index 9a008b48..b854751c 100644 --- a/Projects/Feature/Report/Interface/Sources/ReportUserFeature.swift +++ b/Projects/Feature/Report/Interface/Sources/ReportUserFeature.swift @@ -28,10 +28,19 @@ extension ReportUserFeature { print("tapped") state.destination = .alert(.init( title: { TextState("신고하기")}, - actions: { ButtonState( - role: .destructive, - action: .confirmReport, - label: { TextState("신고하기") }) }, + actions: { + ButtonState( + role: .cancel, + action: .confirmReport, + label: { TextState("계속하기") } + ) + + ButtonState( + role: .destructive, + action: .dismiss, + label: { TextState("중단하기") } + ) + }, message: { TextState("접수 후 취소할 수 없으며 해당 사용자는 차단되요.\n정말 신고하시겠어요?")})) return .none @@ -43,10 +52,17 @@ extension ReportUserFeature { } return .none - case .destination(.presented(.alert(.confirmReport))): - return .run { [userProfile = state.userProfile, reportText = state.reportText] send in - try await reportClient.reportUser(userReportInfo: .init(reason: reportText, userId: userProfile.userID)) - await send(.delegate(.reportDidCompleted)) + case let .destination(.presented(.alert(alert))): + switch alert { + case .confirmReport: + return .run { [userProfile = state.userProfile, reportText = state.reportText] send in + try await reportClient.reportUser(userReportInfo: .init(reason: reportText, userId: userProfile.userID)) + await send(.delegate(.reportDidCompleted)) + } + + case .dismiss: + state.destination = nil + return .none } case .binding(\.reportText): diff --git a/Projects/Feature/Report/Interface/Sources/ReportUserFeatureInterface.swift b/Projects/Feature/Report/Interface/Sources/ReportUserFeatureInterface.swift index 9c8b487f..6b1fe780 100644 --- a/Projects/Feature/Report/Interface/Sources/ReportUserFeatureInterface.swift +++ b/Projects/Feature/Report/Interface/Sources/ReportUserFeatureInterface.swift @@ -70,6 +70,7 @@ public struct ReportUserFeature { case alert(Alert) public enum Alert: Equatable { case confirmReport + case dismiss } // ETC diff --git a/Projects/Feature/Report/Interface/Sources/ReportUserView.swift b/Projects/Feature/Report/Interface/Sources/ReportUserView.swift index 3f626967..bef7669e 100644 --- a/Projects/Feature/Report/Interface/Sources/ReportUserView.swift +++ b/Projects/Feature/Report/Interface/Sources/ReportUserView.swift @@ -35,7 +35,7 @@ public struct ReportUserView: View { } } .padding(.horizontal, .md) - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .toolbar(.hidden, for: .bottomBar) } } diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift index bcfea92b..cb523178 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift @@ -64,7 +64,7 @@ public struct SandBeachView: View { } } } - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .onAppear { store.send(.onAppear) } diff --git a/Projects/Feature/Sources/App/AppDelegateFeature.swift b/Projects/Feature/Sources/App/AppDelegateFeature.swift index 4233bede..fb7b9659 100644 --- a/Projects/Feature/Sources/App/AppDelegateFeature.swift +++ b/Projects/Feature/Sources/App/AppDelegateFeature.swift @@ -7,6 +7,8 @@ import Foundation +import DomainUserInterface + import ComposableArchitecture import KakaoSDKCommon @@ -20,6 +22,7 @@ public struct AppDelegateFeature { public enum Action { case didFinishLunching case didReceivedFcmToken(fcmToken: String) + case pushNotificationAllowStatusDidChanged(isAllow: Bool) // Delegate case delegate(Delegate) @@ -37,6 +40,8 @@ public struct AppDelegateFeature { state: inout State, action: Action ) -> EffectOf { + @Dependency(\.userClient) var userClient + switch action { case .didFinishLunching: guard let kakaoAppKey = Bundle.main.infoDictionary?["KAKAO_APP_KEY"] as? String else { @@ -51,6 +56,10 @@ public struct AppDelegateFeature { await send(.delegate(.fcmTokenDidRecevied(fcmToken: fcmToken))) } + case let .pushNotificationAllowStatusDidChanged(isAllow): + userClient.updatePushNotificationAllowStatus(isAllow: isAllow) + return .none + default: return .none } diff --git a/Projects/Feature/Sources/SplashView/SplashView.swift b/Projects/Feature/Sources/SplashView/SplashView.swift index 52d19d52..7329ec31 100644 --- a/Projects/Feature/Sources/SplashView/SplashView.swift +++ b/Projects/Feature/Sources/SplashView/SplashView.swift @@ -26,7 +26,7 @@ public struct SplashView: View { Image.BottleImageSystem.illustraition(.splash).image } - .alert($store.scope(state: \.destination?.alert, action: \.destination.alert)) + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) .ignoresSafeArea() .task { store.send(.onAppear) diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index ab33dae3..3e24b704 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -10,8 +10,8 @@ import ProjectDescription public extension InfoPlist { static var app: InfoPlist { return .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0.7", - "CFBundleVersion": "27", + "CFBundleShortVersionString": "1.0.8", + "CFBundleVersion": "30", "UIUserInterfaceStyle": "Light", "CFBundleName": "보틀", "UILaunchScreen": [ @@ -43,8 +43,8 @@ public extension InfoPlist { static var example: InfoPlist { return .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0.7", - "CFBundleVersion": "27", + "CFBundleShortVersionString": "1.0.8", + "CFBundleVersion": "30", "UIUserInterfaceStyle": "Light", "UILaunchScreen": [:], "UISupportedInterfaceOrientations": [ From fd7236cca0168800764d4044d6cc6322daf7dafb Mon Sep 17 00:00:00 2001 From: JongHoon Date: Thu, 3 Oct 2024 16:20:22 +0900 Subject: [PATCH 48/90] setting: Create pull-request.yml --- .github/workflows/pull-request.yml | 85 ++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .github/workflows/pull-request.yml diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 00000000..83e44c54 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,85 @@ +name: Bottles Pull Request Workflow + +on: + pull_request: + branches: + - develop + types: [ opened, reopened, synchronize ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: '17' + + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + shell: bash + + - name: Build with gradle + run: ./gradlew clean build + shell: bash + + - name: Send Discord Notification + if: github.event.action == 'opened' || github.event.action == 'reopened' + env: + DATA: | + { + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*서버 PR* :bell: <@U07L8AX9B4N><@U07L87A3WKY>" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Author:*\n" + }, + { + "type": "mrkdwn", + "text": "*Title:*\n${{ github.event.pull_request.title }}" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Description:*\n${{ github.event.pull_request.body }}" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Pull Request URL:*\n<${{ github.event.pull_request.html_url }}|View PR>" + } + } + ] + } + run: | + curl -X POST -H 'Content-type: application/json' \ + -d "$DATA" \ + ${{ secrets.SLACK_WEBHOOK_URL }} From 1f88d7c1da58cea3bb39a587c17358d9c1b1b520 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Thu, 3 Oct 2024 16:32:26 +0900 Subject: [PATCH 49/90] Update pull-request.yml From 85257cd21a260b435055fdc098a43c7216ef080e Mon Sep 17 00:00:00 2001 From: JongHoon Date: Thu, 3 Oct 2024 16:42:27 +0900 Subject: [PATCH 50/90] Update pull-request.yml --- .github/workflows/pull-request.yml | 35 ++++-------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 83e44c54..6dc47f6a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -10,34 +10,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'corretto' - java-version: '17' - - - name: Cache Gradle - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - shell: bash - - - name: Build with gradle - run: ./gradlew clean build - shell: bash - - - name: Send Discord Notification + - name: Send Slack Notification if: github.event.action == 'opened' || github.event.action == 'reopened' env: DATA: | @@ -47,7 +20,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": "*서버 PR* :bell: <@U07L8AX9B4N><@U07L87A3WKY>" + "text": "*서버 PR* :bell: @김미성@손인준" } }, { @@ -55,7 +28,7 @@ jobs: "fields": [ { "type": "mrkdwn", - "text": "*Author:*\n" + "text": "*Author:*\n${{ github.event.sender.login }}" }, { "type": "mrkdwn", @@ -74,7 +47,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": "*Pull Request URL:*\n<${{ github.event.pull_request.html_url }}|View PR>" + "text": "*Pull Request URL:*\nView PR" } } ] From 3bb4a4cae5e762190e01b3105e3334aa6a5f301c Mon Sep 17 00:00:00 2001 From: JongHoon Date: Thu, 3 Oct 2024 16:50:43 +0900 Subject: [PATCH 51/90] Update pull-request.yml --- .github/workflows/pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6dc47f6a..2c41d788 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -20,7 +20,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": "*서버 PR* :bell: @김미성@손인준" + "text": "*iOS PR* :bell: <@D07LESGF5NF> <@D07LTKV81L1>" } }, { @@ -47,7 +47,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": "*Pull Request URL:*\nView PR" + "text": "*Pull Request URL:*\n<${{ github.event.pull_request.html_url }}|View PR>" } } ] From 26ae24d87e2ed5b06862b72dd572db32e2b58403 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Thu, 3 Oct 2024 17:01:51 +0900 Subject: [PATCH 52/90] Update pull-request.yml --- .github/workflows/pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2c41d788..1efa616f 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -20,7 +20,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": "*iOS PR* :bell: <@D07LESGF5NF> <@D07LTKV81L1>" + "text": "*iOS PR* :bell: <@U07LESGBQEP> <@U07LHEEU2BW>" } }, { @@ -28,7 +28,7 @@ jobs: "fields": [ { "type": "mrkdwn", - "text": "*Author:*\n${{ github.event.sender.login }}" + "text": "*Author:*\n" }, { "type": "mrkdwn", From e280a64a57a3bb9c005da65aa36e25900efded4d Mon Sep 17 00:00:00 2001 From: JongHoon Date: Tue, 15 Oct 2024 18:16:54 +0900 Subject: [PATCH 53/90] =?UTF-8?q?[Setting/#303]=20assertion=20error=20?= =?UTF-8?q?=EC=8A=AC=EB=9E=99=20=EB=A1=9C=EA=B9=85=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?#304?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Logger/Interface/Sources/Log.swift | 75 ++++++++++++++++++- .../InfoPlist+Templates.swift | 1 + 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/Projects/Core/Logger/Interface/Sources/Log.swift b/Projects/Core/Logger/Interface/Sources/Log.swift index 6faacd2c..a06b00db 100644 --- a/Projects/Core/Logger/Interface/Sources/Log.swift +++ b/Projects/Core/Logger/Interface/Sources/Log.swift @@ -5,7 +5,7 @@ // Created by 임현규 on 7/23/24. // -import Foundation +import UIKit import OSLog public enum Log { @@ -132,6 +132,79 @@ public extension Log { let logMessage = "\(message ?? "")" log(message: logMessage, level: level, fileName: fileName, line: line, funcName: funcName) assertionFailure(logMessage) +#if !DEBUG + Task { + guard let url = URL(string: Bundle.main.infoDictionary?["SLACK_WEBHOOK_URL"] as? String ?? "") + else { + return + } + guard let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, + let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String + else { + return + } + async let device = UIDevice.current + let systemVersion = await device.systemVersion + let deviceName = await UIDevice.current.name + + let errorLogJsonObject: [String: Any] = [ + "pretext": "iOS Error Occured🔥", + "color": "#36a64f", + "fields": [ + [ + "title": "Message", + "value": "\(message ?? "no message")", + "short": true + ], + [ + "title": "File Name", + "value": "\(fileName)", + "short": true + ], + [ + "title": "Function Name", + "value": "\(funcName)", + "short": true + ], + [ + "title": "Line", + "value": "\(line)", + "short": true + ], + [ + "title": "Version", + "value": "\(appVersion)", + "short": true + ], + [ + "title": "Build Number", + "value": "\(buildNumber)", + "short": true + ], + [ + "title": "Device Name", + "value": "\(deviceName)", + "short": true + ], + [ + "title": "iOS Version", + "value": "\(systemVersion)", + "short": true + ] + ] + ] + do { + let errorLogJsonData = try JSONSerialization.data(withJSONObject: errorLogJsonObject) + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = errorLogJsonData + _ = try await URLSession.shared.data(for: request) + } catch { + Log.debug(error) + } + } +#endif } static func fatal(message: Any?, level: Level = .fault, fileName: String = #fileID, line: Int = #line, funcName: StaticString = #function) { diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index 3e24b704..17f2a117 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -29,6 +29,7 @@ public extension InfoPlist { "APP_STORE_URL": "$(APP_STORE_URL)", "APP_LOOK_UP_URL": "$(APP_LOOK_UP_URL)", "KAKAO_CHANNEL_TALK_URL": "$(KAKAO_CHANNEL_TALK_URL)", + "SLACK_WEBHOOK_URL": "$(SLACK_WEBHOOK_URL)", "LSApplicationQueriesSchemes": ["kakaokompassauth", "kakaotalk"], "CFBundleURLTypes": [ [ From ecd6d0052b58c1851aff7074a34811faecfc53b3 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Tue, 15 Oct 2024 18:18:13 +0900 Subject: [PATCH 54/90] =?UTF-8?q?feat:=20=ED=98=B8=EA=B0=90=20=ED=83=AD=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=98=B8=EA=B0=90=20=EC=9B=B9?= =?UTF-8?q?=EB=B7=B0=20=EC=97=B0=EA=B2=B0=20(#307)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectDescriptionHelpers/Modules.swift | 1 + .../Interface/Sources/BaseWebViewType.swift | 6 ++ .../View/SubViews/Matching/MatchingView.swift | 2 +- .../GoodFeeling/Example/Sources/AppView.swift | 21 +++++++ .../GoodFeeling/GoodFeelingFeature.swift | 34 +++++++++++ .../GoodFeelingFeatureInterface.swift | 20 +++++++ .../Sources/GoodFeeling/GoodFeelingView.swift | 31 ++++++++++ .../Root/FeatureGoodFeelingRootView.swift | 42 +++++++++++++ .../Sources/Root/GoodFeelingRootFeature.swift | 59 +++++++++++++++++++ .../GoodFeelingRootFeatureInterface.swift | 26 ++++++++ Projects/Feature/GoodFeeling/Project.swift | 53 +++++++++++++++++ .../Feature/GoodFeeling/Sources/Source.swift | 1 + .../Testing/Sources/GoodFeelingTesting.swift | 1 + .../Tests/Sources/GoodFeelingTest.swift | 11 ++++ .../Feature/Sources/TabView/MainTabView.swift | 7 ++- .../Sources/TabView/MainTabViewFeature.swift | 20 ++++++- .../TabBar/Interface/Sources/TabType.swift | 13 +++- .../icon/icon_heart.imageset/Contents.json | 24 ++++++++ .../icon/icon_heart.imageset/icon_heart.svg | 10 ++++ .../Components/Alert/BottleAlertView.swift | 2 +- .../OutlinedButton/OutlinedStyleButton.swift | 2 +- .../Button/SolidButton/SolidButton.swift | 4 +- .../Card/PingPong/PingPongContainerView.swift | 2 +- .../Card/UserProfile/UserProfileView.swift | 2 +- .../Components/ETC/ImagePickerButton.swift | 4 +- .../Components/List/ArrowListView.swift | 2 +- .../LineTextField/LineTextField.swift | 2 +- .../Image/BottleImageSystem+Icon.swift | 4 ++ .../Sources/Image/Image+Extensions.swift | 6 +- .../Modifiers/NavigationBarModifier.swift | 4 +- 30 files changed, 394 insertions(+), 22 deletions(-) create mode 100644 Projects/Feature/GoodFeeling/Example/Sources/AppView.swift create mode 100644 Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift create mode 100644 Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift create mode 100644 Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift create mode 100644 Projects/Feature/GoodFeeling/Interface/Sources/Root/FeatureGoodFeelingRootView.swift create mode 100644 Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeature.swift create mode 100644 Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeatureInterface.swift create mode 100644 Projects/Feature/GoodFeeling/Project.swift create mode 100644 Projects/Feature/GoodFeeling/Sources/Source.swift create mode 100644 Projects/Feature/GoodFeeling/Testing/Sources/GoodFeelingTesting.swift create mode 100644 Projects/Feature/GoodFeeling/Tests/Sources/GoodFeelingTest.swift create mode 100644 Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_heart.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_heart.imageset/icon_heart.svg diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift index 8f56cd8c..a6d4af0e 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Modules.swift @@ -27,6 +27,7 @@ public extension ModulePath { // MARK: - FeatureModule public extension ModulePath { enum Feature: String, CaseIterable { + case GoodFeeling case Guide case TabBar case Report diff --git a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift index 11d53307..3cca11f0 100644 --- a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift +++ b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift @@ -26,6 +26,7 @@ public enum BottleWebViewType { case login case bottles case editProfile + case goodFeeling var path: String { switch self { @@ -39,6 +40,8 @@ public enum BottleWebViewType { return "bottles" case .editProfile: return "profile/edit" + case .goodFeeling: + return "bottles/sents" } } @@ -58,6 +61,9 @@ public enum BottleWebViewType { case .editProfile: return makeUrlWithToken(path) + + case .goodFeeling: + return makeUrlWithToken(path) } } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Matching/MatchingView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Matching/MatchingView.swift index c1fbca1a..2515d20b 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Matching/MatchingView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Matching/MatchingView.swift @@ -139,7 +139,7 @@ private extension MatchingView { ) OutlinedStyleButton( - .small(contentType: .image(type: .local(bottleImageSystem: .icom(.share)))), + .small(contentType: .image(type: .local(bottleImageSystem: .icon(.share)))), title: "복사하기", buttonType: .throttle ) { diff --git a/Projects/Feature/GoodFeeling/Example/Sources/AppView.swift b/Projects/Feature/GoodFeeling/Example/Sources/AppView.swift new file mode 100644 index 00000000..021e3614 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Example/Sources/AppView.swift @@ -0,0 +1,21 @@ +import SwiftUI + +import FeatureGoodFeeling +import FeatureGoodFeelingInterface + +import DomainAuth +import DomainAuthInterface + +import ComposableArchitecture + +@main +struct AppView: App { + var body: some Scene { + WindowGroup { + GoodFeelingRootView(store: Store( + initialState: GoodFeelingRootFeature.State(), + reducer: { GoodFeelingRootFeature() } + )) + } + } +} diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift new file mode 100644 index 00000000..50faada6 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift @@ -0,0 +1,34 @@ +// +// GoodFeelingFeatureInterface.swift +// FeatureGoodFeelingInterface +// +// Created by JongHoon on 10/6/24. +// + +import Foundation + +import ComposableArchitecture + +@Reducer +public struct GoodFeelingFeature { + private let reducer: Reduce + + public init(reducer: Reduce) { + self.reducer = reducer + } + + @ObservableState + public struct State: Equatable { + public init() { + + } + } + + public enum Action: BindableAction { + case binding(BindingAction) + } + + public var body: some ReducerOf { + reducer + } +} diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift new file mode 100644 index 00000000..c842b715 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift @@ -0,0 +1,20 @@ +// +// GoodFeelingFeature.swift +// FeatureGoodFeelingInterface +// +// Created by JongHoon on 10/6/24. +// + +import Foundation + +import ComposableArchitecture + +extension GoodFeelingFeature { + public init() { + let reducer = Reduce { state, action in + return .none + } + + self.init(reducer: reducer) + } +} diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift new file mode 100644 index 00000000..747d650a --- /dev/null +++ b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift @@ -0,0 +1,31 @@ +// +// GoodFeelingView.swift +// FeatureGoodFeelingInterface +// +// Created by JongHoon on 10/6/24. +// + +import SwiftUI + +import FeatureBaseWebViewInterface + +import ComposableArchitecture + +public struct GoodFeelingView: View { + @Perception.Bindable private var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithPerceptionTracking { + BaseWebView( + type: .goodFeeling, + actionDidInputted: { action in + // TODO: action handling + } + ) + } + } +} diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/Root/FeatureGoodFeelingRootView.swift b/Projects/Feature/GoodFeeling/Interface/Sources/Root/FeatureGoodFeelingRootView.swift new file mode 100644 index 00000000..59ed4db6 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Interface/Sources/Root/FeatureGoodFeelingRootView.swift @@ -0,0 +1,42 @@ +// +// FeatureGoodFeelingRootView.swift +// FeatureGoodFeeling +// +// Created by JongHoon on 10/6/24. +// + +import SwiftUI + +import SharedDesignSystem + +import ComposableArchitecture + +public struct GoodFeelingRootView: View { + @Perception.Bindable private var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithPerceptionTracking { + NavigationStack(path: $store.scope(state: \.path, action: \.path)) { + VStack(spacing: 0.0) { + GoodFeelingView(store: store.scope( + state: \.goodFeeling, + action: \.goodFeeling + )) + Spacer() + .frame(height: BottleConstants.bottomTabBarHeight.value) + } + .setTabBar(selectedTab: .goodFeeling) { selectedTab in + store.send(.selectedTabDidChanged(selectedTab)) + } + } destination: { store in + WithPerceptionTracking { + + } + } + } + } +} diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeature.swift b/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeature.swift new file mode 100644 index 00000000..8d530da2 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeature.swift @@ -0,0 +1,59 @@ +// +// GoodFeelingRootFeature.swift +// FeatureGoodFeelingInterface +// +// Created by JongHoon on 10/6/24. +// + +import Foundation + +import FeatureTabBarInterface + +import ComposableArchitecture + +@Reducer +public struct GoodFeelingRootFeature { + private let reducer: Reduce + + public init(reducer: Reduce) { + self.reducer = reducer + } + + @Reducer(state: .equatable) + public enum Path { + + } + + @ObservableState + public struct State: Equatable { + public var goodFeeling: GoodFeelingFeature.State + + var path = StackState() + + public init() { + self.goodFeeling = .init() + } + } + + public enum Action { + case selectedTabDidChanged(TabType) + case goodFeeling(GoodFeelingFeature.Action) + + case path(StackAction) + case delegate(Delegate) + + public enum Delegate { + case selectedTabDidChanged(TabType) + } + } + + public var body: some ReducerOf { + Scope(state: \.goodFeeling, action: \.goodFeeling) { + GoodFeelingFeature() + } + + reducer + .forEach(\.path, action: \.path) + } +} + diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeatureInterface.swift b/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeatureInterface.swift new file mode 100644 index 00000000..a1db7c60 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeatureInterface.swift @@ -0,0 +1,26 @@ +// +// GoodFeelingRootFeatureInterface.swift +// FeatureGoodFeelingInterface +// +// Created by JongHoon on 10/6/24. +// + +import Foundation + +import ComposableArchitecture + +extension GoodFeelingRootFeature { + public init() { + let reducer = Reduce { state, action in + switch action { + case let .selectedTabDidChanged(selectedTab): + return .send(.delegate(.selectedTabDidChanged(selectedTab))) + + default: + return .none + } + } + + self.init(reducer: reducer) + } +} diff --git a/Projects/Feature/GoodFeeling/Project.swift b/Projects/Feature/GoodFeeling/Project.swift new file mode 100644 index 00000000..b63af4e7 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Project.swift @@ -0,0 +1,53 @@ +import ProjectDescription +import ProjectDescriptionHelpers +import DependencyPlugin + +let project = Project.makeModule( + name: ModulePath.Feature.name+ModulePath.Feature.GoodFeeling.rawValue, + targets: [ + .feature( + interface: .GoodFeeling, + factory: .init( + dependencies: [ + .domain, + .feature(interface: .TabBar), + .feature(interface: .BaseWebView) + ] + ) + ), + .feature( + implements: .GoodFeeling, + factory: .init( + dependencies: [ + .feature(interface: .GoodFeeling) + ] + ) + ), + .feature( + testing: .GoodFeeling, + factory: .init( + dependencies: [ + .feature(interface: .GoodFeeling) + ] + ) + ), + .feature( + tests: .GoodFeeling, + factory: .init( + dependencies: [ + .feature(testing: .GoodFeeling), + .feature(implements: .GoodFeeling) + ] + ) + ), + .feature( + example: .GoodFeeling, + factory: .init( + dependencies: [ + .feature(testing: .GoodFeeling), + .feature(implements: .GoodFeeling) + ] + ) + ) + ] +) diff --git a/Projects/Feature/GoodFeeling/Sources/Source.swift b/Projects/Feature/GoodFeeling/Sources/Source.swift new file mode 100644 index 00000000..b1853ce6 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Sources/Source.swift @@ -0,0 +1 @@ +// This is for Tuist diff --git a/Projects/Feature/GoodFeeling/Testing/Sources/GoodFeelingTesting.swift b/Projects/Feature/GoodFeeling/Testing/Sources/GoodFeelingTesting.swift new file mode 100644 index 00000000..b1853ce6 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Testing/Sources/GoodFeelingTesting.swift @@ -0,0 +1 @@ +// This is for Tuist diff --git a/Projects/Feature/GoodFeeling/Tests/Sources/GoodFeelingTest.swift b/Projects/Feature/GoodFeeling/Tests/Sources/GoodFeelingTest.swift new file mode 100644 index 00000000..c453bfff --- /dev/null +++ b/Projects/Feature/GoodFeeling/Tests/Sources/GoodFeelingTest.swift @@ -0,0 +1,11 @@ +import XCTest + +final class GoodFeelingTests: XCTestCase { + override func setUpWithError() throws {} + + override func tearDownWithError() throws {} + + func testExample() { + XCTAssertEqual(1, 1) + } +} diff --git a/Projects/Feature/Sources/TabView/MainTabView.swift b/Projects/Feature/Sources/TabView/MainTabView.swift index 32787aa8..60833013 100644 --- a/Projects/Feature/Sources/TabView/MainTabView.swift +++ b/Projects/Feature/Sources/TabView/MainTabView.swift @@ -7,9 +7,10 @@ import SwiftUI +import FeatureSandBeachInterface +import FeatureGoodFeelingInterface import FeatureBottleStorageInterface import FeatureMyPageInterface -import FeatureSandBeachInterface import FeatureTabBarInterface import SharedDesignSystem @@ -30,6 +31,10 @@ public struct MainTabView: View { .tag(TabType.sandBeach) .toolbar(.hidden, for: .tabBar) + GoodFeelingRootView(store: store.scope(state: \.goodFeelingRoot, action: \.goodFeelingRoot)) + .tag(TabType.goodFeeling) + .toolbar(.hidden, for: .tabBar) + BottleStorageView(store: store.scope(state: \.bottleStorage, action: \.bottleStorage)) .tag(TabType.bottleStorage) .toolbar(.hidden, for: .tabBar) diff --git a/Projects/Feature/Sources/TabView/MainTabViewFeature.swift b/Projects/Feature/Sources/TabView/MainTabViewFeature.swift index ecbe49c7..755c5400 100644 --- a/Projects/Feature/Sources/TabView/MainTabViewFeature.swift +++ b/Projects/Feature/Sources/TabView/MainTabViewFeature.swift @@ -7,12 +7,14 @@ import Foundation +import FeatureSandBeach +import FeatureSandBeachInterface +import FeatureGoodFeeling +import FeatureGoodFeelingInterface import FeatureBottleStorage import FeatureBottleStorageInterface import FeatureMyPage import FeatureMyPageInterface -import FeatureSandBeach -import FeatureSandBeachInterface import FeatureTabBarInterface import ComposableArchitecture @@ -23,12 +25,14 @@ public struct MainTabViewFeature { @ObservableState public struct State: Equatable { var sandBeachRoot: SandBeachRootFeature.State + var goodFeelingRoot: GoodFeelingRootFeature.State var bottleStorage: BottleStorageFeature.State var myPageRoot: MyPageRootFeature.State var selectedTab: TabType var isLoading: Bool public init() { self.sandBeachRoot = .init() + self.goodFeelingRoot = .init() self.bottleStorage = .init() self.myPageRoot = .init() self.selectedTab = .sandBeach @@ -38,6 +42,7 @@ public struct MainTabViewFeature { public enum Action: BindableAction { case sandBeachRoot(SandBeachRootFeature.Action) + case goodFeelingRoot(GoodFeelingRootFeature.Action) case bottleStorage(BottleStorageFeature.Action) case myPageRoot(MyPageRootFeature.Action) case selectedTabChanged(TabType) @@ -57,6 +62,9 @@ public struct MainTabViewFeature { Scope(state: \.sandBeachRoot, action: \.sandBeachRoot) { SandBeachRootFeature() } + Scope(state: \.goodFeelingRoot, action: \.goodFeelingRoot) { + GoodFeelingRootFeature() + } Scope(state: \.bottleStorage, action: \.bottleStorage) { BottleStorageFeature() } @@ -87,6 +95,14 @@ public struct MainTabViewFeature { } return .none + // GoodFeeling Delegate + case let .goodFeelingRoot(.delegate(delegate)): + switch delegate { + case let .selectedTabDidChanged(selectedTab): + state.selectedTab = selectedTab + return .none + } + // BottleStorage Delegate case let .bottleStorage(.delegate(delegate)): switch delegate { diff --git a/Projects/Feature/TabBar/Interface/Sources/TabType.swift b/Projects/Feature/TabBar/Interface/Sources/TabType.swift index b001961c..a89c7be8 100644 --- a/Projects/Feature/TabBar/Interface/Sources/TabType.swift +++ b/Projects/Feature/TabBar/Interface/Sources/TabType.swift @@ -10,6 +10,7 @@ import SharedDesignSystem public enum TabType: Hashable, CaseIterable { case sandBeach + case goodFeeling case bottleStorage case myPage @@ -18,6 +19,9 @@ public enum TabType: Hashable, CaseIterable { case .sandBeach: return "모래사장" + case .goodFeeling: + return "호감" + case .bottleStorage: return "보틀 보관함" @@ -29,13 +33,16 @@ public enum TabType: Hashable, CaseIterable { var image: Image.BottleImageSystem { switch self { case .sandBeach: - return .icom(.sandBeach) + return .icon(.sandBeach) + + case .goodFeeling: + return .icon(.goodFeeling) case .bottleStorage: - return .icom(.bottleStorage) + return .icon(.bottleStorage) case .myPage: - return .icom(.myPage) + return .icon(.myPage) } } } diff --git a/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_heart.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_heart.imageset/Contents.json new file mode 100644 index 00000000..3280bd66 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_heart.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "icon_heart.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_heart.imageset/icon_heart.svg b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_heart.imageset/icon_heart.svg new file mode 100644 index 00000000..5c514d3c --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_heart.imageset/icon_heart.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift index 80228c04..a25aae16 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Alert/BottleAlertView.swift @@ -53,7 +53,7 @@ struct BottleAlertView: View where A: View, M: View { private extension BottleAlertView { var alertImage: some View { - BottleImageView(type: .local(bottleImageSystem: .icom(.warning))) + BottleImageView(type: .local(bottleImageSystem: .icon(.warning))) .foregroundStyle(to: ColorToken.icon(.primary)) .padding(.top, .lg) .padding(.bottom, .xs) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/OutlinedButton/OutlinedStyleButton.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/OutlinedButton/OutlinedStyleButton.swift index bfa51416..f6dba8b7 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Button/OutlinedButton/OutlinedStyleButton.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/OutlinedButton/OutlinedStyleButton.swift @@ -132,7 +132,7 @@ private extension OutlinedStyleButton { default: assertionFailure("Wrong Image Configuration") - return .local(bottleImageSystem: .icom(.siren)) + return .local(bottleImageSystem: .icon(.siren)) } case let .local(bottleImageSystem): diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButton.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButton.swift index b3fae586..8df94456 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButton.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/SolidButton/SolidButton.swift @@ -87,10 +87,10 @@ extension SolidButton { var image: some View { switch buttonApperance { case .kakao: - BottleImageView(type: .local(bottleImageSystem: .icom(.kakaoLogo))) + BottleImageView(type: .local(bottleImageSystem: .icon(.kakaoLogo))) case .apple: - BottleImageView(type: .local(bottleImageSystem: .icom(.appleLogo))) + BottleImageView(type: .local(bottleImageSystem: .icon(.appleLogo))) case .solid: EmptyView() case .generalSignIn: diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/PingPong/PingPongContainerView.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/PingPong/PingPongContainerView.swift index f77e13ea..a631779d 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Card/PingPong/PingPongContainerView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/PingPong/PingPongContainerView.swift @@ -62,7 +62,7 @@ private extension PingPongContainer { Spacer() - LocalImageView(.icom(.up)) + LocalImageView(.icon(.up)) .rotationEffect(.degrees((isHidden ? -180 : 0))) .asButton { withAnimation(.snappy) { diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift index 6ae44506..014ad572 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift @@ -70,7 +70,7 @@ private extension UserProfileView { } var verticalLine: some View { - LocalImageView(.icom(.verticalLine)) + LocalImageView(.icon(.verticalLine)) .foregroundStyle(to: ColorToken.border(.secondary)) } diff --git a/Projects/Shared/DesignSystem/Sources/Components/ETC/ImagePickerButton.swift b/Projects/Shared/DesignSystem/Sources/Components/ETC/ImagePickerButton.swift index 8f06031c..9882812c 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/ETC/ImagePickerButton.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/ETC/ImagePickerButton.swift @@ -55,7 +55,7 @@ private extension ImagePickerButton { deleteButton } } else { - LocalImageView(.icom(.plus)) + LocalImageView(.icon(.plus)) .frame(width: width, height: width, alignment: .center) } } @@ -68,7 +68,7 @@ private extension ImagePickerButton { .clipShape(RoundedRectangle(cornerRadius: BottleRadiusType.xs.value)) .frame(width: 36, height: 36) .overlay { - LocalImageView(.icom(.clearDelete)) + LocalImageView(.icon(.clearDelete)) .asThrottleButton { self.selectedImage.removeAll() action() diff --git a/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift index 71f8bd43..a1f91696 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/List/ArrowListView.swift @@ -33,7 +33,7 @@ private extension ArrowListView { var rightArrowImage: some View { BottleImageView( type: .local( - bottleImageSystem: .icom(.right) + bottleImageSystem: .icon(.right) ) ) .foregroundStyle(to: ColorToken.icon(.primary)) diff --git a/Projects/Shared/DesignSystem/Sources/Components/TextField/LineTextField/LineTextField.swift b/Projects/Shared/DesignSystem/Sources/Components/TextField/LineTextField/LineTextField.swift index 8065f33b..1b0ebbcf 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/TextField/LineTextField/LineTextField.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/TextField/LineTextField/LineTextField.swift @@ -49,7 +49,7 @@ private extension LineTextField { if text.isEmpty { EmptyView() } else { - BottleImageView(type: .local(bottleImageSystem: .icom(.delete))) + BottleImageView(type: .local(bottleImageSystem: .icon(.delete))) .foregroundStyle(to: ColorToken.icon(.primary)) .asThrottleButton { self.text = "" diff --git a/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift b/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift index 6abc53c8..c4bccc03 100644 --- a/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift +++ b/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift @@ -22,6 +22,7 @@ public extension Image.BottleImageSystem { case kakaoLogo case sandBeach case bottleStorage + case goodFeeling case myPage case appleLogo case warning @@ -61,6 +62,9 @@ public extension Image.BottleImageSystem.Icon { case .bottleStorage: return SharedDesignSystemAsset.Images.iconBottleStorage.swiftUIImage + case .goodFeeling: + return SharedDesignSystemAsset.Images.iconHeart.swiftUIImage + case .myPage: return SharedDesignSystemAsset.Images.iconMyPage.swiftUIImage diff --git a/Projects/Shared/DesignSystem/Sources/Image/Image+Extensions.swift b/Projects/Shared/DesignSystem/Sources/Image/Image+Extensions.swift index 28d6c5f6..8affd312 100644 --- a/Projects/Shared/DesignSystem/Sources/Image/Image+Extensions.swift +++ b/Projects/Shared/DesignSystem/Sources/Image/Image+Extensions.swift @@ -9,12 +9,12 @@ import SwiftUI public extension Image { enum BottleImageSystem: Imageable { - case icom(Icon) + case icon(Icon) case illustraition(Illustraition) public var image: Image { switch self { - case .icom(let icon): + case .icon(let icon): return icon.image case .illustraition(let illustraition): return illustraition.image @@ -23,7 +23,7 @@ public extension Image { public var description: String { switch self { - case .icom: + case .icon: return "icon" case .illustraition: return "illustraition" diff --git a/Projects/Shared/DesignSystem/Sources/Modifiers/NavigationBarModifier.swift b/Projects/Shared/DesignSystem/Sources/Modifiers/NavigationBarModifier.swift index aacd17ba..9cf2edb6 100644 --- a/Projects/Shared/DesignSystem/Sources/Modifiers/NavigationBarModifier.swift +++ b/Projects/Shared/DesignSystem/Sources/Modifiers/NavigationBarModifier.swift @@ -103,13 +103,13 @@ public extension View { public extension View { func makeNaivgationleftButton(action: (() -> Void)? = nil) -> some View { - BottleImageView(type: .local(bottleImageSystem: .icom(.leftArrow))) + BottleImageView(type: .local(bottleImageSystem: .icon(.leftArrow))) .foregroundStyle(to: ColorToken.icon(.primary)) .asThrottleButton(action: action ?? {}) } func makeNavigationReportButton(action: (() -> Void)? = nil) -> some View { - BottleImageView(type: .local(bottleImageSystem: .icom(.siren))) + BottleImageView(type: .local(bottleImageSystem: .icon(.siren))) .foregroundStyle(to: ColorToken.icon(.primary)) .asThrottleButton(action: action ?? {}) } From bffc70194004dafec53ef3a16d40ece18a6ea42b Mon Sep 17 00:00:00 2001 From: JongHoon Date: Tue, 15 Oct 2024 18:18:34 +0900 Subject: [PATCH 55/90] =?UTF-8?q?[Fix/#308]=20onboarding=204=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20qa=20=EB=B0=98=EC=98=81=20#309?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: image 마진 값 수정 * fix: CTA 문구 완료 -> 확인 으로 수정 --- .../Guide/Interface/Sources/StartGuide/StartGuideView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Projects/Feature/Guide/Interface/Sources/StartGuide/StartGuideView.swift b/Projects/Feature/Guide/Interface/Sources/StartGuide/StartGuideView.swift index 5204bc1a..ae0f6235 100644 --- a/Projects/Feature/Guide/Interface/Sources/StartGuide/StartGuideView.swift +++ b/Projects/Feature/Guide/Interface/Sources/StartGuide/StartGuideView.swift @@ -24,7 +24,7 @@ public struct StartGuideView: View { title ZStack(alignment: .bottom) { GeometryReader { geometry in - let bottleImageTopPadding: CGFloat = 48 + let bottleImageTopPadding: CGFloat = 62.0 HStack(spacing: 0) { Spacer() bottleImage @@ -32,6 +32,7 @@ public struct StartGuideView: View { Spacer() } .offset(y: bottleImageTopPadding) + .padding(.horizontal, 60.0 - BottlePaddingType.md.length) } doneButton .padding(.bottom, .lg) @@ -64,7 +65,7 @@ private extension StartGuideView { var doneButton: some View { SolidButton( - title: "완료", + title: "확인", sizeType: .large, buttonType: .throttle, action: { store.send(.doneButtonDidTapped) } From 74ca31a900db7f48a0bae4b8eb839233fd5ed403 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Tue, 15 Oct 2024 18:19:16 +0900 Subject: [PATCH 56/90] =?UTF-8?q?[refactor/#310]=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A4=91=EB=B3=B5=EB=90=9C=20set?= =?UTF-8?q?TabBar=20=EB=AA=A8=EB=94=94=ED=8C=8C=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20#311?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift | 4 ---- .../Feature/MyPage/Interface/Sources/MyPageRootView.swift | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift index 933780b5..1119dcc2 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPage/MyPageView.swift @@ -54,11 +54,7 @@ public struct MyPageView: View { } .scrollIndicators(.hidden) .background(to: ColorToken.container(.primary)) - .padding(.bottom, 106) .padding(.top, 1) - .setTabBar(selectedTab: .myPage) { selectedTab in - store.send(.selectedTabDidChanged(selectedTab)) - } .onLoad { store.send(.onLoad) } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift index c9fd1793..6619d607 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootView.swift @@ -9,6 +9,8 @@ import SwiftUI import FeatureTabBarInterface +import SharedDesignSystem + import ComposableArchitecture public struct MyPageRootView: View { @@ -22,6 +24,7 @@ public struct MyPageRootView: View { WithPerceptionTracking { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { MyPageView(store: store.scope(state: \.myPage, action: \.myPage)) + .padding(.bottom, BottleConstants.bottomTabBarHeight.value) .setTabBar(selectedTab: .myPage) { selectedTab in store.send(.selectedTabDidChanged(selectedTab: selectedTab)) } From d4f760f1d553ed407d5557d6b88fd11645fcd625 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Tue, 15 Oct 2024 18:20:15 +0900 Subject: [PATCH 57/90] =?UTF-8?q?[Refactor/#296]=20=EC=9B=B9=EB=B7=B0=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=83=9D=EC=84=B1=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=9C=20URL=20=EC=A0=81=EC=9A=A9=20#312?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift index 3cca11f0..eac13c6c 100644 --- a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift +++ b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift @@ -31,7 +31,7 @@ public enum BottleWebViewType { var path: String { switch self { case .createProfile: - return "create-profile" + return "profile/create" case .signUp: return "signup" case .login: From 194ea25b74ff648ff7207558da1d3ef20acbeec1 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Tue, 15 Oct 2024 18:21:47 +0900 Subject: [PATCH 58/90] =?UTF-8?q?[Feature/#306]=20=ED=98=B8=EA=B0=90=20tab?= =?UTF-8?q?=20=EC=9B=B9=EB=B7=B0=20action=20=ED=95=B8=EB=93=A4=EB=A7=81=20?= =?UTF-8?q?#313?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/BottleWebViewAction.swift | 10 ++-- .../WebView/Sources/WebViewClient.swift | 2 +- .../Interface/Sources/BaseWebViewType.swift | 7 +++ .../GoodFeeling/GoodFeelingFeature.swift | 9 ++++ .../GoodFeelingFeatureInterface.swift | 8 ++- .../Sources/GoodFeeling/GoodFeelingView.swift | 13 ++++- .../Root/FeatureGoodFeelingRootView.swift | 10 +++- .../Sources/Root/GoodFeelingRootFeature.swift | 7 ++- .../GoodFeelingRootFeatureInterface.swift | 24 +++++++++ .../SentBottleDetailFeature.swift | 49 ++++++++++++++++++ .../SentBottleDetailFeatureInterface.swift | 37 ++++++++++++++ .../SentBottleDetailView.swift | 50 +++++++++++++++++++ 12 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeature.swift create mode 100644 Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeatureInterface.swift create mode 100644 Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift diff --git a/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift b/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift index 0eec0adc..be67e664 100644 --- a/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift +++ b/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift @@ -24,7 +24,7 @@ public enum BottleWebViewAction: Equatable { /// 회원가입 성공 콜백 case signUpDidComplted(accessToken: String, refreshToken: String) /// 외부 링크 이동 - case openLink(href: String) + case openLink(url: String) // MARK: - LogIn @@ -53,7 +53,7 @@ public enum BottleWebViewAction: Equatable { message: String? = nil, accessToken: String? = nil, refreshToken: String? = nil, - href: String? = nil, + url: String? = nil, isCompletedOnboardingIntroduction: Bool? = nil ) { switch type { @@ -94,14 +94,14 @@ public enum BottleWebViewAction: Equatable { refreshToken: refreshToken ) case "openLink": - guard let href + guard let url else { Log.assertion( - message: "openLink: \(String(describing: href))" + message: "openLink: \(String(describing: url))" ) return nil } - self = .openLink(href: href) + self = .openLink(url: url) diff --git a/Projects/Domain/WebView/Sources/WebViewClient.swift b/Projects/Domain/WebView/Sources/WebViewClient.swift index afa1bede..f9390b9d 100644 --- a/Projects/Domain/WebView/Sources/WebViewClient.swift +++ b/Projects/Domain/WebView/Sources/WebViewClient.swift @@ -35,7 +35,7 @@ extension WebViewClient: DependencyKey { message: dict["message"] as? String ?? "", accessToken: dict["accessToken"] as? String ?? "", refreshToken: dict["refreshToken"] as? String ?? "", - href: dict["href"] as? String ?? "", + url: dict["url"] as? String ?? "", isCompletedOnboardingIntroduction: dict["hasCompleteIntroduction"] as? Bool ?? false ) else { diff --git a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift index eac13c6c..0053be5b 100644 --- a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift +++ b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift @@ -13,6 +13,7 @@ import DomainApplication import CoreWebViewInterface import CoreKeyChainStoreInterface import CoreKeyChainStore +import CoreLoggerInterface import Dependencies @@ -27,6 +28,7 @@ public enum BottleWebViewType { case bottles case editProfile case goodFeeling + case openURL(url: String) var path: String { switch self { @@ -42,6 +44,8 @@ public enum BottleWebViewType { return "profile/edit" case .goodFeeling: return "bottles/sents" + case .openURL: + return "" } } @@ -64,6 +68,9 @@ public enum BottleWebViewType { case .goodFeeling: return makeUrlWithToken(path) + + case let .openURL(url): + return URL(string: url)! } } diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift index 50faada6..62626ac2 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift @@ -25,10 +25,19 @@ public struct GoodFeelingFeature { } public enum Action: BindableAction { + case sentBottleTapped(url: String) + + case delegate(Delegate) + public enum Delegate { + case sentBottleTapped(url: String) + } + case binding(BindingAction) } public var body: some ReducerOf { + BindingReducer() + reducer } } diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift index c842b715..79e8405b 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift @@ -12,7 +12,13 @@ import ComposableArchitecture extension GoodFeelingFeature { public init() { let reducer = Reduce { state, action in - return .none + switch action { + case let .sentBottleTapped(url): + return .send(.delegate(.sentBottleTapped(url: url))) + + default: + return .none + } } self.init(reducer: reducer) diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift index 747d650a..a2647c83 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift @@ -9,6 +9,8 @@ import SwiftUI import FeatureBaseWebViewInterface +import CoreLoggerInterface + import ComposableArchitecture public struct GoodFeelingView: View { @@ -23,7 +25,16 @@ public struct GoodFeelingView: View { BaseWebView( type: .goodFeeling, actionDidInputted: { action in - // TODO: action handling + switch action { + case .webViewLoadingDidCompleted: + break + + case let .openLink(url): + store.send(.sentBottleTapped(url: url)) + + default: + Log.assertion(message: "not handled action: \(action)") + } } ) } diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/Root/FeatureGoodFeelingRootView.swift b/Projects/Feature/GoodFeeling/Interface/Sources/Root/FeatureGoodFeelingRootView.swift index 59ed4db6..f3ec0d05 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/Root/FeatureGoodFeelingRootView.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/Root/FeatureGoodFeelingRootView.swift @@ -34,7 +34,15 @@ public struct GoodFeelingRootView: View { } } destination: { store in WithPerceptionTracking { - + switch store.state { + case .sentBottleDetail: + if let store = store.scope( + state: \.sentBottleDetail, + action: \.sentBottleDetail + ) { + SentBottleDetailView(store: store) + } + } } } } diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeature.swift b/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeature.swift index 8d530da2..1deaa92f 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeature.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeature.swift @@ -21,7 +21,7 @@ public struct GoodFeelingRootFeature { @Reducer(state: .equatable) public enum Path { - + case sentBottleDetail(SentBottleDetailFeature) } @ObservableState @@ -35,7 +35,7 @@ public struct GoodFeelingRootFeature { } } - public enum Action { + public enum Action: BindableAction { case selectedTabDidChanged(TabType) case goodFeeling(GoodFeelingFeature.Action) @@ -45,9 +45,12 @@ public struct GoodFeelingRootFeature { public enum Delegate { case selectedTabDidChanged(TabType) } + + case binding(BindingAction) } public var body: some ReducerOf { + BindingReducer() Scope(state: \.goodFeeling, action: \.goodFeeling) { GoodFeelingFeature() } diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeatureInterface.swift b/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeatureInterface.swift index a1db7c60..956511bb 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeatureInterface.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/Root/GoodFeelingRootFeatureInterface.swift @@ -7,15 +7,39 @@ import Foundation +import CoreToastInterface + import ComposableArchitecture extension GoodFeelingRootFeature { public init() { + @Dependency(\.toastClient) var toastClient let reducer = Reduce { state, action in switch action { case let .selectedTabDidChanged(selectedTab): return .send(.delegate(.selectedTabDidChanged(selectedTab))) + // GoodFeeling Delegate + case let .goodFeeling(.delegate(delegate)): + switch delegate { + case let .sentBottleTapped(url): + state.path.append(.sentBottleDetail(.init(sentBottleDetailURL: url))) + return .none + } + + // GoodFeelingDetail Delegate + case let .path(.element(id: _, action: .sentBottleDetail(.delegate(delegate)))): + switch delegate { + case .backButtonDidTapped: + _ = state.path.popLast() + return .none + + case .bottelDidAccepted: + toastClient.presentToast(message: "이제 문답을 시작할 수 있어요.") + state.path.popLast() + return .send(.delegate(.selectedTabDidChanged(.bottleStorage))) + } + default: return .none } diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeature.swift b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeature.swift new file mode 100644 index 00000000..5e2be585 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeature.swift @@ -0,0 +1,49 @@ +// +// SentBottleDetailFeature.swift +// FeatureGoodFeeling +// +// Created by JongHoon on 10/9/24. +// + +import Foundation + +import ComposableArchitecture + +@Reducer +public struct SentBottleDetailFeature { + private let reducer: Reduce + + public init(reducer: Reduce) { + self.reducer = reducer + } + + @ObservableState + public struct State: Equatable { + let sentBottleDetailURL: String + + public init(sentBottleDetailURL: String) { + self.sentBottleDetailURL = sentBottleDetailURL + } + } + + public enum Action: BindableAction { + case backButtonDidTapped + case bottelDidAccepted + case showToast(message: String) + + case delegate(Delegate) + public enum Delegate { + case backButtonDidTapped + case bottelDidAccepted + } + + case binding(BindingAction) + } + + public var body: some ReducerOf { + BindingReducer() + + reducer + } +} + diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeatureInterface.swift b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeatureInterface.swift new file mode 100644 index 00000000..3087fdd0 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeatureInterface.swift @@ -0,0 +1,37 @@ +// +// SentBottleDetailFeatureInterface.swift +// FeatureGoodFeeling +// +// Created by JongHoon on 10/9/24. +// + +import Foundation + +import CoreToastInterface + +import ComposableArchitecture + +extension SentBottleDetailFeature { + public init() { + @Dependency(\.toastClient) var toastClient + + let reducer = Reduce { state, action in + switch action { + case .backButtonDidTapped: + return .send(.delegate(.backButtonDidTapped)) + + case let .showToast(message): + toastClient.presentToast(message: message) + return .none + + case .bottelDidAccepted: + return .send(.delegate(.bottelDidAccepted)) + + default: + return .none + } + } + + self.init(reducer: reducer) + } +} diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift new file mode 100644 index 00000000..7ec23396 --- /dev/null +++ b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift @@ -0,0 +1,50 @@ +// +// SentBottleDetailView.swift +// FeatureGoodFeeling +// +// Created by JongHoon on 10/9/24. +// + +import SwiftUI + +import FeatureBaseWebViewInterface + +import CoreLoggerInterface + +import ComposableArchitecture + +public struct SentBottleDetailView: View { + @Perception.Bindable private var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithPerceptionTracking { + BaseWebView( + type: .openURL(url: store.sentBottleDetailURL), + actionDidInputted: { action in + switch action { + case .webViewLoadingDidCompleted: + break + + case .closeWebView: + store.send(.backButtonDidTapped) + + case let .showTaost(message): + store.send(.showToast(message: message)) + + case .bottelDidAccepted: + store.send(.bottelDidAccepted) + + default: + Log.assertion(message: "not handled action: \(action)") + } + } + ) + .navigationBarBackButtonHidden() + .ignoresSafeArea(.all, edges: .bottom) + } + } +} From f72537b7de5d9d9b20f791f82956df3d404f63f7 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Tue, 15 Oct 2024 18:22:01 +0900 Subject: [PATCH 59/90] =?UTF-8?q?[Fix/#314]=20=EB=B3=B4=ED=8B=80=20?= =?UTF-8?q?=EB=B3=B4=EA=B4=80=ED=95=A8=20=ED=95=98=EB=8B=A8=EC=97=90=20?= =?UTF-8?q?=ED=83=AD=20=EB=86=92=EC=9D=B4=EB=A7=8C=ED=81=BC=20=ED=8C=A8?= =?UTF-8?q?=EB=94=A9=EA=B0=92=20=EC=B6=94=EA=B0=80=20=ED=95=84=EC=9A=94=20?= =?UTF-8?q?#315?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/BottleStorage/BottleStorageView.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift index 748c78d9..f42c199e 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift @@ -29,16 +29,13 @@ public struct BottleStorageView: View { bottlsList .padding(.horizontal, .md) .padding(.top, 32.0) - .padding(.bottom, 36.0) - - Spacer() } + .frame(maxHeight: .infinity, alignment: .top) + .background(to: ColorToken.background(.primary)) + .padding(.bottom, BottleConstants.bottomTabBarHeight.value) .setTabBar(selectedTab: .bottleStorage) { selectedTab in store.send(.selectedTabDidChanged(selectedTab: selectedTab)) } - - .frame(maxHeight: .infinity, alignment: .top) - .background(to: ColorToken.background(.primary)) } destination: { store in WithPerceptionTracking { switch store.state { @@ -135,6 +132,9 @@ private extension BottleStorageView { } } } + + Spacer() + .frame(height: 36.0) } .scrollIndicators(.hidden) } From cf3f08f04818b09704fe6fde6a0fdb7e0fe8bf8f Mon Sep 17 00:00:00 2001 From: JongHoon Date: Tue, 15 Oct 2024 18:22:26 +0900 Subject: [PATCH 60/90] =?UTF-8?q?[Feature/#316]=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EB=B3=B4=ED=8B=80=20=EC=9B=B9=EB=B7=B0=20=EC=95=A1=EC=85=98=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=A7=81=20#317?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 호감 탭 추가 및 호감 웹뷰 연결 * feat: 호감 탭 웹뷰 액션 핸들링 * feat: 추천 보틀(랜덤 보틀, 떠내려온 보틀) 웹뷰 이벤트 핸들링 --- .../Interface/Sources/BaseWebViewType.swift | 8 ++-- .../BottleArrivalFeature.swift | 4 +- .../BottleArrivalFeatureInterface.swift | 3 +- .../BottleArrivalView.swift | 7 ++- .../BottleArrivalDetailFeature.swift | 47 ++++++++++++++++++ .../BottleArrivalDetailFeatureInterface.swift | 35 ++++++++++++++ .../BottleArrivalDetailView.swift | 48 +++++++++++++++++++ .../Sources/Root/SandBeachRootFeature.swift | 13 +++++ .../Sources/Root/SandBeachRootView.swift | 8 ++++ .../SandBeach/SandBeachFeatureInterface.swift | 2 +- 10 files changed, 163 insertions(+), 12 deletions(-) rename Projects/Feature/BottleArrival/Interface/Sources/{ => BottleArrival}/BottleArrivalFeature.swift (88%) rename Projects/Feature/BottleArrival/Interface/Sources/{ => BottleArrival}/BottleArrivalFeatureInterface.swift (90%) rename Projects/Feature/BottleArrival/Interface/Sources/{ => BottleArrival}/BottleArrivalView.swift (88%) create mode 100644 Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailFeature.swift create mode 100644 Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailFeatureInterface.swift create mode 100644 Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailView.swift diff --git a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift index 0053be5b..bd98a38c 100644 --- a/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift +++ b/Projects/Feature/BaseWebView/Interface/Sources/BaseWebViewType.swift @@ -25,7 +25,7 @@ public enum BottleWebViewType { case createProfile case signUp case login - case bottles + case bottleArrival case editProfile case goodFeeling case openURL(url: String) @@ -38,8 +38,8 @@ public enum BottleWebViewType { return "signup" case .login: return "login" - case .bottles: - return "bottles" + case .bottleArrival: + return "bottles/recommendations" case .editProfile: return "profile/edit" case .goodFeeling: @@ -60,7 +60,7 @@ public enum BottleWebViewType { case .login: return URL(string: baseURL + "/" + path)! - case .bottles: + case .bottleArrival: return makeUrlWithToken(path) case .editProfile: diff --git a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalFeature.swift b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrival/BottleArrivalFeature.swift similarity index 88% rename from Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalFeature.swift rename to Projects/Feature/BottleArrival/Interface/Sources/BottleArrival/BottleArrivalFeature.swift index 75f50fea..187704d9 100644 --- a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalFeature.swift +++ b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrival/BottleArrivalFeature.swift @@ -23,8 +23,8 @@ extension BottleArrivalFeature { state.isLoading = false return .none - case .bottelDidAccepted: - return .send(.delegate(.bottelDidAccepted)) + case let .arrivalBottleTapped(url): + return .send(.delegate(.arrivalBottleTapped(url: url))) case .closeWebView: return .send(.delegate(.closeWebView)) diff --git a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalFeatureInterface.swift b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrival/BottleArrivalFeatureInterface.swift similarity index 90% rename from Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalFeatureInterface.swift rename to Projects/Feature/BottleArrival/Interface/Sources/BottleArrival/BottleArrivalFeatureInterface.swift index 01a1decd..38b0aead 100644 --- a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalFeatureInterface.swift +++ b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrival/BottleArrivalFeatureInterface.swift @@ -29,7 +29,7 @@ public struct BottleArrivalFeature { // View Life Cycle case onAppear case webViewLoadingDidCompleted - case bottelDidAccepted + case arrivalBottleTapped(url: String) case closeWebView case presentToastDidRequired(message: String) // Delegate @@ -38,6 +38,7 @@ public struct BottleArrivalFeature { public enum Delegate { case bottelDidAccepted case closeWebView + case arrivalBottleTapped(url: String) } } diff --git a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalView.swift b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrival/BottleArrivalView.swift similarity index 88% rename from Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalView.swift rename to Projects/Feature/BottleArrival/Interface/Sources/BottleArrival/BottleArrivalView.swift index 1b002cf9..3b2253ff 100644 --- a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalView.swift +++ b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrival/BottleArrivalView.swift @@ -22,13 +22,12 @@ public struct BottleArrivalView: View { public var body: some View { WithPerceptionTracking { - BaseWebView( - type: .bottles) { action in + BaseWebView(type: .bottleArrival) { action in switch action { case .webViewLoadingDidCompleted: store.send(.webViewLoadingDidCompleted) - case .bottelDidAccepted: - store.send(.bottelDidAccepted) + case let .openLink(url): + store.send(.arrivalBottleTapped(url: url)) case .closeWebView: store.send(.closeWebView) case let .showTaost(message): diff --git a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailFeature.swift b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailFeature.swift new file mode 100644 index 00000000..122d3670 --- /dev/null +++ b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailFeature.swift @@ -0,0 +1,47 @@ +// +// BottleArrivalDetailFeature.swift +// FeatureBottleArrival +// +// Created by JongHoon on 10/9/24. +// + +import Foundation + +import ComposableArchitecture + +@Reducer +public struct BottleArrivalDetailFeature { + private let reducer: Reduce + + public init(reducer: Reduce) { + self.reducer = reducer + } + + @ObservableState + public struct State: Equatable { + let bottleArrivalURL: String + + public init(bottleArrivalURL: String) { + self.bottleArrivalURL = bottleArrivalURL + } + } + + public enum Action: BindableAction { + case backButtonDidTapped + case bottelDidAccepted + case showToast(message: String) + + case delegate(Delegate) + public enum Delegate { + case backButtonDidTapped + } + + case binding(BindingAction) + } + + public var body: some ReducerOf { + BindingReducer() + + reducer + } +} diff --git a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailFeatureInterface.swift b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailFeatureInterface.swift new file mode 100644 index 00000000..e1e98b82 --- /dev/null +++ b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailFeatureInterface.swift @@ -0,0 +1,35 @@ +// +// BottleArrivalDetailFeatureInterface.swift +// FeatureBottleArrival +// +// Created by JongHoon on 10/9/24. +// + +import Foundation + +import CoreToastInterface + +import ComposableArchitecture + +extension BottleArrivalDetailFeature { + public init() { + @Dependency(\.toastClient) var toastClient + + let reducer = Reduce { state, action in + switch action { + case .backButtonDidTapped: + return .send(.delegate(.backButtonDidTapped)) + + case let .showToast(message): + toastClient.presentToast(message: message) + return .none + + default: + return .none + } + } + + self.init(reducer: reducer) + } +} + diff --git a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailView.swift b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailView.swift new file mode 100644 index 00000000..1b02ddcf --- /dev/null +++ b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailView.swift @@ -0,0 +1,48 @@ +// +// BottleArrivalDetailView.swift +// FeatureBottleArrival +// +// Created by JongHoon on 10/9/24. +// + +import SwiftUI + +import FeatureBaseWebViewInterface + +import CoreLoggerInterface + +import ComposableArchitecture + +public struct BottleArrivalDetailView: View { + @Perception.Bindable private var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithPerceptionTracking { + BaseWebView( + type: .openURL(url: store.bottleArrivalURL), + actionDidInputted: { action in + switch action { + case .webViewLoadingDidCompleted: + break + + case .closeWebView: + store.send(.backButtonDidTapped) + + case let .showTaost(message): + store.send(.showToast(message: message)) + + default: + Log.assertion(message: "not handled action: \(action)") + } + } + ) + .navigationBarBackButtonHidden() + .ignoresSafeArea(.all, edges: .bottom) + } + } +} + diff --git a/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift b/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift index 0bd62c9a..e69425e0 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift @@ -29,6 +29,7 @@ public struct SandBeachRootFeature { case IntroductionSetup(IntroductionSetupFeature) case ProfileImageUpload(ProfileImageUploadFeature) case BottleArrival(BottleArrivalFeature) + case BottleArrivalDetail(BottleArrivalDetailFeature) } @ObservableState @@ -127,6 +128,10 @@ extension SandBeachRootFeature { case .closeWebView: state.path.removeLast() return .none + + case let .arrivalBottleTapped(url): + state.path.append(.BottleArrivalDetail(.init(bottleArrivalURL: url))) + return .none } // SandBeach Delegate @@ -144,6 +149,14 @@ extension SandBeachRootFeature { return .none } + // BottleArrivalDetail Delegate + case let .path(.element(id: _, action: .BottleArrivalDetail(.delegate(delegate)))): + switch delegate { + case .backButtonDidTapped: + _ = state.path.popLast() + return .none + } + case .profileSetupDidCompleted: state.isLoading = false state.path.removeAll() diff --git a/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootView.swift b/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootView.swift index 2d974631..95181627 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootView.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootView.swift @@ -52,6 +52,14 @@ public struct SandBeachRootView: View { action: \.BottleArrival) { BottleArrivalView(store: store) } + + case .BottleArrivalDetail: + if let store = store.scope( + state: \.BottleArrivalDetail, + action: \.BottleArrivalDetail + ) { + BottleArrivalDetailView(store: store) + } } } } diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift index 715d3da5..e52ec505 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift @@ -109,7 +109,7 @@ extension SandBeachFeature { } let userBottleInfo = try await bottleClient.fetchUserBottleInfo() - let newBottlesCount = userBottleInfo.randomBottleCount + userBottleInfo.sendBottleCount + let newBottlesCount = userBottleInfo.randomBottleCount // 새로 도착한 보틀이 있는 상태 if newBottlesCount > 0 { From 6e3749bc92b110e75b79f16f5723761dba7f5ddf Mon Sep 17 00:00:00 2001 From: JongHoon Date: Tue, 15 Oct 2024 19:43:28 +0900 Subject: [PATCH 61/90] =?UTF-8?q?[Refactor/#319]=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B8=94=EB=9F=AC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?#320?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Introduction/IntroductionView.swift | 1 - .../Interface/Sources/ReportUserView.swift | 1 - .../UserProfileTest/UserProfileTestView.swift | 2 -- .../Card/UserProfile/UserProfileView.swift | 29 +++++-------------- .../ListItem/BottleStorageItem.swift | 2 +- 5 files changed, 8 insertions(+), 27 deletions(-) diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift index 9744ee5a..af6154ed 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Introduction/IntroductionView.swift @@ -27,7 +27,6 @@ public struct IntroductionView: View { } else if store.isStopped == false { UserProfileView( imageURL: store.userImageURL, - isBlurred: true, userName: store.userName, userAge: store.age ) diff --git a/Projects/Feature/Report/Interface/Sources/ReportUserView.swift b/Projects/Feature/Report/Interface/Sources/ReportUserView.swift index bef7669e..c58b520c 100644 --- a/Projects/Feature/Report/Interface/Sources/ReportUserView.swift +++ b/Projects/Feature/Report/Interface/Sources/ReportUserView.swift @@ -50,7 +50,6 @@ private extension ReportUserView { var userProfile: some View { UserProfileView( imageURL: store.userProfile.imageURL, - isBlurred: true, userName: store.userProfile.userName, userAge: store.userProfile.userAge ) diff --git a/Projects/Shared/DesignSystem/Example/Sources/SubViews/UserProfileTest/UserProfileTestView.swift b/Projects/Shared/DesignSystem/Example/Sources/SubViews/UserProfileTest/UserProfileTestView.swift index 17d2d093..86ef0c45 100644 --- a/Projects/Shared/DesignSystem/Example/Sources/SubViews/UserProfileTest/UserProfileTestView.swift +++ b/Projects/Shared/DesignSystem/Example/Sources/SubViews/UserProfileTest/UserProfileTestView.swift @@ -15,7 +15,6 @@ public struct UserProfileTestView: View { VStack(spacing: .xl) { UserProfileView( imageURL: "https://static.wikia.nocookie.net/wallaceandgromit/images/3/38/Gromit-3.png/revision/latest/scale-to-width/360?cb=20191228190308", - isBlurred: true, userName: "임현규", userAge: 26 ) @@ -23,7 +22,6 @@ public struct UserProfileTestView: View { UserProfileView( imageURL: "", - isBlurred: false, userName: "임현규", userAge: 26 ) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift index 014ad572..fbcc45fa 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/UserProfile/UserProfileView.swift @@ -9,18 +9,15 @@ import SwiftUI public struct UserProfileView: View { private let imageURL: String - private let isBlurred: Bool private let userName: String private let userAge: Int public init( imageURL: String, - isBlurred: Bool, userName: String, userAge: Int ) { self.imageURL = imageURL - self.isBlurred = isBlurred self.userName = userName self.userAge = userAge } @@ -41,25 +38,13 @@ private extension UserProfileView { @ViewBuilder var profileImage: some View { - switch isBlurred { - case true: - BlurImageView( - imageURL: imageURL, - downsamplingWidth: 80.0, - downsamplingHeight: 80.0 - ) - .clipShape(Circle()) - .frame(width: 80.0, height: 80.0) - - case false: - RemoteImageView( - imageURL: imageURL, - downsamplingWidth: 80.0, - downsamplingHeight: 80.0 - ) - .clipShape(Circle()) - .frame(width: 80, height: 80) - } + RemoteImageView( + imageURL: imageURL, + downsamplingWidth: 80.0, + downsamplingHeight: 80.0 + ) + .clipShape(Circle()) + .frame(width: 80, height: 80) } var userNameText: some View { diff --git a/Projects/Shared/DesignSystem/Sources/Components/ListItem/BottleStorageItem.swift b/Projects/Shared/DesignSystem/Sources/Components/ListItem/BottleStorageItem.swift index 85fe6d91..c98f873e 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/ListItem/BottleStorageItem.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/ListItem/BottleStorageItem.swift @@ -56,7 +56,7 @@ public struct BottleStorageItem: View { Spacer() - BlurImageView( + RemoteImageView( imageURL: imageURL, downsamplingWidth: 60.0, downsamplingHeight: 60.0 From d2aa549d794bbae5d70a0ea94435b9b886301a5b 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: Wed, 16 Oct 2024 15:57:05 +0900 Subject: [PATCH 62/90] =?UTF-8?q?chore:=20=EB=B2=84=EC=A0=84,=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=EB=84=98=EB=B2=84=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20v1.0.9(31)=20(#321)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: JongHoon --- Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index 17f2a117..a514757b 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -10,8 +10,8 @@ import ProjectDescription public extension InfoPlist { static var app: InfoPlist { return .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0.8", - "CFBundleVersion": "30", + "CFBundleShortVersionString": "1.0.9", + "CFBundleVersion": "31", "UIUserInterfaceStyle": "Light", "CFBundleName": "보틀", "UILaunchScreen": [ @@ -44,8 +44,8 @@ public extension InfoPlist { static var example: InfoPlist { return .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0.8", - "CFBundleVersion": "30", + "CFBundleShortVersionString": "1.0.9", + "CFBundleVersion": "31", "UIUserInterfaceStyle": "Light", "UILaunchScreen": [:], "UISupportedInterfaceOrientations": [ From 152573aaf567567c5cd14b68003b3e4649515e29 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: Wed, 16 Oct 2024 15:58:15 +0900 Subject: [PATCH 63/90] =?UTF-8?q?[Feature/#290]=20=EB=AC=B8=EB=8B=B5?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#292)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: PingPongUserView 구현 - 문답 화면 바뀐 디자인 시스템 View * feat: PingPongUserView 데모 앱 추가 --- .../DesignSystemExampleView.swift | 9 +- .../Sources/SubViews/BottleStorageList.swift | 5 +- .../Card/PingPong/PingPongUserView.swift | 137 ++++++++++++++++++ 3 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 Projects/Shared/DesignSystem/Sources/Components/Card/PingPong/PingPongUserView.swift diff --git a/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift b/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift index 9b851cc9..03aabafa 100644 --- a/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift +++ b/Projects/Shared/DesignSystem/Example/Sources/DesignSystemExampleView/DesignSystemExampleView.swift @@ -255,7 +255,14 @@ struct CardSection: View { label: { Text("StopCardView") } - ) + ) + + NavigationLink( + destination: BottleStorageList(), + label: { + Text("BottleStorageList") + } + ) // NavigationLink( // destination: // QuestionPingPongTestView(), diff --git a/Projects/Shared/DesignSystem/Example/Sources/SubViews/BottleStorageList.swift b/Projects/Shared/DesignSystem/Example/Sources/SubViews/BottleStorageList.swift index 21207d91..5c664feb 100644 --- a/Projects/Shared/DesignSystem/Example/Sources/SubViews/BottleStorageList.swift +++ b/Projects/Shared/DesignSystem/Example/Sources/SubViews/BottleStorageList.swift @@ -75,11 +75,12 @@ struct BottleStorageList: View { var body: some View { VStack(spacing: 20.0) { ForEach(bottles, id: \.id) { bottle in - BottleStorageItem( + PingPongUserView( + status: "문답이 도착했어요", + lastPingPongTime: "3시간 전", userName: bottle.userName, age: bottle.age, mbti: bottle.mbti, - keywords: bottle.keyworkds, imageURL: bottle.imageURL, isRead: bottle.isRead ) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/PingPong/PingPongUserView.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/PingPong/PingPongUserView.swift new file mode 100644 index 00000000..35c7558e --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/PingPong/PingPongUserView.swift @@ -0,0 +1,137 @@ +// +// PingPongUserView.swift +// SharedDesignSystem +// +// Created by 임현규 on 9/28/24. +// + +import SwiftUI + +public struct PingPongUserView: View { + private let status: String + private let lastPingPongTime: String + private let userName: String + private let age: Int + private let mbti: String + private let imageURL: String + private let isRead: Bool + + public init( + status: String, + lastPingPongTime: String, + userName: String, + age: Int, + mbti: String, + imageURL: String, + isRead: Bool + ) { + self.status = status + self.lastPingPongTime = lastPingPongTime + self.userName = userName + self.age = age + self.mbti = mbti + self.imageURL = imageURL + self.isRead = isRead + } + + public var body: some View { + HStack(spacing: 0.0) { + VStack(alignment: .leading, spacing: 0.0) { + bottleStatus + HStack(spacing: .xxs) { + userNameText + activeDot + } + .padding(.bottom, .sm) + infos + } + Spacer() + userImage + } + .padding(.md) + .overlay( + RoundedRectangle(cornerRadius: 20.0) + .strokeBorder( + ColorToken.border(.primary).color, + lineWidth: 1.0 + ) + ) + } +} + +// MARK: - Private Views + +private extension PingPongUserView { + var userNameText: some View { + WantedSansStyleText( + userName, + style: .title2, + color: .secondary + ) + } + + @ViewBuilder + var activeDot: some View { + if isRead == false { + ColorToken.icon(.update).color + .frame(width: 4.0, height: 4.0) + .clipShape(Circle()) + } else { + EmptyView() + } + } + + var userImage: some View { + BlurImageView( + imageURL: imageURL, + downsamplingWidth: 60.0, + downsamplingHeight: 60.0 + ) + .frame(width: 48.0, height: 48.0) + .clipShape(Circle()) + } + + var infos: some View { + HStack(spacing: .xs) { + WantedSansStyleText( + "\(age)세", + style: .caption, + color: .secondary + ) + + verticalSeparator + + WantedSansStyleText( + mbti, + style: .caption, + color: .secondary + ) + } + } + + var verticalSeparator: some View { + ColorToken.border(.secondary).color + .frame(width: 1.0, height: 12.0) + .padding(1.0) + } + + var bottleStatus: some View { + HStack(spacing: .xs) { + WantedSansStyleText( + status, + style: .caption, + color: .secondary + ) + verticalSeparator + WantedSansStyleText( + lastPingPongTime, + style: .caption, + color: .tertiary + ) + } + .padding(.xs) + .background(to: ColorToken.onContainer(.secondary)) + .cornerRadius(.xs, corenrs: .allCorners) + .padding(.bottom, .sm) + } +} From ba3706af66bae88f51ec925aa545f655116516dc 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: Wed, 16 Oct 2024 16:02:48 +0900 Subject: [PATCH 64/90] =?UTF-8?q?[Feature/#294]=20=EB=AC=B8=EB=8B=B5?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20segmented=20control=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=88=98=EC=A0=95=20(#297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: SegmentConrtolButtonStyle 구현 * feat: SegmentControlButton 구현 * feat PingPongDetailView SegmentControlButton 적용 --- .../PingPongDetail/PingPongDetailView.swift | 3 +- .../SegmentControlButton.swift | 55 ++++++++++++++++++ .../SegmentControlButtonStyle.swift | 57 +++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 Projects/Shared/DesignSystem/Sources/Components/Button/SegmentControlButton/SegmentControlButton.swift create mode 100644 Projects/Shared/DesignSystem/Sources/Components/Button/SegmentControlButton/SegmentControlButtonStyle.swift diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift index 9da0026a..c75ac59a 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailView.swift @@ -63,8 +63,7 @@ private extension PingPongDetailView { var tabButtons: some View { HStack(spacing: .xs) { ForEach(PingPongDetailViewTabType.allCases, id: \.title, content: { tab in - OutlinedStyleButton( - .small(contentType: .text), + SegmentControlButton( title: tab.title, buttonType: .throttle, isSelected: store.selectedTab == tab, diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/SegmentControlButton/SegmentControlButton.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/SegmentControlButton/SegmentControlButton.swift new file mode 100644 index 00000000..02498126 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/SegmentControlButton/SegmentControlButton.swift @@ -0,0 +1,55 @@ +// +// SegmentControlButton.swift +// SharedDesignSystem +// +// Created by 임현규 on 10/1/24. +// + +import SwiftUI + +public struct SegmentControlButton: View { + private let title: String + private let buttonType: ButtonType + private let isSelected: Bool? + private let _action: (() -> Void)? + private var action: () -> Void { + return _action ?? {} + } + + public init( + title: String, + buttonType: ButtonType, + isSelected: Bool? = nil, + action: (() -> Void)? = nil + ) { + self.title = title + self.buttonType = buttonType + self.isSelected = isSelected + self._action = action + } + + public var body: some View { + segmentControlButton + .buttonStyle(SegmentControlButtonStyle(isSelected: isSelected)) + } +} + +// MARK: - Private Views +private extension SegmentControlButton { + var titleText: some View { + Text(title) + .font(to: .wantedSans(.body)) + } + + @ViewBuilder + var segmentControlButton: some View { + switch buttonType { + case .debounce: + titleText.asDebounceButton(action: action) + case .throttle: + titleText.asThrottleButton(action: action) + case .normal: + titleText.asButton(action: action) + } + } +} diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/SegmentControlButton/SegmentControlButtonStyle.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/SegmentControlButton/SegmentControlButtonStyle.swift new file mode 100644 index 00000000..114ebd02 --- /dev/null +++ b/Projects/Shared/DesignSystem/Sources/Components/Button/SegmentControlButton/SegmentControlButtonStyle.swift @@ -0,0 +1,57 @@ +// +// SegmentControlButtonStyle.swift +// SharedDesignSystem +// +// Created by 임현규 on 10/1/24. +// + +import SwiftUI + +struct SegmentControlButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled: Bool + private let isSelected: Bool? + + public init(isSelected: Bool? = nil) { + self.isSelected = isSelected + } + + func makeBody(configuration: Configuration) -> some View { + let buttonState = makeButtonState(configuration) + + return configuration.label + .padding(.horizontal, .sm) + .frame(height: 42) + .foregroundStyle(foregroundColor(buttonState)) + .overlay(alignment: .bottom) { + switch buttonState { + case .enabled: + EmptyView() + case .selected: + Divider() + .frame(height: 2) + .background(to: ColorToken.border(.selected)) + case .disabled: + EmptyView() + } + } + } +} + +// MARK: - Private Methods +private extension SegmentControlButtonStyle { + func makeButtonState(_ configuration: Configuration) -> ButtonStateType { + return !isEnabled ? .disabled : configuration.isPressed || isSelected == true ? .selected : .enabled + } + + func foregroundColor(_ buttonState: ButtonStateType) -> Color { + switch buttonState { + case .enabled: + return ColorToken.text(.enableSecondary).color + case .selected: + return ColorToken.text(.selectPrimary).color + case .disabled: + return ColorToken.text(.disableSecondary).color + } + } +} + From 41e22988ba0fa242f17941e6ab472a2fafaa4410 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: Wed, 16 Oct 2024 20:24:04 +0900 Subject: [PATCH 65/90] =?UTF-8?q?[Feature/#324]=20=EB=AC=B8=EB=8B=B5=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=83=88=EB=A1=9C=EC=9A=B4=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EC=A0=81=EC=9A=A9=20(#325)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: pingPong API path 수정 * feat: BottleStorageListResponseDTO 수정 * feat: 보틀보관함 바뀐 디자인 시스템 적용 * feat: 모래사장 보틀 확인 로직 변경 * feat: 보틀 없는 상태에서 섬 클릭 시 BottleArrival 웹뷰 띄우기 --- .../Interface/Sources/API/BottleAPI.swift | 2 +- .../BottleStorageListResponseDTO.swift | 13 +-- .../Sources/Entity/BottleStorageList.swift | 37 +++++++-- .../BottleStorage/BottleStorageFeature.swift | 12 +-- .../BottleStorageFeatureInterface.swift | 48 +++++------ .../BottleStorage/BottleStorageView.swift | 83 +++++++++---------- .../SandBeach/SandBeachFeatureInterface.swift | 5 +- .../Sources/SandBeach/SandBeachView.swift | 6 +- .../Sources/TabView/MainTabViewFeature.swift | 5 +- 9 files changed, 112 insertions(+), 99 deletions(-) diff --git a/Projects/Domain/Bottle/Interface/Sources/API/BottleAPI.swift b/Projects/Domain/Bottle/Interface/Sources/API/BottleAPI.swift index 868ebb7b..2c684145 100644 --- a/Projects/Domain/Bottle/Interface/Sources/API/BottleAPI.swift +++ b/Projects/Domain/Bottle/Interface/Sources/API/BottleAPI.swift @@ -35,7 +35,7 @@ extension BottleAPI: BaseTargetType { case .fetchBottles: return "api/v1/bottles" case .fetchBottleStorageList: - return "api/v1/bottles/ping-pong" + return "api/v2/bottles/ping-pong" case let .fetchBottlePingPong(bottleID): return "api/v1/bottles/ping-pong/\(bottleID)" case let .readBottle(bottleID): diff --git a/Projects/Domain/Bottle/Interface/Sources/DTO/Response/BottleStorageListResponseDTO.swift b/Projects/Domain/Bottle/Interface/Sources/DTO/Response/BottleStorageListResponseDTO.swift index 3a397c74..eb18dbec 100644 --- a/Projects/Domain/Bottle/Interface/Sources/DTO/Response/BottleStorageListResponseDTO.swift +++ b/Projects/Domain/Bottle/Interface/Sources/DTO/Response/BottleStorageListResponseDTO.swift @@ -10,8 +10,7 @@ import Foundation // MARK: - Bottle Storage List public struct BottleStorageListResponseDTO: Decodable { - let activeBottles: [BottleStorageItemResponseDTO]? - let doneBottles: [BottleStorageItemResponseDTO]? + let pingPongBottles: [BottleStorageItemResponseDTO]? public struct BottleStorageItemResponseDTO: Decodable { let age: Int? @@ -21,6 +20,8 @@ public struct BottleStorageListResponseDTO: Decodable { let mbti: String? let userImageUrl: String? let userName: String? + let lastActivatedAt: String? + let lastStatus: String? public func toDomain() -> BottleStorageItem { return BottleStorageItem( @@ -30,15 +31,15 @@ public struct BottleStorageListResponseDTO: Decodable { keyword: keyword ?? [], mbti: mbti ?? "", userImageUrl: userImageUrl ?? "", - userName: userName ?? "" + userName: userName ?? "", + lastActivatedAt: lastActivatedAt ?? "", + lastStatus: PingPongLastStatus(rawValue: lastStatus ?? "") ) } } public func toDomain() -> BottleStorageList { return BottleStorageList( - activeBottles: activeBottles?.map { $0.toDomain() } ?? [], - doneBottles: doneBottles?.map { $0.toDomain() } ?? [] - ) + pingPongBottles: pingPongBottles?.map { $0.toDomain() } ?? []) } } diff --git a/Projects/Domain/Bottle/Interface/Sources/Entity/BottleStorageList.swift b/Projects/Domain/Bottle/Interface/Sources/Entity/BottleStorageList.swift index a8d52ae5..54d7eff4 100644 --- a/Projects/Domain/Bottle/Interface/Sources/Entity/BottleStorageList.swift +++ b/Projects/Domain/Bottle/Interface/Sources/Entity/BottleStorageList.swift @@ -6,17 +6,13 @@ // public struct BottleStorageList: Decodable { - public let activeBottles: [BottleStorageItem] - public let doneBottles: [BottleStorageItem] + public let pingPongBottles: [BottleStorageItem] public init( - activeBottles: [BottleStorageItem], - doneBottles: [BottleStorageItem] + pingPongBottles: [BottleStorageItem] ) { - self.activeBottles = activeBottles - self.doneBottles = doneBottles + self.pingPongBottles = pingPongBottles } - } public struct BottleStorageItem: Decodable, Equatable { @@ -27,6 +23,8 @@ public struct BottleStorageItem: Decodable, Equatable { public let mbti: String public let userImageUrl: String public let userName: String? + public let lastActivatedAt: String? + public let lastStatus: PingPongLastStatus? public init( age: Int?, @@ -35,7 +33,9 @@ public struct BottleStorageItem: Decodable, Equatable { keyword: [String], mbti: String, userImageUrl: String, - userName: String? + userName: String?, + lastActivatedAt: String?, + lastStatus: PingPongLastStatus? ) { self.age = age self.id = id @@ -44,5 +44,26 @@ public struct BottleStorageItem: Decodable, Equatable { self.mbti = mbti self.userImageUrl = userImageUrl self.userName = userName + self.lastActivatedAt = lastActivatedAt + self.lastStatus = lastStatus } } + +public enum PingPongLastStatus: String, Decodable { + /// 대화는 시작했으나 두 사람 모두 문답을 작성하지 않았을 때 + case noAnswerFromBoth = "NO_ANSWER_FROM_BOTH" + /// 상대방이 새로운 문답을 작성했을 때 + case answerFromOther = "ANSWER_FROM_OTHER" + /// 상대방이 사진을 공유했을 때 + case photoSharedByOther = "PHOTO_SHARED_BY_OTHER" + /// 상대방이 연락처를 공유했을 때 + case contactSharedByOther = "CONTACT_SHARED_BY_OTHER" + /// 내가 문답을 작성했을 때 (상대방은 작성X) + case answerFromMeOnly = "ANSWER_FROM_ME_ONLY" + /// 내가 사진을 공유했을 때 (상대방은 공유X) + case photoSharedByMeOnly = "PHOTO_SHARED_BY_ME_ONLY" + /// 내가 연락처를 공유했을 때 (상대방은 공유X) + case contactSharedByMeOnly = "CONTACT_SHARED_BY_ME_ONLY" + /// 대화가 중단됐을 때 + case conversationStopped = "CONVERSATION_STOPPED" +} diff --git a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift index 5b46a6a8..1a5373a3 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift @@ -8,6 +8,7 @@ import CoreLoggerInterface import DomainBottle import FeatureReportInterface +import FeatureBottleArrivalInterface import ComposableArchitecture @@ -18,11 +19,12 @@ extension BottleStorageFeature { let reducer = Reduce { state, action in switch action { case .onAppear: + state.isLoading = true return popToRootAndReload(state: &state) case let .bottleStorageListFetched(bottleStorageList): - state.activeBottleList = bottleStorageList.activeBottles - state.doneBottlsList = bottleStorageList.doneBottles + state.pingPongBottleList = bottleStorageList.pingPongBottles + state.isLoading = false return .none case let .bottleStorageItemDidTapped(bottleID, isRead, userName): @@ -33,9 +35,8 @@ extension BottleStorageFeature { ))) return .none - case let .bottleActiveStateTabButtonTapped(activeState): - state.selectedActiveStateTab = activeState - return .none + case .sandBeachButtonDidTapped: + return .send(.delegate(.sandBeachButtonDidTapped)) case let .path(.element(id: _, action: .pingPongDetail(.delegate(delegate)))): @@ -74,7 +75,6 @@ extension BottleStorageFeature { self.init(reducer: reducer) func popToRootAndReload(state: inout State) -> Effect { - state.selectedActiveStateTab = .active state.path.removeAll() return .run { send in let bottleStorageList = try await bottleClient.fetchBottleStorageList() diff --git a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeatureInterface.swift b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeatureInterface.swift index 96f821ef..e4fe4638 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeatureInterface.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeatureInterface.swift @@ -22,27 +22,15 @@ public struct BottleStorageFeature { @ObservableState public struct State: Equatable { - // 보틀 상태 선택 탭(대화 중, 완료) - let bottleActiveStateTabs: [BottleActiveState] - var selectedActiveStateTab: BottleActiveState - var currentSelectedBottles: [BottleStorageItem] { - switch selectedActiveStateTab { - case .active: - return activeBottleList ?? [] - case .done: - return doneBottlsList ?? [] - } - } // 보틀 리스트 - var activeBottleList: [BottleStorageItem]? - var doneBottlsList: [BottleStorageItem]? + var pingPongBottleList: [BottleStorageItem] var path = StackState() + var isLoading: Bool = false public init() { - self.bottleActiveStateTabs = BottleActiveState.allCases - self.selectedActiveStateTab = .active + self.pingPongBottleList = [] } } @@ -50,9 +38,6 @@ public struct BottleStorageFeature { // View Life Cycle case onAppear - // 보틀 상태 선택 탭(대화 중, 완료) - case bottleActiveStateTabButtonTapped(BottleActiveState) - // 보틀 리스트 case bottleStorageListFetched(BottleStorageList) case bottleStorageItemDidTapped( @@ -60,6 +45,7 @@ public struct BottleStorageFeature { isRead: Bool, userName: String ) + case sandBeachButtonDidTapped case selectedTabDidChanged(selectedTab: TabType) case delegate(Delegate) // ETC. @@ -68,6 +54,7 @@ public struct BottleStorageFeature { public enum Delegate { case selectedTabDidChanged(selectedTab: TabType) + case sandBeachButtonDidTapped } } @@ -78,16 +65,25 @@ public struct BottleStorageFeature { } } -public enum BottleActiveState: String, CaseIterable, Equatable { - case active - case done - +extension PingPongLastStatus { var title: String { switch self { - case .active: - return "대화 중" - case .done: - return "완료" + case .noAnswerFromBoth: + return "문답을 시작해 주세요" + case .answerFromOther: + return "새로운 문답이 도착했어요" + case .photoSharedByOther: + return "사진이 도착했어요" + case .contactSharedByOther: + return "연락처가 도착했어요" + case .answerFromMeOnly: + return "문답을 보냈어요" + case .photoSharedByMeOnly: + return "사진을 공유했어요" + case .contactSharedByMeOnly: + return "연락처를 공유했어요" + case .conversationStopped: + return "대화가 중단됐어요" } } } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift index f42c199e..3fcbacdc 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift @@ -10,6 +10,7 @@ import SwiftUI import SharedDesignSystem import FeatureReportInterface import FeatureTabBarInterface +import FeatureBottleArrivalInterface import ComposableArchitecture @@ -17,25 +18,29 @@ public struct BottleStorageView: View { @Perception.Bindable private var store: StoreOf public init(store: StoreOf) { - self.store = store + self.store = store } public var body: some View { WithPerceptionTracking { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { VStack(spacing: 0.0) { - bottleActiveStateSelectTab - bottlsList .padding(.horizontal, .md) .padding(.top, 32.0) } + .padding(.top, 72) .frame(maxHeight: .infinity, alignment: .top) .background(to: ColorToken.background(.primary)) .padding(.bottom, BottleConstants.bottomTabBarHeight.value) .setTabBar(selectedTab: .bottleStorage) { selectedTab in store.send(.selectedTabDidChanged(selectedTab: selectedTab)) } + .overlay { + if store.pingPongBottleList.isEmpty && store.isLoading { + LoadingIndicator() + } + } } destination: { store in WithPerceptionTracking { switch store.state { @@ -64,62 +69,48 @@ public struct BottleStorageView: View { // MARK: - Private Views private extension BottleStorageView { - var bottleActiveStateSelectTab: some View { - HStack(spacing: .xs) { - OutlinedStyleButton( - .small(contentType: .text), - title: BottleActiveState.active.title, - buttonType: .throttle, - isSelected: store.selectedActiveStateTab == BottleActiveState.active, - action: { - store.send(.bottleActiveStateTabButtonTapped(.active)) - } - ) - - OutlinedStyleButton( - .small(contentType: .text), - title: BottleActiveState.done.title, - buttonType: .throttle, - isSelected: store.selectedActiveStateTab == BottleActiveState.done, - action: { - store.send(.bottleActiveStateTabButtonTapped(.done)) - } - ) - - Spacer() - } - .padding(.md) - } - @ViewBuilder var bottlsList: some View { - if store.currentSelectedBottles.isEmpty && store.activeBottleList != nil { - VStack(spacing: .xxl) { - HStack(spacing: 0.0) { + if store.pingPongBottleList.isEmpty && !store.isLoading { + VStack(alignment: .center, spacing: 0.0) { + + Spacer() + BottleImageView(type: .local(bottleImageSystem: .illustraition(.basket))) + .frame(height: 180) + .frame(width: 180) + .aspectRatio(1.0, contentMode: .fit) + .padding(.bottom, .xl) + WantedSansStyleText( - "아직 보관 중인\n보틀이 없어요!", - style: .title1, + "아직 대화를 시작하지 않으셨군요!", + style: .subTitle1, color: .primary ) - - Spacer() - } + .padding(.bottom, .xs) + + WantedSansStyleText( + "마음에 드는 상대를 찾아\n가치관 문답을 시작해 볼까요?", + style: .body, + color: .tertiary + ) + .lineSpacing(5) + .multilineTextAlignment(.center) + .padding(.bottom, .xl) + + SolidButton(title: "모래사장 바로가기", sizeType: .extraSmall, buttonType: .throttle, action: { store.send(.sandBeachButtonDidTapped) }) - GeometryReader { geometry in - BottleImageView(type: .local(bottleImageSystem: .illustraition(.basket))) - .frame(height: geometry.size.width) + Spacer() } - .aspectRatio(1.0, contentMode: .fit) - } } else { ScrollView { VStack(spacing: .md) { - ForEach(store.currentSelectedBottles, id: \.id) { bottle in - BottleStorageItem( + ForEach(store.pingPongBottleList, id: \.id) { bottle in + PingPongUserView( + status: bottle.lastStatus?.title ?? "", + lastPingPongTime: bottle.lastActivatedAt ?? "", userName: bottle.userName ?? "(없음)", age: bottle.age ?? 0, mbti: bottle.mbti, - keywords: bottle.keyword, imageURL: bottle.userImageUrl, isRead: bottle.isRead ) diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift index e52ec505..fde17d34 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift @@ -119,7 +119,8 @@ extension SandBeachFeature { ) } else { let bottlesStorageList = try await bottleClient.fetchBottleStorageList() - let activeBottlesCount = bottlesStorageList.activeBottles.count + let activeBottlesCount = bottlesStorageList.pingPongBottles + .filter { $0.lastStatus != .conversationStopped && $0.lastStatus != .contactSharedByMeOnly }.count // 자기소개만 작성한 상태 if activeBottlesCount <= 0 { @@ -127,7 +128,7 @@ extension SandBeachFeature { let nextBottleLeftHours = userBottleInfo.nextBottlLeftHours await send(.userStateFetchCompleted( userState: .noBottle(time: nextBottleLeftHours ?? 0), - isDisableButton: true) + isDisableButton: false) ) } else { // 대화 중인 보틀이 있는 상태 await send(.userStateFetchCompleted( diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift index cb523178..ad21d50c 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift @@ -51,10 +51,10 @@ public struct SandBeachView: View { .asThrottleButton { if store.userState.isHasNewBottle { store.send(.newBottleIslandDidTapped) - } - - if store.userState.isHasActiveBottle { + } else if store.userState.isHasActiveBottle { store.send(.bottleStorageIslandDidTapped) + } else if store.userState != .noIntroduction { + store.send(.newBottleIslandDidTapped) } } .disabled(store.isDisableIslandBottle) diff --git a/Projects/Feature/Sources/TabView/MainTabViewFeature.swift b/Projects/Feature/Sources/TabView/MainTabViewFeature.swift index 755c5400..df30a2a9 100644 --- a/Projects/Feature/Sources/TabView/MainTabViewFeature.swift +++ b/Projects/Feature/Sources/TabView/MainTabViewFeature.swift @@ -108,8 +108,11 @@ public struct MainTabViewFeature { switch delegate { case let .selectedTabDidChanged(selectedTab): state.selectedTab = selectedTab + return .none + case .sandBeachButtonDidTapped: + state.selectedTab = .sandBeach + return .send(.sandBeachRoot(.sandBeach(.newBottleIslandDidTapped))) } - return .none // MyPage Delegate case let .myPageRoot(.delegate(delegate)): From 1f78df62bac996790f67b7b26f95dfd918073094 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: Wed, 16 Oct 2024 20:24:56 +0900 Subject: [PATCH 66/90] =?UTF-8?q?[Feature/#323]=20=EB=AC=B8=EB=8B=B5=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=ED=83=AD=EB=B0=94=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?(#326)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 문답 imageAssets 추가 * feat: 문답 탭바 수정 --- .../TabBar/Interface/Sources/TabType.swift | 4 ++-- .../icon/icon_talk.imageset/Contents.json | 24 +++++++++++++++++++ .../icon/icon_talk.imageset/icon_talk.svg | 8 +++++++ .../Image/BottleImageSystem+Icon.swift | 4 ++++ 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_talk.imageset/Contents.json create mode 100644 Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_talk.imageset/icon_talk.svg diff --git a/Projects/Feature/TabBar/Interface/Sources/TabType.swift b/Projects/Feature/TabBar/Interface/Sources/TabType.swift index a89c7be8..ffef7273 100644 --- a/Projects/Feature/TabBar/Interface/Sources/TabType.swift +++ b/Projects/Feature/TabBar/Interface/Sources/TabType.swift @@ -23,7 +23,7 @@ public enum TabType: Hashable, CaseIterable { return "호감" case .bottleStorage: - return "보틀 보관함" + return "문답" case .myPage: return "마이페이지" @@ -39,7 +39,7 @@ public enum TabType: Hashable, CaseIterable { return .icon(.goodFeeling) case .bottleStorage: - return .icon(.bottleStorage) + return .icon(.talk) case .myPage: return .icon(.myPage) diff --git a/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_talk.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_talk.imageset/Contents.json new file mode 100644 index 00000000..168012e1 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_talk.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "icon_talk.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_talk.imageset/icon_talk.svg b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_talk.imageset/icon_talk.svg new file mode 100644 index 00000000..c8971ba2 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Images.xcassets/icon/icon_talk.imageset/icon_talk.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift b/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift index c4bccc03..ecd5d6f1 100644 --- a/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift +++ b/Projects/Shared/DesignSystem/Sources/Image/BottleImageSystem+Icon.swift @@ -26,6 +26,7 @@ public extension Image.BottleImageSystem { case myPage case appleLogo case warning + case talk } } @@ -73,6 +74,9 @@ public extension Image.BottleImageSystem.Icon { case .warning: return SharedDesignSystemAsset.Images.iconWarning.swiftUIImage + + case .talk: + return SharedDesignSystemAsset.Images.iconTalk.swiftUIImage } } } From 06128121783501a68782093b7309d5907a55738e 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: Mon, 4 Nov 2024 23:46:30 +0900 Subject: [PATCH 67/90] =?UTF-8?q?[Feature/#331]=20=EC=BD=94=EC=B9=98=20?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EA=B5=AC=ED=98=84=20(#338)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BottleFontSystem mainTitle 추가 * feat: SandBeachView 디자인 QA 반영 * feat: UserClient CoachMarkState 추가 * feat: SandBeachCoachMarkFeature 구현 * feat: SandBeachCoachMarkView 구현 * feat: SandBeachCoachMarkView 연결 * feat: PopupType CoachMark 추가 * feat: 코치마크 구현 * feat: CoachMark 3번 클릭 시 끝나도록 구현 --- .../User/Interface/Sources/UserClient.swift | 15 ++- Projects/Domain/User/Sources/UserClient.swift | 9 ++ .../Sources/Root/SandBeachRootFeature.swift | 28 ++++- .../Sources/Root/SandBeachRootView.swift | 11 +- .../SandBeach/SandBeachFeatureInterface.swift | 3 +- .../Sources/SandBeach/SandBeachView.swift | 109 +++++++++--------- .../SandBeachCoachMarkFeature.swift | 30 +++++ .../SandBeachCoachMarkFeatureInterface.swift | 38 ++++++ .../SandBeachCoachMarkView.swift | 105 +++++++++++++++++ .../Interface/Sources/TabBarModifier.swift | 2 +- .../Sources/Components/Popup/PopupType.swift | 2 +- .../Sources/Components/Popup/PopupView.swift | 10 +- .../Font/BottleFontSystem+WantedSans.swift | 3 + 13 files changed, 304 insertions(+), 61 deletions(-) create mode 100644 Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkFeature.swift create mode 100644 Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkFeatureInterface.swift create mode 100644 Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkView.swift diff --git a/Projects/Domain/User/Interface/Sources/UserClient.swift b/Projects/Domain/User/Interface/Sources/UserClient.swift index 34fd1927..2bce6164 100644 --- a/Projects/Domain/User/Interface/Sources/UserClient.swift +++ b/Projects/Domain/User/Interface/Sources/UserClient.swift @@ -12,9 +12,11 @@ import Combine public struct UserClient { private let _isLoggedIn: () -> Bool private let _isAppDeleted: () -> Bool + private let _isCoachMarkViewed: () -> Bool private let _fetchFcmToken: () -> String? private let updateLoginState: (Bool) -> Void private let updateDeleteState: (Bool) -> Void + private let updateCoachMarkState: (Bool) -> Void private let updateFcmToken: (String) -> Void private let updatePushNotificationAllowStatus: (Bool) -> Void private let _fetchAlertState: () async throws -> [UserAlertState] @@ -31,11 +33,13 @@ public struct UserClient { public init( isLoggedIn: @escaping () -> Bool, isAppDeleted: @escaping () -> Bool, + isCoachMarkViewed: @escaping () -> Bool, fetchFcmToken: @escaping () -> String?, updateLoginState: @escaping (Bool) -> Void, updateDeleteState: @escaping (Bool) -> Void, updateFcmToken: @escaping (String) -> Void, updatePushNotificationAllowStatus: @escaping (Bool) -> Void, + updateCoachMarkState: @escaping (Bool) -> Void, fetchAlertState: @escaping () async throws -> [UserAlertState], fetchPushNotificationAllowStatus: @escaping () -> Bool, updateAlertState: @escaping (UserAlertState) async throws -> Void, @@ -44,11 +48,13 @@ public struct UserClient { ) { self._isLoggedIn = isLoggedIn self._isAppDeleted = isAppDeleted + self._isCoachMarkViewed = isCoachMarkViewed self._fetchFcmToken = fetchFcmToken self.updateLoginState = updateLoginState self.updateDeleteState = updateDeleteState self.updateFcmToken = updateFcmToken self.updatePushNotificationAllowStatus = updatePushNotificationAllowStatus + self.updateCoachMarkState = updateCoachMarkState self._fetchAlertState = fetchAlertState self._fetchPushNotificationAllowStatus = fetchPushNotificationAllowStatus self.updateAlertState = updateAlertState @@ -64,6 +70,10 @@ public struct UserClient { _isAppDeleted() } + public func isCoachMarkViewd() -> Bool { + _isCoachMarkViewed() + } + public func fetchFcmToken() -> String? { _fetchFcmToken() } @@ -75,7 +85,10 @@ public struct UserClient { public func updateDeleteState(isDelete: Bool) { updateDeleteState(isDelete) } - + + public func updateCoachMarkState(isViewed: Bool) { + updateCoachMarkState(isViewed) + } public func updateFcmToken(fcmToken: String) { updateFcmToken(fcmToken) } diff --git a/Projects/Domain/User/Sources/UserClient.swift b/Projects/Domain/User/Sources/UserClient.swift index c46472f1..b6792a34 100644 --- a/Projects/Domain/User/Sources/UserClient.swift +++ b/Projects/Domain/User/Sources/UserClient.swift @@ -22,6 +22,7 @@ extension UserClient: DependencyKey { case deleteState case fcmToken case alertAllowState + case coachMarkState } static public var liveValue: UserClient = .live() @@ -38,6 +39,10 @@ extension UserClient: DependencyKey { return !UserDefaults.standard.bool(forKey: UserDefaultsKeys.deleteState.rawValue) }, + isCoachMarkViewed: { + return UserDefaults.standard.bool(forKey: UserDefaultsKeys.coachMarkState.rawValue) + }, + fetchFcmToken: { return UserDefaults.standard.string(forKey: UserDefaultsKeys.fcmToken.rawValue) }, @@ -58,6 +63,10 @@ extension UserClient: DependencyKey { UserDefaults.standard.set(isAllow, forKey: UserDefaultsKeys.alertAllowState.rawValue) }, + updateCoachMarkState: { isViewed in + UserDefaults.standard.set(isViewed, forKey: UserDefaultsKeys.coachMarkState.rawValue) + }, + fetchAlertState: { let responseData = try await networkManager.reqeust(api: .apiType(UserAPI.fetchAlertState), dto: [AlertStateResponseDTO].self) return responseData.map { $0.toDomain() } diff --git a/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift b/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift index e69425e0..a17bbf52 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootFeature.swift @@ -10,7 +10,9 @@ import Foundation import FeatureProfileSetupInterface import FeatureBottleArrivalInterface import FeatureTabBarInterface + import DomainProfile +import DomainUserInterface import ComposableArchitecture @@ -38,19 +40,24 @@ public struct SandBeachRootFeature { var introduction: String var profileImageData: Data var isLoading: Bool + public var isCoachMarkViewed: Bool = true + public var sandBeach: SandBeachFeature.State + public var sandBeachCoachMark: SandBeachCoachMarkFeature.State public init( path: StackState = StackState(), introduction: String = "", profileImageData: Data = .init(), sandBeach: SandBeachFeature.State = .init(), + sandBeachCoachMark: SandBeachCoachMarkFeature.State = .init(), isLoading: Bool = false ) { self.path = path self.introduction = introduction self.profileImageData = profileImageData self.sandBeach = sandBeach + self.sandBeachCoachMark = sandBeachCoachMark self.isLoading = isLoading } } @@ -58,6 +65,7 @@ public struct SandBeachRootFeature { public enum Action { case path(StackAction) case sandBeach(SandBeachFeature.Action) + case sandBeachCoachMark(SandBeachCoachMarkFeature.Action) case profileSetupDidCompleted case delegate(Delegate) case selectedTabDidChanged(selectedTab: TabType) @@ -74,6 +82,10 @@ public struct SandBeachRootFeature { SandBeachFeature() } + Scope(state: \.sandBeachCoachMark, action: \.sandBeachCoachMark) { + SandBeachCoachMarkFeature() + } + reducer .forEach(\.path, action: \.path) } @@ -84,7 +96,8 @@ extension SandBeachRootFeature { let reducer = Reduce { state, action in @Dependency(\.profileClient) var profileClient - + @Dependency(\.userClient) var userClient + switch action { // IntrodctionSetup Delegate @@ -147,6 +160,10 @@ extension SandBeachRootFeature { case .writeButtonDidTapped: state.path.append(.IntroductionSetup(IntroductionSetupFeature.State())) return .none + + case .sandBeachLoadCompleted: + state.isCoachMarkViewed = userClient.isCoachMarkViewd() + return .none } // BottleArrivalDetail Delegate @@ -157,6 +174,15 @@ extension SandBeachRootFeature { return .none } + // SandBeachCoachMark Delegate + case let .sandBeachCoachMark(.delegate(delegate)): + switch delegate { + case .coachMarkDidCompleted: + userClient.updateCoachMarkState(isViewed: true) + state.isCoachMarkViewed = true + return .none + } + case .profileSetupDidCompleted: state.isLoading = false state.path.removeAll() diff --git a/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootView.swift b/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootView.swift index 95181627..514c8a0e 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootView.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/Root/SandBeachRootView.swift @@ -25,10 +25,17 @@ public struct SandBeachRootView: View { public var body: some View { WithPerceptionTracking { NavigationStack(path: $store.scope(state: \.path, action: \.path)) { + ZStack { SandBeachView(store: store.scope(state: \.sandBeach, action: \.sandBeach)) - .setTabBar(selectedTab: .sandBeach) { selectedTab in - store.send(.selectedTabDidChanged(selectedTab: selectedTab)) + .setTabBar(selectedTab: .sandBeach) { selectedTab in + store.send(.selectedTabDidChanged(selectedTab: selectedTab)) + } + + if !store.isCoachMarkViewed && store.sandBeach.userState == .noIntroduction { + SandBeachCoachMarkView( + store: store.scope(state: \.sandBeachCoachMark, action: \.sandBeachCoachMark)) } + } } destination: { store in WithPerceptionTracking { switch store.state { diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift index fde17d34..eadd391a 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift @@ -56,6 +56,7 @@ public struct SandBeachFeature { case writeButtonDidTapped case newBottleIslandDidTapped case bottleStorageIslandDidTapped + case sandBeachLoadCompleted } case alert(Alert) @@ -152,7 +153,7 @@ extension SandBeachFeature { state.userState = userState state.isDisableIslandBottle = isDisableButton state.isLoading = false - return .none + return .send(.delegate(.sandBeachLoadCompleted)) case .writeButtonDidTapped: return .send(.delegate(.writeButtonDidTapped)) diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift index ad21d50c..07cb9a22 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachView.swift @@ -20,67 +20,72 @@ public struct SandBeachView: View { public var body: some View { WithPerceptionTracking { - GeometryReader { geo in - WithPerceptionTracking { - if store.userState == .none && store.isLoading { - LoadingIndicator() - } else { - VStack(spacing: 0) { - Spacer() - BottleImageView(type: .local(bottleImageSystem: .illustraition(.logo))) - .frame(width: 78.06, height: 20) - .padding(.top, geo.safeAreaInsets.top + 14) - .padding(.bottom, 38) - - WantedSansStyleText( - store.userState.title, style: .title1, color: .secondary) - .frame(height: 62) - .multilineTextAlignment(.center) - .padding(.bottom, 24) - Spacer() - - popup - .padding(.bottom, 8) - - BottleImageView(type: .local( - bottleImageSystem: - store.userState.isEmptyBottle ? .illustraition(.islandEmptyBottle) : .illustraition(.islandHasBottle)) - ) - .frame(width: geo.size.width) - .frame(height: geo.size.width) - .asThrottleButton { - if store.userState.isHasNewBottle { - store.send(.newBottleIslandDidTapped) - } else if store.userState.isHasActiveBottle { - store.send(.bottleStorageIslandDidTapped) - } else if store.userState != .noIntroduction { - store.send(.newBottleIslandDidTapped) - } - } - .disabled(store.isDisableIslandBottle) - - Spacer() - } - } + if store.userState == .none && store.isLoading { + LoadingIndicator() + } else { + VStack(spacing: 0) { + Spacer() + .frame(height: 1) + logoImage + userStateTitle + popup + islandImage + Spacer() } } - .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) - .onAppear { - store.send(.onAppear) - } - .background { - BottleImageView( - type: .local(bottleImageSystem: .illustraition(.sandBeachBackground)) - ) - } } - .edgesIgnoringSafeArea([.top, .bottom]) + .bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert)) + .onAppear { + store.send(.onAppear) + } + .background { + BottleImageView( + type: .local(bottleImageSystem: .illustraition(.sandBeachBackground)) + ) + .edgesIgnoringSafeArea(.all) + } } } // MARK: - Views public extension SandBeachView { + var logoImage: some View { + BottleImageView(type: .local(bottleImageSystem: .illustraition(.logo))) + .frame(width: 78.06, height: 20) + .padding(.top, 14) + .padding(.bottom, 46) + } + + var userStateTitle: some View { + WantedSansStyleText( + store.userState.title, style: .mainTitle, color: .secondary) + .multilineTextAlignment(.center) + .padding(.bottom, store.userState == .noIntroduction ? 32 : 64) + .lineSpacing(5) + } + + var islandImage: some View { + GeometryReader { geo in + BottleImageView(type: .local( + bottleImageSystem: + store.userState.isEmptyBottle ? .illustraition(.islandEmptyBottle) : .illustraition(.islandHasBottle)) + ) + .frame(width: geo.size.width) + .frame(height: geo.size.width) + .asThrottleButton { + if store.userState.isHasNewBottle { + store.send(.newBottleIslandDidTapped) + } else if store.userState.isHasActiveBottle { + store.send(.bottleStorageIslandDidTapped) + } else if store.userState != .noIntroduction { + store.send(.newBottleIslandDidTapped) + } + } + .disabled(store.isDisableIslandBottle) + } + } + @ViewBuilder var popup: some View { let userState = store.userState diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkFeature.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkFeature.swift new file mode 100644 index 00000000..a2ed0f81 --- /dev/null +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkFeature.swift @@ -0,0 +1,30 @@ +// +// SandBeachCoachMarkFeature.swift +// FeatureSandBeachInterface +// +// Created by 임현규 on 10/28/24. +// + +import ComposableArchitecture + +extension SandBeachCoachMarkFeature { + public init() { + let reducer = Reduce { state, action in + switch action { + case .coachMarkDidTapped: + state.count += 1 + + if state.count == 3 { + return .send(.delegate(.coachMarkDidCompleted)) + } else { + return .none + } + + case .delegate: + return .none + } + } + + self.init(reducer: reducer) + } +} diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkFeatureInterface.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkFeatureInterface.swift new file mode 100644 index 00000000..85043236 --- /dev/null +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkFeatureInterface.swift @@ -0,0 +1,38 @@ +// +// SandBeachCoachMarkFeatureInterface.swift +// FeatureSandBeachInterface +// +// Created by 임현규 on 10/28/24. +// + +import ComposableArchitecture + +@Reducer +public struct SandBeachCoachMarkFeature { + private let reducer: Reduce + + + public init(reducer: Reduce) { + self.reducer = reducer + } + + @ObservableState + public struct State: Equatable { + public var count: Int = 0 + + public init() {} + } + + public enum Action { + case coachMarkDidTapped + case delegate(Delegate) + + public enum Delegate { + case coachMarkDidCompleted + } + } + + public var body: some ReducerOf { + reducer + } +} diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkView.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkView.swift new file mode 100644 index 00000000..00788278 --- /dev/null +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeachCoachMark/SandBeachCoachMarkView.swift @@ -0,0 +1,105 @@ +// +// SandBeachCoachMarkView.swift +// FeatureSandBeachInterface +// +// Created by 임현규 on 10/28/24. +// + +import SwiftUI + +import SharedDesignSystem + +import ComposableArchitecture + +public struct SandBeachCoachMarkView: View { + @Perception.Bindable private var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + ZStack { + Color.black.opacity(0.6) + .edgesIgnoringSafeArea(.all) + if store.count == 0 { + firstCoachMark + } else if store.count == 1 { + secondCoachMark + } else { + thirdCoachMark + } + } + .compositingGroup() + .asButton { + store.send(.coachMarkDidTapped) + } + } +} + +private extension SandBeachCoachMarkView { + var firstCoachMark: some View { + VStack(spacing: 0) { + Spacer() + .frame(height: 96) + + PopupView(popupType: .coachMark(content: "나의 첫인상이 될\n자기소개를 작성해주세요")) + + RoundedRectangle(cornerRadius: 20) + .frame(width: 267, height: 106) + .foregroundColor(.white) + .blendMode(.destinationOut) + .padding(.top, .xl) + + Spacer() + } + } + + var secondCoachMark: some View { + VStack(spacing: 0.0) { + Spacer() + .frame(height: 239) + + PopupView(popupType: .coachMark(content: "바구니를 클릭하면\n보틀 속 자기소개를 읽어볼 수 있어요")) + + GeometryReader { geo in + HStack(spacing: 0.0) { + Spacer() + RoundedRectangle(cornerRadius: 20) + .frame(width: geo.size.width - 200, height: geo.size.width - 200) + .foregroundColor(.white) + .blendMode(.destinationOut) + .padding(.top, .xl) + Spacer() + } + } + Spacer() + } + } + + var thirdCoachMark: some View { + GeometryReader { geo in + + VStack(spacing: 0.0) { + Spacer() + + PopupView(popupType: .coachMark(content: "가치관 문답을 시작한 경우\n문답에서 확인할 수 있어요")) + .offset(x: geo.size.width * 0.09) + + HStack(spacing: 0.0) { + Spacer() + RoundedRectangle(cornerRadius: 20) + .frame(width: 72, height: 72) + .foregroundColor(.white) + .blendMode(.destinationOut) + .padding(.top, .xl) + .offset(x: geo.size.width * 0.09) + Spacer() + + } + } + .offset(y: -34) + } + .ignoresSafeArea(.all, edges: .bottom) + } +} diff --git a/Projects/Feature/TabBar/Interface/Sources/TabBarModifier.swift b/Projects/Feature/TabBar/Interface/Sources/TabBarModifier.swift index 0c151aa6..c3f1f38a 100644 --- a/Projects/Feature/TabBar/Interface/Sources/TabBarModifier.swift +++ b/Projects/Feature/TabBar/Interface/Sources/TabBarModifier.swift @@ -42,7 +42,7 @@ private struct TabBarModifier: ViewModifier { color: selectedTab == item ? .primary : .enableTertiary ) } - .offset(y: -9) + .offset(y: -15) .asThrottleButton { action(item) } diff --git a/Projects/Shared/DesignSystem/Sources/Components/Popup/PopupType.swift b/Projects/Shared/DesignSystem/Sources/Components/Popup/PopupType.swift index 0306e841..629f5efe 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Popup/PopupType.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Popup/PopupType.swift @@ -10,5 +10,5 @@ import Foundation public enum PopupType { case text(content: String) case button(content: String, buttonTitle: String) - + case coachMark(content: String) } diff --git a/Projects/Shared/DesignSystem/Sources/Components/Popup/PopupView.swift b/Projects/Shared/DesignSystem/Sources/Components/Popup/PopupView.swift index b7b03e54..d0c78e30 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Popup/PopupView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Popup/PopupView.swift @@ -42,6 +42,8 @@ private extension PopupView { WantedSansStyleText(content, style: .subTitle2, color: .secondary) case .button(let content, _): WantedSansStyleText(content, style: .subTitle2, color: .secondary) + case .coachMark(let content): + WantedSansStyleText(content, style: .subTitle2, color: .secondary) } } @@ -69,6 +71,9 @@ private extension PopupView { .frame(width: 227) } .padding(.lg) + case .coachMark: + popupText + .padding(.lg) } } } @@ -84,8 +89,9 @@ private extension PopupView { var height: CGFloat { switch popupType { - case .button: return 106 - case .text: return 42 + case .button: return 106 + case .text: return 42 + case .coachMark: return 42 } } } diff --git a/Projects/Shared/DesignSystem/Sources/Font/BottleFontSystem+WantedSans.swift b/Projects/Shared/DesignSystem/Sources/Font/BottleFontSystem+WantedSans.swift index 84339409..424a2ccf 100644 --- a/Projects/Shared/DesignSystem/Sources/Font/BottleFontSystem+WantedSans.swift +++ b/Projects/Shared/DesignSystem/Sources/Font/BottleFontSystem+WantedSans.swift @@ -15,6 +15,7 @@ public extension Font.BottleFontSystem { case subTitle2 case body case caption + case mainTitle } } @@ -33,6 +34,8 @@ public extension Font.BottleFontSystem.WantedSans { return SharedDesignSystemFontFamily.WantedSans.medium.swiftUIFont(size: 14) case .caption: return SharedDesignSystemFontFamily.WantedSans.medium.swiftUIFont(size: 12) + case .mainTitle: + return SharedDesignSystemFontFamily.WantedSans.bold.swiftUIFont(size: 32) } } } From 2531eb04efabf0a97afbb4bdb712a13354d8174d Mon Sep 17 00:00:00 2001 From: JongHoon Date: Wed, 6 Nov 2024 00:06:58 +0900 Subject: [PATCH 68/90] =?UTF-8?q?chore:=20=EC=97=B0=EB=9D=BD=EC=B2=98=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=20=EA=B6=8C=ED=95=9C=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InfoPlist+Templates.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index a514757b..d43ef6bd 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -10,8 +10,8 @@ import ProjectDescription public extension InfoPlist { static var app: InfoPlist { return .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0.9", - "CFBundleVersion": "31", + "CFBundleShortVersionString": "1.0.10", + "CFBundleVersion": "34", "UIUserInterfaceStyle": "Light", "CFBundleName": "보틀", "UILaunchScreen": [ @@ -22,7 +22,7 @@ public extension InfoPlist { "UISupportedInterfaceOrientations": [ "UIInterfaceOrientationPortrait" ], - "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다.", + "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다. 허용하시면 연락처가 서버에 업로드됩니다.", "BASE_URL": "$(BASE_URL)", "WEB_VIEW_BASE_URL": "$(WEB_VIEW_BASE_URL)", "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", @@ -44,14 +44,14 @@ public extension InfoPlist { static var example: InfoPlist { return .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0.9", - "CFBundleVersion": "31", + "CFBundleShortVersionString": "1.0.10", + "CFBundleVersion": "34", "UIUserInterfaceStyle": "Light", "UILaunchScreen": [:], "UISupportedInterfaceOrientations": [ "UIInterfaceOrientationPortrait" ], - "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다.", + "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다. 허용하시면 연락처가 서버에 업로드됩니다.", "BASE_URL": "$(BASE_URL)", "WEB_VIEW_BASE_URL": "$(WEB_VIEW_BASE_URL)", "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", From dbd1c0b9e321191439654b33fc87112fd8d46c09 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Wed, 6 Nov 2024 13:40:37 +0900 Subject: [PATCH 69/90] =?UTF-8?q?feat:=20=EB=AC=B8=EB=8B=B5=20=EC=9D=B4?= =?UTF-8?q?=ED=9B=84=20=EC=83=81=ED=83=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/BottleStorage/BottleStorageFeature.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift index 1a5373a3..570f8126 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift @@ -42,8 +42,7 @@ extension BottleStorageFeature { switch delegate { case .backButtonDidTapped: - state.path.removeLast() - return .none + return popToRootAndReload(state: &state) case .reportButtonDidTapped(let userReportProfile): state.path.append(.report(ReportUserFeature.State(userProfile: userReportProfile))) return .none From 1688aa96984863d1593cbf133313008228ad0cbb 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:43:57 +0900 Subject: [PATCH 70/90] =?UTF-8?q?[Feature/#335]=20=EB=AC=B8=EB=8B=B5=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=ED=85=8D=EC=8A=A4=ED=8A=B8=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20(#336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: TextField 글자수 다시 0 됐을 때 PlaceHolder 보이게 처리 * feat: 텍스트필드 관리 FocusField enum 구현 * feat: 포커스된 텍스트필드와 글자수에 따라서 텍스트필드 활성상태 로직 추가 * chore: 오탈자 수정 - previousFocustedField -> previousFocusedField * chore: 오탈자 수정 --- .../QuestionAndAnswerFeature.swift | 16 ++++++++++++++ .../QuestionAndAnswerFeatureInterface.swift | 10 ++++++++- .../QuestionAndAnswerView.swift | 21 ++++++++----------- .../LinesTextField/LinesTextField.swift | 4 ++++ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift index a03ed829..0f982c5f 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift @@ -24,6 +24,22 @@ extension QuestionAndAnswerFeature { state.configurePingPong(pingPong) return .none + case let .focusedFieldDidChanged(field): + guard let previousFocusedField = state.focusedField else { + state.focusedField = field + return .none + } + + if (previousFocusedField == .firstLetter && state.firstLetterTextFieldContent.count >= 50) || + (previousFocusedField == .secondLetter && state.secondLetterTextFieldContent.count >= 50) || + (previousFocusedField == .thirdLetter && state.thirdLetterTextFieldContent.count >= 50) { + state.textFieldState = .active + } else { + state.textFieldState = .enabled + } + + return .none + case let .texFieldDidFocused(isFocused): state.textFieldState = isFocused ? .focused : .active return .none diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift index 770ba42a..f04ba697 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeatureInterface.swift @@ -20,6 +20,12 @@ public struct QuestionAndAnswerFeature { self.reducer = reducer } + public enum FocusField: Hashable { + case firstLetter + case secondLetter + case thirdLetter + } + @ObservableState public struct State: Equatable { let bottleID: Int @@ -82,7 +88,8 @@ public struct QuestionAndAnswerFeature { var thirdLetterTextFieldContent: String var textFieldState: TextFieldState - + var focusedField: FocusField? = nil + // 사진 선택 var photoShareIsActive: Bool { guard let photoStatus = pingPong?.photo.photoStatus @@ -177,6 +184,7 @@ public struct QuestionAndAnswerFeature { case stopTalkButtonDidTapped case refreshDidPulled + case focusedFieldDidChanged(FocusField?) // ETC. case binding(BindingAction) diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift index 24270272..23b2f9a1 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift @@ -14,7 +14,7 @@ import ComposableArchitecture public struct QuestionAndAnswerView: View { @Perception.Bindable private var store: StoreOf - @FocusState private var isTextFieldFocused: Bool + @FocusState private var focusedField: QuestionAndAnswerFeature.FocusField? public init(store: StoreOf) { self.store = store @@ -39,8 +39,8 @@ public struct QuestionAndAnswerView: View { )) } ) - .focused($isTextFieldFocused) - + .focused($focusedField, equals: .firstLetter) + QuestionPingPongView( pingpongTitle: "두 번째 질문", textFieldContent: $store.secondLetterTextFieldContent, @@ -55,8 +55,8 @@ public struct QuestionAndAnswerView: View { )) } ) - .focused($isTextFieldFocused) - + .focused($focusedField, equals: .secondLetter) + QuestionPingPongView( pingpongTitle: "세 번째 질문", textFieldContent: $store.thirdLetterTextFieldContent, @@ -71,8 +71,8 @@ public struct QuestionAndAnswerView: View { )) } ) - .focused($isTextFieldFocused) - + .focused($focusedField, equals: .thirdLetter) + PhotoSharePingPongView( isActive: store.photoShareIsActive, pingPongTitle: "사진 공개", @@ -117,11 +117,8 @@ public struct QuestionAndAnswerView: View { } .padding(.md) .frame(maxWidth: .infinity) - .onChange(of: isTextFieldFocused) { isFocused in - store.send(.texFieldDidFocused(isFocused: isFocused)) - } - .onChange(of: store.textFieldState) { textFieldState in - isTextFieldFocused = textFieldState == .active || textFieldState == .enabled ? false : true + .onChange(of: focusedField) { field in + store.send(.focusedFieldDidChanged(field)) } } .refreshable { diff --git a/Projects/Shared/DesignSystem/Sources/Components/TextField/LinesTextField/LinesTextField.swift b/Projects/Shared/DesignSystem/Sources/Components/TextField/LinesTextField/LinesTextField.swift index a8dd57db..ea971cb9 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/TextField/LinesTextField/LinesTextField.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/TextField/LinesTextField/LinesTextField.swift @@ -109,6 +109,10 @@ private extension LinesTextField { if newValue.count >= textLimit { text = String(text.prefix(textLimit)) } + + if newValue.count == 0 { + textFieldState = .enabled + } } } } From 59e6243631d1ba36bac6f3cb0c63897903514eea 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:44:25 +0900 Subject: [PATCH 71/90] =?UTF-8?q?[Feature/#329]=20=EC=82=AC=EC=A7=84=203?= =?UTF-8?q?=EA=B0=9C=20=EB=93=B1=EB=A1=9D=20=EB=8C=80=EC=9D=91=20(#337)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BottlePingPongResonseDTO otherImageURLs 추가 * feat: 사진 스크롤 TabView 구현 * feat: 사진 공개 하단 indicator 추가 --- .../Response/BottlePingPongResponseDTO.swift | 6 +- .../Sources/Entity/BottlePingPong.swift | 9 +- .../View/PhotoSharePingPongView.swift | 111 ++++++++++-------- .../QuestionAndAnswerView.swift | 5 +- 4 files changed, 66 insertions(+), 65 deletions(-) diff --git a/Projects/Domain/Bottle/Interface/Sources/DTO/Response/BottlePingPongResponseDTO.swift b/Projects/Domain/Bottle/Interface/Sources/DTO/Response/BottlePingPongResponseDTO.swift index bc27ab18..75c31058 100644 --- a/Projects/Domain/Bottle/Interface/Sources/DTO/Response/BottlePingPongResponseDTO.swift +++ b/Projects/Domain/Bottle/Interface/Sources/DTO/Response/BottlePingPongResponseDTO.swift @@ -98,8 +98,7 @@ public struct BottlePingPongResponseDTO: Decodable { public struct PhotoDTO: Decodable { let photoStatus: String? - let myImageUrl: String? - let otherImageUrl: String? + let otherImageUrls: [String]? public func toDomain() -> Photo { let photoStatus: PingPongPhotoStatus = switch photoStatus { @@ -122,8 +121,7 @@ public struct BottlePingPongResponseDTO: Decodable { } return .init( photoStatus: photoStatus, - myProfileImageURL: myImageUrl, - otherProfileImageURL: otherImageUrl + otherProfileImageURLs: otherImageUrls ) } } diff --git a/Projects/Domain/Bottle/Interface/Sources/Entity/BottlePingPong.swift b/Projects/Domain/Bottle/Interface/Sources/Entity/BottlePingPong.swift index 5eab45b4..512c61b6 100644 --- a/Projects/Domain/Bottle/Interface/Sources/Entity/BottlePingPong.swift +++ b/Projects/Domain/Bottle/Interface/Sources/Entity/BottlePingPong.swift @@ -110,17 +110,14 @@ public enum PingPongMatchStatus { public struct Photo: Equatable { public let photoStatus: PingPongPhotoStatus - public let myProfileImageURL: String? - public let otherProfileImageURL: String? + public let otherProfileImageURLs: [String]? public init( photoStatus: PingPongPhotoStatus, - myProfileImageURL: String? = nil, - otherProfileImageURL: String? = nil + otherProfileImageURLs: [String]? = nil ) { self.photoStatus = photoStatus - self.myProfileImageURL = myProfileImageURL - self.otherProfileImageURL = otherProfileImageURL + self.otherProfileImageURLs = otherProfileImageURLs } } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PhotoSharePingPongView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PhotoSharePingPongView.swift index 93ef4ff5..c551279f 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PhotoSharePingPongView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PhotoSharePingPongView.swift @@ -15,30 +15,28 @@ public struct PhotoSharePingPongView: View { private let isActive: Bool private let pingPongTitle: String private let photoShareState: PingPongPhotoStatus - private let myProfileImageURL: String? - private let otherProfileImageURL: String? + private let otherProfileImageURLs: [String]? @Binding var isSelctedYesButton: Bool @Binding var isSelctedNoButton: Bool private let doneButtonAction: (() -> Void)? + @State var selectedIndex: Int = 0 public init( isActive: Bool, - pingPongTitle: String, + pingPongTitle: String, photoShareState: PingPongPhotoStatus, - myProfileImageURL: String?, - otherProfileImageURL: String?, isSelctedYesButton: Binding = .constant(false), - isSelctedNoButton: Binding = .constant(false), - doneButtonAction: (() -> Void)? = nil + isSelctedNoButton: Binding = .constant(false), + doneButtonAction: (() -> Void)? = nil, + otherProfileImageURLs: [String]? ) { self.isActive = isActive self.pingPongTitle = pingPongTitle self.photoShareState = photoShareState - self.myProfileImageURL = myProfileImageURL - self.otherProfileImageURL = otherProfileImageURL self._isSelctedYesButton = isSelctedYesButton self._isSelctedNoButton = isSelctedNoButton self.doneButtonAction = doneButtonAction + self.otherProfileImageURLs = otherProfileImageURLs } public var body: some View { @@ -140,15 +138,51 @@ private extension PhotoSharePingPongView { makeRightBubbleText(text: "사진 공개가 실패했어요") } + @ViewBuilder var bothPublicView: some View { - HStack(spacing: .sm) { - peerProfileImage - .frame(maxWidth: .infinity) - myProfileImage - .frame(maxWidth: .infinity) + if let images = otherProfileImageURLs { + VStack(spacing: .lg) { + GeometryReader { geo in + TabView(selection: $selectedIndex) { + ForEach(images.indices, id: \.self) { index in + makePeerProfileImage(url: images[index]) + .tag(index) + } + } + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) + .frame(height: geo.size.width - 10) + } + .aspectRatio(1, contentMode: .fit) + + photoIndicatorView + } + } else { + EmptyView() } } + var photoIndicatorView: some View { + HStack(spacing: .xxl) { + BottleImageView(type: .local(bottleImageSystem: .icon(.leftArrow))) + .foregroundStyle(to: ColorToken.icon(.primary)) + .asButton { + if selectedIndex > 0 { + selectedIndex -= 1 + } + } + + PageIndicatorView(pageInfo: .init(nowPage: selectedIndex + 1, totalCount: otherProfileImageURLs?.count ?? 0)) + + BottleImageView(type: .local(bottleImageSystem: .icon(.leftArrow))) + .foregroundStyle(to: ColorToken.icon(.primary)) + .rotationEffect(.degrees(180)) + .asButton { + if selectedIndex + 1 < otherProfileImageURLs?.count ?? 0 { + selectedIndex += 1 + } + } + } + } @ViewBuilder var questionText: some View { @@ -168,43 +202,16 @@ private extension PhotoSharePingPongView { } } - @ViewBuilder - var peerProfileImage: some View { - if let peerProfileImageURL = otherProfileImageURL { - GeometryReader { geo in - RemoteImageView( - imageURL: peerProfileImageURL, - downsamplingWidth: 150, - downsamplingHeight: 150 - ) - .frame(height: geo.size.width) - .preventScreenshot() - } - .aspectRatio(1, contentMode: .fit) - .clipped() - .cornerRadius(.md, corenrs: [.topRight, .bottomLeft, .bottomRight]) - } else { - EmptyView() - } - } - - @ViewBuilder - var myProfileImage: some View { - if let myProfileImageURL = myProfileImageURL { - GeometryReader { geo in - RemoteImageView( - imageURL: myProfileImageURL, - downsamplingWidth: 150, - downsamplingHeight: 150 - ) - .frame(height: geo.size.width) - .preventScreenshot() - } - .aspectRatio(1, contentMode: .fit) - .clipped() - .cornerRadius(.md, corenrs: [.topRight, .topLeft, .bottomLeft]) - } else { - EmptyView() - } + func makePeerProfileImage(url: String) -> some View { + RemoteImageView( + imageURL: url, + downsamplingWidth: 150, + downsamplingHeight: 150 + ) + .preventScreenshot() + .aspectRatio(1, contentMode: .fit) + .clipped() + .cornerRadius(.md, corenrs: [.topRight, .bottomLeft, .bottomRight]) } } + diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift index 23b2f9a1..94cf6c35 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift @@ -77,13 +77,12 @@ public struct QuestionAndAnswerView: View { isActive: store.photoShareIsActive, pingPongTitle: "사진 공개", photoShareState: store.photoShareStateType, - myProfileImageURL: store.photoInfo?.myProfileImageURL, - otherProfileImageURL: store.photoInfo?.otherProfileImageURL, isSelctedYesButton: $store.photoIsSelctedYesButton, isSelctedNoButton: $store.photoIsSelctedNoButton, doneButtonAction: { store.send(.sharePhotoSelectButtonDidTapped(willShare: store.photoIsSelctedYesButton)) - } + }, + otherProfileImageURLs: store.photoInfo?.otherProfileImageURLs ) FinalSelectPingPongView( From 1fb142ed6163b946e7ae4a1ba067fcee7edcfdf7 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:49:27 +0900 Subject: [PATCH 72/90] =?UTF-8?q?[Feature/#343]=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=88=98=EC=A0=95=20=EC=99=84=EB=A3=8C=20=ED=9B=84?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?(#344)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: onProfileImageEditComplete 브릿지 추가 * feat: onProfileImageEditComplete 브릿지 호출 시 유저 정보 업데이트 --- .../Core/WebView/Interface/Sources/BottleWebViewAction.swift | 5 +++++ .../Interface/Sources/EditProfile/EditProfileFeature.swift | 3 +++ .../Sources/EditProfile/EditProfileFeatureInterface.swift | 2 ++ .../Interface/Sources/EditProfile/ProfileEditView.swift | 3 +++ .../Interface/Sources/MyPageRootFeatureInterface.swift | 2 ++ 5 files changed, 15 insertions(+) diff --git a/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift b/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift index be67e664..eb555528 100644 --- a/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift +++ b/Projects/Core/WebView/Interface/Sources/BottleWebViewAction.swift @@ -47,6 +47,8 @@ public enum BottleWebViewAction: Equatable { case logOutButtonDidTapped /// 회원탈퇴 case withdrawalButtonDidTap + /// 프로필 사진 수정 완료 + case profileImageDidChanged public init?( type: String, @@ -142,6 +144,9 @@ public enum BottleWebViewAction: Equatable { case "deleteUser": self = .withdrawalButtonDidTap + case "onProfileImageEditComplete": + self = .profileImageDidChanged + default: return nil } diff --git a/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeature.swift b/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeature.swift index e711cc73..85b53e11 100644 --- a/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeature.swift @@ -28,6 +28,9 @@ extension EditProfileFeature { case .backButtonDidTapped: return .send(.delegate(.closeEditProfileView)) + case .profileImageDidChanged: + return .send(.delegate(.profileImageDidChanged)) + default: return .none } diff --git a/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeatureInterface.swift index 43b47e82..8198a418 100644 --- a/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/EditProfile/EditProfileFeatureInterface.swift @@ -31,9 +31,11 @@ public struct EditProfileFeature { case presentToast(message: String) case backButtonDidTapped case delegate(Delegate) + case profileImageDidChanged public enum Delegate { case closeEditProfileView + case profileImageDidChanged } // binding diff --git a/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift b/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift index 7addfb6b..8bc811d0 100644 --- a/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift +++ b/Projects/Feature/MyPage/Interface/Sources/EditProfile/ProfileEditView.swift @@ -35,6 +35,9 @@ public struct ProfileEditView: View { case .closeWebView: store.send(.backButtonDidTapped) + case .profileImageDidChanged: + store.send(.profileImageDidChanged) + default: Log.assertion(message: "\(action) - not handled action") } diff --git a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift index a7d175b3..0be43eef 100644 --- a/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift +++ b/Projects/Feature/MyPage/Interface/Sources/MyPageRootFeatureInterface.swift @@ -57,6 +57,8 @@ extension MyPageRootFeature { case .closeEditProfileView: _ = state.path.popLast() return .none + case .profileImageDidChanged: + return .send(.myPage(.userProfileUpdateDidRequest)) } default: From a062fdfff77c5c5ec99fecdbcba7129200706c37 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:50:20 +0900 Subject: [PATCH 73/90] =?UTF-8?q?[Feature/#346]=20=EC=9B=B9=EB=B7=B0=20saf?= =?UTF-8?q?earea=20top=20ingore=20=EC=B2=98=EB=A6=AC=20(#347)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: BottleArrivalDetailView ignoreSafeArea top 추가 * feat: SentBottleDetailView ignoreSafeArea top 추가 --- .../Sources/BottleArrivalDetail/BottleArrivalDetailView.swift | 2 +- .../Sources/SentBottleDetail/SentBottleDetailView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailView.swift b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailView.swift index 1b02ddcf..eb9f202c 100644 --- a/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailView.swift +++ b/Projects/Feature/BottleArrival/Interface/Sources/BottleArrivalDetail/BottleArrivalDetailView.swift @@ -41,7 +41,7 @@ public struct BottleArrivalDetailView: View { } ) .navigationBarBackButtonHidden() - .ignoresSafeArea(.all, edges: .bottom) + .ignoresSafeArea(.all, edges: [.top, .bottom]) } } } diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift index 7ec23396..a9966c48 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift @@ -44,7 +44,7 @@ public struct SentBottleDetailView: View { } ) .navigationBarBackButtonHidden() - .ignoresSafeArea(.all, edges: .bottom) + .ignoresSafeArea(.all, edges: [.top, .bottom]) } } } 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 74/90] =?UTF-8?q?[Feature/#328]=20=EC=9E=90=EA=B8=B0=20?= =?UTF-8?q?=EC=86=8C=EA=B0=9C=20=EC=9B=B9=EB=B7=B0=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?(#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: 에러 핸들링 From 1146d853e174e037616e512fc8f19dc5381781ed 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:53:04 +0900 Subject: [PATCH 75/90] =?UTF-8?q?[Feature/#349]=20=EB=B3=B4=ED=8B=80=20?= =?UTF-8?q?=EB=B3=B4=EA=B4=80=ED=95=A8=20=EB=94=94=EC=9E=90=EC=9D=B8=20qa?= =?UTF-8?q?=20=EB=B0=98=EC=98=81=20(#350)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 질문 Text "Q. " 추가 * feat: 문답화면 top padding 추가 * feat: 대화중단 이미지 frame size 수정 * feat: 최종 선택 이후 매칭 탭 활성화 * feat: 카카오톡 아이디 Text vertical padding 수정 * feat: 매칭 결과 bottomButton 하단 고정 * feat: 매칭 실패 image padding 수정 * feat: 문답 중단 Alert 내용 수정 --- .../PingPongDetailFeature.swift | 2 +- .../View/QuestionPingPongView.swift | 2 +- .../View/SubViews/Matching/MatchingView.swift | 36 +++++++++---------- .../QuestionAndAnswerFeature.swift | 3 +- .../QuestionAndAnswerView.swift | 3 +- .../Components/Card/Stop/StopCardView.swift | 4 +-- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift index 9ee14df5..d7b914e8 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/PingPongDetail/PingPongDetailFeature.swift @@ -57,7 +57,7 @@ extension PingPongDetailFeature { action: .confirmStopTalk, label: { TextState("중단하기") }) }, - message: { TextState("중단 시 모든 핑퐁 내용이 사라져요. 정말 중단하시겠어요?") } + message: { TextState("중단 시 모든 내용이 사라져요. 정말 중단하시겠어요?") } )) return .none diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/QuestionPingPongView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/QuestionPingPongView.swift index 69738fc5..2852222f 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/QuestionPingPongView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/QuestionPingPongView.swift @@ -97,7 +97,7 @@ private extension QuestionPingPongView { var questionText: some View { HStack(spacing: 0) { WantedSansStyleText( - questionContent, + "Q. " + questionContent, style: .subTitle1, color: .focusePrimary ) diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Matching/MatchingView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Matching/MatchingView.swift index 2515d20b..962c10c9 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Matching/MatchingView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/Matching/MatchingView.swift @@ -21,26 +21,26 @@ public struct MatchingView: View { public var body: some View { WithPerceptionTracking { - ScrollView { - VStack(alignment: .leading, spacing: 0.0) { - title - .padding(.vertical, 32) - - matchingInfo + ZStack(alignment: .bottom) { + ScrollView { + VStack(alignment: .leading, spacing: 0.0) { + title + .padding(.vertical, 32) - Spacer() - - bottomButton - - Spacer() - .frame(height: 30) + matchingInfo + } + .padding(.horizontal, .md) + .frame(maxWidth: .infinity) } - .padding(.horizontal, .md) - .frame(maxHeight: .infinity) .background(to: ColorToken.background(.primary)) + .scrollIndicators(.hidden) + + bottomButton + .padding(.horizontal, .md) + .padding(.bottom, 30) + .shadow(color: .white, radius: 15, y: -30) } .background(to: ColorToken.background(.primary)) - .scrollIndicators(.hidden) } } } @@ -77,7 +77,7 @@ private extension MatchingView { case .waitingOtherAnswer: GeometryReader { geometryProxy in WithPerceptionTracking { - let width = geometryProxy.size.width - 60.0 + let width = geometryProxy.size.width - 120.0 HStack(spacing: 0 ) { Spacer() BottleImageView( @@ -98,7 +98,7 @@ private extension MatchingView { case .matchFailed: GeometryReader { geometryProxy in WithPerceptionTracking { - let width = geometryProxy.size.width - 50 + let width = geometryProxy.size.width - 120.0 HStack(spacing: 0 ) { Spacer() BottleImageView( @@ -125,7 +125,7 @@ private extension MatchingView { style: .body, color: .quinary ) - .padding(.vertical, 2) + .padding(.vertical, 5) .padding(.horizontal, .xs) .background { RoundedRectangle(cornerRadius: BottleRadiusType.xs.value) diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift index 0f982c5f..ce722b95 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerFeature.swift @@ -84,7 +84,6 @@ extension QuestionAndAnswerFeature { } case let .finalSelectButtonDidTapped(willMatch: willMatch): - state.isShowLoadingIndicator = true return .run { [bottleID = state.bottleID] send in try await bottleClient.finalSelect( bottleID: bottleID, @@ -92,7 +91,7 @@ extension QuestionAndAnswerFeature { ) switch willMatch { case true: - await send(.refreshPingPongDidRequired) + await send(.delegate(.refreshPingPong)) case false: await send(.delegate(.popToRootDidRequired)) } diff --git a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift index 94cf6c35..ef3d78ab 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/PingPongDetail/View/SubViews/QuestionAndAnswer/QuestionAndAnswerView.swift @@ -114,7 +114,8 @@ public struct QuestionAndAnswerView: View { Spacer() .frame(height: 14) } - .padding(.md) + .padding(.horizontal, .md) + .padding(.top, 32) .frame(maxWidth: .infinity) .onChange(of: focusedField) { field in store.send(.focusedFieldDidChanged(field)) diff --git a/Projects/Shared/DesignSystem/Sources/Components/Card/Stop/StopCardView.swift b/Projects/Shared/DesignSystem/Sources/Components/Card/Stop/StopCardView.swift index 558957cb..8842ba97 100644 --- a/Projects/Shared/DesignSystem/Sources/Components/Card/Stop/StopCardView.swift +++ b/Projects/Shared/DesignSystem/Sources/Components/Card/Stop/StopCardView.swift @@ -63,7 +63,7 @@ private extension StopCardView { // TODO: - 아직 디자인 안나옴 나오면 수정 var image: some View { LocalImageView(.illustraition(.loudspeark)) - .frame(width: 120) - .frame(height: 120) + .frame(width: 200) + .frame(height: 200) } } From e294b2d1c23fdb391d3ddcafad1d20f3a507b48a Mon Sep 17 00:00:00 2001 From: JongHoon Date: Fri, 8 Nov 2024 18:10:04 +0900 Subject: [PATCH 76/90] =?UTF-8?q?[Feature/#339]=20=EC=95=B1=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=84=A4=EC=A0=95=20=EC=9C=A0=EB=AC=B4=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20API=20=EC=97=B0=EA=B2=B0=20#342?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MoyaPulgins/MoyaLoggerPlugin.swift | 4 +- .../User/Interface/Sources/API/UserAPI.swift | 7 ++ ...ushNotificationAllowStatusRequestDTO.swift | 27 ++++++ ...hNotificationAllowStatusRemotelyType.swift | 13 +++ .../User/Interface/Sources/UserClient.swift | 48 +++++++-- Projects/Domain/User/Sources/UserClient.swift | 97 ++++++++++++++++++- .../AlertSetting/AlertSettingFeature.swift | 2 +- .../Sources/App/AppDelegateFeature.swift | 2 +- .../Sources/SplashView/SplashFeature.swift | 18 +++- .../Interface/Sources/UtilInterface.swift | 5 - 10 files changed, 201 insertions(+), 22 deletions(-) create mode 100644 Projects/Domain/User/Interface/Sources/DTO/Request/UpdatePushNotificationAllowStatusRequestDTO.swift create mode 100644 Projects/Domain/User/Interface/Sources/NeedUpdatePushNotificationAllowStatusRemotelyType.swift delete mode 100644 Projects/Shared/Util/Interface/Sources/UtilInterface.swift diff --git a/Projects/Core/Network/Sources/MoyaPulgins/MoyaLoggerPlugin.swift b/Projects/Core/Network/Sources/MoyaPulgins/MoyaLoggerPlugin.swift index ddec0549..ae54590f 100644 --- a/Projects/Core/Network/Sources/MoyaPulgins/MoyaLoggerPlugin.swift +++ b/Projects/Core/Network/Sources/MoyaPulgins/MoyaLoggerPlugin.swift @@ -33,7 +33,7 @@ final class MoyaLoggerPlugin: PluginType { log += "header: \(headers)\n" } if let body = httpRequest.httpBody, let bodyString = String(bytes: body, encoding: String.Encoding.utf8) { - log += "bodyString: \(bodyString)" + log += "bodyString: \(bodyString)\n" } log += "---------------------------------------------" @@ -79,7 +79,7 @@ final class MoyaLoggerPlugin: PluginType { if let data = error.response?.data { log += "Data - \(data)\n" } else { - log += "Data - empty" + log += "Data - empty\n" } log += "---------------------------------------------" diff --git a/Projects/Domain/User/Interface/Sources/API/UserAPI.swift b/Projects/Domain/User/Interface/Sources/API/UserAPI.swift index 59a2206d..30999f43 100644 --- a/Projects/Domain/User/Interface/Sources/API/UserAPI.swift +++ b/Projects/Domain/User/Interface/Sources/API/UserAPI.swift @@ -15,6 +15,7 @@ public enum UserAPI { case fetchAlertState case updateAlertState(reqeustData: AlertStateRequestDTO) case updateBlockContacts(blockContactRequestDTO: BlockContactRequestDTO) + case updatePushNotificationAllowStatus(requestDTO: UpdatePushNotificationAllowStatusRequestDTO) } extension UserAPI: BaseTargetType { @@ -26,6 +27,8 @@ extension UserAPI: BaseTargetType { return "api/v1/user/alimy" case .updateBlockContacts: return "api/v1/user/block/contact-list" + case .updatePushNotificationAllowStatus: + return "api/v1/user/native-setting" } } @@ -37,6 +40,8 @@ extension UserAPI: BaseTargetType { return .post case .updateBlockContacts: return .post + case .updatePushNotificationAllowStatus: + return .post } } @@ -48,6 +53,8 @@ extension UserAPI: BaseTargetType { return .requestJSONEncodable(requestData) case let .updateBlockContacts(blockContactRequestDTO): return .requestJSONEncodable(blockContactRequestDTO) + case let .updatePushNotificationAllowStatus(requestDTO): + return .requestJSONEncodable(requestDTO) } } } diff --git a/Projects/Domain/User/Interface/Sources/DTO/Request/UpdatePushNotificationAllowStatusRequestDTO.swift b/Projects/Domain/User/Interface/Sources/DTO/Request/UpdatePushNotificationAllowStatusRequestDTO.swift new file mode 100644 index 00000000..bc5071a5 --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/DTO/Request/UpdatePushNotificationAllowStatusRequestDTO.swift @@ -0,0 +1,27 @@ +// +// UpdatePushNotificationAllowStatusRequestDTO.swift +// DomainUserInterface +// +// Created by JongHoon on 11/3/24. +// + +import Foundation + +public struct UpdatePushNotificationAllowStatusRequestDTO: Encodable { + public let alimyTurnedOn: Bool + public let deviceName: String + public let appVersion: String + public let deviceId: String + + public init( + turnOn: Bool, + deviceName: String, + appVersion: String, + deviceId: String + ) { + self.alimyTurnedOn = turnOn + self.deviceName = deviceName + self.appVersion = appVersion + self.deviceId = deviceId + } +} diff --git a/Projects/Domain/User/Interface/Sources/NeedUpdatePushNotificationAllowStatusRemotelyType.swift b/Projects/Domain/User/Interface/Sources/NeedUpdatePushNotificationAllowStatusRemotelyType.swift new file mode 100644 index 00000000..27acfeec --- /dev/null +++ b/Projects/Domain/User/Interface/Sources/NeedUpdatePushNotificationAllowStatusRemotelyType.swift @@ -0,0 +1,13 @@ +// +// NeedUpdatePushNotificationAllowStatusRemotelyType.swift +// DomainUserInterface +// +// Created by JongHoon on 11/2/24. +// + +import Foundation + +public enum NeedUpdatePushNotificationAllowStatusRemotelyType { + case notNeed + case need(isAllow: Bool) +} diff --git a/Projects/Domain/User/Interface/Sources/UserClient.swift b/Projects/Domain/User/Interface/Sources/UserClient.swift index 2bce6164..439466b1 100644 --- a/Projects/Domain/User/Interface/Sources/UserClient.swift +++ b/Projects/Domain/User/Interface/Sources/UserClient.swift @@ -14,13 +14,17 @@ public struct UserClient { private let _isAppDeleted: () -> Bool private let _isCoachMarkViewed: () -> Bool private let _fetchFcmToken: () -> String? + var _remotelyUploadedPushNotificationAllowStatus: () -> Bool? private let updateLoginState: (Bool) -> Void private let updateDeleteState: (Bool) -> Void private let updateCoachMarkState: (Bool) -> Void private let updateFcmToken: (String) -> Void - private let updatePushNotificationAllowStatus: (Bool) -> Void + private let updatePushNotificationAllowStatusLocally: (Bool) -> Void + private let updatePushNotificationAllowStatusRemotely: (Bool) async throws -> Void + private let updateRemotelyUploadedPushNotificationAllowStatus: (Bool) -> Void + private let _isNeedUpdatePushNotificationRemotely: () async -> NeedUpdatePushNotificationAllowStatusRemotelyType private let _fetchAlertState: () async throws -> [UserAlertState] - private let _fetchPushNotificationAllowStatus: () -> Bool + private let _fetchPushNotificationAllowStatusLocally: () -> Bool private let updateAlertState: (UserAlertState) async throws -> Void private let fetchContacts: () async throws -> [String] private let updateBlockContacts: ([String]) async throws -> Void @@ -35,13 +39,17 @@ public struct UserClient { isAppDeleted: @escaping () -> Bool, isCoachMarkViewed: @escaping () -> Bool, fetchFcmToken: @escaping () -> String?, + remotelyUploadedPushNotificationAllowStatus: @escaping () -> Bool?, updateLoginState: @escaping (Bool) -> Void, updateDeleteState: @escaping (Bool) -> Void, updateFcmToken: @escaping (String) -> Void, - updatePushNotificationAllowStatus: @escaping (Bool) -> Void, + updatePushNotificationAllowStatusLocally: @escaping (Bool) -> Void, + updatePushNotificationAllowStatusRemotely: @escaping (Bool) async throws -> Void, + updateRemotelyUploadedPushNotificationAllowStatus: @escaping (Bool) -> Void, + isNeedUpdatePushNotificationRemotely: @escaping () async -> NeedUpdatePushNotificationAllowStatusRemotelyType, updateCoachMarkState: @escaping (Bool) -> Void, fetchAlertState: @escaping () async throws -> [UserAlertState], - fetchPushNotificationAllowStatus: @escaping () -> Bool, + fetchPushNotificationAllowStatusLocally: @escaping () -> Bool, updateAlertState: @escaping (UserAlertState) async throws -> Void, fetchContacts: @escaping () async throws -> [String], updateBlockContacts: @escaping ([String]) async throws -> Void @@ -50,13 +58,17 @@ public struct UserClient { self._isAppDeleted = isAppDeleted self._isCoachMarkViewed = isCoachMarkViewed self._fetchFcmToken = fetchFcmToken + self._remotelyUploadedPushNotificationAllowStatus = remotelyUploadedPushNotificationAllowStatus self.updateLoginState = updateLoginState self.updateDeleteState = updateDeleteState self.updateFcmToken = updateFcmToken - self.updatePushNotificationAllowStatus = updatePushNotificationAllowStatus + self.updatePushNotificationAllowStatusLocally = updatePushNotificationAllowStatusLocally + self.updatePushNotificationAllowStatusRemotely = updatePushNotificationAllowStatusRemotely + self.updateRemotelyUploadedPushNotificationAllowStatus = updateRemotelyUploadedPushNotificationAllowStatus + self._isNeedUpdatePushNotificationRemotely = isNeedUpdatePushNotificationRemotely self.updateCoachMarkState = updateCoachMarkState self._fetchAlertState = fetchAlertState - self._fetchPushNotificationAllowStatus = fetchPushNotificationAllowStatus + self._fetchPushNotificationAllowStatusLocally = fetchPushNotificationAllowStatusLocally self.updateAlertState = updateAlertState self.fetchContacts = fetchContacts self.updateBlockContacts = updateBlockContacts @@ -78,6 +90,10 @@ public struct UserClient { _fetchFcmToken() } + public func remotelyUploadedPushNotificationAllowStatus() -> Bool? { + _remotelyUploadedPushNotificationAllowStatus() + } + public func updateLoginState(isLoggedIn: Bool) { updateLoginState(isLoggedIn) } @@ -93,17 +109,29 @@ public struct UserClient { updateFcmToken(fcmToken) } - public func updatePushNotificationAllowStatus(isAllow: Bool) { + public func updatePushNotificationAllowStatusLocally(isAllow: Bool) { pushNotificationAllowStatusSubject.send(isAllow) - updatePushNotificationAllowStatus(isAllow) + updatePushNotificationAllowStatusLocally(isAllow) + } + + public func updatePushNotificationAllowStatusRemotely(isAllow: Bool) async throws { + try await updatePushNotificationAllowStatusRemotely(isAllow) + } + + public func updateRemotelyUploadedPushNotificationAllowStatus(isAllow: Bool) { + updateRemotelyUploadedPushNotificationAllowStatus(isAllow) + } + + public func isNeedUpdatePushNotificationRemotely() async -> NeedUpdatePushNotificationAllowStatusRemotelyType { + await _isNeedUpdatePushNotificationRemotely() } public func fetchAlertState() async throws -> [UserAlertState] { try await _fetchAlertState() } - public func fetchPushNotificationAllowStatus() -> Bool { - _fetchPushNotificationAllowStatus() + public func fetchPushNotificationAllowStatusLocally() -> Bool { + _fetchPushNotificationAllowStatusLocally() } public func updateAlertState(alertState: UserAlertState) async throws { diff --git a/Projects/Domain/User/Sources/UserClient.swift b/Projects/Domain/User/Sources/UserClient.swift index b6792a34..b1baab84 100644 --- a/Projects/Domain/User/Sources/UserClient.swift +++ b/Projects/Domain/User/Sources/UserClient.swift @@ -5,13 +5,17 @@ // Created by 임현규 on 8/22/24. // +import UIKit import Foundation +import UserNotifications import Contacts import DomainUserInterface import CoreKeyChainStore import CoreNetwork +import CoreLoggerInterface +import SharedUtilInterface import ComposableArchitecture import Moya @@ -22,6 +26,7 @@ extension UserClient: DependencyKey { case deleteState case fcmToken case alertAllowState + case remotelyUploadedPushNotificationAllowStatus case coachMarkState } @@ -47,6 +52,16 @@ extension UserClient: DependencyKey { return UserDefaults.standard.string(forKey: UserDefaultsKeys.fcmToken.rawValue) }, + remotelyUploadedPushNotificationAllowStatus: { + let status = UserDefaults.standard.object(forKey: UserDefaultsKeys.remotelyUploadedPushNotificationAllowStatus.rawValue) + guard let status = status as? Bool + else { + return nil + } + + return status + }, + updateLoginState: { isLoggedIn in UserDefaults.standard.set(isLoggedIn, forKey: UserDefaultsKeys.loginState.rawValue) }, @@ -59,10 +74,88 @@ extension UserClient: DependencyKey { UserDefaults.standard.set(fcmToken, forKey: UserDefaultsKeys.fcmToken.rawValue) }, - updatePushNotificationAllowStatus: { isAllow in + updatePushNotificationAllowStatusLocally: { isAllow in UserDefaults.standard.set(isAllow, forKey: UserDefaultsKeys.alertAllowState.rawValue) }, + updatePushNotificationAllowStatusRemotely: { isAllow in + @Dependency(\.userClient) var userClient + + var deviceName: String? { + if let simulatorModelIdentifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] { + return simulatorModelIdentifier + } else { + var systemInfo = utsname() + uname(&systemInfo) + let modelIdentifier = withUnsafePointer(to: &systemInfo.machine) { + $0.withMemoryRebound(to: CChar.self, capacity: 1) { ptr in + String(validatingUTF8: ptr) + } + } + return modelIdentifier + } + } + + let requestDTO = await UpdatePushNotificationAllowStatusRequestDTO( + turnOn: isAllow, + deviceName: deviceName ?? "", + appVersion: (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "", + deviceId: UIDevice.current.identifierForVendor?.uuidString ?? "" + ) + + try await networkManager.reqeust(api: .apiType(UserAPI.updatePushNotificationAllowStatus(requestDTO: requestDTO))) + userClient.updateRemotelyUploadedPushNotificationAllowStatus(isAllow: isAllow) + }, + + updateRemotelyUploadedPushNotificationAllowStatus: { isAllow in + UserDefaults.standard.set(isAllow, forKey: UserDefaultsKeys.remotelyUploadedPushNotificationAllowStatus.rawValue) + }, + + isNeedUpdatePushNotificationRemotely: { + @Dependency(\.userClient) var userClient + + guard userClient.isLoggedIn() + else { + return .notNeed + } + + let remotelyUploadedStatus = userClient.remotelyUploadedPushNotificationAllowStatus() + let isAuthorized = await withCheckedContinuation { continuation in + UNUserNotificationCenter.current().getNotificationSettings { settings in + switch settings.authorizationStatus { + case .notDetermined: + continuation.resume(returning: false) + + case .denied: + continuation.resume(returning: false) + + case .authorized: + continuation.resume(returning: true) + + case .provisional: + continuation.resume(returning: false) + + case .ephemeral: + continuation.resume(returning: false) + + @unknown default: + continuation.resume(returning: false) + Log.assertion(message: "not handled status") + } + } + } + + let isNeedType: NeedUpdatePushNotificationAllowStatusRemotelyType = switch remotelyUploadedStatus { + case .none: + .need(isAllow: isAuthorized) + + case let .some(localAllowStatus): + (localAllowStatus == isAuthorized) ? .notNeed : .need(isAllow: isAuthorized) + } + + return isNeedType + }, + updateCoachMarkState: { isViewed in UserDefaults.standard.set(isViewed, forKey: UserDefaultsKeys.coachMarkState.rawValue) }, @@ -72,7 +165,7 @@ extension UserClient: DependencyKey { return responseData.map { $0.toDomain() } }, - fetchPushNotificationAllowStatus: { + fetchPushNotificationAllowStatusLocally: { return UserDefaults.standard.bool(forKey: UserDefaultsKeys.alertAllowState.rawValue) }, diff --git a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift index 4a719871..ca869d12 100644 --- a/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift +++ b/Projects/Feature/MyPage/Interface/Sources/AlertSetting/AlertSettingFeature.swift @@ -153,7 +153,7 @@ extension AlertSettingFeature { } func updatePushNotificationAllowStatus(state: inout State) { - let isAllow = userClient.fetchPushNotificationAllowStatus() + let isAllow = userClient.fetchPushNotificationAllowStatusLocally() state.isAllowPushNotification = isAllow } } diff --git a/Projects/Feature/Sources/App/AppDelegateFeature.swift b/Projects/Feature/Sources/App/AppDelegateFeature.swift index fb7b9659..0cbd733c 100644 --- a/Projects/Feature/Sources/App/AppDelegateFeature.swift +++ b/Projects/Feature/Sources/App/AppDelegateFeature.swift @@ -57,7 +57,7 @@ public struct AppDelegateFeature { } case let .pushNotificationAllowStatusDidChanged(isAllow): - userClient.updatePushNotificationAllowStatus(isAllow: isAllow) + userClient.updatePushNotificationAllowStatusLocally(isAllow: isAllow) return .none default: diff --git a/Projects/Feature/Sources/SplashView/SplashFeature.swift b/Projects/Feature/Sources/SplashView/SplashFeature.swift index 6f009b86..c2463401 100644 --- a/Projects/Feature/Sources/SplashView/SplashFeature.swift +++ b/Projects/Feature/Sources/SplashView/SplashFeature.swift @@ -18,6 +18,7 @@ import ComposableArchitecture @Reducer public struct SplashFeature { @Dependency(\.authClient) private var authClient + @Dependency(\.userClient) private var userClient @ObservableState public struct State: Equatable { @@ -59,7 +60,11 @@ public struct SplashFeature { switch action { case .onAppear: return .run { send in - try await authClient.checkUpdateVersion() + async let checkUpdateVersionTask: () = try await authClient.checkUpdateVersion() + async let updatePushNotificationAllowStatusTask: () = try await updatePushNotificationAllowStatusRemotely() + + let _ = try await (checkUpdateVersionTask, updatePushNotificationAllowStatusTask) + await send(.delegate(.initialCheckCompleted)) } catch: { error, send in Log.error(error) @@ -100,6 +105,17 @@ public struct SplashFeature { case .alert, .delegate, .destination, .binding: return .none } + + @Sendable func updatePushNotificationAllowStatusRemotely() async throws { + let isNeed = await userClient.isNeedUpdatePushNotificationRemotely() + switch isNeed { + case let .need(isAllow): + try await userClient.updatePushNotificationAllowStatusRemotely(isAllow: isAllow) + + case .notNeed: + return + } + } } } diff --git a/Projects/Shared/Util/Interface/Sources/UtilInterface.swift b/Projects/Shared/Util/Interface/Sources/UtilInterface.swift deleted file mode 100644 index b5477d2b..00000000 --- a/Projects/Shared/Util/Interface/Sources/UtilInterface.swift +++ /dev/null @@ -1,5 +0,0 @@ -// This is for Tuist - -public protocol UtilInterface { - -} From 559f6c88d7e67760343d698d12b46e86cb4c34d5 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Fri, 8 Nov 2024 18:10:37 +0900 Subject: [PATCH 77/90] =?UTF-8?q?[Fix/#345]=20pingPongBottleList=20empty?= =?UTF-8?q?=20=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=A2=8C=EC=9A=B0=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=20=EC=88=98=EC=A0=95=20#351?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BottleStorage/BottleStorageView.swift | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift index 3fcbacdc..365b544c 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageView.swift @@ -31,6 +31,7 @@ public struct BottleStorageView: View { } .padding(.top, 72) .frame(maxHeight: .infinity, alignment: .top) + .frame(maxWidth: .infinity) .background(to: ColorToken.background(.primary)) .padding(.bottom, BottleConstants.bottomTabBarHeight.value) .setTabBar(selectedTab: .bottleStorage) { selectedTab in @@ -73,7 +74,6 @@ private extension BottleStorageView { var bottlsList: some View { if store.pingPongBottleList.isEmpty && !store.isLoading { VStack(alignment: .center, spacing: 0.0) { - Spacer() BottleImageView(type: .local(bottleImageSystem: .illustraition(.basket))) .frame(height: 180) @@ -81,26 +81,25 @@ private extension BottleStorageView { .aspectRatio(1.0, contentMode: .fit) .padding(.bottom, .xl) - WantedSansStyleText( - "아직 대화를 시작하지 않으셨군요!", - style: .subTitle1, - color: .primary - ) - .padding(.bottom, .xs) + WantedSansStyleText( + "아직 대화를 시작하지 않으셨군요!", + style: .subTitle1, + color: .primary + ) + .padding(.bottom, .xs) - WantedSansStyleText( - "마음에 드는 상대를 찾아\n가치관 문답을 시작해 볼까요?", - style: .body, - color: .tertiary - ) - .lineSpacing(5) - .multilineTextAlignment(.center) - .padding(.bottom, .xl) + WantedSansStyleText( + "마음에 드는 상대를 찾아\n가치관 문답을 시작해 볼까요?", + style: .body, + color: .tertiary + ) + .lineSpacing(5) + .multilineTextAlignment(.center) + .padding(.bottom, .xl) SolidButton(title: "모래사장 바로가기", sizeType: .extraSmall, buttonType: .throttle, action: { store.send(.sandBeachButtonDidTapped) }) - Spacer() - } + } } else { ScrollView { VStack(spacing: .md) { From c122beaadea9b7ea8b1da7a5eb3cd40cfd148001 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Fri, 8 Nov 2024 18:11:00 +0900 Subject: [PATCH 78/90] =?UTF-8?q?[Feature/#341]=20=ED=86=B5=EC=8B=A0=20API?= =?UTF-8?q?=20=ED=97=A4=EB=8D=94=EC=97=90=20=EB=B6=80=EA=B0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=93=A4=20=EC=B6=94=EA=B0=80=20#352?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interceptor/TokenInterceptor.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Projects/Core/Network/Sources/Interceptor/TokenInterceptor.swift b/Projects/Core/Network/Sources/Interceptor/TokenInterceptor.swift index ed6f5c1a..9ea6784e 100644 --- a/Projects/Core/Network/Sources/Interceptor/TokenInterceptor.swift +++ b/Projects/Core/Network/Sources/Interceptor/TokenInterceptor.swift @@ -6,6 +6,7 @@ // import Foundation +import UIKit import CoreKeyChainStore import CoreLoggerInterface @@ -30,7 +31,32 @@ public class TokenInterceptor: RequestInterceptor { urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") } + var deviceName: String { + if let simulatorModelIdentifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] { + return simulatorModelIdentifier + } else { + var systemInfo = utsname() + uname(&systemInfo) + let modelIdentifier = withUnsafePointer(to: &systemInfo.machine) { + $0.withMemoryRebound(to: CChar.self, capacity: 1) { ptr in + String(validatingUTF8: ptr) + } + } + return modelIdentifier ?? "" + } + } + let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "" + let deviceID = UIDevice.current.identifierForVendor?.uuidString ?? "" + var osVersion = UIDevice.current.systemVersion + + urlRequest.setValue(appVersion, forHTTPHeaderField: "X-App-Version") + urlRequest.setValue(deviceName, forHTTPHeaderField: "X-Device-Model") + urlRequest.setValue(osVersion, forHTTPHeaderField: "X-OS-Version") + urlRequest.setValue(deviceID, forHTTPHeaderField: "X-Device-ID") + urlRequest.setValue("iOS", forHTTPHeaderField: "X-App-Platform") + print(urlRequest.headers) + completion(.success(urlRequest)) } From 5f3d552363182f9afa407b4ec01910d3f6182aa0 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Wed, 6 Nov 2024 00:06:58 +0900 Subject: [PATCH 79/90] =?UTF-8?q?chore:=20=EC=97=B0=EB=9D=BD=EC=B2=98=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=20=EA=B6=8C=ED=95=9C=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InfoPlist+Templates.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index a514757b..d43ef6bd 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -10,8 +10,8 @@ import ProjectDescription public extension InfoPlist { static var app: InfoPlist { return .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0.9", - "CFBundleVersion": "31", + "CFBundleShortVersionString": "1.0.10", + "CFBundleVersion": "34", "UIUserInterfaceStyle": "Light", "CFBundleName": "보틀", "UILaunchScreen": [ @@ -22,7 +22,7 @@ public extension InfoPlist { "UISupportedInterfaceOrientations": [ "UIInterfaceOrientationPortrait" ], - "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다.", + "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다. 허용하시면 연락처가 서버에 업로드됩니다.", "BASE_URL": "$(BASE_URL)", "WEB_VIEW_BASE_URL": "$(WEB_VIEW_BASE_URL)", "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", @@ -44,14 +44,14 @@ public extension InfoPlist { static var example: InfoPlist { return .extendingDefault(with: [ - "CFBundleShortVersionString": "1.0.9", - "CFBundleVersion": "31", + "CFBundleShortVersionString": "1.0.10", + "CFBundleVersion": "34", "UIUserInterfaceStyle": "Light", "UILaunchScreen": [:], "UISupportedInterfaceOrientations": [ "UIInterfaceOrientationPortrait" ], - "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다.", + "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다. 허용하시면 연락처가 서버에 업로드됩니다.", "BASE_URL": "$(BASE_URL)", "WEB_VIEW_BASE_URL": "$(WEB_VIEW_BASE_URL)", "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", From 032b7d91ffaac9379a21645dd9b3112e7a02e162 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Wed, 6 Nov 2024 13:40:37 +0900 Subject: [PATCH 80/90] =?UTF-8?q?feat:=20=EB=AC=B8=EB=8B=B5=20=EC=9D=B4?= =?UTF-8?q?=ED=9B=84=20=EC=83=81=ED=83=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/BottleStorage/BottleStorageFeature.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift index 1a5373a3..570f8126 100644 --- a/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift +++ b/Projects/Feature/BottleStorage/Interface/Sources/BottleStorage/BottleStorageFeature.swift @@ -42,8 +42,7 @@ extension BottleStorageFeature { switch delegate { case .backButtonDidTapped: - state.path.removeLast() - return .none + return popToRootAndReload(state: &state) case .reportButtonDidTapped(let userReportProfile): state.path.append(.report(ReportUserFeature.State(userProfile: userReportProfile))) return .none From 7c03dcaa9445700203c59cf83cb5d3f0f0415ad4 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Sat, 9 Nov 2024 23:44:29 +0900 Subject: [PATCH 81/90] =?UTF-8?q?feat:=20=EB=AA=A8=EB=9E=98=EC=82=AC?= =?UTF-8?q?=EC=9E=A5=20=ED=99=94=EB=A9=B4=20=EC=9E=90=EA=B8=B0=EC=86=8C?= =?UTF-8?q?=EA=B0=9C=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20=EB=B3=B4=ED=8B=80?= =?UTF-8?q?=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9B=90=EB=B3=B5=20(#354)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SandBeach/SandBeachFeatureInterface.swift | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift index f7f4e9a4..e9e8f338 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift @@ -97,39 +97,51 @@ extension SandBeachFeature { Log.error(error) } }) - + return .run { send in - async let _ = authClient.checkUpdateVersion() - let userProfileStatus = try await profileClient.fetchUserProfileSelect() - let userBottleInfo = try await bottleClient.fetchUserBottleInfo() + async let versionCheckTask: Void = authClient.checkUpdateVersion() + async let userProfileStatusTask = profileClient.fetchUserProfileSelect() + async let userBottleInfoTask = bottleClient.fetchUserBottleInfo() + async let bottlesStorageListTask = bottleClient.fetchBottleStorageList() + + let (_, userProfileStatus, userBottleInfo, bottlesStorageList) = try await ( + versionCheckTask, + userProfileStatusTask, + userBottleInfoTask, + bottlesStorageListTask + ) + 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 { + + if userProfileStatus == .empty || userProfileStatus == .doneIntroduction { await send(.userStateFetchCompleted( - userState: .hasNewBottle(bottleCount: newBottlesCount), - isDisableButton: false)) + userState: .noIntroduction, + isDisableButton: true)) return } - if activeBottlesCount > 0 { + if userProfileStatus == .doneProfileImage { await send(.userStateFetchCompleted( - userState: .hasActiveBottle(bottleCount: activeBottlesCount), + userState: .noBottle(time: nextBottleLeftHours ?? 0), isDisableButton: false)) return } - if userProfileStatus == .empty || userProfileStatus == .doneIntroduction { + if newBottlesCount > 0 { await send(.userStateFetchCompleted( - userState: .noIntroduction, - isDisableButton: true)) - } else if userProfileStatus == .doneProfileImage { + userState: .hasNewBottle(bottleCount: newBottlesCount), + isDisableButton: false)) + return + } + + if activeBottlesCount > 0 { await send(.userStateFetchCompleted( - userState: .noBottle(time: nextBottleLeftHours ?? 0), + userState: .hasActiveBottle(bottleCount: activeBottlesCount), isDisableButton: false)) + return } } catch: { error, send in // TODO: 에러 핸들링 From 0f02a2df83b61dd498762e5e20d547f8680f49fa Mon Sep 17 00:00:00 2001 From: JongHoon Date: Sat, 9 Nov 2024 23:51:15 +0900 Subject: [PATCH 82/90] =?UTF-8?q?[Feature/#355]=20=EC=9B=B9=EB=B7=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=B9=B4=EB=A9=94=EB=9D=BC=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=20=EC=8B=9C=20=ED=95=B8=EB=93=A4=EB=A7=81=20#356?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index d43ef6bd..3ad16871 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -23,6 +23,7 @@ public extension InfoPlist { "UIInterfaceOrientationPortrait" ], "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다. 허용하시면 연락처가 서버에 업로드됩니다.", + "NSCameraUsageDescription": "카메라는 자기소개 사진을 찍기 위해 사용됩니다.", "BASE_URL": "$(BASE_URL)", "WEB_VIEW_BASE_URL": "$(WEB_VIEW_BASE_URL)", "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", @@ -52,6 +53,7 @@ public extension InfoPlist { "UIInterfaceOrientationPortrait" ], "NSContactsUsageDescription": "매칭 차단 기능을 위해 연락처가 필요합니다. 허용하시면 연락처가 서버에 업로드됩니다.", + "NSCameraUsageDescription": "카메라는 자기소개 사진을 찍기 위해 사용됩니다.", "BASE_URL": "$(BASE_URL)", "WEB_VIEW_BASE_URL": "$(WEB_VIEW_BASE_URL)", "WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME": "$(WEB_VIEW_MESSAGE_HANDLER_DEFAULT_NAME)", From fb8a51a01df365a128f3b624d3c10db2bce246ec Mon Sep 17 00:00:00 2001 From: JongHoon Date: Sat, 9 Nov 2024 23:51:34 +0900 Subject: [PATCH 83/90] =?UTF-8?q?[Feature/#357]=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4=ED=84=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=9C=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20#358?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Bottle_loading_ellipse.json | 1 + .../Contents.json | 2 +- .../ProgressIndicator.dataset/ProgressIndicator.lottie | 1 - .../Sources/ProgressIndicator/LoadingIndicator.swift | 7 +++---- 4 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 Projects/Shared/DesignSystem/Resources/Lotties.xcassets/Bottle_loading_ellipse.dataset/Bottle_loading_ellipse.json rename Projects/Shared/DesignSystem/Resources/Lotties.xcassets/{ProgressIndicator.dataset => Bottle_loading_ellipse.dataset}/Contents.json (70%) delete mode 100644 Projects/Shared/DesignSystem/Resources/Lotties.xcassets/ProgressIndicator.dataset/ProgressIndicator.lottie diff --git a/Projects/Shared/DesignSystem/Resources/Lotties.xcassets/Bottle_loading_ellipse.dataset/Bottle_loading_ellipse.json b/Projects/Shared/DesignSystem/Resources/Lotties.xcassets/Bottle_loading_ellipse.dataset/Bottle_loading_ellipse.json new file mode 100644 index 00000000..c2b27b49 --- /dev/null +++ b/Projects/Shared/DesignSystem/Resources/Lotties.xcassets/Bottle_loading_ellipse.dataset/Bottle_loading_ellipse.json @@ -0,0 +1 @@ +{"assets":[],"ddd":0,"fr":60,"h":100,"ip":0,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"progress","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[30,30]},"o":{"a":0,"k":100},"p":{"a":0,"k":[50,50]},"r":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":0.88,"y":0.77},"o":{"x":0.5,"y":0}},{"t":7.2,"s":[90],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":14.4,"s":[180],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":21.6,"s":[270],"i":{"x":0.75,"y":0.75},"o":{"x":0.25,"y":0.25}},{"t":28.8,"s":[360],"i":{"x":0,"y":0},"o":{"x":1,"y":1}}]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":30,"st":0,"bm":0,"shapes":[{"ty":"el","hd":false,"nm":"progress","p":{"a":0,"k":[30,30]},"s":{"a":0,"k":[60,60]},"d":1},{"ty":"gs","hd":false,"bm":0,"o":{"a":0,"k":100},"e":{"a":0,"k":[66.00000236034398,25.500000196695332]},"g":{"p":2,"k":{"a":0,"k":[0,0.306,0.396,0.945,1,0.306,0.396,0.945,0,0,1,1]}},"t":1,"a":{"a":0,"k":0},"h":{"a":0,"k":0},"s":{"a":0,"k":[-2.93843085448443e-14,25.500000196695332]},"lc":2,"lj":3,"ml":28.96,"w":{"a":0,"k":10}},{"ty":"tm","hd":false,"bm":0,"e":{"a":0,"k":20},"o":{"a":0,"k":0},"s":{"a":0,"k":0},"m":1}]},{"ddd":0,"ind":2,"ty":4,"nm":"oval bg","hd":true,"sr":1,"ks":{"a":{"a":0,"k":[40,40]},"o":{"a":0,"k":100},"p":{"a":0,"k":[180,320]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":30,"st":0,"bm":0,"shapes":[{"ty":"el","hd":true,"nm":"oval bg","p":{"a":0,"k":[40,40]},"s":{"a":0,"k":[80,80]},"d":1},{"ty":"st","hd":false,"bm":0,"c":{"a":0,"k":[0.961,0.961,0.961]},"lc":2,"lj":1,"ml":28.96,"o":{"a":0,"k":100},"w":{"a":0,"k":20}}]},{"ddd":0,"ind":3,"ty":4,"nm":"Screen","hd":false,"sr":1,"ks":{"a":{"a":0,"k":[50,50]},"o":{"a":0,"k":100},"p":{"a":0,"k":[50,50]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}},"ao":0,"ip":0,"op":30,"st":0,"bm":0,"shapes":[{"ty":"gr","hd":false,"nm":"Screen Group","bm":0,"it":[{"ty":"rc","hd":false,"nm":"Screen","d":1,"p":{"a":0,"k":[50,50]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]}},{"ty":"fl","hd":false,"bm":0,"c":{"a":0,"k":[1,1,1]},"r":1,"o":{"a":0,"k":0}},{"ty":"tr","nm":"Transform","a":{"a":0,"k":[0,0]},"o":{"a":0,"k":100},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"np":0}]}],"meta":{"g":"@phase-software/lottie-exporter 0.7.0"},"nm":"","op":29,"v":"5.6.0","w":100} \ No newline at end of file diff --git a/Projects/Shared/DesignSystem/Resources/Lotties.xcassets/ProgressIndicator.dataset/Contents.json b/Projects/Shared/DesignSystem/Resources/Lotties.xcassets/Bottle_loading_ellipse.dataset/Contents.json similarity index 70% rename from Projects/Shared/DesignSystem/Resources/Lotties.xcassets/ProgressIndicator.dataset/Contents.json rename to Projects/Shared/DesignSystem/Resources/Lotties.xcassets/Bottle_loading_ellipse.dataset/Contents.json index 74fb6963..f2d2dcdf 100644 --- a/Projects/Shared/DesignSystem/Resources/Lotties.xcassets/ProgressIndicator.dataset/Contents.json +++ b/Projects/Shared/DesignSystem/Resources/Lotties.xcassets/Bottle_loading_ellipse.dataset/Contents.json @@ -1,7 +1,7 @@ { "data" : [ { - "filename" : "ProgressIndicator.lottie", + "filename" : "Bottle_loading_ellipse.json", "idiom" : "universal" } ], diff --git a/Projects/Shared/DesignSystem/Resources/Lotties.xcassets/ProgressIndicator.dataset/ProgressIndicator.lottie b/Projects/Shared/DesignSystem/Resources/Lotties.xcassets/ProgressIndicator.dataset/ProgressIndicator.lottie deleted file mode 100644 index 5632421c..00000000 --- a/Projects/Shared/DesignSystem/Resources/Lotties.xcassets/ProgressIndicator.dataset/ProgressIndicator.lottie +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.7.11","fr":60,"ip":0,"op":81,"w":1920,"h":1080,"nm":"Loading Dots","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Dot4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":39,"s":[100]},{"t":55,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[1142,540,0],"to":null,"ti":null},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":39,"s":[1142,500,0],"to":null,"ti":null},{"t":55,"s":[1142,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":25,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":39,"s":[75,75,100]},{"t":55,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false,"_render":true},{"ty":"fl","c":{"a":0,"k":[0.7608,0.7608,0.7608,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":360,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":2,"ty":4,"nm":"Dot3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":17,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":31,"s":[100]},{"t":47,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":17,"s":[1022,540,0],"to":null,"ti":null},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":31,"s":[1022,500,0],"to":null,"ti":null},{"t":47,"s":[1022,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":17,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":31,"s":[75,75,100]},{"t":47,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false,"_render":true},{"ty":"fl","c":{"a":0,"k":[0.7608,0.7608,0.7608,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":360,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":3,"ty":4,"nm":"Dot2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[100]},{"t":39,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[902,540,0],"to":null,"ti":null},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":23,"s":[902,500,0],"to":null,"ti":null},{"t":39,"s":[902,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":9,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":23,"s":[75,75,100]},{"t":39,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false,"_render":true},{"ty":"fl","c":{"a":0,"k":[0.7608,0.7608,0.7608,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":360,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":4,"ty":4,"nm":"Dot1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14,"s":[100]},{"t":30,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[782,540,0],"to":null,"ti":null},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":14,"s":[782,500,0],"to":null,"ti":null},{"t":30,"s":[782,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":14,"s":[75,75,100]},{"t":30,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false,"_render":true},{"ty":"fl","c":{"a":0,"k":[0.7608,0.7608,0.7608,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":360,"st":0,"bm":0,"completed":true}],"markers":[],"__complete":true} \ No newline at end of file diff --git a/Projects/Shared/DesignSystem/Sources/ProgressIndicator/LoadingIndicator.swift b/Projects/Shared/DesignSystem/Sources/ProgressIndicator/LoadingIndicator.swift index ff001aba..2e464026 100644 --- a/Projects/Shared/DesignSystem/Sources/ProgressIndicator/LoadingIndicator.swift +++ b/Projects/Shared/DesignSystem/Sources/ProgressIndicator/LoadingIndicator.swift @@ -15,12 +15,11 @@ public struct LoadingIndicator: View { public var body: some View { ZStack { - Color(.black) - .opacity(0.5) + ColorToken.background(.primary).color - LottieView(animation: try? .from(data: SharedDesignSystemAsset.Lotties.progressIndicator.data.data)) + LottieView(animation: try? .from(data: SharedDesignSystemAsset.Lotties.bottleLoadingEllipse.data.data)) .looping() - .frame(width: 150.0, height: 84.0) + .frame(width: 100.0, height: 100.0) } .ignoresSafeArea() } From f02f768c87614ddef96493c543442ec88d7f458e Mon Sep 17 00:00:00 2001 From: JongHoon Date: Sat, 9 Nov 2024 23:51:50 +0900 Subject: [PATCH 84/90] =?UTF-8?q?[Feature/#318]=20=EB=8F=84=EC=B0=A9?= =?UTF-8?q?=ED=95=9C=20=EB=B3=B4=ED=8B=80=20=EB=A6=AC=EC=8A=A4=ED=8A=B8,?= =?UTF-8?q?=20=EB=8F=84=EC=B0=A9=ED=95=9C=20=EB=B3=B4=ED=8B=80=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EB=A1=9C=EB=94=A9=20=EC=9D=B8?= =?UTF-8?q?=EB=94=94=EC=BC=80=EC=9D=B4=ED=84=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/GoodFeeling/GoodFeelingFeature.swift | 8 +++++++- .../GoodFeeling/GoodFeelingFeatureInterface.swift | 7 +++++++ .../Interface/Sources/GoodFeeling/GoodFeelingView.swift | 9 ++++++++- .../SentBottleDetail/SentBottleDetailFeature.swift | 5 +++++ .../SentBottleDetailFeatureInterface.swift | 7 +++++++ .../Sources/SentBottleDetail/SentBottleDetailView.swift | 9 ++++++++- 6 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift index 62626ac2..937ad4c6 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeature.swift @@ -19,13 +19,19 @@ public struct GoodFeelingFeature { @ObservableState public struct State: Equatable { + var isLoading: Bool + public init() { - + self.isLoading = true } } public enum Action: BindableAction { case sentBottleTapped(url: String) + case webViewLoadingDidCompleted + + case configureIsLoading(_: Bool) + case delegate(Delegate) public enum Delegate { diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift index 79e8405b..d9c3acfe 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingFeatureInterface.swift @@ -13,9 +13,16 @@ extension GoodFeelingFeature { public init() { let reducer = Reduce { state, action in switch action { + case .webViewLoadingDidCompleted: + return .send(.configureIsLoading(false)) + case let .sentBottleTapped(url): return .send(.delegate(.sentBottleTapped(url: url))) + case let .configureIsLoading(isLoading): + state.isLoading = isLoading + return .none + default: return .none } diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift index a2647c83..401f0c86 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/GoodFeeling/GoodFeelingView.swift @@ -11,6 +11,8 @@ import FeatureBaseWebViewInterface import CoreLoggerInterface +import SharedDesignSystem + import ComposableArchitecture public struct GoodFeelingView: View { @@ -27,7 +29,7 @@ public struct GoodFeelingView: View { actionDidInputted: { action in switch action { case .webViewLoadingDidCompleted: - break + store.send(.webViewLoadingDidCompleted) case let .openLink(url): store.send(.sentBottleTapped(url: url)) @@ -37,6 +39,11 @@ public struct GoodFeelingView: View { } } ) + .overlay { + if store.isLoading { + LoadingIndicator() + } + } } } } diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeature.swift b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeature.swift index 5e2be585..8893aa67 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeature.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeature.swift @@ -19,16 +19,21 @@ public struct SentBottleDetailFeature { @ObservableState public struct State: Equatable { + var isLoading: Bool let sentBottleDetailURL: String public init(sentBottleDetailURL: String) { + self.isLoading = true self.sentBottleDetailURL = sentBottleDetailURL } } public enum Action: BindableAction { + case webViewLoadingDidCompleted case backButtonDidTapped case bottelDidAccepted + + case configureIsLoading(_: Bool) case showToast(message: String) case delegate(Delegate) diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeatureInterface.swift b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeatureInterface.swift index 3087fdd0..a34362d7 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeatureInterface.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailFeatureInterface.swift @@ -17,6 +17,9 @@ extension SentBottleDetailFeature { let reducer = Reduce { state, action in switch action { + case .webViewLoadingDidCompleted: + return .send(.configureIsLoading(false)) + case .backButtonDidTapped: return .send(.delegate(.backButtonDidTapped)) @@ -24,6 +27,10 @@ extension SentBottleDetailFeature { toastClient.presentToast(message: message) return .none + case let .configureIsLoading(isLoading): + state.isLoading = isLoading + return .none + case .bottelDidAccepted: return .send(.delegate(.bottelDidAccepted)) diff --git a/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift index a9966c48..49e059fd 100644 --- a/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift +++ b/Projects/Feature/GoodFeeling/Interface/Sources/SentBottleDetail/SentBottleDetailView.swift @@ -11,6 +11,8 @@ import FeatureBaseWebViewInterface import CoreLoggerInterface +import SharedDesignSystem + import ComposableArchitecture public struct SentBottleDetailView: View { @@ -27,7 +29,7 @@ public struct SentBottleDetailView: View { actionDidInputted: { action in switch action { case .webViewLoadingDidCompleted: - break + store.send(.webViewLoadingDidCompleted) case .closeWebView: store.send(.backButtonDidTapped) @@ -45,6 +47,11 @@ public struct SentBottleDetailView: View { ) .navigationBarBackButtonHidden() .ignoresSafeArea(.all, edges: [.top, .bottom]) + .overlay { + if store.isLoading { + LoadingIndicator() + } + } } } } From 640a23c81acf12775c9aa62c2ada1d93aef617f8 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Sun, 10 Nov 2024 00:03:01 +0900 Subject: [PATCH 85/90] =?UTF-8?q?chore:=20=EB=B9=8C=EB=93=9C=20=EB=84=98?= =?UTF-8?q?=EB=B2=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8(36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index 3ad16871..918f18b1 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -11,7 +11,7 @@ public extension InfoPlist { static var app: InfoPlist { return .extendingDefault(with: [ "CFBundleShortVersionString": "1.0.10", - "CFBundleVersion": "34", + "CFBundleVersion": "36", "UIUserInterfaceStyle": "Light", "CFBundleName": "보틀", "UILaunchScreen": [ @@ -46,7 +46,7 @@ public extension InfoPlist { static var example: InfoPlist { return .extendingDefault(with: [ "CFBundleShortVersionString": "1.0.10", - "CFBundleVersion": "34", + "CFBundleVersion": "36", "UIUserInterfaceStyle": "Light", "UILaunchScreen": [:], "UISupportedInterfaceOrientations": [ From 24b77715b3ad53efa2f5dd63239a43882615ff37 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Sun, 10 Nov 2024 00:43:48 +0900 Subject: [PATCH 86/90] =?UTF-8?q?chore:=20profile=20setup=20interface?= =?UTF-8?q?=EC=97=90=20base=20webview=20interface=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Feature/ProfileSetup/Project.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Projects/Feature/ProfileSetup/Project.swift b/Projects/Feature/ProfileSetup/Project.swift index 6a0b52d9..c358fd31 100644 --- a/Projects/Feature/ProfileSetup/Project.swift +++ b/Projects/Feature/ProfileSetup/Project.swift @@ -9,7 +9,8 @@ let project = Project.makeModule( interface: .ProfileSetup, factory: .init( dependencies: [ - .domain + .domain, + .feature(interface: .BaseWebView) ] ) ), From 49dc891b43f3a0b3d465198825eff2974eb12549 Mon Sep 17 00:00:00 2001 From: JongHoon Date: Sun, 10 Nov 2024 00:44:26 +0900 Subject: [PATCH 87/90] =?UTF-8?q?fix:=20=EB=AA=A8=EB=9E=98=EC=82=AC?= =?UTF-8?q?=EC=9E=A5=20=EB=B0=94=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EB=B3=B4=ED=8B=80?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EB=84=98=EC=96=B4?= =?UTF-8?q?=EA=B0=80=EC=A7=80=20=EC=95=8A=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Feature/Sources/TabView/MainTabViewFeature.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Feature/Sources/TabView/MainTabViewFeature.swift b/Projects/Feature/Sources/TabView/MainTabViewFeature.swift index df30a2a9..91805137 100644 --- a/Projects/Feature/Sources/TabView/MainTabViewFeature.swift +++ b/Projects/Feature/Sources/TabView/MainTabViewFeature.swift @@ -111,7 +111,7 @@ public struct MainTabViewFeature { return .none case .sandBeachButtonDidTapped: state.selectedTab = .sandBeach - return .send(.sandBeachRoot(.sandBeach(.newBottleIslandDidTapped))) + return .none } // MyPage Delegate From a8cf2725bedaa2322e524e7aeccf157e2d231b33 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sun, 10 Nov 2024 01:28:37 +0900 Subject: [PATCH 88/90] =?UTF-8?q?fix:=20feat:=20=EB=AA=A8=EB=9E=98?= =?UTF-8?q?=EC=82=AC=EC=9E=A5=20=ED=99=94=EB=A9=B4=20=EC=9E=90=EA=B8=B0?= =?UTF-8?q?=EC=86=8C=EA=B0=9C=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20=EB=B3=B4?= =?UTF-8?q?=ED=8B=80=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EA=B8=B0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SandBeach/SandBeachFeatureInterface.swift | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift index e9e8f338..76894279 100644 --- a/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift +++ b/Projects/Feature/SandBeach/Interface/Sources/SandBeach/SandBeachFeatureInterface.swift @@ -123,26 +123,23 @@ extension SandBeachFeature { return } - if userProfileStatus == .doneProfileImage { - await send(.userStateFetchCompleted( - userState: .noBottle(time: nextBottleLeftHours ?? 0), - isDisableButton: false)) - return - } - - if newBottlesCount > 0 { + if userProfileStatus == .doneProfileImage && newBottlesCount > 0 { await send(.userStateFetchCompleted( userState: .hasNewBottle(bottleCount: newBottlesCount), isDisableButton: false)) return } - if activeBottlesCount > 0 { + if userProfileStatus == .doneProfileImage && activeBottlesCount > 0 { await send(.userStateFetchCompleted( userState: .hasActiveBottle(bottleCount: activeBottlesCount), isDisableButton: false)) return } + + await send(.userStateFetchCompleted( + userState: .noBottle(time: nextBottleLeftHours ?? 0), + isDisableButton: false)) } catch: { error, send in // TODO: 에러 핸들링 Log.error(error) From 9efce44e86ec01312067aab96845addbd8b92878 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sun, 10 Nov 2024 01:29:55 +0900 Subject: [PATCH 89/90] =?UTF-8?q?chore:=20=EB=B9=8C=EB=93=9C=20=EB=84=98?= =?UTF-8?q?=EB=B2=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8(37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift index 918f18b1..3258a57d 100644 --- a/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Templates.swift @@ -11,7 +11,7 @@ public extension InfoPlist { static var app: InfoPlist { return .extendingDefault(with: [ "CFBundleShortVersionString": "1.0.10", - "CFBundleVersion": "36", + "CFBundleVersion": "37", "UIUserInterfaceStyle": "Light", "CFBundleName": "보틀", "UILaunchScreen": [ @@ -46,7 +46,7 @@ public extension InfoPlist { static var example: InfoPlist { return .extendingDefault(with: [ "CFBundleShortVersionString": "1.0.10", - "CFBundleVersion": "36", + "CFBundleVersion": "37", "UIUserInterfaceStyle": "Light", "UILaunchScreen": [:], "UISupportedInterfaceOrientations": [ From a351a9879c59dd012e8e44e13843e9bd30dd5727 Mon Sep 17 00:00:00 2001 From: leemhyungyu Date: Sun, 10 Nov 2024 01:35:56 +0900 Subject: [PATCH 90/90] =?UTF-8?q?fix:=20=EB=AA=A8=EB=9E=98=EC=82=AC?= =?UTF-8?q?=EC=9E=A5=20=EB=B0=94=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EB=B3=B4=ED=8B=80?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EB=84=98=EC=96=B4?= =?UTF-8?q?=EA=B0=80=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Feature/Sources/TabView/MainTabViewFeature.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Projects/Feature/Sources/TabView/MainTabViewFeature.swift b/Projects/Feature/Sources/TabView/MainTabViewFeature.swift index 91805137..df30a2a9 100644 --- a/Projects/Feature/Sources/TabView/MainTabViewFeature.swift +++ b/Projects/Feature/Sources/TabView/MainTabViewFeature.swift @@ -111,7 +111,7 @@ public struct MainTabViewFeature { return .none case .sandBeachButtonDidTapped: state.selectedTab = .sandBeach - return .none + return .send(.sandBeachRoot(.sandBeach(.newBottleIslandDidTapped))) } // MyPage Delegate