From 473e87c7e1caf13f66f7ea7d38b3d98840e2b490 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 30 Jul 2026 20:56:59 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20role:=20confirm=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20prominent=20=ED=9A=A8=EA=B3=BC=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Extension/View+Alert.swift | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 Application/Presentation/PresentationShared/Sources/Extension/View+Alert.swift diff --git a/Application/Presentation/PresentationShared/Sources/Extension/View+Alert.swift b/Application/Presentation/PresentationShared/Sources/Extension/View+Alert.swift new file mode 100644 index 00000000..0e691d75 --- /dev/null +++ b/Application/Presentation/PresentationShared/Sources/Extension/View+Alert.swift @@ -0,0 +1,62 @@ +// +// View+Alert.swift +// PresentationShared +// +// Created by opfic on 7/30/26. +// + +import ComposableArchitecture +import SwiftUI + +public extension View { + @preconcurrency @MainActor + @ViewBuilder + func prominentAlert( + _ store: Store, + state: KeyPath?>, + action: CaseKeyPath> + ) -> some View where State: ObservableState { + @Bindable var store = store + let item = $store.scope(state: state, action: action) + let alertStore = item.wrappedValue + let alertState = store.state[keyPath: state] + + alert( + alertState.map(\.title).map(Text.init) ?? Text(verbatim: ""), + isPresented: Binding(item), + presenting: alertState, + actions: { alertState in + ForEach(alertState.buttons) { button in + let usesDefaultAction = alertState.usesDefaultAction(for: button) + let role = usesDefaultAction ? nil : button.role.map(ButtonRole.init) + + Button( + role: role, + action: { + button.withAction { action in + if let action { + alertStore?.send(action) + } + } + } + ) { + Text(button.label) + } + .keyboardShortcut( + usesDefaultAction ? .defaultAction : nil + ) + } + }, + message: { + $0.message.map(Text.init) + } + ) + } +} + +extension AlertState { + func usesDefaultAction(for button: ButtonState) -> Bool { + guard 1 < buttons.count else { return true } + return buttons.first { $0.role == .destructive }?.id == button.id + } +} From 9b37029580f98b6787a55482d2f98d2ab6e32f3d Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 30 Jul 2026 20:57:05 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20=EB=AA=A8=EB=94=94=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/Presentation/Entry/Sources/Login/LoginView.swift | 2 +- Application/Presentation/Entry/Sources/Main/MainView.swift | 2 +- Application/Presentation/Entry/Sources/Root/RootView.swift | 2 +- .../HomeTab/Sources/Home/Category/CategoryManageView.swift | 2 +- .../Presentation/HomeTab/Sources/Home/Home/HomeView.swift | 2 +- .../Presentation/HomeTab/Sources/Home/Search/SearchView.swift | 2 +- .../Sources/PushNotification/PushNotificationListView.swift | 2 +- .../PresentationShared/Sources/Todo/Detail/TodoDetailView.swift | 2 +- .../PresentationShared/Sources/Todo/Editor/TodoEditorView.swift | 2 +- .../PresentationShared/Sources/Todo/List/TodoListView.swift | 2 +- .../Presentation/ProfileTab/Sources/Profile/ProfileView.swift | 2 +- .../Presentation/ProfileTab/Sources/Settings/AccountView.swift | 2 +- .../Sources/Settings/PushNotificationSettingsView.swift | 2 +- .../Presentation/ProfileTab/Sources/Settings/SettingsView.swift | 2 +- Application/Presentation/TodayTab/Sources/Today/TodayView.swift | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Application/Presentation/Entry/Sources/Login/LoginView.swift b/Application/Presentation/Entry/Sources/Login/LoginView.swift index 41db45f9..58ce4316 100644 --- a/Application/Presentation/Entry/Sources/Login/LoginView.swift +++ b/Application/Presentation/Entry/Sources/Login/LoginView.swift @@ -59,7 +59,7 @@ struct LoginView: View { .multilineTextAlignment(.center) .padding(.vertical) } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .background { WindowSceneIdentifierReader { store.send(.setPresentationContext( diff --git a/Application/Presentation/Entry/Sources/Main/MainView.swift b/Application/Presentation/Entry/Sources/Main/MainView.swift index b80772ee..9be0b5a3 100644 --- a/Application/Presentation/Entry/Sources/Main/MainView.swift +++ b/Application/Presentation/Entry/Sources/Main/MainView.swift @@ -74,7 +74,7 @@ struct MainView: View { profileViewCoordinator.fetchData() } } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .toastHost() } diff --git a/Application/Presentation/Entry/Sources/Root/RootView.swift b/Application/Presentation/Entry/Sources/Root/RootView.swift index 8c3efbb5..6a24a21f 100644 --- a/Application/Presentation/Entry/Sources/Root/RootView.swift +++ b/Application/Presentation/Entry/Sources/Root/RootView.swift @@ -66,7 +66,7 @@ public struct RootView: View { guard let mainTab = widgetURLTab(url) else { return } store.send(.openWidgetRoute(mainTab)) } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .sheet(item: $store.scope(state: \.sheet, action: \.sheet)) { sheetStore in sheetContent(todoId: sheetStore.todoId) { sheetStore.send(.tapCloseButton) diff --git a/Application/Presentation/HomeTab/Sources/Home/Category/CategoryManageView.swift b/Application/Presentation/HomeTab/Sources/Home/Category/CategoryManageView.swift index 6e9baf11..f3a69db6 100644 --- a/Application/Presentation/HomeTab/Sources/Home/Category/CategoryManageView.swift +++ b/Application/Presentation/HomeTab/Sources/Home/Category/CategoryManageView.swift @@ -55,7 +55,7 @@ struct CategoryManageView: View { .sheet(item: $store.scope(state: \.categorySheet, action: \.categorySheet)) { sheetStore in sheetContent(sheetStore) } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button { diff --git a/Application/Presentation/HomeTab/Sources/Home/Home/HomeView.swift b/Application/Presentation/HomeTab/Sources/Home/Home/HomeView.swift index b00fc21e..0d846249 100644 --- a/Application/Presentation/HomeTab/Sources/Home/Home/HomeView.swift +++ b/Application/Presentation/HomeTab/Sources/Home/Home/HomeView.swift @@ -35,7 +35,7 @@ public struct HomeView: View { .listStyle(.insetGrouped) .navigationTitle(String(localized: "nav_home")) .toolbar { toolbar } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .sheet(item: $store.scope(state: \.sheet, action: \.sheet), content: sheetContent) .fullScreenCover(item: $store.scope(state: \.fullScreenCover, action: \.fullScreenCover), content: coverContent) } diff --git a/Application/Presentation/HomeTab/Sources/Home/Search/SearchView.swift b/Application/Presentation/HomeTab/Sources/Home/Search/SearchView.swift index d47fb38d..db434e03 100644 --- a/Application/Presentation/HomeTab/Sources/Home/Search/SearchView.swift +++ b/Application/Presentation/HomeTab/Sources/Home/Search/SearchView.swift @@ -55,7 +55,7 @@ struct SearchView: View { dismiss() } } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) } } diff --git a/Application/Presentation/NotificationTab/Sources/PushNotification/PushNotificationListView.swift b/Application/Presentation/NotificationTab/Sources/PushNotification/PushNotificationListView.swift index 1c622d5a..d93f6f64 100644 --- a/Application/Presentation/NotificationTab/Sources/PushNotification/PushNotificationListView.swift +++ b/Application/Presentation/NotificationTab/Sources/PushNotification/PushNotificationListView.swift @@ -46,7 +46,7 @@ public struct PushNotificationListView: View { .navigationTitle(String(localized: "nav_push_notifications")) .listStyle(.plain) } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .sheet(item: sheetStore) { store in sheetContent(store) } diff --git a/Application/Presentation/PresentationShared/Sources/Todo/Detail/TodoDetailView.swift b/Application/Presentation/PresentationShared/Sources/Todo/Detail/TodoDetailView.swift index 6bf4d7c9..0b9832de 100644 --- a/Application/Presentation/PresentationShared/Sources/Todo/Detail/TodoDetailView.swift +++ b/Application/Presentation/PresentationShared/Sources/Todo/Detail/TodoDetailView.swift @@ -37,7 +37,7 @@ public struct TodoDetailView: View { } .onAppear { store.send(.onAppear) } .navigationBarTitleDisplayMode(.inline) - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .sheet(item: $store.scope(state: \.sheet, action: \.sheet)) { store in sheetContent(store) } diff --git a/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift b/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift index 80874080..70549c04 100644 --- a/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift +++ b/Application/Presentation/PresentationShared/Sources/Todo/Editor/TodoEditorView.swift @@ -80,7 +80,7 @@ public struct TodoEditorView: View { .disabled(!store.isReadyToSubmit) } } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) } } diff --git a/Application/Presentation/PresentationShared/Sources/Todo/List/TodoListView.swift b/Application/Presentation/PresentationShared/Sources/Todo/List/TodoListView.swift index ce7c21dd..150532a9 100644 --- a/Application/Presentation/PresentationShared/Sources/Todo/List/TodoListView.swift +++ b/Application/Presentation/PresentationShared/Sources/Todo/List/TodoListView.swift @@ -57,7 +57,7 @@ public struct TodoListView: View { ) } } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .navigationTitle(TodoCategoryItem(from: store.category).localizedName) .fullScreenCover( item: $store.scope(state: \.fullScreenCover, action: \.fullScreenCover) diff --git a/Application/Presentation/ProfileTab/Sources/Profile/ProfileView.swift b/Application/Presentation/ProfileTab/Sources/Profile/ProfileView.swift index fb6807da..4cadab18 100644 --- a/Application/Presentation/ProfileTab/Sources/Profile/ProfileView.swift +++ b/Application/Presentation/ProfileTab/Sources/Profile/ProfileView.swift @@ -42,7 +42,7 @@ public struct ProfileView: View { .onChange(of: focused) { _, newValue in store.send(.updateStatusTextFieldFocus(newValue), animation: .default) } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .sheet(isPresented: $store.showQuarterPicker) { quarterPickerSheet } .overlay { if store.isLoading { diff --git a/Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift b/Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift index e07b3fb4..285146aa 100644 --- a/Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift @@ -66,7 +66,7 @@ struct AccountView: View { .listStyle(.insetGrouped) .navigationTitle(String(localized: "nav_account")) .onAppear { store.send(.onAppear) } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .background { WindowSceneIdentifierReader { store.send(.setPresentationContext( diff --git a/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsView.swift b/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsView.swift index d45e6b2a..4b5b02b7 100644 --- a/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsView.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/PushNotificationSettingsView.swift @@ -70,7 +70,7 @@ struct PushNotificationSettingsView: View { .listStyle(.insetGrouped) .navigationTitle(String(localized: "nav_push_settings")) .onAppear { store.send(.fetchSettings) } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .sheet(item: $store.scope(state: \.timePicker, action: \.timePicker)) { timePickerStore in TimePickerView( store: timePickerStore, diff --git a/Application/Presentation/ProfileTab/Sources/Settings/SettingsView.swift b/Application/Presentation/ProfileTab/Sources/Settings/SettingsView.swift index 38e12f77..717a896f 100644 --- a/Application/Presentation/ProfileTab/Sources/Settings/SettingsView.swift +++ b/Application/Presentation/ProfileTab/Sources/Settings/SettingsView.swift @@ -126,7 +126,7 @@ struct SettingsView: View { } .navigationTitle(String(localized: "nav_settings")) .navigationBarTitleDisplayMode(.inline) - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .onAppear { store.send(.updateDirSize) } diff --git a/Application/Presentation/TodayTab/Sources/Today/TodayView.swift b/Application/Presentation/TodayTab/Sources/Today/TodayView.swift index d6f86320..5efb898f 100644 --- a/Application/Presentation/TodayTab/Sources/Today/TodayView.swift +++ b/Application/Presentation/TodayTab/Sources/Today/TodayView.swift @@ -40,7 +40,7 @@ public struct TodayView: View { .toolbar { toolbarContent } .background(NavigationBarConfigurator()) .refreshable { await store.send(.refresh).finish() } - .alert($store.scope(state: \.alert, action: \.alert)) + .prominentAlert(store, state: \.alert, action: \.alert) .overlay { if store.isLoading { LoadingView() From 51535778b69b8a87bf2534b5cf44f7ed35fd0de1 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 30 Jul 2026 21:33:33 +0900 Subject: [PATCH 3/5] =?UTF-8?q?chore:=20superpowers=20git=20=EC=B6=94?= =?UTF-8?q?=EC=A0=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b2044eeb..02788119 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +## AI +docs/superpowers + # MACOS .DS_Store From 7c8edfc4b976d3ddce3dba24d89ac190f1dc65e9 Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 31 Jul 2026 00:36:22 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20os=EB=B3=84=20Alert=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=97=AD=ED=95=A0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Extension/View+Alert.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Application/Presentation/PresentationShared/Sources/Extension/View+Alert.swift b/Application/Presentation/PresentationShared/Sources/Extension/View+Alert.swift index 0e691d75..f169c85e 100644 --- a/Application/Presentation/PresentationShared/Sources/Extension/View+Alert.swift +++ b/Application/Presentation/PresentationShared/Sources/Extension/View+Alert.swift @@ -28,10 +28,9 @@ public extension View { actions: { alertState in ForEach(alertState.buttons) { button in let usesDefaultAction = alertState.usesDefaultAction(for: button) - let role = usesDefaultAction ? nil : button.role.map(ButtonRole.init) Button( - role: role, + role: alertState.buttonRole(for: button), action: { button.withAction { action in if let action { @@ -55,6 +54,12 @@ public extension View { } extension AlertState { + func buttonRole(for button: ButtonState) -> ButtonRole? { + if #available(iOS 26, *), usesDefaultAction(for: button) { return .confirm } + if buttons.count == 1 { return nil } + return button.role.map(ButtonRole.init) + } + func usesDefaultAction(for button: ButtonState) -> Bool { guard 1 < buttons.count else { return true } return buttons.first { $0.role == .destructive }?.id == button.id From a99f331bd970b628d2308afa180c5fbbc23824fb Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 31 Jul 2026 01:04:55 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore:=20superpowers=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20Git=20=EC=B6=94=EC=A0=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-06-01-tuist-migration.md | 297 ------------------ .../2026-06-01-tuist-migration-design.md | 136 -------- 2 files changed, 433 deletions(-) delete mode 100644 docs/superpowers/plans/2026-06-01-tuist-migration.md delete mode 100644 docs/superpowers/specs/2026-06-01-tuist-migration-design.md diff --git a/docs/superpowers/plans/2026-06-01-tuist-migration.md b/docs/superpowers/plans/2026-06-01-tuist-migration.md deleted file mode 100644 index 04485513..00000000 --- a/docs/superpowers/plans/2026-06-01-tuist-migration.md +++ /dev/null @@ -1,297 +0,0 @@ -# Tuist Migration Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** DevLog의 기존 모듈 의존성 구조를 유지한 채 Tuist 표준 매니페스트와 생성 워크플로로 전환 - -**Architecture:** 루트 workspace와 모듈별 project manifest를 분리하고, 공통 설정과 패키지 선언은 `Tuist/ProjectDescriptionHelpers`와 `Tuist/Package.swift`로 중앙화한다. 기존 Xcode 프로젝트의 논리적 타깃 경계는 유지하되, `WidgetExtension`은 Tuist 표준에 맞게 독립 프로젝트로 분리한다. - -**Tech Stack:** Tuist 4.194.4, Xcode workspace, SwiftPM, SwiftLint build tool plugin - ---- - -### Task 1: Tuist 기반 골격과 버전 고정 추가 - -**Files:** -- Create: `docs/superpowers/specs/2026-06-01-tuist-migration-design.md` -- Create: `docs/superpowers/plans/2026-06-01-tuist-migration.md` -- Create: `.mise.toml` -- Create: `Tuist.swift` -- Create: `Workspace.swift` -- Create: `Tuist/Package.swift` -- Create: `Tuist/ProjectDescriptionHelpers/Project+Templates.swift` -- Create: `Tuist/ProjectDescriptionHelpers/Project+Settings.swift` -- Create: `Tuist/ProjectDescriptionHelpers/Project+Packages.swift` - -- [ ] **Step 1: Tuist 버전 고정 파일을 추가** - -```toml -[tools] -tuist = "4.194.4" -``` - -- [ ] **Step 2: 루트 Tuist 설정과 workspace manifest를 추가** - -```swift -import ProjectDescription - -let tuist = Config( - project: .tuist( - generationOptions: .options() - ) -) -``` - -```swift -import ProjectDescription - -let workspace = Workspace( - name: "DevLog", - projects: [ - "Application/DevLogApp", - "Application/DevLogCore", - "Application/DevLogData", - "Application/DevLogDomain", - "Application/DevLogInfra", - "Application/DevLogPersistence", - "Application/DevLogPresentation", - "Widget/DevLogWidgetCore", - "Widget/DevLogWidgetExtension", - ] -) -``` - -- [ ] **Step 3: 외부 패키지와 공통 helper 뼈대를 추가** - -```swift -// Tuist/Package.swift -// swift-tools-version: 5.9 -import PackageDescription - -#if TUIST -import ProjectDescription - -let packageSettings = PackageSettings( - productTypes: [ - "OrderedCollections": .framework, - "MarkdownUI": .framework, - "SwiftLintBuildToolPlugin": .plugin, - "FirebaseAnalyticsCore": .framework, - "FirebaseCore": .framework, - "FirebaseFunctions": .framework, - "FirebaseAuth": .framework, - "FirebaseMessaging": .framework, - "FirebaseFirestore": .framework, - "GoogleSignIn": .framework, - "Nexa": .framework, - ] -) -#endif - -let package = Package( - name: "DevLogDependencies", - dependencies: [ - .package(url: "https://github.com/realm/SwiftLint", exact: "0.62.1"), - .package(url: "https://github.com/gonzalezreal/swift-markdown-ui.git", exact: "2.4.1"), - .package(url: "https://github.com/apple/swift-collections.git", exact: "1.3.0"), - .package(url: "https://github.com/firebase/firebase-ios-sdk", exact: "11.15.0"), - .package(url: "https://github.com/google/GoogleSignIn-iOS", exact: "9.0.0"), - .package(url: "https://github.com/opficdev/Nexa", exact: "1.1.0"), - ] -) -``` - -- [ ] **Step 4: Task 1 변경사항 커밋** - -Run: - -```bash -git add docs/superpowers/specs/2026-06-01-tuist-migration-design.md docs/superpowers/plans/2026-06-01-tuist-migration.md .mise.toml Tuist.swift Workspace.swift Tuist -git commit -m "chore: tuist 전환 기반 추가" -``` - -Expected: 문서와 Tuist 골격만 포함된 첫 커밋 생성 - -### Task 2: 모듈별 Project.swift와 의존성 이관 - -**Files:** -- Create: `Application/DevLogApp/Project.swift` -- Create: `Application/DevLogCore/Project.swift` -- Create: `Application/DevLogData/Project.swift` -- Create: `Application/DevLogDomain/Project.swift` -- Create: `Application/DevLogInfra/Project.swift` -- Create: `Application/DevLogPersistence/Project.swift` -- Create: `Application/DevLogPresentation/Project.swift` -- Create: `Widget/DevLogWidgetCore/Project.swift` -- Create: `Widget/DevLogWidgetExtension/Project.swift` - -- [ ] **Step 1: 공통 framework/test 템플릿을 helper에 구현** - -```swift -import ProjectDescription - -public extension Project { - static func devlogFramework( - name: String, - dependencies: [TargetDependency] = [], - testDependencies: [TargetDependency] = [] - ) -> Project { - Project( - name: name, - targets: [ - .target( - name: name, - destinations: .iOS, - product: .framework, - bundleId: "com.opfic.DevLog.\(name)", - deploymentTargets: .iOS("17.0"), - infoPlist: .default, - sources: ["Sources/**"], - dependencies: dependencies + [.package(product: "SwiftLintBuildToolPlugin", type: .plugin)], - settings: .devlogFrameworkSettings - ), - .testTarget( - name: "\(name)Tests", - destinations: .iOS, - product: .unitTests, - bundleId: "com.opfic.DevLog.\(name)Tests", - infoPlist: .default, - sources: ["Tests/**"], - dependencies: [.target(name: name)] + testDependencies, - settings: .devlogTestSettings(testTargetName: name) - ), - ] - ) - } -} -``` - -- [ ] **Step 2: App, WidgetExtension, 개별 모듈 manifest를 구현** - -```swift -// 예시: Application/DevLogPresentation/Project.swift -import ProjectDescription -import ProjectDescriptionHelpers - -let project = Project.devlogFramework( - name: "DevLogPresentation", - dependencies: [ - .project(target: "DevLogDomain", path: "../DevLogDomain"), - .project(target: "DevLogCore", path: "../DevLogCore"), - .external(name: "MarkdownUI"), - .external(name: "OrderedCollections"), - ] -) -``` - -```swift -// 예시: Widget/DevLogWidgetExtension/Project.swift -import ProjectDescription -import ProjectDescriptionHelpers - -let project = Project( - name: "DevLogWidgetExtension", - targets: [ - .target( - name: "DevLogWidgetExtension", - destinations: .iOS, - product: .appExtension, - bundleId: "opfic.DevLog.DevLogWidget", - deploymentTargets: .iOS("17.0"), - infoPlist: .file(path: "Resource/Info.plist"), - sources: ["**/*.swift"], - resources: ["Resource/**"], - entitlements: .file(path: "Resource/DevLogWidget.entitlements"), - dependencies: [ - .project(target: "DevLogWidgetCore", path: "../DevLogWidgetCore"), - ], - settings: .devlogWidgetSettings - ), - ] -) -``` - -- [ ] **Step 3: App test host / resource / bundle id 설정을 기존 값으로 맞춘다** - -Run: - -```bash -rg -n "TEST_HOST|BUNDLE_LOADER|PRODUCT_BUNDLE_IDENTIFIER|INFOPLIST_FILE|CODE_SIGN_ENTITLEMENTS" Application/DevLogApp/DevLogApp.xcodeproj/project.pbxproj -``` - -Expected: App 테스트와 Widget/App 리소스 경로를 manifest 설정으로 모두 옮길 수 있을 만큼 기존 값이 반영됨 - -- [ ] **Step 4: Task 2 변경사항 커밋** - -Run: - -```bash -git add Application/DevLogApp/Project.swift Application/DevLogCore/Project.swift Application/DevLogData/Project.swift Application/DevLogDomain/Project.swift Application/DevLogInfra/Project.swift Application/DevLogPersistence/Project.swift Application/DevLogPresentation/Project.swift Widget/DevLogWidgetCore/Project.swift Widget/DevLogWidgetExtension/Project.swift Tuist/ProjectDescriptionHelpers -git commit -m "feat: tuist 모듈 매니페스트 구성" -``` - -Expected: 각 모듈 manifest와 helper만 포함된 두 번째 커밋 생성 - -### Task 3: 설치, 생성, 교체, 검증 - -**Files:** -- Modify: `DevLog.xcworkspace/**` -- Modify: `Application/*/*.xcodeproj/**` -- Modify: `Widget/*/*.xcodeproj/**` -- Modify: `DevLog.xcworkspace/xcshareddata/swiftpm/Package.resolved` - -- [ ] **Step 1: Tuist 4.194.4 설치와 의존성 설치** - -Run: - -```bash -brew install mise -mise install tuist@4.194.4 -mise use -p tuist@4.194.4 -tuist version -tuist install -``` - -Expected: `tuist version`이 `4.194.4`를 출력하고, 패키지 설치가 완료됨 - -- [ ] **Step 2: 프로젝트를 생성하고 산출물을 교체** - -Run: - -```bash -tuist generate -``` - -Expected: `DevLog.xcworkspace`와 각 모듈 `.xcodeproj`가 Tuist 생성 산출물로 갱신됨 - -- [ ] **Step 3: 스킴과 빌드를 검증** - -Run: - -```bash -xcodebuild -workspace DevLog.xcworkspace -list -xcodebuild -workspace DevLog.xcworkspace -scheme DevLog -resolvePackageDependencies -``` - -Expected: `DevLog` 스킴이 존재하고 패키지 해석이 성공함 - -- [ ] **Step 4: iOS Simulator 빌드 검증** - -Run: - -```bash -xcodebuild -workspace DevLog.xcworkspace -scheme DevLog -configuration Debug -destination "platform=iOS Simulator,name=iPhone 16" build -``` - -Expected: simulator build 성공 - -- [ ] **Step 5: Task 3 변경사항 커밋** - -Run: - -```bash -git add DevLog.xcworkspace Application Widget .mise.toml Tuist.swift Workspace.swift Tuist -git commit -m "refactor: tuist 생성 구조로 전환" -``` - -Expected: 생성 산출물과 최종 워크플로가 반영된 세 번째 커밋 생성 diff --git a/docs/superpowers/specs/2026-06-01-tuist-migration-design.md b/docs/superpowers/specs/2026-06-01-tuist-migration-design.md deleted file mode 100644 index 872a1c30..00000000 --- a/docs/superpowers/specs/2026-06-01-tuist-migration-design.md +++ /dev/null @@ -1,136 +0,0 @@ -# DevLog Tuist Migration Design - -## 목표 - -기존 `DevLog_iOS`의 모듈 경계와 의존성 방향을 유지한 채, 수동 관리 중인 Xcode 프로젝트/워크스페이스를 Tuist 기반 생성 구조로 전환한다. - -## 버전 선택 - -- 선택 버전: `Tuist 4.194.4` -- 설치 방식: `mise` 기반 프로젝트 고정 - -### 선정 근거 - -- Tuist 공식 설치 문서는 팀 단위의 결정적 버전 관리를 위해 `mise` 사용을 권장한다. -- 2026년 CLI 릴리스 분포를 확인한 결과, `4.195.x`가 가장 최신이지만 `4.195.11`에는 공개 이슈 2건이 즉시 연결된다. -- 동일 조사 기준에서 `4.194.4`, `4.193.4`, `4.192.4`, `4.191.8`은 정확한 패치 버전 문자열 기준 공개 이슈 연결이 보이지 않았다. -- 그중 `4.194.4`는 가장 최신에 가까우면서 `4.195.11` 대비 공개 회귀 신호가 적은 버전이다. - -### 해석 주의 - -- “가장 버그가 없는 버전”은 절대적으로 증명할 수 없다. -- 본 작업에서는 `공개 GitHub 이슈 검색`, `릴리스 최신성`, `패치 수렴 정도`를 합친 근거 기반 추정으로 선택한다. - -## 목표 구조 - -Tuist 표준 구조를 사용한다. - -- 루트 - - `Tuist.swift` - - `Workspace.swift` - - `Tuist/Package.swift` - - `Tuist/ProjectDescriptionHelpers/*` -- 모듈별 - - `Application/DevLogApp/Project.swift` - - `Application/DevLogCore/Project.swift` - - `Application/DevLogData/Project.swift` - - `Application/DevLogDomain/Project.swift` - - `Application/DevLogInfra/Project.swift` - - `Application/DevLogPersistence/Project.swift` - - `Application/DevLogPresentation/Project.swift` - - `Widget/DevLogWidgetCore/Project.swift` - - `Widget/DevLogWidgetExtension/Project.swift` - -## 모듈 구조 결정 - -### 유지할 논리 모듈 - -- App: `DevLog` -- App test bundle: `DevLogAppTests` -- Frameworks: `DevLogCore`, `DevLogDomain`, `DevLogData`, `DevLogInfra`, `DevLogPersistence`, `DevLogPresentation`, `DevLogWidgetCore` -- Framework test bundles: 각 모듈의 `*Tests` -- Widget extension: `DevLogWidgetExtension` - -### Tuist 표준에 맞춘 구조 변경 - -- 기존에는 `DevLogWidgetExtension` 타깃이 `Application/DevLogApp.xcodeproj` 내부에 있었다. -- 전환 후에는 `Widget/DevLogWidgetExtension/Project.swift`를 만들어 위젯 확장을 별도 프로젝트로 분리한다. -- 이는 Xcode 프로젝트 배치만 표준화하는 것이며, 논리 모듈과 의존성 방향은 유지한다. - -## 의존성 유지 기준 - -### 내부 의존성 - -- `DevLogDomain` -> `DevLogCore` -- `DevLogData` -> `DevLogDomain`, `DevLogCore` -- `DevLogInfra` -> `DevLogData`, `DevLogDomain`, `DevLogCore` -- `DevLogPersistence` -> `DevLogData`, `DevLogCore`, `DevLogWidgetCore` -- `DevLogPresentation` -> `DevLogDomain`, `DevLogCore` -- `DevLogWidgetCore` -> `DevLogCore` -- `DevLog` -> `DevLogPresentation`, `DevLogPersistence`, `DevLogInfra`, `DevLogData`, `DevLogDomain`, `DevLogCore`, `DevLogWidgetCore` -- `DevLogWidgetExtension` -> `DevLogWidgetCore` - -### 외부 패키지 - -- `DevLogPresentation` - - `MarkdownUI` - - `OrderedCollections` -- `DevLogInfra` - - `FirebaseAnalyticsCore` - - `FirebaseCore` - - `FirebaseFunctions` - - `FirebaseAuth` - - `FirebaseMessaging` - - `FirebaseFirestore` - - `GoogleSignIn` - - `Nexa` -- 전 모듈 공통 - - `SwiftLintBuildToolPlugin` - -## 설정 유지 기준 - -- 배포 타깃: 기본 `iOS 17` -- 공통 마케팅 버전: `Application/Shared/Version.xcconfig` 유지 -- App bundle id: `opfic.DevLog` -- Widget bundle id: `opfic.DevLog.DevLogWidget` -- 각 프레임워크/테스트 번들 식별자는 현재 값 유지 -- App entitlements: `Application/DevLogApp/Sources/Resource/DevLog.entitlements` -- Widget entitlements: `Widget/DevLogWidgetExtension/Resource/DevLogWidget.entitlements` -- App/Widget Info.plist와 리소스 경로 유지 -- App tests는 기존과 동일하게 `DevLog`를 host target으로 유지 - -## 생성 결과 목표 - -- 생성된 루트 워크스페이스 이름은 계속 `DevLog.xcworkspace` -- 주요 CI 빌드 기준은 계속 `workspace=DevLog.xcworkspace`, `scheme=DevLog` -- 기존 수동 `project.pbxproj` 수정 흐름을 Tuist manifest 수정 흐름으로 대체 - -## 커밋 분할 원칙 - -### 1단계 - -- Tuist 버전 고정 -- 루트 매니페스트 -- 공통 helper / 패키지 선언 -- 설계 문서와 계획 문서 - -### 2단계 - -- 모듈별 `Project.swift` -- App / WidgetExtension 분리 -- 테스트 타깃 / 의존성 / 리소스 / 빌드 설정 이관 - -### 3단계 - -- `tuist install` -- `tuist generate` -- 생성 산출물 반영 -- 워크스페이스/스킴/빌드 검증 - -## 검증 기준 - -- `tuist install` 성공 -- `tuist generate` 성공 -- 생성된 `DevLog.xcworkspace`에 `DevLog` 스킴 존재 -- iOS Simulator 대상 `DevLog` 빌드 성공 -- 의존성 방향이 기존과 동일함을 manifest와 생성 결과에서 확인