diff --git a/Application/App/Sources/App/Dependency/AppGraph+PresentationDependencies.swift b/Application/App/Sources/App/Dependency/AppGraph+PresentationDependencies.swift index 6929fa46..fb1df14e 100644 --- a/Application/App/Sources/App/Dependency/AppGraph+PresentationDependencies.swift +++ b/Application/App/Sources/App/Dependency/AppGraph+PresentationDependencies.swift @@ -12,7 +12,6 @@ extension AppGraph { prepareDependencies { dependencies in prepareEntryDependencies(&dependencies) prepareDevelopmentDependencies(&dependencies) - prepareDevelopmentTabDependencies(&dependencies) prepareTodoDependencies(&dependencies) prepareHomeDependencies(&dependencies) prepareTodayDependencies(&dependencies) @@ -24,7 +23,7 @@ extension AppGraph { private extension AppGraph { func prepareDevelopmentDependencies(_ dependencies: inout DependencyValues) { - DevelopmentTabPresentationDependencyPreparation.prepareGoal( + DevelopmentPresentationDependencyPreparation.prepareGoal( &dependencies, createGoalUseCase: developmentGraphSet .developmentGoalUseCaseGraph @@ -36,7 +35,7 @@ private extension AppGraph { .developmentGoalUseCaseGraph .updateDevelopmentGoalStatusUseCase ) - DevelopmentTabPresentationDependencyPreparation.prepareQuery( + DevelopmentPresentationDependencyPreparation.prepareQuery( &dependencies, fetchRecordsUseCase: developmentGraphSet .developmentRecordQueryUseCaseGraph @@ -48,7 +47,7 @@ private extension AppGraph { .developmentRecordQueryUseCaseGraph .fetchDevelopmentRecordVersionUseCase ) - DevelopmentTabPresentationDependencyPreparation.prepareMutation( + DevelopmentPresentationDependencyPreparation.prepareMutation( &dependencies, createRecordUseCase: developmentGraphSet .developmentRecordMutationUseCaseGraph @@ -63,25 +62,13 @@ private extension AppGraph { .developmentRecordMutationUseCaseGraph .restoreDevelopmentRecordUseCase ) - DevelopmentTabPresentationDependencyPreparation.prepareTodo( + DevelopmentPresentationDependencyPreparation.prepareTodo( &dependencies, fetchTodosUseCase: todoGraphSet.todoUseCaseGraph.fetchTodosUseCase, updateTodoGoalUseCase: todoGraphSet.todoGoalUseCaseGraph.updateTodoGoalUseCase ) } - func prepareDevelopmentTabDependencies(_ dependencies: inout DependencyValues) { - DevelopmentTabPresentationDependencyPreparation.prepareGoalList( - &dependencies, - goalsUseCase: developmentGraphSet - .developmentGoalUseCaseGraph - .fetchDevelopmentGoalsUseCase, - recentRecordUseCase: developmentGraphSet - .developmentRecordQueryUseCaseGraph - .fetchRecentDevelopmentRecordUseCase - ) - } - func prepareEntryDependencies(_ dependencies: inout DependencyValues) { PresentationDependencyPreparation.prepareRoot( &dependencies, diff --git a/Application/Presentation/DevelopmentTab/Sources/.swiftlint.yml b/Application/Presentation/DevelopmentTab/Sources/.swiftlint.yml deleted file mode 100644 index 6a9d7380..00000000 --- a/Application/Presentation/DevelopmentTab/Sources/.swiftlint.yml +++ /dev/null @@ -1 +0,0 @@ -parent_config: ../../../../.swiftlint.yml diff --git a/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalCard.swift b/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalCard.swift deleted file mode 100644 index fe408b99..00000000 --- a/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalCard.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// DevelopmentGoalCard.swift -// DevelopmentTab -// -// Created by opfic on 10/5/26. -// - -import Domain -import PresentationShared -import SwiftUI - -struct DevelopmentGoalCard: View { - let goal: DevelopmentGoal - let recentRecord: DevelopmentGoalListFeature.RecentRecord? - - var body: some View { - VStack(alignment: .leading, spacing: 14) { - HStack(alignment: .top, spacing: 12) { - Text(goal.title) - .font(.title3.weight(.semibold)) - .foregroundStyle(Color.primary) - .lineLimit(2) - .frame(maxWidth: .infinity, alignment: .leading) - GoalStatusBadge(status: goal.status) - } - if !goal.description.isEmpty { - Text(goal.description) - .font(.subheadline) - .foregroundStyle(Color.textSecondary) - .lineLimit(3) - .frame(maxWidth: .infinity, alignment: .leading) - } - if case .loaded(let version?) = recentRecord { - HStack(alignment: .top, spacing: 8) { - Image(systemName: "clock") - .foregroundStyle(Color.textTertiary) - VStack(alignment: .leading, spacing: 6) { - Text(String.localizedStringWithFormat( - String(localized: "development_goals_recent_record_format", bundle: PresentationResources.bundle), - version.title - )) - .font(.subheadline) - .foregroundStyle(Color.textSecondary) - .lineLimit(1) - TimelineView(.periodic(from: .now, by: 1.0)) { context in - Text(String.localizedStringWithFormat( - String(localized: "development_goals_recent_record_date_format", bundle: PresentationResources.bundle), - RelativeTime.text(from: version.confirmedAt, now: context.date), - version.confirmedAt.formatted(.dateTime.month().day().hour().minute()), - RecordPresentation.versionLabel(version.number) - )) - .font(.caption) - .foregroundStyle(Color.textTertiary) - .lineLimit(2) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } - .padding(12) - .background(Color.appBackground, in: .rect(cornerRadius: 12)) - } - } - .padding(18) - .background(Color.surface, in: .rect(cornerRadius: 24)) - .contentShape(.rect) - } -} diff --git a/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalListFeature+Dependencies.swift b/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalListFeature+Dependencies.swift deleted file mode 100644 index fc6c7ad1..00000000 --- a/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalListFeature+Dependencies.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// DevelopmentGoalListFeature+Dependencies.swift -// DevelopmentTab -// -// Created by opfic on 10/5/26. -// - -import Domain -import PresentationShared - -extension DependencyValues { - var developmentTabFetchGoalsUseCase: FetchDevelopmentGoalsUseCase { - get { self[FetchDevelopmentGoalsUseCaseKey.self] } - set { self[FetchDevelopmentGoalsUseCaseKey.self] = newValue } - } - - var developmentTabFetchRecentRecordUseCase: FetchRecentDevelopmentRecordUseCase { - get { self[FetchRecentDevelopmentRecordUseCaseKey.self] } - set { self[FetchRecentDevelopmentRecordUseCaseKey.self] = newValue } - } -} - -private enum FetchDevelopmentGoalsUseCaseKey: DependencyKey { - static var liveValue: FetchDevelopmentGoalsUseCase { - preconditionFailure("FetchDevelopmentGoalsUseCase must be provided.") - } -} - -private enum FetchRecentDevelopmentRecordUseCaseKey: DependencyKey { - static var liveValue: FetchRecentDevelopmentRecordUseCase { - preconditionFailure("FetchRecentDevelopmentRecordUseCase must be provided.") - } -} diff --git a/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalListFeature.swift b/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalListFeature.swift deleted file mode 100644 index 5160d4d0..00000000 --- a/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalListFeature.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// DevelopmentGoalListFeature.swift -// DevelopmentTab -// -// Created by opfic on 10/5/26. -// - -import Domain -import Foundation -import PresentationShared - -@Reducer -struct DevelopmentGoalListFeature { - @ObservableState - struct State: Equatable { - @Presents var sheet: SheetState? - var goals = [DevelopmentGoal]() - var selectedStatus = DevelopmentGoal.Status.inProgress - var recentRecords = [String: RecentRecord]() - var visibleGoalIDs = Set() - var hasLoaded = false - var isLoading = false - var hasLoadFailure = false - var requestID = 0 - var recordRevision = 0 - - var filteredGoals: [DevelopmentGoal] { - goals.filter { $0.status == selectedStatus } - } - - func count(for status: DevelopmentGoal.Status) -> Int { - goals.count { $0.status == status } - } - } - - @ObservableState - @CasePathable - enum SheetState: Equatable { - case create - case detail(String) - } - - enum RecentRecord: Equatable { - case loading - case loaded(DevelopmentRecord.Version?) - case failed - } - - enum Action: Equatable { - case sheet(PresentationAction) - case selected - case refresh - case retry - case selectStatus(DevelopmentGoal.Status) - case showCreate - case showDetail(String) - case goalCreated(DevelopmentGoal) - case cardAppeared(String) - case cardDisappeared(String) - case goalsLoaded([DevelopmentGoal], requestID: Int) - case goalsFailed(requestID: Int) - case recentRecordLoaded(String, RecentRecord, revision: Int) - } - - @Dependency(\.developmentTabFetchGoalsUseCase) private var goalsUseCase - @Dependency(\.developmentTabFetchRecentRecordUseCase) private var recentRecordUseCase - - var body: some ReducerOf { - Reduce { state, action in - reduce(action, state: &state) - } - .ifLet(\.$sheet, action: \.sheet) { - EmptyReducer() - } - } - - private func reduce(_ action: Action, state: inout State) -> Effect { - switch action { - case .selected, .retry: - return fetchGoals(state: &state, showsLoading: !state.hasLoaded) - case .refresh: - return fetchGoals(state: &state, showsLoading: false) - case .sheet(.dismiss): - state.sheet = nil - return fetchGoals(state: &state, showsLoading: false) - case .selectStatus(let status): - state.selectedStatus = status - case .showCreate: - state.sheet = .create - case .showDetail(let goalID): - state.sheet = .detail(goalID) - case .sheet: - break - case .goalCreated(let goal): - state.requestID += 1 - state.isLoading = false - state.hasLoadFailure = false - state.hasLoaded = true - state.selectedStatus = .inProgress - state.goals.removeAll { $0.id == goal.id } - state.goals.append(goal) - state.goals = Self.sortedGoals(state.goals) - case .cardAppeared(let goalID): - state.visibleGoalIDs.insert(goalID) - guard state.recentRecords[goalID] == nil, - state.goals.contains(where: { $0.id == goalID }) else { break } - state.recentRecords[goalID] = .loading - return fetchRecentRecord(goalID, revision: state.recordRevision) - case .cardDisappeared(let goalID): - state.visibleGoalIDs.remove(goalID) - case .goalsLoaded(let goals, let requestID): - guard requestID == state.requestID else { break } - state.goals = Self.sortedGoals(goals) - state.hasLoaded = true - state.isLoading = false - state.hasLoadFailure = false - state.recordRevision += 1 - state.recentRecords.removeAll() - state.visibleGoalIDs.formIntersection(goals.map(\.id)) - let effects = state.filteredGoals.compactMap { goal -> Effect? in - guard state.visibleGoalIDs.contains(goal.id) else { return nil } - state.recentRecords[goal.id] = .loading - return fetchRecentRecord(goal.id, revision: state.recordRevision) - } - return .merge(effects) - case .goalsFailed(let requestID): - guard requestID == state.requestID else { break } - state.isLoading = false - state.hasLoadFailure = true - case .recentRecordLoaded(let goalID, let recentRecord, let revision): - guard revision == state.recordRevision, - state.goals.contains(where: { $0.id == goalID }) else { break } - state.recentRecords[goalID] = recentRecord - } - return .none - } - - private func fetchGoals(state: inout State, showsLoading: Bool) -> Effect { - state.requestID += 1 - state.isLoading = showsLoading - state.hasLoadFailure = false - let requestID = state.requestID - return .run { [goalsUseCase] send in - do { - let goals = try await goalsUseCase.execute(.init(status: nil)) - await send(.goalsLoaded(goals, requestID: requestID)) - } catch { - await send(.goalsFailed(requestID: requestID)) - } - } - } - - private func fetchRecentRecord(_ goalID: String, revision: Int) -> Effect { - .run { [recentRecordUseCase] send in - do { - let version = try await recentRecordUseCase.execute(goalId: goalID) - await send(.recentRecordLoaded(goalID, .loaded(version), revision: revision)) - } catch { - await send(.recentRecordLoaded(goalID, .failed, revision: revision)) - } - } - } - - private static func sortedGoals(_ goals: [DevelopmentGoal]) -> [DevelopmentGoal] { - goals.sorted { - if $0.createdAt == $1.createdAt { return $0.id < $1.id } - return $0.createdAt < $1.createdAt - } - } -} diff --git a/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalListView.swift b/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalListView.swift deleted file mode 100644 index 83505fa4..00000000 --- a/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalListView.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// DevelopmentGoalListView.swift -// DevelopmentTab -// -// Created by opfic on 10/5/26. -// - -import Domain -import PresentationShared -import SwiftUI - -public struct DevelopmentGoalListView: View { - @State private var store: StoreOf - private let isSelected: Bool - - public init(isSelected: Bool = true) { - self._store = State(initialValue: Store(initialState: DevelopmentGoalListFeature.State()) { - DevelopmentGoalListFeature() - }) - self.isSelected = isSelected - } - - public var body: some View { - NavigationStack { - ScrollView { - LazyVStack(alignment: .leading, spacing: 16) { - if store.hasLoadFailure { - loadFailure - } - if store.hasLoaded { - if store.filteredGoals.isEmpty { - emptyState - } else { - ForEach(store.filteredGoals, id: \.id) { goal in - Button { - store.send(.showDetail(goal.id)) - } label: { - DevelopmentGoalCard( - goal: goal, - recentRecord: store.recentRecords[goal.id] - ) - } - .buttonStyle(.plain) - .onAppear { - store.send(.cardAppeared(goal.id)) - } - .onDisappear { - store.send(.cardDisappeared(goal.id)) - } - } - } - } - } - .padding(.horizontal, 16) - .padding(.bottom, 24) - } - .refreshable { - await store.send(.refresh).finish() - } - .safeAreaInset(edge: .top, spacing: 0) { - VStack(alignment: .leading, spacing: 12) { - HStack { - Text("nav_development_goals", bundle: PresentationResources.bundle) - .font(.largeTitle.bold()) - Spacer() - Button { - store.send(.showCreate) - } label: { - Image(systemName: "plus") - } - .topBarButtonStyle() - } - statusTabs - } - .padding(.horizontal, 16) - .padding(.bottom, 8) - .background(Color.appBackground) - .toolbarBackground(Color.appBackground) - } - .background(Color.appBackground.ignoresSafeArea()) - .toolbarVisibility(.hidden, for: .navigationBar) - .sheet( - item: $store.scope(state: \.sheet, action: \.sheet) - .activePresentation(when: isSelected) - ) { sheetStore in - switch sheetStore.state { - case .create: - GoalCreateView { goal in - store.send(.goalCreated(goal)) - } - case .detail(let goalID): - GoalDetailView(goalId: goalID) - } - } - } - .onChange(of: isSelected, initial: true) { _, isSelected in - guard isSelected else { return } - store.send(.selected) - } - .overlay { - if store.isLoading { - LoadingView() - } - } - } - - private var statusTabs: some View { - ScrollView(.horizontal) { - HStack(spacing: 8) { - ForEach(DevelopmentGoal.Status.tabs, id: \.self) { status in - let isSelected = store.selectedStatus == status - Button { - store.send(.selectStatus(status)) - } label: { - HStack(spacing: 4) { - Text(status.title) - Text(store.state.count(for: status).formatted()) - .opacity(0.6) - } - .font(.callout) - .foregroundStyle(isSelected ? Color.onPrimaryContainer : Color.onControlBackground) - .adaptiveButtonStyle(color: isSelected ? .primaryContainer : .controlBackground) - } - } - } - } - .scrollIndicators(.hidden) - .fixedSize(horizontal: false, vertical: true) - .padding(.horizontal, -16) - .contentMargins(.horizontal, 16, for: .scrollContent) - } - - private var loadFailure: some View { - ContentUnavailableView { - Label( - String(localized: "common_error_title", bundle: PresentationResources.bundle), - systemImage: "exclamationmark.triangle" - ) - } description: { - Text(String(localized: "common_error_message", bundle: PresentationResources.bundle)) - } actions: { - Button(String(localized: "development_record_timeline_retry", bundle: PresentationResources.bundle)) { - store.send(.retry) - } - .buttonStyle(.borderedProminent) - } - } - - private var emptyState: some View { - ContentUnavailableView { - Label(store.selectedStatus.emptyTitle, systemImage: "flag.checkered") - } actions: { - if store.selectedStatus == .inProgress { - Button { - store.send(.showCreate) - } label: { - Text("development_goals_create", bundle: PresentationResources.bundle) - } - .buttonStyle(.borderedProminent) - } - } - } -} diff --git a/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalStatus+Presentation.swift b/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalStatus+Presentation.swift deleted file mode 100644 index 41d4d78f..00000000 --- a/Application/Presentation/DevelopmentTab/Sources/List/DevelopmentGoalStatus+Presentation.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// DevelopmentGoalStatus+Presentation.swift -// DevelopmentTab -// -// Created by opfic on 10/5/26. -// - -import Domain -import PresentationShared -import SwiftUI - -extension DevelopmentGoal.Status { - static let tabs = [Self.inProgress, .completed, .archived] - - var emptyTitle: String { - switch self { - case .inProgress: - String(localized: "development_goals_empty_in_progress", bundle: PresentationResources.bundle) - case .completed: - String(localized: "development_goals_empty_completed", bundle: PresentationResources.bundle) - case .archived: - String(localized: "development_goals_empty_archived", bundle: PresentationResources.bundle) - } - } -} diff --git a/Application/Presentation/DevelopmentTab/Tests/.swiftlint.yml b/Application/Presentation/DevelopmentTab/Tests/.swiftlint.yml deleted file mode 100644 index 7bb38c07..00000000 --- a/Application/Presentation/DevelopmentTab/Tests/.swiftlint.yml +++ /dev/null @@ -1 +0,0 @@ -parent_config: ../../../../.swiftlint-tests.yml diff --git a/Application/Presentation/DevelopmentTab/Tests/DevelopmentGoalListFeatureTests.swift b/Application/Presentation/DevelopmentTab/Tests/DevelopmentGoalListFeatureTests.swift deleted file mode 100644 index 174e31ad..00000000 --- a/Application/Presentation/DevelopmentTab/Tests/DevelopmentGoalListFeatureTests.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// DevelopmentGoalListFeatureTests.swift -// DevelopmentTabTests -// -// Created by opfic on 10/5/26. -// - -import Testing -@testable import DevelopmentTab - -@MainActor -struct DevelopmentGoalListFeatureTests { - @Test("최근 기록은 카드가 나타날 때 조회하고 조회 중이거나 캐시가 있으면 중복 조회하지 않는다") - func lazyRecentRecordAndCache() async throws { - let goal = try makeDevelopmentGoal(id: "goal") - let version = try makeDevelopmentRecordVersion(recordID: "record") - let adapter = StoreTestAdapter( - goals: [goal], - recentRecords: ["goal": version] - ) - await adapter.load(.selected) - #expect(await adapter.recentRecordSpy.goalIDs.isEmpty) - - await adapter.recentRecordSpy.suspend() - await adapter.send(.cardAppeared("goal")) - #expect(adapter.state.recentRecords["goal"] == .loading) - await adapter.send(.cardAppeared("goal")) - #expect(adapter.state.recentRecords["goal"] == .loading) - await adapter.recentRecordSpy.resume() - await adapter.settle() - await adapter.send(.cardDisappeared("goal")) - await adapter.load(.cardAppeared("goal")) - #expect(adapter.state.recentRecords["goal"] == .loaded(version)) - #expect(await adapter.recentRecordSpy.goalIDs == ["goal"]) - } - - @Test("최근 기록 조회 실패는 해당 카드에만 반영하며 갱신하면 다시 조회한다") - func recordFailurePreservesList() async throws { - let goals = try [makeDevelopmentGoal(id: "a"), makeDevelopmentGoal(id: "b")] - let adapter = StoreTestAdapter(goals: goals) - await adapter.recentRecordSpy.setFailure(goalID: "a", fails: true) - await adapter.load(.selected) - await adapter.load(.cardAppeared("a")) - await adapter.load(.cardAppeared("b")) - #expect(adapter.state.goals == goals) - #expect(!adapter.state.hasLoadFailure) - #expect(adapter.state.recentRecords["a"] == .failed) - #expect(adapter.state.recentRecords["b"] == .loaded(nil)) - await adapter.load(.cardAppeared("a")) - #expect(await adapter.recentRecordSpy.goalIDs == ["a", "b"]) - - await adapter.recentRecordSpy.setFailure(goalID: "a", fails: false) - await adapter.load(.refresh) - #expect(adapter.state.recentRecords["a"] == .loaded(nil)) - #expect(await adapter.recentRecordSpy.goalIDs.count == 4) - } - - @Test("목표 상태 변경 후 목록과 개수를 갱신하고 이전 기록 응답은 무시한다") - func refreshUpdatesStatusAndRejectsStaleRecords() async throws { - let adapter = try StoreTestAdapter( - goals: [makeDevelopmentGoal(id: "goal")] - ) - await adapter.load(.selected) - await adapter.load(.cardAppeared("goal")) - let recordRevision = adapter.state.recordRevision - let goal = try makeDevelopmentGoal(id: "goal", status: .completed) - await adapter.goalsSpy.setResult(.success([goal])) - await adapter.send(.showDetail("goal")) - await adapter.load(.sheet(.dismiss)) - #expect(adapter.state.filteredGoals.isEmpty) - #expect(adapter.state.count(for: .completed) == 1) - await adapter.send(.recentRecordLoaded("goal", .failed, revision: recordRevision)) - #expect(adapter.state.recentRecords["goal"] == nil) - await adapter.send(.selectStatus(.completed)) - await adapter.load(.cardAppeared("goal")) - #expect(adapter.state.recentRecords["goal"] == .loaded(nil)) - } - - @Test("이전 목록 요청의 성공과 실패 응답은 최신 목록을 덮어쓰지 않는다") - func staleGoalResponses() async throws { - let adapter = try StoreTestAdapter( - goals: [makeDevelopmentGoal(id: "goal")] - ) - await adapter.load(.selected) - let requestID = adapter.state.requestID - await adapter.load(.refresh) - let state = adapter.state - await adapter.send(.goalsLoaded([], requestID: requestID)) - await adapter.send(.goalsFailed(requestID: requestID)) - #expect(adapter.state == state) - } -} diff --git a/Application/Presentation/DevelopmentTab/Tests/DevelopmentGoalListTestDoubles.swift b/Application/Presentation/DevelopmentTab/Tests/DevelopmentGoalListTestDoubles.swift deleted file mode 100644 index ed86e15e..00000000 --- a/Application/Presentation/DevelopmentTab/Tests/DevelopmentGoalListTestDoubles.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// DevelopmentGoalListTestDoubles.swift -// DevelopmentTabTests -// -// Created by opfic on 10/5/26. -// - -import Domain -import Foundation -import PresentationShared -@testable import DevelopmentTab - -@MainActor -struct StoreTestAdapter { - let goalsSpy: FetchDevelopmentGoalsUseCaseSpy - let recentRecordSpy: FetchRecentDevelopmentRecordUseCaseSpy - private let store: TestStoreOf - - var state: DevelopmentGoalListFeature.State { store.state } - - init( - goals: [DevelopmentGoal] = [], - recentRecords records: [String: DevelopmentRecord.Version] = [:] - ) { - let goalsSpy = FetchDevelopmentGoalsUseCaseSpy(goals: goals) - let recentRecordSpy = FetchRecentDevelopmentRecordUseCaseSpy(recentRecords: records) - self.goalsSpy = goalsSpy - self.recentRecordSpy = recentRecordSpy - store = TestStore(initialState: DevelopmentGoalListFeature.State()) { - DevelopmentGoalListFeature() - } withDependencies: { - DevelopmentTabDependencyPreparation.prepareGoalList( - &$0, - goalsUseCase: goalsSpy, - recentRecordUseCase: recentRecordSpy - ) - } - store.exhaustivity = .off(showSkippedAssertions: false) - } - - func send(_ action: DevelopmentGoalListFeature.Action) async { - await store.send(action) - } - - func load(_ action: DevelopmentGoalListFeature.Action) async { - await store.send(action) - await settle() - } - - func settle() async { - await store.finish() - await store.skipReceivedActions(strict: false) - } -} - -actor FetchDevelopmentGoalsUseCaseSpy: FetchDevelopmentGoalsUseCase { - private var result: Result<[DevelopmentGoal], Error> - - init(goals: [DevelopmentGoal]) { - result = .success(goals) - } - - func setResult(_ result: Result<[DevelopmentGoal], Error>) { - self.result = result - } - - func execute(_ query: DevelopmentGoal.Query) async throws -> [DevelopmentGoal] { - try result.get() - } -} - -actor FetchRecentDevelopmentRecordUseCaseSpy: FetchRecentDevelopmentRecordUseCase { - private(set) var goalIDs = [String]() - private var recentRecords: [String: DevelopmentRecord.Version] - private var failures = Set() - private var isSuspended = false - private var continuations = [CheckedContinuation]() - - init(recentRecords records: [String: DevelopmentRecord.Version]) { - self.recentRecords = records - } - - func setFailure(goalID: String, fails: Bool) { - if fails { - failures.insert(goalID) - } else { - failures.remove(goalID) - } - } - - func suspend() { - isSuspended = true - } - - func resume() { - isSuspended = false - let continuations = self.continuations - self.continuations.removeAll() - for continuation in continuations { - continuation.resume() - } - } - - func execute(goalId: String) async throws -> DevelopmentRecord.Version? { - goalIDs.append(goalId) - if isSuspended { - await withCheckedContinuation { continuations.append($0) } - } - if failures.contains(goalId) { throw DevelopmentGoalListTestError.failed } - return recentRecords[goalId] - } -} - -enum DevelopmentGoalListTestError: Error { - case failed -} - -func makeDevelopmentGoal( - id: String, - status: DevelopmentGoal.Status = .inProgress -) throws -> DevelopmentGoal { - try DevelopmentGoal( - id: id, - title: id, - description: "목표 설명", - status: status, - createdAt: Date(timeIntervalSince1970: 0), - updatedAt: Date(timeIntervalSince1970: 0), - completedAt: nil - ) -} - -func makeDevelopmentRecordVersion( - recordID id: String -) throws -> DevelopmentRecord.Version { - try DevelopmentRecord.Version( - id: "\(id)-v1", - recordId: id, - number: 1, - title: "기록 \(id)", - markdownContent: "기록 내용", - kind: .initial, - sourceVersionId: nil, - confirmedAt: Date(timeIntervalSince1970: 0) - ) -} diff --git a/Application/Presentation/Entry/Sources/Dependency/PresentationDependencyPreparation.swift b/Application/Presentation/Entry/Sources/Dependency/PresentationDependencyPreparation.swift index 04826c6e..877cf9e5 100644 --- a/Application/Presentation/Entry/Sources/Dependency/PresentationDependencyPreparation.swift +++ b/Application/Presentation/Entry/Sources/Dependency/PresentationDependencyPreparation.swift @@ -5,7 +5,6 @@ // Created by opfic on 9/7/26. // -import DevelopmentTab import Domain import HomeTab import NotificationTab @@ -47,4 +46,4 @@ public typealias NotificationDependencyPreparation = PushNotificationDependencyP public typealias ProfilePresentationDependencyPreparation = ProfileDependencyPreparation public typealias TodayPresentationDependencyPreparation = TodayDependencyPreparation -public typealias DevelopmentTabPresentationDependencyPreparation = DevelopmentTabDependencyPreparation +public typealias DevelopmentPresentationDependencyPreparation = DevelopmentDependencyPreparation diff --git a/Application/Presentation/Entry/Sources/Main/MainFeature.swift b/Application/Presentation/Entry/Sources/Main/MainFeature.swift index 4952241d..18466735 100644 --- a/Application/Presentation/Entry/Sources/Main/MainFeature.swift +++ b/Application/Presentation/Entry/Sources/Main/MainFeature.swift @@ -151,8 +151,6 @@ private extension MainTab { switch self { case .home: return "home" - case .developmentGoals: - return "development_goals" case .today: return "today" case .notification: diff --git a/Application/Presentation/Entry/Sources/Main/MainView.swift b/Application/Presentation/Entry/Sources/Main/MainView.swift index 26667ab6..721dab02 100644 --- a/Application/Presentation/Entry/Sources/Main/MainView.swift +++ b/Application/Presentation/Entry/Sources/Main/MainView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import DevelopmentTab import HomeTab import NotificationTab import ProfileTab @@ -53,13 +52,6 @@ struct MainView: View { ) { tabContent(.home) } - Tab( - MainTab.developmentGoals.title, - systemImage: MainTab.developmentGoals.symbolName, - value: MainTab.developmentGoals - ) { - tabContent(.developmentGoals) - } Tab( MainTab.today.title, systemImage: MainTab.today.symbolName, @@ -112,8 +104,6 @@ struct MainView: View { isSelected: isSelected, windowEvent: windowEvent ) - case .developmentGoals: - DevelopmentGoalListView(isSelected: isSelected) case .today: TodayView( isSelected: isSelected, diff --git a/Application/Presentation/Entry/Sources/Routing/MainTab.swift b/Application/Presentation/Entry/Sources/Routing/MainTab.swift index 245d33a4..0db4451f 100644 --- a/Application/Presentation/Entry/Sources/Routing/MainTab.swift +++ b/Application/Presentation/Entry/Sources/Routing/MainTab.swift @@ -10,7 +10,6 @@ import PresentationShared public enum MainTab: Hashable, CaseIterable { case home - case developmentGoals case today case notification case profile @@ -19,8 +18,6 @@ public enum MainTab: Hashable, CaseIterable { switch self { case .home: String(localized: "nav_home", bundle: PresentationResources.bundle) - case .developmentGoals: - String(localized: "nav_development_goals", bundle: PresentationResources.bundle) case .today: String(localized: "nav_today", bundle: PresentationResources.bundle) case .notification: @@ -34,8 +31,6 @@ public enum MainTab: Hashable, CaseIterable { switch self { case .home: "house.fill" - case .developmentGoals: - "flag.checkered" case .today: "sun.max.fill" case .notification: diff --git a/Application/Presentation/Entry/Tests/Main/MainFeatureTests.swift b/Application/Presentation/Entry/Tests/Main/MainFeatureTests.swift index 9cf36418..5e5a7392 100644 --- a/Application/Presentation/Entry/Tests/Main/MainFeatureTests.swift +++ b/Application/Presentation/Entry/Tests/Main/MainFeatureTests.swift @@ -40,28 +40,25 @@ struct MainFeatureTests { let expectedEffects = [ reference.reduce(.selectedTabChanged(.home)), - reference.reduce(.selectedTabChanged(.developmentGoals)), reference.reduce(.selectedTabChanged(.today)), reference.reduce(.selectedTabChanged(.notification)), reference.reduce(.selectedTabChanged(.profile)) ] await store.send(.view(.selectedTabChanged(.home))) - await store.send(.view(.selectedTabChanged(.developmentGoals))) await store.send(.view(.selectedTabChanged(.today))) await store.send(.view(.selectedTabChanged(.notification))) await store.send(.view(.selectedTabChanged(.profile))) await waitUntil { - trackAnalyticsEventUseCase.screenNames == ["home", "development_goals", "today", "profile"] + trackAnalyticsEventUseCase.screenNames == ["home", "today", "profile"] } #expect(expectedEffects == [ [.trackScreenView("home")], - [.trackScreenView("development_goals")], [.trackScreenView("today")], [], [.trackScreenView("profile")] ]) - #expect(trackAnalyticsEventUseCase.screenNames == ["home", "development_goals", "today", "profile"]) + #expect(trackAnalyticsEventUseCase.screenNames == ["home", "today", "profile"]) } @Test("MainFeature는 기존 Main 상태관리처럼 unread count를 갱신하고 badge 갱신을 요청한다") @@ -290,8 +287,6 @@ private extension MainTab { switch self { case .home: return "home" - case .developmentGoals: - return "development_goals" case .today: return "today" case .notification: diff --git a/Application/Presentation/DevelopmentTab/Sources/Dependency/DevelopmentTabDependencyPreparation.swift b/Application/Presentation/HomeTab/Sources/Dependency/DevelopmentDependencyPreparation.swift similarity index 93% rename from Application/Presentation/DevelopmentTab/Sources/Dependency/DevelopmentTabDependencyPreparation.swift rename to Application/Presentation/HomeTab/Sources/Dependency/DevelopmentDependencyPreparation.swift index 99c1bcf5..95c758c7 100644 --- a/Application/Presentation/DevelopmentTab/Sources/Dependency/DevelopmentTabDependencyPreparation.swift +++ b/Application/Presentation/HomeTab/Sources/Dependency/DevelopmentDependencyPreparation.swift @@ -1,6 +1,6 @@ // -// DevelopmentTabDependencyPreparation.swift -// DevelopmentTab +// DevelopmentDependencyPreparation.swift +// HomeTab // // Created by opfic on 10/5/26. // @@ -8,16 +8,7 @@ import Domain import PresentationShared -public enum DevelopmentTabDependencyPreparation { - public static func prepareGoalList( - _ dependencies: inout DependencyValues, - goalsUseCase: FetchDevelopmentGoalsUseCase, - recentRecordUseCase: FetchRecentDevelopmentRecordUseCase - ) { - dependencies.developmentTabFetchGoalsUseCase = goalsUseCase - dependencies.developmentTabFetchRecentRecordUseCase = recentRecordUseCase - } - +public enum DevelopmentDependencyPreparation { public static func prepareGoal( _ dependencies: inout DependencyValues, createGoalUseCase: CreateDevelopmentGoalUseCase, diff --git a/Application/Presentation/DevelopmentTab/Sources/Goal/Create/GoalCreateFeature.swift b/Application/Presentation/HomeTab/Sources/Goal/Create/GoalCreateFeature.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Goal/Create/GoalCreateFeature.swift rename to Application/Presentation/HomeTab/Sources/Goal/Create/GoalCreateFeature.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Goal/Create/GoalCreateTodoSelectionFeature.swift b/Application/Presentation/HomeTab/Sources/Goal/Create/GoalCreateTodoSelectionFeature.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Goal/Create/GoalCreateTodoSelectionFeature.swift rename to Application/Presentation/HomeTab/Sources/Goal/Create/GoalCreateTodoSelectionFeature.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Goal/Create/GoalCreateView.swift b/Application/Presentation/HomeTab/Sources/Goal/Create/GoalCreateView.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Goal/Create/GoalCreateView.swift rename to Application/Presentation/HomeTab/Sources/Goal/Create/GoalCreateView.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Goal/GoalDescriptionCard.swift b/Application/Presentation/HomeTab/Sources/Goal/GoalDescriptionCard.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Goal/GoalDescriptionCard.swift rename to Application/Presentation/HomeTab/Sources/Goal/GoalDescriptionCard.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Goal/TodoSelection/GoalCreateTodoSelectionSheet.swift b/Application/Presentation/HomeTab/Sources/Goal/TodoSelection/GoalCreateTodoSelectionSheet.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Goal/TodoSelection/GoalCreateTodoSelectionSheet.swift rename to Application/Presentation/HomeTab/Sources/Goal/TodoSelection/GoalCreateTodoSelectionSheet.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Goal/TodoSelection/GoalTodoSelectionViews.swift b/Application/Presentation/HomeTab/Sources/Goal/TodoSelection/GoalTodoSelectionViews.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Goal/TodoSelection/GoalTodoSelectionViews.swift rename to Application/Presentation/HomeTab/Sources/Goal/TodoSelection/GoalTodoSelectionViews.swift diff --git a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift index 35454204..70b5dee8 100644 --- a/Application/Presentation/HomeTab/Sources/Home/HomeView.swift +++ b/Application/Presentation/HomeTab/Sources/Home/HomeView.swift @@ -7,7 +7,6 @@ import SwiftUI import Combine -import DevelopmentTab import Domain import PresentationShared diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/Detail/RecordDetailFeature.swift b/Application/Presentation/HomeTab/Sources/Record/Detail/RecordDetailFeature.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/Detail/RecordDetailFeature.swift rename to Application/Presentation/HomeTab/Sources/Record/Detail/RecordDetailFeature.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/Detail/RecordDetailView.swift b/Application/Presentation/HomeTab/Sources/Record/Detail/RecordDetailView.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/Detail/RecordDetailView.swift rename to Application/Presentation/HomeTab/Sources/Record/Detail/RecordDetailView.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/Editor/RecordEditorFeature.swift b/Application/Presentation/HomeTab/Sources/Record/Editor/RecordEditorFeature.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/Editor/RecordEditorFeature.swift rename to Application/Presentation/HomeTab/Sources/Record/Editor/RecordEditorFeature.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/Editor/RecordEditorView.swift b/Application/Presentation/HomeTab/Sources/Record/Editor/RecordEditorView.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/Editor/RecordEditorView.swift rename to Application/Presentation/HomeTab/Sources/Record/Editor/RecordEditorView.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalDetailFeature+Support.swift b/Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalDetailFeature+Support.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalDetailFeature+Support.swift rename to Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalDetailFeature+Support.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalDetailFeature.swift b/Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalDetailFeature.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalDetailFeature.swift rename to Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalDetailFeature.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalDetailRecordActions.swift b/Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalDetailRecordActions.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalDetailRecordActions.swift rename to Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalDetailRecordActions.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalDetailView.swift b/Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalDetailView.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalDetailView.swift rename to Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalDetailView.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalDetailViewSupport.swift b/Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalDetailViewSupport.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalDetailViewSupport.swift rename to Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalDetailViewSupport.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalLinkedTodoCard.swift b/Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalLinkedTodoCard.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalLinkedTodoCard.swift rename to Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalLinkedTodoCard.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalTodoLinkFeature.swift b/Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalTodoLinkFeature.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalTodoLinkFeature.swift rename to Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalTodoLinkFeature.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalTodoLinkSheet.swift b/Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalTodoLinkSheet.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/GoalDetail/GoalTodoLinkSheet.swift rename to Application/Presentation/HomeTab/Sources/Record/GoalDetail/GoalTodoLinkSheet.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/History/RecordVersionDetailView.swift b/Application/Presentation/HomeTab/Sources/Record/History/RecordVersionDetailView.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/History/RecordVersionDetailView.swift rename to Application/Presentation/HomeTab/Sources/Record/History/RecordVersionDetailView.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/History/RecordVersionHistoryView.swift b/Application/Presentation/HomeTab/Sources/Record/History/RecordVersionHistoryView.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/History/RecordVersionHistoryView.swift rename to Application/Presentation/HomeTab/Sources/Record/History/RecordVersionHistoryView.swift diff --git a/Application/Presentation/DevelopmentTab/Sources/Record/RecordPresentation.swift b/Application/Presentation/HomeTab/Sources/Record/RecordPresentation.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Sources/Record/RecordPresentation.swift rename to Application/Presentation/HomeTab/Sources/Record/RecordPresentation.swift diff --git a/Application/Presentation/DevelopmentTab/Tests/Goal/Create/GoalCreateFeatureTests.swift b/Application/Presentation/HomeTab/Tests/Goal/Create/GoalCreateFeatureTests.swift similarity index 99% rename from Application/Presentation/DevelopmentTab/Tests/Goal/Create/GoalCreateFeatureTests.swift rename to Application/Presentation/HomeTab/Tests/Goal/Create/GoalCreateFeatureTests.swift index 143a0d43..4e90402c 100644 --- a/Application/Presentation/DevelopmentTab/Tests/Goal/Create/GoalCreateFeatureTests.swift +++ b/Application/Presentation/HomeTab/Tests/Goal/Create/GoalCreateFeatureTests.swift @@ -8,7 +8,7 @@ import Testing import Domain import PresentationShared -@testable import DevelopmentTab +@testable import HomeTab @MainActor struct GoalCreateFeatureTests { diff --git a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSpies.swift b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSpies.swift index 7fa000cd..5680c2ff 100644 --- a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSpies.swift +++ b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSpies.swift @@ -73,7 +73,7 @@ actor FetchRecentDevelopmentRecordUseCaseSpy: FetchRecentDevelopmentRecordUseCas } } -final class FetchTodosUseCaseSpy: FetchTodosUseCase { +final class HomeFetchTodosUseCaseSpy: FetchTodosUseCase { private(set) var queries = [TodoQuery]() var page = TodoPage(items: [], nextCursor: nil) diff --git a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift index a7dd6ef3..8a2c2df0 100644 --- a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift +++ b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTestSupport.swift @@ -37,7 +37,7 @@ struct StoreTestAdapter { networkConnectivityUseCase: ObserveNetworkConnectivityUseCase = ObserveNetworkConnectivityUseCaseSpy(), fetchDevelopmentGoalsUseCase: FetchDevelopmentGoalsUseCase = FetchDevelopmentGoalsUseCaseSpy(), recentRecordUseCase: FetchRecentDevelopmentRecordUseCase = FetchRecentDevelopmentRecordUseCaseSpy(), - fetchTodosUseCase: FetchTodosUseCase = FetchTodosUseCaseSpy(), + fetchTodosUseCase: FetchTodosUseCase = HomeFetchTodosUseCaseSpy(), trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase = TrackAnalyticsEventUseCaseSpy(), configureDependencies: ((inout DependencyValues) -> Void)? = nil ) { diff --git a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTests.swift b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTests.swift index 02b97a07..965fd6e0 100644 --- a/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTests.swift +++ b/Application/Presentation/HomeTab/Tests/Home/HomeFeatureTests.swift @@ -54,7 +54,7 @@ struct HomeFeatureTests { @Test("HomeFeature fetchData는 연결 Todo 완료 수로 목표 진행률을 계산한다") func HomeFeature_fetchData는_연결_Todo_완료_수로_목표_진행률을_계산한다() async throws { let goal = try makeDevelopmentGoal(id: "goal", createdAt: 1) - let todosSpy = FetchTodosUseCaseSpy() + let todosSpy = HomeFetchTodosUseCaseSpy() todosSpy.page = TodoPage( items: [ makeHomeTodo(id: "completed", goalID: goal.id, isCompleted: true), diff --git a/Application/Presentation/DevelopmentTab/Tests/Record/GoalDetailDestinationTests.swift b/Application/Presentation/HomeTab/Tests/Record/GoalDetailDestinationTests.swift similarity index 98% rename from Application/Presentation/DevelopmentTab/Tests/Record/GoalDetailDestinationTests.swift rename to Application/Presentation/HomeTab/Tests/Record/GoalDetailDestinationTests.swift index c923d010..81d342f9 100644 --- a/Application/Presentation/DevelopmentTab/Tests/Record/GoalDetailDestinationTests.swift +++ b/Application/Presentation/HomeTab/Tests/Record/GoalDetailDestinationTests.swift @@ -9,7 +9,7 @@ import Domain import Foundation import PresentationShared import Testing -@testable import DevelopmentTab +@testable import HomeTab @MainActor struct GoalDetailDestinationTests { diff --git a/Application/Presentation/DevelopmentTab/Tests/Record/GoalDetailFeatureTests.swift b/Application/Presentation/HomeTab/Tests/Record/GoalDetailFeatureTests.swift similarity index 99% rename from Application/Presentation/DevelopmentTab/Tests/Record/GoalDetailFeatureTests.swift rename to Application/Presentation/HomeTab/Tests/Record/GoalDetailFeatureTests.swift index a702b819..d2a092a8 100644 --- a/Application/Presentation/DevelopmentTab/Tests/Record/GoalDetailFeatureTests.swift +++ b/Application/Presentation/HomeTab/Tests/Record/GoalDetailFeatureTests.swift @@ -9,7 +9,7 @@ import Testing import Domain import Foundation import PresentationShared -@testable import DevelopmentTab +@testable import HomeTab @MainActor struct GoalDetailFeatureTests { diff --git a/Application/Presentation/DevelopmentTab/Tests/Record/GoalDetailTodoFeatureTests.swift b/Application/Presentation/HomeTab/Tests/Record/GoalDetailTodoFeatureTests.swift similarity index 99% rename from Application/Presentation/DevelopmentTab/Tests/Record/GoalDetailTodoFeatureTests.swift rename to Application/Presentation/HomeTab/Tests/Record/GoalDetailTodoFeatureTests.swift index 73afbd8d..a264e8ca 100644 --- a/Application/Presentation/DevelopmentTab/Tests/Record/GoalDetailTodoFeatureTests.swift +++ b/Application/Presentation/HomeTab/Tests/Record/GoalDetailTodoFeatureTests.swift @@ -9,7 +9,7 @@ import Testing import Core import Domain import PresentationShared -@testable import DevelopmentTab +@testable import HomeTab @MainActor struct GoalDetailTodoFeatureTests { diff --git a/Application/Presentation/DevelopmentTab/Tests/Record/GoalDetailTodoTestSupport.swift b/Application/Presentation/HomeTab/Tests/Record/GoalDetailTodoTestSupport.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Tests/Record/GoalDetailTodoTestSupport.swift rename to Application/Presentation/HomeTab/Tests/Record/GoalDetailTodoTestSupport.swift diff --git a/Application/Presentation/DevelopmentTab/Tests/Record/GoalTodoLinkFeatureTests.swift b/Application/Presentation/HomeTab/Tests/Record/GoalTodoLinkFeatureTests.swift similarity index 99% rename from Application/Presentation/DevelopmentTab/Tests/Record/GoalTodoLinkFeatureTests.swift rename to Application/Presentation/HomeTab/Tests/Record/GoalTodoLinkFeatureTests.swift index 4238e013..0d4bb779 100644 --- a/Application/Presentation/DevelopmentTab/Tests/Record/GoalTodoLinkFeatureTests.swift +++ b/Application/Presentation/HomeTab/Tests/Record/GoalTodoLinkFeatureTests.swift @@ -9,7 +9,7 @@ import Testing import Core import Domain import PresentationShared -@testable import DevelopmentTab +@testable import HomeTab @MainActor struct GoalTodoLinkFeatureTests { diff --git a/Application/Presentation/DevelopmentTab/Tests/Record/RecordDetailFeatureTests.swift b/Application/Presentation/HomeTab/Tests/Record/RecordDetailFeatureTests.swift similarity index 98% rename from Application/Presentation/DevelopmentTab/Tests/Record/RecordDetailFeatureTests.swift rename to Application/Presentation/HomeTab/Tests/Record/RecordDetailFeatureTests.swift index d07ad59b..8193dd9d 100644 --- a/Application/Presentation/DevelopmentTab/Tests/Record/RecordDetailFeatureTests.swift +++ b/Application/Presentation/HomeTab/Tests/Record/RecordDetailFeatureTests.swift @@ -8,14 +8,19 @@ import Foundation import Testing import PresentationShared -@testable import DevelopmentTab +@testable import HomeTab @MainActor struct RecordDetailFeatureTests { @Test("편집과 이력 화면 표시는 Feature 상태를 전환한다") func 편집과_이력_화면_표시는_Feature_상태를_전환한다() async throws { - let currentVersion = try makeDevelopmentRecordVersion(id: "version-2", number: 2) let previousVersion = try makeDevelopmentRecordVersion(id: "version-1") + let currentVersion = try makeDevelopmentRecordVersion( + id: "version-2", + number: 2, + kind: .correction, + sourceVersionId: previousVersion.id + ) let record = try makeConfirmedDevelopmentRecord( versionId: currentVersion.id, versionNumber: currentVersion.number diff --git a/Application/Presentation/DevelopmentTab/Tests/Record/RecordEditorFeatureTests.swift b/Application/Presentation/HomeTab/Tests/Record/RecordEditorFeatureTests.swift similarity index 95% rename from Application/Presentation/DevelopmentTab/Tests/Record/RecordEditorFeatureTests.swift rename to Application/Presentation/HomeTab/Tests/Record/RecordEditorFeatureTests.swift index 38d514a0..132183c0 100644 --- a/Application/Presentation/DevelopmentTab/Tests/Record/RecordEditorFeatureTests.swift +++ b/Application/Presentation/HomeTab/Tests/Record/RecordEditorFeatureTests.swift @@ -8,7 +8,7 @@ import Testing import Domain import PresentationShared -@testable import DevelopmentTab +@testable import HomeTab @MainActor struct RecordEditorFeatureTests { @@ -114,6 +114,9 @@ struct RecordEditorFeatureTests { ) { RecordEditorFeature() } withDependencies: { + $0.developmentCreateRecordUseCase = CreateDevelopmentRecordUseCaseStub( + result: .failure(RecordTestError.failed) + ) $0.developmentSaveRecordDraftUseCase = SaveDevelopmentRecordDraftUseCaseStub( result: .success(preparedRecord) ) @@ -171,6 +174,12 @@ struct RecordEditorFeatureTests { RecordEditorFeature() } withDependencies: { $0.developmentCreateRecordUseCase = createSpy + $0.developmentSaveRecordDraftUseCase = SaveDevelopmentRecordDraftUseCaseStub( + result: .failure(RecordTestError.failed) + ) + $0.developmentConfirmRecordUseCase = ConfirmDevelopmentRecordUseCaseStub( + result: .failure(RecordTestError.failed) + ) } await store.send(.binding(.set(\.title, "기록 제목"))) { $0.title = "기록 제목" diff --git a/Application/Presentation/DevelopmentTab/Tests/Record/RecordFeatureTestSupport.swift b/Application/Presentation/HomeTab/Tests/Record/RecordFeatureTestSupport.swift similarity index 100% rename from Application/Presentation/DevelopmentTab/Tests/Record/RecordFeatureTestSupport.swift rename to Application/Presentation/HomeTab/Tests/Record/RecordFeatureTestSupport.swift diff --git a/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings b/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings index 7614e770..32893f59 100644 --- a/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings +++ b/Application/Presentation/PresentationShared/Resources/Localizable.xcstrings @@ -1433,108 +1433,6 @@ } } }, - "development_goals_create" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Create a Goal" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "새 목표 만들기" - } - } - } - }, - "development_goals_empty_archived" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No archived development goals." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "보관한 개발 목표가 없어요." - } - } - } - }, - "development_goals_empty_completed" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No completed development goals." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "완료한 개발 목표가 없어요." - } - } - } - }, - "development_goals_empty_in_progress" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No development goals in progress." - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "진행 중인 개발 목표가 없어요." - } - } - } - }, - "development_goals_recent_record_date_format" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Written %1$@ · %2$@ (%3$@)" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ 작성됨 · %2$@ (%3$@)" - } - } - } - }, - "development_goals_recent_record_format" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Latest record: %@" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "최근 기록: %@" - } - } - } - }, "development_record_add" : { "extractionState" : "manual", "localizations" : { @@ -2793,23 +2691,6 @@ } } }, - "nav_development_goals" : { - "extractionState" : "manual", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Development Goals" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "개발 목표" - } - } - } - }, "nav_home" : { "extractionState" : "manual", "localizations" : { diff --git a/Application/Presentation/Project.swift b/Application/Presentation/Project.swift index 89853291..7f69ea25 100644 --- a/Application/Presentation/Project.swift +++ b/Application/Presentation/Project.swift @@ -104,7 +104,6 @@ let project = Project( dependencies: [ .project(target: "Domain", path: "../Domain"), .project(target: "Core", path: "../Core"), - .target(name: "DevelopmentTab"), .target(name: "PresentationShared") ], settings: frameworkBuildSettings @@ -122,63 +121,17 @@ let project = Project( configPath: "HomeTab/Tests/.swiftlint.yml" ) ], - dependencies: [ - .target(name: "HomeTab"), - .target(name: "DevelopmentTab"), - .target(name: "PresentationShared"), - thirdPartyDependency, - ], - settings: .devlog( - base: [ - "ENABLE_USER_SCRIPT_SANDBOXING": "NO", - "TEST_TARGET_NAME": "HomeTab" - ] - ) - ), - .target( - name: "DevelopmentTab", - destinations: .iOS, - product: .staticFramework, - bundleId: "com.opfic.DevLog.DevelopmentTab", - infoPlist: .file(path: frameworkInfoPlistPath), - sources: ["DevelopmentTab/Sources/**/*.swift"], - scripts: [ - DevLogScripts.swiftLint( - sourcePath: "DevelopmentTab/Sources", - configPath: "DevelopmentTab/Sources/.swiftlint.yml" - ) - ], - dependencies: [ - .project(target: "Domain", path: "../Domain"), - .project(target: "Core", path: "../Core"), - .target(name: "PresentationShared") - ], - settings: frameworkBuildSettings - ), - .target( - name: "DevelopmentTabTests", - destinations: .iOS, - product: .unitTests, - bundleId: "com.opfic.DevLog.DevelopmentTabTests", - infoPlist: .file(path: testsInfoPlistPath), - sources: ["DevelopmentTab/Tests/**/*.swift"], - scripts: [ - DevLogScripts.swiftLint( - sourcePath: "DevelopmentTab/Tests", - configPath: "DevelopmentTab/Tests/.swiftlint.yml" - ) - ], dependencies: [ .project(target: "Domain", path: "../Domain"), .project(target: "Core", path: "../Core"), - .target(name: "DevelopmentTab"), + .target(name: "HomeTab"), .target(name: "PresentationShared"), thirdPartyDependency, ], settings: .devlog( base: [ "ENABLE_USER_SCRIPT_SANDBOXING": "NO", - "TEST_TARGET_NAME": "DevelopmentTab" + "TEST_TARGET_NAME": "HomeTab" ] ) ), @@ -338,7 +291,6 @@ let project = Project( .project(target: "Domain", path: "../Domain"), .project(target: "Core", path: "../Core"), .target(name: "HomeTab"), - .target(name: "DevelopmentTab"), .target(name: "TodayTab"), .target(name: "NotificationTab"), .target(name: "ProfileTab"), @@ -388,7 +340,6 @@ let project = Project( ], dependencies: [ .target(name: "Entry"), - .target(name: "DevelopmentTab"), .target(name: "PresentationShared") ], settings: frameworkBuildSettings diff --git a/Application/Presentation/Sources/EntryExports.swift b/Application/Presentation/Sources/EntryExports.swift index 2aeb24b5..cef7895c 100644 --- a/Application/Presentation/Sources/EntryExports.swift +++ b/Application/Presentation/Sources/EntryExports.swift @@ -6,5 +6,4 @@ // @_exported import Entry -@_exported import DevelopmentTab @_exported import PresentationShared diff --git a/README.md b/README.md index 2985f4e6..31e0e807 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ - `Presentation` target은 `App`의 기존 import를 유지하는 re-export 역할 - `Entry`: root/auth/tab shell/window 흐름 소유 - `PresentationShared`: 공통 Todo/Search/Loading 흐름 소유 - - `DevelopmentTab`: 개발 목표 탭과 개발 목표, 개발 기록 화면 소유 - - `HomeTab`, `TodayTab`, `NotificationTab`, `ProfileTab`: 탭 단위 흐름 소유 + - `HomeTab`: 홈 탭과 개발 목표, 개발 기록 화면 소유 + - `TodayTab`, `NotificationTab`, `ProfileTab`: 탭 단위 흐름 소유 - `MarkdownRenderer` target은 SwiftUI 공개 화면과 참조 값, 내부 `WKWebView` 연결, HTML/JavaScript/CSS 자원과 TypeScript Tooling을 소유함 - `PresentationShared`에서는 `TodoMarkdownContentView`만 `MarkdownRenderer`를 직접 import하며 재노출하지 않음 diff --git a/docs/graph.png b/docs/graph.png index a86e9d76..9b96822d 100644 Binary files a/docs/graph.png and b/docs/graph.png differ