Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0ff9735
refactor: CoreAnalytics 구조 개선 - #281
May 9, 2026
f738eaa
fix: 홈에서 카드 상단탭 -> 통계상세에서 수정하기 안되는 이슈 수정 - #285
May 10, 2026
3fcf47c
chore: stroke 얇은 icon 이미지 에셋 추가 - #285
May 11, 2026
700716a
fix: 목표 생성 얇은 아이콘 이미지로 변경 - #285
May 11, 2026
6aee766
fix: modal padding 수정 및 아이콘 storke 대응 - #285
May 11, 2026
a9293a5
fix: 인증샷 상세 reacotionBar 그림자 추가 - #285
May 11, 2026
a638e3c
fix: 찌르기 버튼 UI 수정 - #285
May 11, 2026
9d3da78
feat: TXButton Round illust disabled 구현 - #286
May 11, 2026
df2191e
fix: 주간 캘린더 인터랙션 요구사항에 맞게 수정 - #285
May 11, 2026
51c1c0c
fix: navigation pop시 흰색 화면 보이는 버그 수정 - #285
May 11, 2026
8bec322
fix: AddButton 이미지 변경 - #285
May 11, 2026
943808d
fix: 인증샷 상세 뷰 네비게이션 버튼 터치 간헐적으로 안되는 버그 수정 - #285
May 11, 2026
6fb020b
fix: 홈에서 찌르기 쿨타임일 때 disable 처리 - #285
May 12, 2026
a6ac6d1
fix: 목표 편집 뷰 로딩 ux 저하되는 이슈 수정 - #285
May 12, 2026
629da1f
refactor: 목표 수정 시 기존 목표 데이터 주입식으로 변경 - #285
May 13, 2026
be3ca48
feat: EditableGoal Entity 구현 - #285
May 14, 2026
7e4b748
refactor: EditableGoal Entity -> MakeGoal에서 사용하는 GoalForm 타입 변환에 따른 작…
May 14, 2026
1847b1b
refactor: EditableGoal을 source of truth로 갖게끔 EditGoalList 수정 - #285
May 14, 2026
563527b
refactor: StatsDetail EditableGoal 주입 - #285
May 14, 2026
8c7bf4f
fix: 찌르기 응답 받기 전 날짜 변경 시 다른 날짜 데이터 업데이트 할 수 있던 리스크 제거 - #285
May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions Projects/Core/Analytics/Interface/Sources/AnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,6 @@
import Foundation

/// 분석 도구로 전송할 이벤트가 따라야 하는 공통 인터페이스입니다.
///
/// ## 사용 예시
/// ```swift
/// enum HomeAnalyticsEvent: AnalyticsEvent {
/// case homeViewed
///
/// var name: String { "home_viewed" }
/// var parameters: [String: Any]? { nil }
/// }
/// ```
public protocol AnalyticsEvent {
var name: String { get }
var parameters: [String: Any]? { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ final class CaptureSessionManager: NSObject, @unchecked Sendable {
private let photoOutput = AVCapturePhotoOutput()
private var continuation: CheckedContinuation<Data, Error>?
private var flashMode: AVCaptureDevice.FlashMode = .off
private var currentPosition: AVCaptureDevice.Position = .back

func requestAuthorization() async -> Bool {
switch AVCaptureDevice.authorizationStatus(for: .video) {
Expand All @@ -40,6 +41,7 @@ final class CaptureSessionManager: NSObject, @unchecked Sendable {
await performOnSessionQueue { [weak self] in
guard let self else { return }

self.currentPosition = position
self.session.beginConfiguration()
self.session.sessionPreset = .photo
self.session.inputs.forEach { self.session.removeInput($0) }
Expand Down Expand Up @@ -86,6 +88,11 @@ final class CaptureSessionManager: NSObject, @unchecked Sendable {
}

self.continuation = continuation

if let connection = self.photoOutput.connection(with: .video), connection.isVideoMirroringSupported {
connection.isVideoMirrored = self.currentPosition == .front
}

let settings = AVCapturePhotoSettings()
if self.photoOutput.supportedFlashModes.contains(self.flashMode) {
settings.flashMode = self.flashMode
Expand Down
38 changes: 38 additions & 0 deletions Projects/Domain/Goal/Interface/Sources/Entity/EditableGoal.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// GoalCreation.swift
// DomainGoalInterface
//
// Created by 정지훈 on 5/14/26.
//

import Foundation

import DomainCommonInterface

public struct EditableGoal: Equatable, Identifiable {
public let id: Int64
public let name: String
public let icon: String
public let repeatCycle: RepeatCycle
public let repeatCount: Int?
public let startDate: String
public let endDate: String?

public init(
id: Int64,
name: String,
icon: String,
repeatCycle: RepeatCycle,
repeatCount: Int?,
startDate: String,
endDate: String?
) {
self.id = id
self.name = name
self.icon = icon
self.repeatCycle = repeatCycle
self.repeatCount = repeatCount
self.startDate = startDate
self.endDate = endDate
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ extension StatsDetailCalendarResponseDTO {
StatsDetail(
goalId: goalId,
goalName: goalName,
goalIcon: goalIcon,
isCompleted: isCompleted,
yearMonth: yearMonth,
completedDate: completedDates.map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import DomainCommonInterface
public struct StatsDetail: Equatable {
public let goalId: Int64
public let goalName: String
public let goalIcon: String
public var isCompleted: Bool
public let yearMonth: String
public let completedDate: [CompletedDate]
Expand Down
1 change: 1 addition & 0 deletions Projects/Domain/Stats/Interface/Sources/StatsClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ extension StatsClient: TestDependencyKey {
return .init(
goalId: 1,
goalName: "밥 잘 챙겨먹기",
goalIcon: "ICON_DEFAULT",
isCompleted: false,
yearMonth: "2026-02",
completedDate: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public struct GoalDetailView: View {
public var body: some View {
VStack(spacing: 0) {
navigationBar
.zIndex(1)

if store.item != nil {
cardView
Expand Down
76 changes: 47 additions & 29 deletions Projects/Feature/GoalDetail/Sources/Detail/ReactionBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,37 +25,14 @@ struct ReactionBarView: View {

var body: some View {
GeometryReader { proxy in
HStack(spacing: 0) {
ForEach(ReactionEmoji.allCases, id: \.self) { emoji in
Button {
onSelect(emoji)
flyingReactionEmitter.emit(
emoji: emoji,
config: .reactionBar(width: proxy.size.width)
)
} label: {
emoji.image
.padding(.horizontal, 8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(selectedEmoji == emoji ? Color.Gray.gray300 : Color.clear)

if emoji != ReactionEmoji.allCases.last {
Rectangle()
.frame(width: 1)
}
}
ZStack(alignment: .top) {
shadowView(proxy: proxy)
.offset(y: 10)

reactionBar(proxy: proxy)
}
.frame(width: proxy.size.width, height: proxy.size.height)
}
.frame(maxWidth: .infinity)
.frame(height: 67)
.background(Color.Gray.gray100)
.clipShape(.capsule)
.overlay(
Capsule()
.stroke(Color.black, lineWidth: 1)
)
.frame(height: 77)
.overlay(alignment: .bottomLeading) {
FlyingReactionOverlay(
reactions: flyingReactionEmitter.reactions,
Expand All @@ -65,6 +42,47 @@ struct ReactionBarView: View {
}
}

// MARK: - SubViews

private extension ReactionBarView {
func shadowView(proxy: GeometryProxy) -> some View {
Color.Gray.gray200
.frame(width: proxy.size.width, height: 67)
.clipShape(.capsule)
}

func reactionBar(proxy: GeometryProxy) -> some View {
HStack(spacing: 0) {
ForEach(ReactionEmoji.allCases, id: \.self) { emoji in
Button {
onSelect(emoji)
flyingReactionEmitter.emit(
emoji: emoji,
config: .reactionBar(width: proxy.size.width)
)
} label: {
emoji.image
.padding(.horizontal, 8)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(selectedEmoji == emoji ? Color.Gray.gray300 : Color.clear)

if emoji != ReactionEmoji.allCases.last {
Rectangle()
.frame(width: 1)
}
}
}
.background(Color.Gray.gray100)
.frame(width: proxy.size.width, height: 68)
.clipShape(.capsule)
.overlay(
Capsule()
.stroke(Color.Gray.gray500, lineWidth: 1)
)
}
}

private extension ReactionBarView {
static func reactionBarConfig(width: CGFloat) -> FlyingReactionConfig {
let minX: CGFloat = 8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation

import ComposableArchitecture
import DomainGoalInterface
import FeatureCommonInterface
import SharedDesignSystem
import SharedUtil
Expand Down Expand Up @@ -37,6 +38,7 @@ public struct EditGoalListReducer {

public var calendarDate: TXCalendarDate
public var calendarWeeks: [[TXCalendarDateItem]]
public var editableGoals: [EditableGoal]?
public var cards: [GoalEditCardItem]?
public var hasCards: Bool { !(cards?.isEmpty ?? true) }
public var selectedCardMenu: GoalEditCardItem?
Expand Down Expand Up @@ -84,7 +86,7 @@ public struct EditGoalListReducer {
// MARK: - Update State
case setCalendarDate(TXCalendarDate)
case fetchGoals
case fetchGoalsCompleted([GoalEditCardItem], date: TXCalendarDate)
case fetchGoalsCompleted([EditableGoal], date: TXCalendarDate)
case deleteGoalCompleted(goalId: Int64)
case completeGoalCompleted(goalId: Int64)
case apiError(String)
Expand All @@ -95,7 +97,7 @@ public struct EditGoalListReducer {

public enum Delegate {
case navigateBack
case goToGoalEdit(goalId: Int64)
case goToGoalEdit(EditableGoal)
case goToCompletedStats
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public struct HomeReducer {
case setCalendarDate(TXCalendarDate)
case setCalendarSheetPresented(Bool)
case showToast(TXToastType)
case setPokeButtonDisabled(goalId: Int64, Bool, date: TXCalendarDate)
case authorizationCompleted(id: Int64, isAuthorized: Bool)
case proofPhotoDismissed
case addGoalButtonTapped(GoalCategory)
Expand Down
53 changes: 38 additions & 15 deletions Projects/Feature/Home/Sources/Goal/EditGoalListReducer+Impl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import Foundation
import SwiftUI

import ComposableArchitecture
import DomainCommonInterface
import DomainGoalInterface
import FeatureCommonInterface
import FeatureHomeInterface
import SharedDesignSystem
import SharedUtil

extension EditGoalListReducer {
/// 실제 로직을 포함한 EditGoalListReducer를 생성합니다.
Expand Down Expand Up @@ -87,7 +87,10 @@ extension EditGoalListReducer {
if isPast {
state.toast = .warning(message: "이미 완료한 목표입니다!")
} else {
return .send(.delegate(.goToGoalEdit(goalId: card.id)))
guard let editableGoal = state.editableGoals?.first(where: { $0.id == card.id }) else {
return .send(.apiError("목표 수정에 실패했어요"))
}
return .send(.delegate(.goToGoalEdit(editableGoal)))
}

case .finish:
Expand Down Expand Up @@ -170,28 +173,46 @@ extension EditGoalListReducer {
let date = state.calendarDate
return .run { send in
do {
let items = try await goalClient.fetchGoalEditList(TXCalendarUtil.apiDateString(for: date))
let editItems = items.map {
GoalEditCardItem(
id: $0.id,
goalName: $0.title,
iconImage: GoalIcon(from: $0.goalIcon).image,
repeatCycle: $0.repeatCycle?.text ?? "",
startDate: $0.startDate?.dateDisplayString ?? "",
endDate: $0.endDate?.dateDisplayString ?? "미설정"
)
}
await send(.fetchGoalsCompleted(editItems, date: date))
let goals = try await goalClient.fetchGoalEditList(TXCalendarUtil.apiDateString(for: date))
.compactMap { goal -> EditableGoal? in
guard let repeatCycle = goal.repeatCycle,
let startDate = goal.startDate else {
return nil
}

return EditableGoal(
id: goal.id,
name: goal.title,
icon: goal.goalIcon,
repeatCycle: repeatCycle,
repeatCount: goal.repeatCount,
startDate: startDate,
endDate: goal.endDate
)
}
await send(.fetchGoalsCompleted(goals, date: date))
} catch {
await send(.apiError("목표 조회에 실패했어요"))
}
}

case let .fetchGoalsCompleted(items, date):
case let .fetchGoalsCompleted(goals, date):
if date != state.calendarDate {
return .none
}
state.isLoading = false
state.editableGoals = goals
let items = goals.map {
GoalEditCardItem(
id: $0.id,
goalName: $0.name,
goalIcon: GoalIcon(from: $0.icon),
iconImage: GoalIcon(from: $0.icon).thinImage,
repeatCycle: $0.repeatCycle.text,
startDate: $0.startDate.dateDisplayString,
endDate: $0.endDate?.dateDisplayString ?? "미설정"
)
}
if state.cards != items {
state.cards = items
}
Expand All @@ -201,13 +222,15 @@ extension EditGoalListReducer {
state.isLoading = false
state.pendingGoalId = nil
state.pendingAction = nil
state.editableGoals?.removeAll { $0.id == goalId }
state.cards?.removeAll { $0.id == goalId }
return .send(.showToast(.delete(message: "목표가 삭제되었어요")))

case let .completeGoalCompleted(goalId):
state.isLoading = false
state.pendingGoalId = nil
state.pendingAction = nil
state.editableGoals?.removeAll { $0.id == goalId }
state.cards?.removeAll { $0.id == goalId }
return .send(.showToast(.success(message: "목표를 이뤘어요", buttonText: "보러가기")))

Expand Down
6 changes: 6 additions & 0 deletions Projects/Feature/Home/Sources/Goal/EditGoalListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ struct EditGoalListView: View {
guard store.selectedCardMenu != nil else { return }
store.send(.backgroundTapped)
}
.transaction { transaction in
transaction.animation = nil
}
.txModal(
item: $store.modal,
onAction: { action in
Expand Down Expand Up @@ -75,6 +78,9 @@ private extension EditGoalListView {
weeks: store.calendarWeeks,
onSelect: { item in
store.send(.calendarDateSelected(item))
},
onSwipe: { swipe in
store.send(.weekCalendarSwipe(swipe))
}
)
.frame(maxWidth: .infinity, maxHeight: 76)
Expand Down
Loading
Loading