diff --git a/Plugins/DependencyPackagePlugin/ProjectDescriptionHelpers/DependencyPackage/Extension+TargetDependencySPM.swift b/Plugins/DependencyPackagePlugin/ProjectDescriptionHelpers/DependencyPackage/Extension+TargetDependencySPM.swift index 791e1a9..8132d0b 100644 --- a/Plugins/DependencyPackagePlugin/ProjectDescriptionHelpers/DependencyPackage/Extension+TargetDependencySPM.swift +++ b/Plugins/DependencyPackagePlugin/ProjectDescriptionHelpers/DependencyPackage/Extension+TargetDependencySPM.swift @@ -12,4 +12,7 @@ public extension TargetDependency.SPM { static let composableArchitecture = TargetDependency.external(name: "ComposableArchitecture", condition: .none) static let tcaCoordinator = TargetDependency.external(name: "TCACoordinators", condition: .none) static let weaveDI = TargetDependency.external(name: "WeaveDI", condition: .none) + + static let googleSignIn = TargetDependency.external(name: "GoogleSignIn", condition: .none) + static let appAuth: TargetDependency = .external(name: "AppAuth") } diff --git a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/TargetDependency+Module/Modules.swift b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/TargetDependency+Module/Modules.swift index 7f3c83c..082f4db 100644 --- a/Plugins/DependencyPlugin/ProjectDescriptionHelpers/TargetDependency+Module/Modules.swift +++ b/Plugins/DependencyPlugin/ProjectDescriptionHelpers/TargetDependency+Module/Modules.swift @@ -35,6 +35,8 @@ public extension ModulePath { case Foundations public static let name: String = "Network" + case Networks + case ThirdPartys } } diff --git a/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Project+Template.swift b/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Project+Template.swift index 5dc2a3e..041e760 100644 --- a/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Project+Template.swift +++ b/Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Project+Template.swift @@ -6,6 +6,37 @@ // import ProjectDescription +import Foundation + +// MARK: - Helper Functions +private func ensureTestsSourcesDirectoryExists(for projectName: String) { + let fileManager = FileManager.default + + // Try to find the current module directory + let currentDirectory = fileManager.currentDirectoryPath + let possiblePaths = [ + "\(currentDirectory)/Tests/Sources", + "./Tests/Sources" + ] + + for testsSourcesPath in possiblePaths { + let parentDir = URL(fileURLWithPath: testsSourcesPath).deletingLastPathComponent().path + + if fileManager.fileExists(atPath: parentDir) || parentDir == "." { + if !fileManager.fileExists(atPath: testsSourcesPath) { + do { + try fileManager.createDirectory(atPath: testsSourcesPath, withIntermediateDirectories: true, attributes: nil) + print("๐Ÿ“ Created Tests/Sources directory for \(projectName) at \(testsSourcesPath)") + return + } catch { + print("โš ๏ธ Failed to create Tests/Sources directory for \(projectName) at \(testsSourcesPath): \(error)") + } + } else { + return // Directory already exists + } + } + } +} public extension Project { static func makeAppModule( @@ -23,7 +54,8 @@ public extension Project { resources: ProjectDescription.ResourceFileElements? = nil, infoPlist: ProjectDescription.InfoPlist = .default, entitlements: ProjectDescription.Entitlements? = nil, - schemes: [ProjectDescription.Scheme] = [] + schemes: [ProjectDescription.Scheme] = [], + hasTests: Bool = false ) -> Project { let appTarget: Target = .target( @@ -84,18 +116,24 @@ public extension Project { dependencies: dependencies ) - let appTestTarget : Target = .target( - name: "\(name)Tests", - destinations: destinations, - product: .unitTests, - bundleId: "\(bundleId).\(name)Tests", - deploymentTargets: deploymentTarget, - infoPlist: .default, - sources: ["\(name)Tests/Sources/**"], - dependencies: [.target(name: name)] - ) + var targets: [Target] = [appTarget, appDevTarget, appStageTarget, appProdTarget] + + if hasTests { + // Ensure Tests/Sources directory exists + ensureTestsSourcesDirectoryExists(for: name) - let targets = [appTarget, appDevTarget, appStageTarget, appProdTarget ,appTestTarget] + let appTestTarget : Target = .target( + name: "\(name)Tests", + destinations: destinations, + product: .unitTests, + bundleId: "\(bundleId).\(name)Tests", + deploymentTargets: deploymentTarget, + infoPlist: .default, + sources: ["Tests/Sources/**"], + dependencies: [.target(name: name)] + ) + targets.append(appTestTarget) + } return Project( name: name, @@ -125,9 +163,10 @@ public extension Project { resources: ProjectDescription.ResourceFileElements? = nil, infoPlist: ProjectDescription.InfoPlist = .default, entitlements: ProjectDescription.Entitlements? = nil, - schemes: [ProjectDescription.Scheme] = [] + schemes: [ProjectDescription.Scheme] = [], + hasTests: Bool = false ) -> Project { - + let appTarget: Target = .target( name: name, destinations: destinations, @@ -141,34 +180,26 @@ public extension Project { scripts: scripts, dependencies: dependencies ) - - let appDevTarget: Target = .target( - name: "\(name)-QA", - destinations: destinations, - product: product, - bundleId: "\(bundleId)", - deploymentTargets: deploymentTarget, - infoPlist: infoPlist, - sources: sources, - resources: resources, - entitlements: entitlements, - scripts: scripts, - dependencies: dependencies - ) - - let appTestTarget : Target = .target( - name: "\(name)Tests", - destinations: destinations, - product: .unitTests, - bundleId: "\(bundleId).\(name)Tests", - deploymentTargets: deploymentTarget, - infoPlist: .default, - sources: ["\(name)Tests/Sources/**"], - dependencies: [.target(name: name)] - ) - - let targets = [appTarget, appDevTarget, appTestTarget] - + + var targets: [Target] = [appTarget] + + if hasTests { + // Ensure Tests/Sources directory exists + ensureTestsSourcesDirectoryExists(for: name) + + let appTestTarget : Target = .target( + name: "\(name)Tests", + destinations: destinations, + product: .unitTests, + bundleId: "\(bundleId).\(name)Tests", + deploymentTargets: deploymentTarget, + infoPlist: .default, + sources: ["Tests/Sources/**"], + dependencies: [.target(name: name)] + ) + targets.append(appTestTarget) + } + return Project( name: name, packages: packages, diff --git a/Projects/App/MultiModuleTests/Sources/MultiModuleTemplateTests.swift b/Projects/App/MultiModuleTests/Sources/MultiModuleTemplateTests.swift deleted file mode 100644 index 393bad2..0000000 --- a/Projects/App/MultiModuleTests/Sources/MultiModuleTemplateTests.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation -import XCTest - -final class MultiModuleTemplateTests: XCTestCase { - func test_twoPlusTwo_isFour() { - XCTAssertEqual(2+2, 4) - } -} \ No newline at end of file diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 12529ae..703ac56 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -20,6 +20,7 @@ let project = Project.makeAppModule( // ํ…Œ์ŠคํŠธ ํ”Œ๋žœ ์Šคํ‚ด: ์ปค์Šคํ…€ ๊ตฌ์„ฑ๋ช… ์‚ฌ์šฉ (.dev / .stage / .prod ์ค‘ ํƒ1) Scheme.makeTestPlanScheme(target: .dev, name: Project.Environment.appName), - ] + ], + hasTests: true ) diff --git a/Projects/App/Sources/Application/NomadSpotApp.swift b/Projects/App/Sources/Application/NomadSpotApp.swift index 335c152..cf620e9 100644 --- a/Projects/App/Sources/Application/NomadSpotApp.swift +++ b/Projects/App/Sources/Application/NomadSpotApp.swift @@ -4,7 +4,7 @@ import SwiftUI import ComposableArchitecture @main -struct AttendanceApp: App { +struct NomadSpotApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate init() { diff --git a/Projects/App/Sources/Di/DiRegister.swift b/Projects/App/Sources/Di/DiRegister.swift index da7ffa4..779acef 100644 --- a/Projects/App/Sources/Di/DiRegister.swift +++ b/Projects/App/Sources/Di/DiRegister.swift @@ -26,11 +26,11 @@ public class AppDIManager: @unchecked Sendable { public func registerDefaultDependencies() async { // ๐Ÿ—๏ธ 1. WeaveDI.builder ํŒจํ„ด์œผ๋กœ ์‹ค์ œ ๊ตฌํ˜„์ฒด๋“ค ๋“ฑ๋ก WeaveDI.builder -// .register { KeychainManager() as KeychainManaging } -// .register { -// let keychainManager = UnifiedDI.resolve(KeychainManaging.self) ?? KeychainManager() -// return KeychainTokenProvider(keychainManager: keychainManager) as TokenProviding -// } + .register { KeychainManager() as KeychainManagingInterface } + .register { + let keychainManager = UnifiedDI.resolve(KeychainManagingInterface.self) ?? KeychainManager() + return KeychainTokenProvider(keychainManager: keychainManager) as TokenProviding + } // .register(ProfileInterface.self) { ProfileRepositoryImpl() } // // MARK: - ๋กœ๊ทธ์ธ // .register { AuthRepositoryImpl() as AuthInterface } diff --git a/Projects/App/Sources/Di/KeychainTokenProvider.swift b/Projects/App/Sources/Di/KeychainTokenProvider.swift index 63359ec..b89e473 100644 --- a/Projects/App/Sources/Di/KeychainTokenProvider.swift +++ b/Projects/App/Sources/Di/KeychainTokenProvider.swift @@ -5,23 +5,23 @@ // Created by Wonji Suh on 3/1/26. // -//import Foundation -// -//import DomainInterface -//import Foundations -// -//struct KeychainTokenProvider: TokenProviding { -// private let keychainManager: KeychainManaging -// -// init(keychainManager: KeychainManaging) { -// self.keychainManager = keychainManager -// } -// -// func accessToken() -> String? { -// keychainManager.accessToken() -// } -// -// func saveAccessToken(_ token: String) { -// keychainManager.saveAccessToken(token) -// } -//} +import Foundation + +import DomainInterface +import Foundations + +struct KeychainTokenProvider: TokenProviding { + private let keychainManager: KeychainManagingInterface + + init(keychainManager: KeychainManagingInterface) { + self.keychainManager = keychainManager + } + + func accessToken() -> String? { + keychainManager.accessToken() + } + + func saveAccessToken(_ token: String) { + keychainManager.saveAccessToken(token) + } +} diff --git a/Projects/App/NomadSpotTests/Sources/NomadSpotTests.swift b/Projects/App/Tests/Sources/NomadSpotTests.swift similarity index 100% rename from Projects/App/NomadSpotTests/Sources/NomadSpotTests.swift rename to Projects/App/Tests/Sources/NomadSpotTests.swift diff --git a/Projects/Core/Core/Sources/Exported/CoreExport.swift b/Projects/Core/Core/Sources/Exported/CoreExport.swift deleted file mode 100644 index a76e006..0000000 --- a/Projects/Core/Core/Sources/Exported/CoreExport.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// CoreExport.swift -// Core -// -// Created by Wonji Suh on 9/5/25. -// - -@_exported import UseCase -@_exported import Network diff --git a/Projects/Core/ThirdParty/Sources/Base.swift b/Projects/Core/ThirdParty/Sources/Base.swift deleted file mode 100644 index 8610729..0000000 --- a/Projects/Core/ThirdParty/Sources/Base.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// base.swift -// DDDAttendance. -// -// Created by Roy on 2025-11-04 -// Copyright ยฉ 2025 DDD , Ltd., All rights reserved. -// - -import SwiftUI - -struct BaseView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") - } - .padding() - } -} - diff --git a/Projects/Core/ThirdParty/ThirdPartyTests/Sources/Test.swift b/Projects/Core/ThirdParty/ThirdPartyTests/Sources/Test.swift deleted file mode 100644 index 1e4cfe2..0000000 --- a/Projects/Core/ThirdParty/ThirdPartyTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-11-04 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Data/API/APITests/Sources/Test.swift b/Projects/Data/API/APITests/Sources/Test.swift deleted file mode 100644 index 88831a3..0000000 --- a/Projects/Data/API/APITests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-10-22 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Data/API/Project.swift b/Projects/Data/API/Project.swift index 4c250e0..f861039 100644 --- a/Projects/Data/API/Project.swift +++ b/Projects/Data/API/Project.swift @@ -10,7 +10,8 @@ let project = Project.makeAppModule( product: .staticFramework, settings: .settings(), dependencies: [ - + .Network(implements: .ThirdPartys) ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: false ) diff --git a/Projects/Data/Model/ModelTests/Sources/Test.swift b/Projects/Data/Model/ModelTests/Sources/Test.swift deleted file mode 100644 index a9c810e..0000000 --- a/Projects/Data/Model/ModelTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-09-04 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Data/Model/Project.swift b/Projects/Data/Model/Project.swift index e4aff7c..f4ed56f 100644 --- a/Projects/Data/Model/Project.swift +++ b/Projects/Data/Model/Project.swift @@ -12,5 +12,6 @@ let project = Project.makeAppModule( dependencies: [ .Domain(implements: .Entity) ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: false ) diff --git a/Projects/Data/Repository/Project.swift b/Projects/Data/Repository/Project.swift index 00b1491..60b3216 100644 --- a/Projects/Data/Repository/Project.swift +++ b/Projects/Data/Repository/Project.swift @@ -10,8 +10,11 @@ let project = Project.makeAppModule( product: .staticFramework, settings: .settings(), dependencies: [ - .Network(implements: .Networking), - .Domain(implements: .DomainInterface) + .Data(implements: .Service), + .Domain(implements: .DomainInterface), + + .SPM.googleSignIn ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: true ) diff --git a/Projects/Data/Service/Project.swift b/Projects/Data/Service/Project.swift index bd2f256..308933f 100644 --- a/Projects/Data/Service/Project.swift +++ b/Projects/Data/Service/Project.swift @@ -10,7 +10,10 @@ let project = Project.makeAppModule( product: .staticFramework, settings: .settings(), dependencies: [ - + .Network(implements: .ThirdPartys), + .Data(implements: .API), + .Network(implements: .Foundations), ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: false ) diff --git a/Projects/Data/Service/ServiceTests/Sources/Test.swift b/Projects/Data/Service/ServiceTests/Sources/Test.swift deleted file mode 100644 index 88831a3..0000000 --- a/Projects/Data/Service/ServiceTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-10-22 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Domain/DomainInterface/DomainInterfaceTests/Sources/Test.swift b/Projects/Domain/DomainInterface/DomainInterfaceTests/Sources/Test.swift deleted file mode 100644 index a9c810e..0000000 --- a/Projects/Domain/DomainInterface/DomainInterfaceTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-09-04 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Domain/DomainInterface/Project.swift b/Projects/Domain/DomainInterface/Project.swift index 4e343c1..3fd98df 100644 --- a/Projects/Domain/DomainInterface/Project.swift +++ b/Projects/Domain/DomainInterface/Project.swift @@ -10,7 +10,10 @@ let project = Project.makeAppModule( product: .framework, settings: .settings(), dependencies: [ - .Domain(implements: .Entity) + .Data(implements: .Model), + .SPM.composableArchitecture, + .SPM.weaveDI, ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: false ) diff --git a/Projects/Domain/DomainInterface/Sources/Manger/InMemoryKeychainManager.swift b/Projects/Domain/DomainInterface/Sources/Manger/InMemoryKeychainManager.swift new file mode 100644 index 0000000..17c20cc --- /dev/null +++ b/Projects/Domain/DomainInterface/Sources/Manger/InMemoryKeychainManager.swift @@ -0,0 +1,85 @@ +// +// InMemoryKeychainManager.swift +// UseCase +// +// Created by Wonji Suh on 3/4/26. +// + +import Foundation + +public actor InMemoryKeychainManager: KeychainManagingInterface { + private var accessTokenStorage: String? + private var refreshTokenStorage: String? + + public init() {} + + // MARK: - Legacy Sync API (Backward Compatibility) + + public nonisolated func save(accessToken: String, refreshToken: String) { + Task { [weak self] in + guard let self = self else { return } + try? await self.save(accessToken: accessToken, refreshToken: refreshToken) + } + } + + public nonisolated func saveAccessToken(_ token: String) { + Task { [weak self] in + guard let self = self else { return } + try? await self.saveAccessToken(token) + } + } + + public nonisolated func saveRefreshToken(_ token: String) { + Task { [weak self] in + guard let self = self else { return } + try? await self.saveRefreshToken(token) + } + } + + public nonisolated func accessToken() -> String? { + // โš ๏ธ Sync access - use async version for better safety + // For testing purposes, return nil in sync context + return nil + } + + public nonisolated func refreshToken() -> String? { + // โš ๏ธ Sync access - use async version for better safety + // For testing purposes, return nil in sync context + return nil + } + + public nonisolated func clear() { + Task { [weak self] in + guard let self = self else { return } + try? await self.clear() + } + } + + // MARK: - Modern Async API (iOS 17+) + + public func save(accessToken: String, refreshToken: String) async throws { + accessTokenStorage = accessToken + refreshTokenStorage = refreshToken + } + + public func saveAccessToken(_ token: String) async throws { + accessTokenStorage = token + } + + public func saveRefreshToken(_ token: String) async throws { + refreshTokenStorage = token + } + + public func accessToken() async -> String? { + accessTokenStorage + } + + public func refreshToken() async -> String? { + refreshTokenStorage + } + + public func clear() async throws { + accessTokenStorage = nil + refreshTokenStorage = nil + } +} diff --git a/Projects/Domain/DomainInterface/Sources/Manger/KeychainManagingInterface.swift b/Projects/Domain/DomainInterface/Sources/Manger/KeychainManagingInterface.swift new file mode 100644 index 0000000..4ff5f28 --- /dev/null +++ b/Projects/Domain/DomainInterface/Sources/Manger/KeychainManagingInterface.swift @@ -0,0 +1,45 @@ +// +// KeychainManagingInterface.swift +// UseCase +// +// Created by Wonji Suh on 3/4/26. +// + +import Foundation +import WeaveDI + +public protocol KeychainManagingInterface: Sendable { + func save(accessToken: String, refreshToken: String) + func saveAccessToken(_ token: String) + func saveRefreshToken(_ token: String) + func accessToken() -> String? + func refreshToken() -> String? + func clear() + + // MARK: - Modern Async API (iOS 17+) + func save(accessToken: String, refreshToken: String) async throws + func saveAccessToken(_ token: String) async throws + func saveRefreshToken(_ token: String) async throws + func accessToken() async -> String? + func refreshToken() async -> String? + func clear() async throws +} + +public struct KeychainManagerDependency: DependencyKey { + public static var liveValue: KeychainManagingInterface { + UnifiedDI.resolve(KeychainManagingInterface.self) ?? InMemoryKeychainManager() + } + + public static var testValue: KeychainManagingInterface { + InMemoryKeychainManager() + } + + public static var previewValue: KeychainManagingInterface = testValue +} + +public extension DependencyValues { + var keychainManager: KeychainManagingInterface { + get { self[KeychainManagerDependency.self] } + set { self[KeychainManagerDependency.self] = newValue } + } +} diff --git a/Projects/Domain/Entity/EntityTests/Sources/Test.swift b/Projects/Domain/Entity/EntityTests/Sources/Test.swift deleted file mode 100644 index 88831a3..0000000 --- a/Projects/Domain/Entity/EntityTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-10-22 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Domain/Entity/Project.swift b/Projects/Domain/Entity/Project.swift index cc955e0..5da5098 100644 --- a/Projects/Domain/Entity/Project.swift +++ b/Projects/Domain/Entity/Project.swift @@ -12,5 +12,6 @@ let project = Project.makeAppModule( dependencies: [ ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: false ) diff --git a/Projects/Domain/UseCase/Project.swift b/Projects/Domain/UseCase/Project.swift index e473034..d3cf127 100644 --- a/Projects/Domain/UseCase/Project.swift +++ b/Projects/Domain/UseCase/Project.swift @@ -10,10 +10,8 @@ let project = Project.makeAppModule( product: .staticFramework, settings: .settings(), dependencies: [ - .Data(implements: .Repository), - .Domain(implements: .DomainInterface), - .SPM.composableArchitecture, - .SPM.weaveDI, + .Domain(implements: .DomainInterface) ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: true ) diff --git a/Projects/Domain/UseCase/Sources/Base.swift b/Projects/Domain/UseCase/Sources/Base.swift deleted file mode 100644 index 6297cc4..0000000 --- a/Projects/Domain/UseCase/Sources/Base.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// base.swift -// DDDAttendance. -// -// Created by Roy on 2025-09-04 -// Copyright ยฉ 2025 DDD , Ltd., All rights reserved. -// - -import SwiftUI - -struct BaseView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") - } - .padding() - } -} - diff --git a/Projects/Domain/UseCase/Sources/Manger/KeychainManager.swift b/Projects/Domain/UseCase/Sources/Manger/KeychainManager.swift new file mode 100644 index 0000000..75671d2 --- /dev/null +++ b/Projects/Domain/UseCase/Sources/Manger/KeychainManager.swift @@ -0,0 +1,242 @@ +// +// KeychainManager.swift +// UseCase +// +// Created by Wonji Suh on 3/4/26. +// + +import Foundation + +import DomainInterface +import Security +import ComposableArchitecture +import WeaveDI + +public actor KeychainManager: KeychainManagingInterface { + private let service: String + private let accessGroup: String? + + private enum Key { + static let accessToken = "ACCESS_TOKEN" + static let refreshToken = "REFRESH_TOKEN" + } + + public init(service: String = Bundle.main.bundleIdentifier ?? "com.nomadspot.app", + accessGroup: String? = nil) { + self.service = service + self.accessGroup = accessGroup + } + + // MARK: - Legacy Sync API (Backward Compatibility) + + public nonisolated func save(accessToken: String, refreshToken: String) { + Task { [weak self] in + guard let self = self else { return } + do { + try await self.save(accessToken: accessToken, refreshToken: refreshToken) + } catch { + // TODO: Add proper logging in production + print("โš ๏ธ Failed to save tokens: \(error)") + } + } + } + + public nonisolated func saveAccessToken(_ token: String) { + Task { [weak self] in + guard let self = self else { return } + do { + try await self.saveAccessToken(token) + } catch { + // TODO: Add proper logging in production + print("โš ๏ธ Failed to save access token: \(error)") + } + } + } + + public nonisolated func saveRefreshToken(_ token: String) { + Task { [weak self] in + guard let self = self else { return } + do { + try await self.saveRefreshToken(token) + } catch { + // TODO: Add proper logging in production + print("โš ๏ธ Failed to save refresh token: \(error)") + } + } + } + + public nonisolated func accessToken() -> String? { + // โš ๏ธ Sync access - use async version for better safety + return legacyRead(for: Key.accessToken) + } + + public nonisolated func refreshToken() -> String? { + // โš ๏ธ Sync access - use async version for better safety + return legacyRead(for: Key.refreshToken) + } + + public nonisolated func clear() { + Task { [weak self] in + guard let self = self else { return } + do { + try await self.clear() + } catch { + // TODO: Add proper logging in production + print("โš ๏ธ Failed to clear keychain: \(error)") + } + } + } + + // MARK: - Modern Async API (iOS 17+) + + public func save(accessToken: String, refreshToken: String) async throws { + try save(accessToken, for: Key.accessToken) + try save(refreshToken, for: Key.refreshToken) + } + + public func saveAccessToken(_ token: String) async throws { + try save(token, for: Key.accessToken) + } + + public func saveRefreshToken(_ token: String) async throws { + try save(token, for: Key.refreshToken) + } + + public func accessToken() async -> String? { + read(for: Key.accessToken) + } + + public func refreshToken() async -> String? { + read(for: Key.refreshToken) + } + + public func clear() async throws { + try delete(for: Key.accessToken) + try delete(for: Key.refreshToken) + } + + // MARK: - Actor Internal Methods + + private func save(_ value: String, for key: String) throws { + let data = Data(value.utf8) + var query = baseQuery(for: key) + + let attributes: [CFString: Any] = [ + kSecValueData: data + ] + + let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) + if status == errSecItemNotFound { + // Item doesn't exist, create new one with security attributes + query[kSecValueData] = data + query[kSecAttrAccessible] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly + + let addStatus = SecItemAdd(query as CFDictionary, nil) + guard addStatus == errSecSuccess else { + throw KeychainError.unableToSave(status: addStatus) + } + } else if status != errSecSuccess { + throw KeychainError.unableToUpdate(status: status) + } + } + + private func read(for key: String) -> String? { + var query = baseQuery(for: key) + query[kSecReturnData] = true + query[kSecMatchLimit] = kSecMatchLimitOne + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + guard status == errSecSuccess, let data = result as? Data else { + return nil + } + return String(data: data, encoding: .utf8) + } + + private func delete(for key: String) throws { + let query = baseQuery(for: key) + let status = SecItemDelete(query as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { + throw KeychainError.unableToDelete(status: status) + } + } + + // MARK: - Helper Methods + + private func baseQuery(for key: String) -> [CFString: Any] { + var query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: key + ] + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup] = accessGroup + } + + return query + } + + // MARK: - Legacy Support (nonisolated) + + private nonisolated func legacyRead(for key: String) -> String? { + var query: [CFString: Any] = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: key, + kSecReturnData: true, + kSecMatchLimit: kSecMatchLimitOne + ] + + if let accessGroup = accessGroup { + query[kSecAttrAccessGroup] = accessGroup + } + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + guard status == errSecSuccess, let data = result as? Data else { + return nil + } + return String(data: data, encoding: .utf8) + } +} + +// MARK: - Error Types + +public enum KeychainError: Error, LocalizedError { + case unableToSave(status: OSStatus) + case unableToUpdate(status: OSStatus) + case unableToDelete(status: OSStatus) + + public var errorDescription: String? { + switch self { + case .unableToSave(let status): + return "Unable to save to keychain. Status: \(status)" + case .unableToUpdate(let status): + return "Unable to update keychain item. Status: \(status)" + case .unableToDelete(let status): + return "Unable to delete from keychain. Status: \(status)" + } + } +} + +// MARK: - TCA Dependency + +public struct KeychainManagerDependency: DependencyKey { + public static var liveValue: KeychainManagingInterface { + UnifiedDI.resolve(KeychainManagingInterface.self) ?? KeychainManager() + } + + public static var testValue: KeychainManagingInterface { + InMemoryKeychainManager() + } + + public static var previewValue: KeychainManagingInterface = testValue +} + +public extension DependencyValues { + var keychainManager: KeychainManagingInterface { + get { self[KeychainManagerDependency.self] } + set { self[KeychainManagerDependency.self] = newValue } + } +} diff --git a/Projects/Core/Core/CoreTests/Sources/Test.swift b/Projects/Domain/UseCase/Tests/Sources/Test.swift similarity index 100% rename from Projects/Core/Core/CoreTests/Sources/Test.swift rename to Projects/Domain/UseCase/Tests/Sources/Test.swift diff --git a/Projects/Domain/UseCase/UseCaseTests/Sources/Test.swift b/Projects/Domain/UseCase/UseCaseTests/Sources/Test.swift deleted file mode 100644 index a9c810e..0000000 --- a/Projects/Domain/UseCase/UseCaseTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-09-04 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Network/Foundations/FoundationsTests/Sources/Test.swift b/Projects/Network/Foundations/FoundationsTests/Sources/Test.swift deleted file mode 100644 index 88831a3..0000000 --- a/Projects/Network/Foundations/FoundationsTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-10-22 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Network/Foundations/Project.swift b/Projects/Network/Foundations/Project.swift index f9d1cb4..473a952 100644 --- a/Projects/Network/Foundations/Project.swift +++ b/Projects/Network/Foundations/Project.swift @@ -10,7 +10,9 @@ let project = Project.makeAppModule( product: .staticFramework, settings: .settings(), dependencies: [ - + .Network(implements: .ThirdPartys), + .Domain(implements: .UseCase) ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: false ) diff --git a/Projects/Network/Foundations/Sources/APIHeader/APIHeader.swift b/Projects/Network/Foundations/Sources/APIHeader/APIHeader.swift new file mode 100644 index 0000000..e545ebe --- /dev/null +++ b/Projects/Network/Foundations/Sources/APIHeader/APIHeader.swift @@ -0,0 +1,75 @@ +// +// APIHeader.swift +// Foundations +// +// Created by Wonji Suh on 3/4/26. +// + +import Foundation + +import Foundation +import ComposableArchitecture + +public struct APIHeader { + + public static let contentType = "Content-Type" + public static let accessToken = "Authorization" + public static let accept = "accept" + + @Dependency(\.tokenProvider) private static var tokenProvider + + public static var accessTokenKeyChain: String { + get { + let token = tokenProvider.accessToken() ?? "" + return token + } + set { updateAccessToken(newValue) } + } + + public static func updateAccessToken(_ token: String?) { + guard let newToken = token, !newToken.isEmpty else { return } + tokenProvider.saveAccessToken(newToken) + } + + public init() {} +} + +extension APIHeader { + static func baseHeaders(_ headers: [String: String]?) -> [String: String] { + var baseHeaders = baseHeader + if let headers = headers { + baseHeaders.merge(headers) { $1 } + } + return baseHeaders + } + + public static var baseHeader: Dictionary { + [ + contentType: APIHeaderManger.contentType, + accessToken: "Bearer \(accessTokenKeyChain)", + accept: APIHeaderManger.contentType + ] + } + + public static var notAccessTokenHeader: Dictionary { + [ + contentType: APIHeaderManger.contentType, + accept: APIHeaderManger.contentType + ] + } + + public static var mutiPartbaseHeader: Dictionary { + [ + contentType: APIHeaderManger.multipartContentType, + accessToken: "Bearer \(accessTokenKeyChain)", + ] + } + + public static var applebaseHeader: Dictionary { + [ + contentType: APIHeaderManger.contentType, + accept: APIHeaderManger.contentType + ] + } +} + diff --git a/Projects/Network/Foundations/Sources/APIHeader/APIHeaderManger.swift b/Projects/Network/Foundations/Sources/APIHeader/APIHeaderManger.swift new file mode 100644 index 0000000..ab2f9db --- /dev/null +++ b/Projects/Network/Foundations/Sources/APIHeader/APIHeaderManger.swift @@ -0,0 +1,16 @@ +// +// APIHeaderManger.swift +// Foundations +// +// Created by Wonji Suh on 3/4/26. +// + +import Foundation + +public enum APIHeaderManger { + + static let appPackageName: String = "-" + static let contentType: String = "application/json" + static let multipartContentType: String = "multipart/form-data" + static let contentAppleType: String = "application/x-www-form-urlencoded" +} diff --git a/Projects/Network/Foundations/Sources/APIHeader/TokenProviding.swift b/Projects/Network/Foundations/Sources/APIHeader/TokenProviding.swift new file mode 100644 index 0000000..75c7be9 --- /dev/null +++ b/Projects/Network/Foundations/Sources/APIHeader/TokenProviding.swift @@ -0,0 +1,48 @@ +// +// TokenProviding.swift +// Foundations +// +// Created by Wonji Suh on 3/4/26. +// + +import Foundation + +import Dependencies +import WeaveDI + +public protocol TokenProviding: Sendable { + func accessToken() -> String? + func saveAccessToken(_ token: String) +} + +private enum TokenProviderKey: DependencyKey { + static var liveValue: TokenProviding { + UnifiedDI.resolve(TokenProviding.self) ?? InMemoryTokenProvider() + } +} + +public extension DependencyValues { + var tokenProvider: TokenProviding { + get { self[TokenProviderKey.self] } + set { self[TokenProviderKey.self] = newValue } + } +} + +public final class InMemoryTokenProvider: TokenProviding, @unchecked Sendable { + private var storage: String? + private let lock = NSLock() + + public init() {} + + public func accessToken() -> String? { + lock.lock() + defer { lock.unlock() } + return storage + } + + public func saveAccessToken(_ token: String) { + lock.lock() + storage = token + lock.unlock() + } +} diff --git a/Projects/Network/Foundations/Sources/Base.swift b/Projects/Network/Foundations/Sources/Base.swift deleted file mode 100644 index fc5212e..0000000 --- a/Projects/Network/Foundations/Sources/Base.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// base.swift -// DDDAttendance. -// -// Created by Roy on 2025-10-22 -// Copyright ยฉ 2025 DDD , Ltd., All rights reserved. -// - -import SwiftUI - -struct BaseView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") - } - .padding() - } -} - diff --git a/Projects/Network/Networking/NetworkingTests/Sources/Test.swift b/Projects/Network/Networking/NetworkingTests/Sources/Test.swift deleted file mode 100644 index a9c810e..0000000 --- a/Projects/Network/Networking/NetworkingTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-09-04 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Network/Networking/Sources/Exorted/NetworkExported.swift b/Projects/Network/Networking/Sources/Exorted/NetworkExported.swift deleted file mode 100644 index fa650f3..0000000 --- a/Projects/Network/Networking/Sources/Exorted/NetworkExported.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// NetworkExported.swift -// Network -// -// Created by Wonji Suh on 9/5/25. -// - - diff --git a/Projects/Network/Networks/NetworksTests/Sources/Test.swift b/Projects/Network/Networks/NetworksTests/Sources/Test.swift new file mode 100644 index 0000000..cc6b0f3 --- /dev/null +++ b/Projects/Network/Networks/NetworksTests/Sources/Test.swift @@ -0,0 +1,8 @@ +// +// base.swift +// DDDAttendance +// +// Created by Roy on 2026-03-03 +// Copyright ยฉ 2026 DDD , Ltd. All rights reserved. +// + diff --git a/Projects/Network/Networking/Project.swift b/Projects/Network/Networks/Project.swift similarity index 66% rename from Projects/Network/Networking/Project.swift rename to Projects/Network/Networks/Project.swift index 0a16d40..b4b2035 100644 --- a/Projects/Network/Networking/Project.swift +++ b/Projects/Network/Networks/Project.swift @@ -2,15 +2,16 @@ import Foundation import ProjectDescription import DependencyPlugin import ProjectTemplatePlugin +import ProjectTemplatePlugin import DependencyPackagePlugin let project = Project.makeAppModule( - name: "Networking", - bundleId: .appBundleID(name: ".Networking"), + name: "Networks", + bundleId: .appBundleID(name: ".Networks"), product: .staticFramework, settings: .settings(), dependencies: [ - .Network(implements: .Foundations) + .Network(implements: .Foundations), ], sources: ["Sources/**"] ) diff --git a/Projects/Network/Networks/Sources/NetworkExport.swift b/Projects/Network/Networks/Sources/NetworkExport.swift new file mode 100644 index 0000000..ee1175f --- /dev/null +++ b/Projects/Network/Networks/Sources/NetworkExport.swift @@ -0,0 +1,8 @@ +// +// NetworkExport.swift +// Networks +// +// Created by Wonji Suh on 3/4/26. +// + +@_exported import ThirdPartys diff --git a/Projects/Network/ThirdPartys/Project.swift b/Projects/Network/ThirdPartys/Project.swift new file mode 100644 index 0000000..91c4eb0 --- /dev/null +++ b/Projects/Network/ThirdPartys/Project.swift @@ -0,0 +1,18 @@ +import Foundation +import ProjectDescription +import DependencyPlugin +import ProjectTemplatePlugin +import ProjectTemplatePlugin +import DependencyPackagePlugin + +let project = Project.makeAppModule( + name: "ThirdPartys", + bundleId: .appBundleID(name: ".ThirdPartys"), + product: .staticFramework, + settings: .settings(), + dependencies: [ + .SPM.asyncMoya, + .SPM.weaveDI + ], + sources: ["Sources/**"] +) diff --git a/Projects/Domain/DomainInterface/Sources/Base.swift b/Projects/Network/ThirdPartys/Sources/Base.swift similarity index 78% rename from Projects/Domain/DomainInterface/Sources/Base.swift rename to Projects/Network/ThirdPartys/Sources/Base.swift index 6297cc4..2800225 100644 --- a/Projects/Domain/DomainInterface/Sources/Base.swift +++ b/Projects/Network/ThirdPartys/Sources/Base.swift @@ -2,8 +2,8 @@ // base.swift // DDDAttendance. // -// Created by Roy on 2025-09-04 -// Copyright ยฉ 2025 DDD , Ltd., All rights reserved. +// Created by Roy on 2026-03-03 +// Copyright ยฉ 2026 DDD , Ltd., All rights reserved. // import SwiftUI diff --git a/Projects/Network/ThirdPartys/ThirdPartysTests/Sources/Test.swift b/Projects/Network/ThirdPartys/ThirdPartysTests/Sources/Test.swift new file mode 100644 index 0000000..cc6b0f3 --- /dev/null +++ b/Projects/Network/ThirdPartys/ThirdPartysTests/Sources/Test.swift @@ -0,0 +1,8 @@ +// +// base.swift +// DDDAttendance +// +// Created by Roy on 2026-03-03 +// Copyright ยฉ 2026 DDD , Ltd. All rights reserved. +// + diff --git a/Projects/Presentation/Presentation/PresentationTests/Sources/Test.swift b/Projects/Presentation/Presentation/PresentationTests/Sources/Test.swift deleted file mode 100644 index a9c810e..0000000 --- a/Projects/Presentation/Presentation/PresentationTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-09-04 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Presentation/Presentation/Project.swift b/Projects/Presentation/Presentation/Project.swift index e1aea5a..a5676a7 100644 --- a/Projects/Presentation/Presentation/Project.swift +++ b/Projects/Presentation/Presentation/Project.swift @@ -12,5 +12,6 @@ let project = Project.makeAppModule( dependencies: [ .Presentation(implements: .Splash) ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: false ) diff --git a/Projects/Presentation/Splash/Project.swift b/Projects/Presentation/Splash/Project.swift index 11c8dd3..f704afc 100644 --- a/Projects/Presentation/Splash/Project.swift +++ b/Projects/Presentation/Splash/Project.swift @@ -14,5 +14,6 @@ let project = Project.makeAppModule( .Domain(implements: .UseCase), .Shared(implements: .DesignSystem), ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: true ) diff --git a/Projects/Shared/DesignSystem/DesignSystemTests/Sources/Test.swift b/Projects/Shared/DesignSystem/DesignSystemTests/Sources/Test.swift deleted file mode 100644 index a9c810e..0000000 --- a/Projects/Shared/DesignSystem/DesignSystemTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-09-04 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Shared/DesignSystem/Project.swift b/Projects/Shared/DesignSystem/Project.swift index 57ec548..2a9b571 100644 --- a/Projects/Shared/DesignSystem/Project.swift +++ b/Projects/Shared/DesignSystem/Project.swift @@ -13,5 +13,6 @@ let project = Project.makeAppModule( .SPM.composableArchitecture ], sources: ["Sources/**"], - resources: ["Resources/**", "FontAsset"] + resources: ["Resources/**", "FontAsset"], + hasTests: false ) diff --git a/Projects/Shared/Shared/Project.swift b/Projects/Shared/Shared/Project.swift index 6a00e3e..67c62d7 100644 --- a/Projects/Shared/Shared/Project.swift +++ b/Projects/Shared/Shared/Project.swift @@ -13,5 +13,6 @@ let project = Project.makeAppModule( .Shared(implements: .DesignSystem), .Shared(implements: .Utill), ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: false ) diff --git a/Projects/Shared/Shared/SharedTests/Sources/Test.swift b/Projects/Shared/Shared/SharedTests/Sources/Test.swift deleted file mode 100644 index a9c810e..0000000 --- a/Projects/Shared/Shared/SharedTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-09-04 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Projects/Shared/Utill/Project.swift b/Projects/Shared/Utill/Project.swift index 6670562..8542661 100644 --- a/Projects/Shared/Utill/Project.swift +++ b/Projects/Shared/Utill/Project.swift @@ -12,5 +12,6 @@ let project = Project.makeAppModule( dependencies: [ .SPM.composableArchitecture ], - sources: ["Sources/**"] + sources: ["Sources/**"], + hasTests: false ) diff --git a/Projects/Shared/Utill/UtillTests/Sources/Test.swift b/Projects/Shared/Utill/UtillTests/Sources/Test.swift deleted file mode 100644 index a9c810e..0000000 --- a/Projects/Shared/Utill/UtillTests/Sources/Test.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// base.swift -// DDDAttendance -// -// Created by Roy on 2025-09-04 -// Copyright ยฉ 2025 DDD , Ltd. All rights reserved. -// - diff --git a/Tuist/Package.swift b/Tuist/Package.swift index 2cb23d7..127df2e 100644 --- a/Tuist/Package.swift +++ b/Tuist/Package.swift @@ -5,7 +5,24 @@ import PackageDescription import struct ProjectDescription.PackageSettings let packageSettings = PackageSettings( - productTypes: [:] + productTypes: [ + "ComposableArchitecture": .staticFramework, + "TCACoordinators": .staticFramework, + "Moya": .staticFramework, + "LogMacro": .staticFramework, + "AsyncMoya": .staticFramework, + "AppAuth": .staticFramework, + "AppAuthCore": .staticFramework, + "GTMAppAuth": .staticFramework, + "GTMSessionFetcherCore": .staticFramework, + "IssueReporting": .staticFramework, + "IssueReportingPackageSupport": .staticFramework, + "XCTestDynamicOverlay": .staticFramework, + "Clocks": .staticFramework, + "ConcurrencyExtras": .staticFramework, + "SDWebImageSwiftUI": .staticFramework, + "WeaveDI": .staticFramework + ] ) #endif @@ -14,6 +31,9 @@ let package = Package( dependencies: [ .package(url: "https://github.com/pointfreeco/swift-composable-architecture", exact: "1.18.0"), .package(url: "https://github.com/johnpatrickmorgan/TCACoordinators.git", exact: "0.11.1"), - .package(url: "https://github.com/Roy-wonji/WeaveDI.git", from: "3.4.0") + .package(url: "https://github.com/Roy-wonji/WeaveDI.git", from: "3.4.0"), + .package(url: "https://github.com/google/GoogleSignIn-iOS", from: "9.0.0"), + .package(url: "https://github.com/Roy-wonji/AsyncMoya", from: "1.1.8"), + .package(url: "https://github.com/openid/AppAuth-iOS.git", from: "2.0.0"), ] ) diff --git a/TuistTool.swift b/TuistTool.swift index 2ec9aab..f10568b 100755 --- a/TuistTool.swift +++ b/TuistTool.swift @@ -62,6 +62,9 @@ func generate() { // โœ… ํ”„๋ฆฌ๋ทฐ ๋ชจ๋“œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์ถ”๊ฐ€ setenv("TUIST_FOR_PREVIEW", "TRUE", 1) + // ๐Ÿ“ hasTests: true์ธ ๋ชจ๋“ˆ๋“ค์˜ Tests/Sources ๋””๋ ‰ํ† ๋ฆฌ ์ž๋™ ์ƒ์„ฑ + ensureTestsDirectoriesForHasTestsModules() + // โœ… tuist generate ์‹คํ–‰ run("tuist", arguments: ["generate"]) } @@ -417,6 +420,55 @@ private func ensureDirectoryExists(at path: String) { } } +// MARK: - Tests ๋””๋ ‰ํ† ๋ฆฌ ์ž๋™ ์ƒ์„ฑ +private func ensureTestsDirectoriesForHasTestsModules() { + print("๐Ÿ” hasTests: true์ธ ๋ชจ๋“ˆ๋“ค์˜ Tests/Sources ๋””๋ ‰ํ† ๋ฆฌ ํ™•์ธ ์ค‘...") + + let fileManager = FileManager.default + guard let enumerator = fileManager.enumerator(atPath: "Projects") else { + print("โš ๏ธ Projects ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + return + } + + var createdCount = 0 + var existingCount = 0 + + while let relativePath = enumerator.nextObject() as? String { + guard relativePath.hasSuffix("Project.swift") else { continue } + + let fullPath = "Projects/\(relativePath)" + let projectDir = URL(fileURLWithPath: fullPath).deletingLastPathComponent().path + + // Project.swift ํŒŒ์ผ์—์„œ hasTests: true ํ™•์ธ + do { + let content = try String(contentsOfFile: fullPath, encoding: .utf8) + if content.contains("hasTests: true") { + let testsSourcesPath = "\(projectDir)/Tests/Sources" + + if !fileManager.fileExists(atPath: testsSourcesPath) { + ensureDirectoryExists(at: testsSourcesPath) + print("๐Ÿ“ Created Tests/Sources for \(URL(fileURLWithPath: projectDir).lastPathComponent)") + createdCount += 1 + } else { + existingCount += 1 + } + } + } catch { + print("โš ๏ธ \(fullPath) ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ: \(error)") + } + } + + if createdCount > 0 { + print("โœ… \(createdCount)๊ฐœ์˜ Tests/Sources ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค") + } + if existingCount > 0 { + print("โ„น๏ธ \(existingCount)๊ฐœ์˜ Tests/Sources ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค") + } + if createdCount == 0 && existingCount == 0 { + print("โ„น๏ธ hasTests: true์ธ ๋ชจ๋“ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + } +} + private func updateEnvironmentDefaults(oldName: String, newName: String, bundleIdPrefix: String, teamId: String) { let environmentPath = "Plugins/ProjectTemplatePlugin/ProjectDescriptionHelpers/Project+Templete/Project+Enviorment.swift" diff --git a/make b/make index 78c4776..8cd9401 100755 Binary files a/make and b/make differ diff --git a/mise.toml b/mise.toml index ee27ddd..90d826f 100644 --- a/mise.toml +++ b/mise.toml @@ -1,2 +1,2 @@ [tools] -tuist = "4.50.2" +tuist = "4.154.0"