From aeb110634719a10fecd9d2da78d50bfd5f14b8b8 Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 16 Mar 2026 19:45:19 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20certification-0008=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=EC=83=B7=20=EC=88=98=EC=A0=95=20-=20=EC=83=81?= =?UTF-8?q?=EB=8C=80=20=EB=AF=B8=EC=99=84=EB=A3=8C=20&=20=EB=82=98=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=EB=90=9C=EA=B1=B0=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=9B=84=20=EC=88=98=EC=A0=95=EB=90=9C=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=83=81=EB=8C=80=EA=BA=BC=EC=97=90=20=EB=B3=B4?= =?UTF-8?q?=EC=9E=84=20-=20#210?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/GoalDetailReducer.swift | 6 +++++- .../GoalDetail/Sources/Detail/GoalDetailView.swift | 2 +- .../Sources/Detail/SwipeableCardView.swift | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Projects/Feature/GoalDetail/Interface/Sources/GoalDetailReducer.swift b/Projects/Feature/GoalDetail/Interface/Sources/GoalDetailReducer.swift index 87a2eb1a..e486ddb8 100644 --- a/Projects/Feature/GoalDetail/Interface/Sources/GoalDetailReducer.swift +++ b/Projects/Feature/GoalDetail/Interface/Sources/GoalDetailReducer.swift @@ -52,6 +52,10 @@ public struct GoalDetailReducer { } } + public var currentEditedImageData: Data? { + currentUser == .mySelf ? pendingEditedImageData : nil + } + public var canSwipeLeft: Bool { switch entryPoint { case .home: @@ -89,7 +93,7 @@ public struct GoalDetailReducer { } public var isCompleted: Bool { - pendingEditedImageData != nil || currentCard?.imageUrl != nil + currentEditedImageData != nil || currentCard?.imageUrl != nil } public var comment: String { currentCard?.comment ?? "" } public var naviBarRightText: String { diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index cf2a6bcb..b8588406 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -177,7 +177,7 @@ private extension GoalDetailView { @ViewBuilder var completedImageCard: some View { - if let editImageData = store.pendingEditedImageData, + if let editImageData = store.currentEditedImageData, let editedImage = UIImage(data: editImageData) { completedImageCardContainer { Image(uiImage: editedImage) diff --git a/Projects/Feature/GoalDetail/Sources/Detail/SwipeableCardView.swift b/Projects/Feature/GoalDetail/Sources/Detail/SwipeableCardView.swift index 9d551563..0ca51fbd 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/SwipeableCardView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/SwipeableCardView.swift @@ -57,6 +57,10 @@ struct SwipeableCardView: View { // MARK: - Private Methods private extension SwipeableCardView { + var isSwipeEnabled: Bool { + canSwipeLeft || canSwipeRight + } + var swipeRotation: Double { max(-8, min(8, Double(cardOffset.width / 28))) } @@ -64,6 +68,11 @@ private extension SwipeableCardView { var cardSwipeGesture: some Gesture { DragGesture(minimumDistance: 16) .onChanged { value in + guard isSwipeEnabled else { + resetCardOffset() + return + } + let translation = value.translation guard abs(translation.width) >= abs(translation.height) else { @@ -74,6 +83,11 @@ private extension SwipeableCardView { cardOffset = CGSize(width: translation.width, height: 0) } .onEnded { value in + guard isSwipeEnabled else { + resetCardOffset() + return + } + guard let direction = swipeDirection(for: value.translation) else { resetCardOffset() return From 68c21951f1536b70ed438802dc268c3bf2eeefec Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 16 Mar 2026 20:58:56 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20home=20=EC=9D=B4=EB=B2=88=EC=A3=BC?= =?UTF-8?q?=20=EC=95=84=EB=8B=90=20=EB=95=8C=EB=A7=8C=20resetButton=20?= =?UTF-8?q?=EB=82=98=EC=98=A4=EA=B2=8C=20=EC=88=98=EC=A0=95=20-=20#210?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift b/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift index 3c28625e..10a66fbe 100644 --- a/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift +++ b/Projects/Feature/Home/Sources/Home/HomeReducer+Impl.swift @@ -275,7 +275,12 @@ extension HomeReducer { case let .setCalendarDate(date): guard date != state.calendarDate else { return .none } - let today = TXCalendarDate() + let now = state.nowDate + let today = TXCalendarDate( + year: now.year, + month: now.month, + day: now.day + ) let calendar = Calendar(identifier: .gregorian) state.calendarDate = date From 8d97e6ef28638f12c2d55255132a3e55f056ccec Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 16 Mar 2026 21:00:23 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20photologs-0004=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=ED=99=94=EB=A9=B4=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=88=98=EC=A0=95=20-=20#210?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Detail/GoalDetailView.swift | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index b8588406..3b4ec173 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -160,19 +160,21 @@ private extension GoalDetailView { .animation(.spring(response: 0.36, dampingFraction: 0.86), value: store.currentUser) } + @ViewBuilder var backgroundRect: some View { - RoundedRectangle(cornerRadius: 20) - .fill(Color.Gray.gray200) - .insideBorder( - Color.Gray.gray500, - shape: RoundedRectangle(cornerRadius: 20), - lineWidth: 1.6 - ) - .frame(maxWidth: .infinity) - .aspectRatio(1, contentMode: .fit) - .overlay(dimmedView) - .clipShape(RoundedRectangle(cornerRadius: 20)) - .rotationEffect(.degrees(degree(isBackground: true))) + if !store.isEditing { + RoundedRectangle(cornerRadius: 20) + .fill(Color.Gray.gray200) + .insideBorder( + Color.Gray.gray500, + shape: RoundedRectangle(cornerRadius: 20), + lineWidth: 1.6 + ) + .frame(width: 336, height: 336) + .overlay(dimmedView) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .rotationEffect(.degrees(degree(isBackground: true))) + } } @ViewBuilder From fa4c060908f1bc6a2a69b85764167c7402e206bd Mon Sep 17 00:00:00 2001 From: jihun Date: Mon, 16 Mar 2026 21:09:53 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20photologs-0006=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EC=83=B7=20=EC=83=81=EC=84=B8=20=EC=B0=8C=EB=A5=B4=EA=B8=B0=20?= =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20#210?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Detail/GoalDetailReducer+Impl.swift | 51 ++++++++++++++++++- .../Sources/Detail/GoalDetailView.swift | 1 + 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailReducer+Impl.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailReducer+Impl.swift index 60404a5d..7a5d3bc2 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailReducer+Impl.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailReducer+Impl.swift @@ -17,6 +17,41 @@ import SharedDesignSystem import SharedUtil import SharedUtil +private enum PokeCooldownManager { + private static let userDefaultsKey = "pokeCooldownTimestamps" + private static let cooldownInterval: TimeInterval = 3 * 60 * 60 + + static func remainingCooldown(goalId: Int64) -> TimeInterval? { + guard let timestamps = UserDefaults.standard.dictionary(forKey: userDefaultsKey) as? [String: TimeInterval], + let lastPokeTime = timestamps[String(goalId)] else { + return nil + } + let elapsed = Date().timeIntervalSince1970 - lastPokeTime + let remaining = cooldownInterval - elapsed + return remaining > 0 ? remaining : nil + } + + static func formatRemainingTime(_ seconds: TimeInterval) -> String { + let totalMinutes = Int(ceil(seconds / 60)) + let hours = totalMinutes / 60 + let minutes = totalMinutes % 60 + + if hours > 0 && minutes > 0 { + return "\(hours)시간 \(minutes)분" + } else if hours > 0 { + return "\(hours)시간" + } else { + return "\(max(1, minutes))분" + } + } + + static func recordPoke(goalId: Int64) { + var timestamps = UserDefaults.standard.dictionary(forKey: userDefaultsKey) as? [String: TimeInterval] ?? [:] + timestamps[String(goalId)] = Date().timeIntervalSince1970 + UserDefaults.standard.set(timestamps, forKey: userDefaultsKey) + } +} + extension GoalDetailReducer { // swiftlint: disable function_body_length /// 실제 로직을 포함한 GoalDetailReducer를 생성합니다. @@ -64,7 +99,21 @@ extension GoalDetailReducer { await send(.authorizationCompleted(isAuthorized: isAuthorized)) } } - return .none + guard state.currentUser == .you, !state.isCompleted else { return .none } + let goalId = state.currentGoalId + if let remaining = PokeCooldownManager.remainingCooldown(goalId: goalId) { + let timeText = PokeCooldownManager.formatRemainingTime(remaining) + return .send(.showToast(.warning(message: "\(timeText) 뒤에 다시 찌를 수 있어요"))) + } + return .run { send in + do { + try await goalClient.pokePartner(goalId) + PokeCooldownManager.recordPoke(goalId: goalId) + await send(.showToast(.poke(message: "상대방을 찔렀어요!"))) + } catch { + await send(.showToast(.warning(message: "찌르기에 실패했어요"))) + } + } case let .navigationBarTapped(action): if case .backTapped = action { diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index 3b4ec173..1629d3dc 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -115,6 +115,7 @@ public struct GoalDetailView: View { ProgressView() } } + .txToast(item: $store.toast, customPadding: 54) } } From d6c4189cff92ada92634abd940172f80633b5854 Mon Sep 17 00:00:00 2001 From: jihun Date: Tue, 24 Mar 2026 13:59:19 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20photologs-0003=20=EC=B0=8C=EB=A5=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=9D=BC=EB=9F=AC=EC=8A=A4=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20#210?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Interface/Sources/GoalDetailReducer.swift | 2 +- .../GoalDetail/Sources/Detail/GoalDetailView.swift | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Projects/Feature/GoalDetail/Interface/Sources/GoalDetailReducer.swift b/Projects/Feature/GoalDetail/Interface/Sources/GoalDetailReducer.swift index e486ddb8..256c1f1a 100644 --- a/Projects/Feature/GoalDetail/Interface/Sources/GoalDetailReducer.swift +++ b/Projects/Feature/GoalDetail/Interface/Sources/GoalDetailReducer.swift @@ -229,7 +229,7 @@ extension GoalDetailReducer.State { return isEditing ? "다시 찍기" : "업로드하기" case .you: - return "찔러보세요" + return "찌르기" } } } diff --git a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift index 1629d3dc..8e4778ab 100644 --- a/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift +++ b/Projects/Feature/GoalDetail/Sources/Detail/GoalDetailView.swift @@ -72,10 +72,9 @@ public struct GoalDetailView: View { } else { bottomButton .padding(.top, 105) - .frame(maxWidth: .infinity) - .overlay(alignment: .topTrailing) { + .overlay(alignment: .bottomLeading) { pokeImage - .offset(x: -20, y: -20) + .offset(x: 79, y: -45) } } } @@ -264,8 +263,7 @@ private extension GoalDetailView { var pokeImage: some View { Image.Illustration.poke .resizable() - .frame(width: 173, height: 173) - .allowsHitTesting(false) + .frame(width: 184, height: 160) } var bottomButton: some View { @@ -310,8 +308,7 @@ private extension GoalDetailView { let shape = RoundedRectangle(cornerRadius: 20) return Color.clear - .frame(maxWidth: .infinity) - .aspectRatio(1, contentMode: .fit) + .frame(width: 336, height: 336) .readSize { rectFrame = $0 } .overlay { content()