diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift index 591b129..ed7267a 100644 --- a/Projects/App/Project.swift +++ b/Projects/App/Project.swift @@ -10,7 +10,10 @@ import ProjectDescriptionHelpers let project = Project.dori( targets: [ - DoriAppTarget.make( + .app( + name: "DoriApp", + bundleId: Environment.App.baseBundleId, + resources: [.glob(pattern: "Resources/**", excluding: ["Resources/info.plist"])], dependencies: [ DoriModules.onboarding.module.projectDependency, DoriModules.calendar.module.projectDependency, @@ -22,8 +25,8 @@ let project = Project.dori( DoriModules.keychain.module.projectDependency, DoriModules.designSystem.module.projectDependency, DoriModules.core.module.projectDependency, - DoriDependency.composableArchitecture, - ] + .external(.composableArchitecture) + ], ), ] ) diff --git a/Projects/Core/DoriCore/Sources/Extensions/Date+Extensions.swift b/Projects/Core/DoriCore/Sources/Extensions/Date+Extensions.swift new file mode 100644 index 0000000..b9d5cb0 --- /dev/null +++ b/Projects/Core/DoriCore/Sources/Extensions/Date+Extensions.swift @@ -0,0 +1,96 @@ +// +// Date+Extensions.swift +// Dori-iOS +// +// Created by 강동영 on 2/11/26. +// + +import Foundation + +public extension Date { + private static let koreanLocale = Locale(identifier: "ko_KR") + private static let koreanCalendar: Calendar = { + var calendar = Calendar(identifier: .gregorian) + calendar.locale = koreanLocale + return calendar + }() + + // "12월 12일(금)" 형식 + var koreanDateWithWeekday: String { + let formatter = DateFormatter() + formatter.locale = Self.koreanLocale + formatter.dateFormat = "M월 d일(E)" + return formatter.string(from: self) + } + + // "2024년 12월" 형식 + var koreanYearMonth: String { + let formatter = DateFormatter() + formatter.locale = Self.koreanLocale + formatter.dateFormat = "yyyy년 M월" + return formatter.string(from: self) + } + + // "12월" 형식 + var koreanMonth: String { + let formatter = DateFormatter() + formatter.locale = Self.koreanLocale + formatter.dateFormat = "M월" + return formatter.string(from: self) + } + + // 해당 월의 첫째 날 + var startOfMonth: Date { + Self.koreanCalendar.date(from: Self.koreanCalendar.dateComponents([.year, .month], from: self)) ?? self + } + + // 해당 월의 마지막 날 + var endOfMonth: Date { + Self.koreanCalendar.date(byAdding: DateComponents(month: 1, day: -1), to: startOfMonth) ?? self + } + + // 해당 월의 일 수 + var daysInMonth: Int { + Self.koreanCalendar.range(of: .day, in: .month, for: self)?.count ?? 30 + } + + // 해당 월 1일의 요일 (일요일 = 1) + var firstWeekdayOfMonth: Int { + Self.koreanCalendar.component(.weekday, from: startOfMonth) + } + + // 해당 날짜의 일(day) + var day: Int { + Self.koreanCalendar.component(.day, from: self) + } + + // 해당 날짜의 월(month) + var month: Int { + Self.koreanCalendar.component(.month, from: self) + } + + // 해당 날짜의 연(year) + var year: Int { + Self.koreanCalendar.component(.year, from: self) + } + + // 이전 달 + var previousMonth: Date { + Self.koreanCalendar.date(byAdding: .month, value: -1, to: self) ?? self + } + + // 다음 달 + var nextMonth: Date { + Self.koreanCalendar.date(byAdding: .month, value: 1, to: self) ?? self + } + + // 같은 날인지 확인 + func isSameDay(as other: Date) -> Bool { + Self.koreanCalendar.isDate(self, inSameDayAs: other) + } + + // 같은 달인지 확인 + func isSameMonth(as other: Date) -> Bool { + Self.koreanCalendar.isDate(self, equalTo: other, toGranularity: .month) + } +} diff --git a/Projects/Core/DoriCore/Sources/Extensions/Int+Extensions.swift b/Projects/Core/DoriCore/Sources/Extensions/Int+Extensions.swift new file mode 100644 index 0000000..207aead --- /dev/null +++ b/Projects/Core/DoriCore/Sources/Extensions/Int+Extensions.swift @@ -0,0 +1,27 @@ +// +// Int+Extensions.swift +// Dori-iOS +// +// Created by 강동영 on 2/11/26. +// + +import Foundation + +public extension Int { + // "100,000원" 형식 + var wonFormatted: String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.locale = Locale(identifier: "ko_KR") + let formatted = formatter.string(from: NSNumber(value: self)) ?? "\(self)" + return "\(formatted)원" + } + + // "100,000" 형식 (원 없이) + var decimalFormatted: String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.locale = Locale(identifier: "ko_KR") + return formatter.string(from: NSNumber(value: self)) ?? "\(self)" + } +} diff --git a/Projects/Core/DoriCore/Sources/Models/EventType.swift b/Projects/Core/DoriCore/Sources/Models/EventType.swift new file mode 100644 index 0000000..90b71af --- /dev/null +++ b/Projects/Core/DoriCore/Sources/Models/EventType.swift @@ -0,0 +1,19 @@ +// +// EventType.swift +// Dori-iOS +// +// Created by 강동영 on 2/15/26. +// + +import Foundation + +public enum EventType: String, CaseIterable, Codable, Equatable, Sendable, Identifiable { + case wedding = "결혼식" + case funeral = "장례식" + case firstBirthday = "돌잔치" + case housewarming = "집들이" + case birthday = "생일" + case other = "기타" + + public var id: String { rawValue } +} diff --git a/Projects/Core/DoriCore/Sources/Models/Relationship.swift b/Projects/Core/DoriCore/Sources/Models/Relationship.swift new file mode 100644 index 0000000..98be58f --- /dev/null +++ b/Projects/Core/DoriCore/Sources/Models/Relationship.swift @@ -0,0 +1,17 @@ +// +// Relationship.swift +// Dori-iOS +// +// Created by 강동영 on 2/15/26. +// + +import Foundation + +public enum Relationship: String, CaseIterable, Codable, Equatable, Sendable, Hashable, Identifiable { + case friend = "친구" + case family = "가족" + case company = "회사" + case other = "기타" + + public var id: String { rawValue } +} diff --git a/Projects/Core/DoriCore/Sources/Models/TransactionType.swift b/Projects/Core/DoriCore/Sources/Models/TransactionType.swift new file mode 100644 index 0000000..8588d33 --- /dev/null +++ b/Projects/Core/DoriCore/Sources/Models/TransactionType.swift @@ -0,0 +1,15 @@ +// +// TransactionType.swift +// Dori-iOS +// +// Created by 강동영 on 2/15/26. +// + +import Foundation + +public enum TransactionType: String, CaseIterable, Codable, Equatable, Sendable, Hashable, Identifiable { + case given = "주도리" + case received = "받도리" + + public var id: String { rawValue } +} diff --git a/Projects/Core/DoriDesignSystem/Sources/AmountLabel.swift b/Projects/Core/DoriDesignSystem/Sources/AmountLabel.swift new file mode 100644 index 0000000..2d6c37a --- /dev/null +++ b/Projects/Core/DoriDesignSystem/Sources/AmountLabel.swift @@ -0,0 +1,21 @@ +// +// AmountLabel.swift +// Dori-iOS +// +// Created by 강동영 on 2/18/26. +// + +import SwiftUI +import DoriCore + +public struct AmountLabel: View { + let amount: Int + + public init(_ amount: Int) { + self.amount = amount + } + + public var body: some View { + Text(amount.wonFormatted) + } +} diff --git a/Projects/Core/DoriDesignSystem/Sources/FloatingActionButton.swift b/Projects/Core/DoriDesignSystem/Sources/FloatingActionButton.swift new file mode 100644 index 0000000..93ef7ef --- /dev/null +++ b/Projects/Core/DoriDesignSystem/Sources/FloatingActionButton.swift @@ -0,0 +1,55 @@ +// +// FloatingActionButton.swift +// Dori-iOS +// +// Created by 강동영 on 2/15/26. +// + +import SwiftUI + +public struct FloatingActionButton: View { + let action: () -> Void + + public init(action: @escaping () -> Void) { + self.action = action + } + + public var body: some View { + Button(action: action) { + Image(systemName: "plus") + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.white) + .frame( + width: 56, + height: 56 + ) + .background(DoriColors.main.color) + .clipShape(Circle()) + .shadow( + color: .black.opacity(0.3), + radius: 4, + x: 0, + y: 2 + ) + } + } +} + +#Preview { + ZStack { + Color.gray.opacity(0.2) + .ignoresSafeArea() + + VStack { + Spacer() + HStack { + Spacer() + FloatingActionButton { + print("FAB tapped") + } + .padding() + } + } + } +} diff --git a/Projects/Core/DoriDesignSystem/Sources/PageIndicator.swift b/Projects/Core/DoriDesignSystem/Sources/PageIndicator.swift new file mode 100644 index 0000000..d3d3cc7 --- /dev/null +++ b/Projects/Core/DoriDesignSystem/Sources/PageIndicator.swift @@ -0,0 +1,42 @@ +// +// PageIndicator.swift +// Dori-iOS +// +// Created by 강동영 on 2/15/26. +// + +import SwiftUI + +public struct PageIndicator: View { + let count: Int + @Binding var currentIndex: Int? + + public init( + count: Int, + currentIndex: Binding + ) { + self.count = count + if currentIndex.wrappedValue == nil { + self._currentIndex = .constant(0) + } else { + self._currentIndex = currentIndex + } + } + + public var body: some View { + HStack { + ForEach(0.. FontStyle { let spec = styleSpec let fontName = spec.weight.getFontName(from: provider) - return FontStyle(.custom(fontName), size: spec.size, lineHeight: spec.lineHeight) + return FontStyle(.custom(fontName), size: spec.size) } } -public extension TypoStyle { - enum Heading: Int, CaseIterable { - case h11 = 11, h13 = 13, h14 = 14, h15 = 15, h16 = 16, h20 = 20, h30 = 30 +public extension TypoSemantic { + enum Heading: CaseIterable { + case h1 - var size: CGFloat { CGFloat(rawValue) } + var token: TypoToken { TypoToken.bold(.b16) } + } + + enum Title: CaseIterable { + case t1 - // 등차수열 적용 - var lineHeight: CGFloat { 2 * size - 6 } + var token: TypoToken { TypoToken.medium(.m16) } } - enum SubTitle: Int, CaseIterable { - case t12 = 12, t14 = 14, t15 = 15, t16 = 16, t20 = 20 + enum SubTitle: CaseIterable { + case sb1, sb2, m2 - var size: CGFloat { CGFloat(rawValue) } + var token: TypoToken { + switch self { + case .sb1: + .semiBold(.sb20) + case .sb2: + .semiBold(.sb14) + case .m2: + .medium(.m14) + } + } + } + + enum Body: CaseIterable { + case b1, b4 + case sb2, sb3, sb6 + case m3, m5 + case r2, r3, r4, r6 - // 등차수열 적용 - var lineHeight: CGFloat { 2 * size - 6 } + var token: TypoToken { + switch self { + case .b1: + .bold(.b30) + case .b4: + .bold(.b14) + case .sb2: + .semiBold(.sb16) + case .sb3: + .semiBold(.sb15) + case .sb6: + .semiBold(.sb12) + case .m3: + .medium(.m15) + case .m5: + .medium(.m13) + case .r2: + .regular(.r16) + case .r3: + .regular(.r15) + case .r4: + .regular(.r14) + case .r6: + .regular(.r12) + } + } + } + + enum Caption: CaseIterable { + case b1, b2 + case m2 + case r1, r2 + + var token: TypoToken { + switch self { + case .b1: + .bold(.b13) + case .b2: + .bold(.b11) + case .m2: + .medium(.m11) + case .r1: + .regular(.r13) + case .r2: + .regular(.r11) + } + } + } +} + +@MainActor +public enum TypoToken { + case bold(TypoToken.Bold) + case semiBold(TypoToken.SemiBold) + case medium(TypoToken.Medium) + case regular(TypoToken.Regular) + + // MARK: - 폰트에 의존하지 않는 스타일 정의 + private var styleSpec: FontSpec { + switch self { + case .bold(let heading): + return .init(.bold, heading.size) + + case .semiBold(let subtitle): + return .init(.semiBold, subtitle.size) + + case .medium(let body): + return .init(.medium, body.size) + + case .regular(let caption): + return .init(.regular, caption.size) + } + } + + // MARK: - FontProvider를 받아서 FontStyle 생성 + public func getFontStyle(with provider: FontProvider) -> FontStyle { + let spec = styleSpec + let fontName = spec.weight.getFontName(from: provider) + return FontStyle(.custom(fontName), size: spec.size) } - enum Body: Int, CaseIterable { - case b11 = 11, b12, b13, b14, b15, b16 + public func getFontSpec() -> FontSpec { + return styleSpec + } +} + +public struct FontSpec { + let weight: FontWeight + let size: CGFloat + + init(_ weight: FontWeight, _ size: CGFloat) { + self.weight = weight + self.size = size + } +} + +public extension TypoToken { + enum Bold: Int, CaseIterable { + case b11 = 11, b13 = 13, b14 = 14, b15 = 15, b16 = 16, b20 = 20, b30 = 30 var size: CGFloat { CGFloat(rawValue) } + } + + enum SemiBold: Int, CaseIterable { + case sb12 = 12, sb14 = 14, sb15 = 15, sb16 = 16, sb20 = 20 - // 등차수열 적용 - var lineHeight: CGFloat { 2 * size - 6 } + var size: CGFloat { CGFloat(rawValue) } } - enum Caption: Int, CaseIterable { - case c11 = 11, c12, c13, c14, c15, c16, c18 = 18, c20 = 20 + enum Medium: Int, CaseIterable { + case m11 = 11, m12, m13, m14, m15, m16 var size: CGFloat { CGFloat(rawValue) } + } + + enum Regular: Int, CaseIterable { + case r11 = 11, r12, r13, r14, r15, r16, r18 = 18, r20 = 20 - // 등차수열 적용 - var lineHeight: CGFloat { 2 * size - 6 } + var size: CGFloat { CGFloat(rawValue) } } } diff --git a/Projects/Core/DoriDesignSystem/Sources/Typography/TypoToken.swift b/Projects/Core/DoriDesignSystem/Sources/Typography/TypoToken.swift new file mode 100644 index 0000000..3409516 --- /dev/null +++ b/Projects/Core/DoriDesignSystem/Sources/Typography/TypoToken.swift @@ -0,0 +1,78 @@ +// +// TypoStyle.swift +// Dori-iOS +// +// Created by 강동영 on 2/6/26. +// + +import Foundation + +@MainActor +public enum TypoStyle { + case heading(TypoStyle.Heading) + case subtitle(TypoStyle.SubTitle) + case body(TypoStyle.Body) + case caption(TypoStyle.Caption) + + // MARK: - 폰트에 의존하지 않는 스타일 정의 + private var styleSpec: (weight: FontWeight, size: CGFloat, lineHeight: CGFloat) { + switch self { + case .heading(let heading): + return (.bold, heading.size, heading.lineHeight) + + case .subtitle(let subtitle): + return (.semiBold, subtitle.size, subtitle.lineHeight) + + case .body(let body): + return (.medium, body.size, body.lineHeight) + + case .caption(let caption): + return (.regular, caption.size, caption.lineHeight) + } + } + + // MARK: - FontProvider를 받아서 FontStyle 생성 + public func getFontStyle(with provider: FontProvider) -> FontStyle { + let spec = styleSpec + let fontName = spec.weight.getFontName(from: provider) + return FontStyle(.custom(fontName), size: spec.size) + } +} + +public extension TypoStyle { + enum Heading: Int, CaseIterable { + case h11 = 11, h13 = 13, h14 = 14, h15 = 15, h16 = 16, h20 = 20, h30 = 30 + + var size: CGFloat { CGFloat(rawValue) } + + // 등차수열 적용 + var lineHeight: CGFloat { 2 * size - 6 } + } + + enum SubTitle: Int, CaseIterable { + case t12 = 12, t14 = 14, t15 = 15, t16 = 16, t20 = 20 + + var size: CGFloat { CGFloat(rawValue) } + + // 등차수열 적용 + var lineHeight: CGFloat { 2 * size - 6 } + } + + enum Body: Int, CaseIterable { + case b11 = 11, b12, b13, b14, b15, b16 + + var size: CGFloat { CGFloat(rawValue) } + + // 등차수열 적용 + var lineHeight: CGFloat { 2 * size - 6 } + } + + enum Caption: Int, CaseIterable { + case c11 = 11, c12, c13, c14, c15, c16, c18 = 18, c20 = 20 + + var size: CGFloat { CGFloat(rawValue) } + + // 등차수열 적용 + var lineHeight: CGFloat { 2 * size - 6 } + } +} diff --git a/Projects/Core/DoriDesignSystem/Sources/Typography/View+.swift b/Projects/Core/DoriDesignSystem/Sources/Typography/View+.swift index dec186e..2e503de 100644 --- a/Projects/Core/DoriDesignSystem/Sources/Typography/View+.swift +++ b/Projects/Core/DoriDesignSystem/Sources/Typography/View+.swift @@ -9,12 +9,20 @@ import SwiftUI public extension View { // MARK: - font, linespacing 적용되어있음 (기본값: Pretendard) - func pretendard(_ style: TypoStyle) -> some View { + func pretendard(_ semantic: TypoSemantic) -> some View { let pretendardProvider = PretendardProvider() - let fontStyle = style.getFontStyle(with: pretendardProvider) + let fontStyle = semantic.getFontStyle(with: pretendardProvider) return self .font(fontStyle.font) - .lineSpacing(fontStyle.lineHeight) + .lineSpacing(fontStyle.lineSpacing) + } + + func pretendard(_ token: TypoToken) -> some View { + let pretendardProvider = PretendardProvider() + let fontStyle = token.getFontStyle(with: pretendardProvider) + return self + .font(fontStyle.font) + .lineSpacing(fontStyle.lineSpacing) } func hopangche( @@ -22,9 +30,9 @@ public extension View { lineHeight: CGFloat = 55 ) -> some View { let fontName = SamlipHopangProvider.FontName.basic.name - let fontStyle = FontStyle(.custom(fontName), size: size, lineHeight: lineHeight) + let fontStyle = FontStyle(.custom(fontName), size: size) return self .font(fontStyle.font) - .lineSpacing(fontStyle.lineHeight) + .lineSpacing(fontStyle.lineSpacing) } } diff --git a/Projects/Feature/Onboarding/Sources/IntroView.swift b/Projects/Feature/Onboarding/Sources/IntroView.swift index 3cb5b64..5408e72 100644 --- a/Projects/Feature/Onboarding/Sources/IntroView.swift +++ b/Projects/Feature/Onboarding/Sources/IntroView.swift @@ -105,14 +105,14 @@ public struct IntroView: View { .hopangche(size: 55) .foregroundStyle(.main) Text(prop.subtitle) - .pretendard(.caption(.c18)) + .pretendard(.regular(.r18)) .foregroundStyle(.main) } else { Text(prop.title) - .pretendard(.subtitle(.t20)) + .pretendard(.subtitle(.sb1)) .foregroundStyle(.main) Text(prop.subtitle) - .pretendard(.subtitle(.t20)) + .pretendard(.subtitle(.sb1)) .foregroundStyle(.main) } @@ -142,7 +142,7 @@ public struct IntroView: View { store.send(.kakaoLoginButtonTapped) } label: { Text("카카오로 시작하기") - .pretendard(.subtitle(.t15)) + .pretendard(.semiBold(.sb15)) .foregroundStyle(DoriColors.doriBlack.color) .frame(maxWidth: .infinity) .frame(height: 46) @@ -170,41 +170,6 @@ public struct IntroView: View { } } -struct PageIndicator: View { - let count: Int - @Binding var currentIndex: Int? - - init( - count: Int, - currentIndex: Binding - ) { - self.count = count - if currentIndex.wrappedValue == nil { - self._currentIndex = .constant(0) - } else { - self._currentIndex = currentIndex - } - - } - - var body: some View { - HStack { - ForEach(0.. Target { - .target( - name: name, - destinations: .iOS, - product: .app, - bundleId: DoriManifest.bundleIDPrefix, - deploymentTargets: DoriManifest.deploymentTarget, - infoPlist: .extendingDefault(with: [ - "UILaunchScreen": .dictionary([:]), - "BASE_URL": "$(BASE_URL)", - "KAKAO_NATIVE_APP_KEY": "$(KAKAO_NATIVE_APP_KEY)", - "Appearance": "Light", - "CFBundleURLTypes": [ - [ - "CFBundleTypeRole": "Editor", - "CFBundleURLName": Plist.Value.string(DoriManifest.bundleIDPrefix), - "CFBundleURLSchemes": ["$(KAKAO_CAllBACK)"], - ], - ], - "LSApplicationQueriesSchemes": [ - "kakaokompassauth", - "kakaolink", - ], - ]), - sources: ["Sources/**"], - resources: [.glob(pattern: "Resources/**", excluding: ["Resources/info.plist"])], - dependencies: dependencies, - settings: .settings( - base: DoriManifest.commonSettings, - configurations: [ - .debug(name: "Debug", xcconfig: .relativeToRoot(xcconfigPath)), - .release(name: "Release", xcconfig: .relativeToRoot(xcconfigPath)), - ] - ) - ) - } -} - -public extension Target { - static func doriFramework( - _ module: DoriModule, - dependencies: [TargetDependency] = [], - hasResources: Bool = false - ) -> Target { - .target( - name: module.name, - destinations: .iOS, - product: .framework, - bundleId: "\(DoriManifest.bundleIDPrefix).\(module.name)", - deploymentTargets: DoriManifest.deploymentTarget, - sources: ["\(module.localPath)/Sources/**"], - resources: hasResources ? ["\(module.localPath)/Resources/**"] : nil, - dependencies: dependencies, - settings: .settings(base: DoriManifest.commonSettings) - ) - } - - static func doriUnitTests( - _ module: DoriModule, - dependencies: [TargetDependency] = [] - ) -> Target { - .target( - name: "\(module.name)Tests", - destinations: .iOS, - product: .unitTests, - bundleId: "\(DoriManifest.bundleIDPrefix).\(module.name)Tests", - deploymentTargets: DoriManifest.deploymentTarget, - sources: ["\(module.localPath)/Tests/**"], - dependencies: [.target(name: module.name)] + dependencies, - settings: .settings(base: DoriManifest.commonSettings) - ) - } -} - -public extension Project { - static func dori( - name: String = DoriManifest.projectName, - packages: [Package] = [], - targets: [Target], - resourceSynthesizers: [ResourceSynthesizer] = [] - ) -> Project { - Project( - name: name, - organizationName: DoriManifest.organizationName, - packages: packages, - settings: .settings(base: DoriManifest.commonSettings), - targets: targets, - resourceSynthesizers: resourceSynthesizers - ) - } -} diff --git a/Tuist/ProjectDescriptionHelpers/Environment.swift b/Tuist/ProjectDescriptionHelpers/Environment.swift new file mode 100644 index 0000000..db881b3 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Environment.swift @@ -0,0 +1,69 @@ +// +// Environment.swift +// ProjectDescriptionHelpers +// +// Created by 강동영 on 2/12/26. +// + +import Foundation + +import ProjectDescription + +// MARK: - Build Configuration +public enum BuildConfiguration: String, CaseIterable { + case debug = "Debug" + case release = "Release" + + public var bundleIdSuffix: String { + switch self { + case .debug: return "" + case .release: return "" + } + } + + public var appName: String { + switch self { + case .debug: return "Dori-Debug" + case .release: return "Dori" + } + } +} + +// MARK: - Environment +public struct Environment { + public static let deploymentTarget = "17.6" + public static let teamID = "T5D2PB4P5T" + public static let organizationName = "com.arex" + public static let defaultRegion = "ko" + public static let projectName = "Dori-iOS" + + public struct App { + public static let baseBundleId = "\(organizationName).dori" + public static let displayName = "도리" + public static let version = "1.0.0" + public static let buildNumber = "1" + + public static func bundleId(for configuration: BuildConfiguration = .release) -> String { + baseBundleId + configuration.bundleIdSuffix + } + } + + public static func bundleId(for module: String, configuration: BuildConfiguration = .release) -> String { + "\(organizationName).\(module.lowercased())\(configuration.bundleIdSuffix)" + } + + public static func bundleId(category: ModuleCategory, module: String, configuration: BuildConfiguration = .release) -> String { + "\(organizationName).\(category.rawValue).\(module.lowercased())\(configuration.bundleIdSuffix)" + } +} + +// MARK: - Module Categories +public enum ModuleCategory: String, CaseIterable { + case app = "app" + case core = "core" + case feature = "feature" + case domain = "domain" + case data = "data" + case shared = "shared" + case plugin = "plugin" +} diff --git a/Tuist/ProjectDescriptionHelpers/InfoPlist+Extension.swift b/Tuist/ProjectDescriptionHelpers/InfoPlist+Extension.swift new file mode 100644 index 0000000..1df6cef --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/InfoPlist+Extension.swift @@ -0,0 +1,32 @@ +// +// InfoPlist+Extension.swift +// Manifests +// +// Created by 강동영 on 2/13/26. +// + +import ProjectDescription + +extension InfoPlist { + static let commonDictionary: [String: Plist.Value] = [ + "UILaunchScreen": .dictionary([:]), + "BASE_URL": "$(BASE_URL)", + "KAKAO_NATIVE_APP_KEY": "$(KAKAO_NATIVE_APP_KEY)", + "Appearance": "Light", + "CFBundleURLTypes": [ + [ + "CFBundleTypeRole": "Editor", + "CFBundleURLName": Plist.Value.string(Environment.App.baseBundleId), + "CFBundleURLSchemes": ["$(KAKAO_CAllBACK)"], + ], + ], + "LSApplicationQueriesSchemes": [ + "kakaokompassauth", + "kakaolink", + ], + ] + + public static func baseInfoPlist() -> InfoPlist { + return .extendingDefault(with: commonDictionary) + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Project+Extension.swift b/Tuist/ProjectDescriptionHelpers/Project+Extension.swift new file mode 100644 index 0000000..f504eba --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Project+Extension.swift @@ -0,0 +1,26 @@ +// +// Project+Extension.swift +// ProjectDescriptionHelpers +// +// Created by 강동영 on 2/13/26. +// + +import ProjectDescription + +public extension Project { + static func dori( + name: String = Environment.projectName, + packages: [Package] = [], + targets: [Target], + resourceSynthesizers: [ResourceSynthesizer] = [] + ) -> Project { + Project( + name: name, + organizationName: Environment.organizationName, + packages: packages, + settings: .settings(), + targets: targets, + resourceSynthesizers: resourceSynthesizers + ) + } +} diff --git a/Tuist/ProjectDescriptionHelpers/Settings+Extension.swift b/Tuist/ProjectDescriptionHelpers/Settings+Extension.swift new file mode 100644 index 0000000..6c08689 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Settings+Extension.swift @@ -0,0 +1,96 @@ +// +// Settings+Extension.swift +// ProjectDescriptionHelpers +// +// Created by 강동영 on 2/12/26. +// + +import ProjectDescription + +public extension Settings { + /// 프레임워크용 기본 설정 + static let frameworkSettings: Settings = .settings( + base: [ + "SKIP_INSTALL": "YES", + "DEFINES_MODULE": "YES", + "ENABLE_BITCODE": "NO", + "IPHONEOS_DEPLOYMENT_TARGET": .string(Environment.deploymentTarget), + "SWIFT_VERSION": "6.0", + "CLANG_ENABLE_MODULES": "YES" + ] + ) + + /// 테스트용 기본 설정 + static let testSettings: Settings = .settings( + base: [ + "ENABLE_TESTING_SEARCH_PATHS": "YES", + "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES": "YES", + "ENABLE_TESTABILITY": "YES", + "IPHONEOS_DEPLOYMENT_TARGET": .string(Environment.deploymentTarget), + "SWIFT_VERSION": "6.0" + ] + ) + + /// 앱용 설정 + static func appSettings( + teamID: String = Environment.teamID + ) -> Settings { + let rootPath = "Projects/App" + let xcconfigPath = "\(rootPath)/Resources/Common.xcconfig" + + let baseSettings: [String: SettingValue] = [ + "APP_NAME": .string(Environment.App.displayName), + "CODE_SIGN_STYLE": "Automatic", + "DEVELOPMENT_TEAM": .string(teamID), + "MARKETING_VERSION": .string(Environment.App.version), + "CURRENT_PROJECT_VERSION": .string(Environment.App.buildNumber), + "ENABLE_BITCODE": "NO", + "IPHONEOS_DEPLOYMENT_TARGET": .string(Environment.deploymentTarget), + "SWIFT_VERSION": "6.0", + ] + + let debugSettings: [String: SettingValue] = [ + "PRODUCT_NAME": .string(BuildConfiguration.debug.appName), + "ENABLE_TESTABILITY": "YES", + "GCC_OPTIMIZATION_LEVEL": "0", + "SWIFT_OPTIMIZATION_LEVEL": "-Onone", + "DEBUG_INFORMATION_FORMAT": "dwarf", + "GCC_PREPROCESSOR_DEFINITIONS": .array(["DEBUG=1"]) + ] + + let releaseSettings: [String: SettingValue] = [ + "PRODUCT_NAME": .string(BuildConfiguration.release.appName), + "SWIFT_OPTIMIZATION_LEVEL": "-O", + "ENABLE_TESTABILITY": "NO", + "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym", + "SWIFT_COMPILATION_MODE": "wholemodule" + ] + + return .settings( + base: baseSettings, + configurations: [ + .debug( + name: .debug, + settings: debugSettings, + xcconfig: .relativeToRoot(xcconfigPath) + ), + .release( + name: .release, + settings: releaseSettings, + xcconfig: .relativeToRoot(xcconfigPath) + ) + ] + ) + } + + /// 데모 앱용 설정 + static let demoAppSettings: Settings = .settings( + base: [ + "CODE_SIGN_STYLE": "Automatic", + "DEVELOPMENT_TEAM": .string(Environment.teamID), + "IPHONEOS_DEPLOYMENT_TARGET": .string(Environment.deploymentTarget), + "SWIFT_VERSION": "6.0", + "ENABLE_TESTABILITY": "YES" + ] + ) +} diff --git a/Tuist/ProjectDescriptionHelpers/Target+Extension.swift b/Tuist/ProjectDescriptionHelpers/Target+Extension.swift new file mode 100644 index 0000000..a392906 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/Target+Extension.swift @@ -0,0 +1,68 @@ +// +// Target+Extension.swift +// ProjectDescriptionHelpers +// +// Created by 강동영 on 2/13/26. +// + +import ProjectDescription + +public extension Target { + static func doriFramework( + _ module: DoriModule, + dependencies: [TargetDependency] = [], + hasResources: Bool = false + ) -> Target { + .target( + name: module.name, + destinations: .iOS, + product: .framework, + bundleId: "\(Environment.App.baseBundleId).\(module.name)", + deploymentTargets: .iOS(Environment.deploymentTarget), + sources: ["\(module.localPath)/Sources/**"], + resources: hasResources ? ["\(module.localPath)/Resources/**"] : nil, + dependencies: dependencies, + settings: .frameworkSettings + ) + } + + static func doriUnitTests( + _ module: DoriModule, + dependencies: [TargetDependency] = [] + ) -> Target { + .target( + name: "\(module.name)Tests", + destinations: .iOS, + product: .unitTests, + bundleId: "\(Environment.App.baseBundleId).\(module.name)Tests", + deploymentTargets: .iOS(Environment.deploymentTarget), + sources: ["\(module.localPath)/Tests/**"], + dependencies: [.target(name: module.name)] + dependencies, + settings: .testSettings + ) + } + + static func app( + name: String, + bundleId: String, + infoPlist: InfoPlist? = .baseInfoPlist(), + sources: SourceFilesList = ["Sources/**"], + resources: ResourceFileElements = ["Resources/**"], + dependencies: [TargetDependency] = [], + settings: Settings? = .appSettings(), + entitlements: Entitlements? = nil + ) -> Target { + .target( + name: name, + destinations: .iOS, + product: .app, + bundleId: bundleId, + deploymentTargets: .iOS(Environment.deploymentTarget), + infoPlist: infoPlist, + sources: sources, + resources: resources, + dependencies: dependencies, + settings: settings + ) + } +} diff --git a/Tuist/ProjectDescriptionHelpers/TargetDependency+Extension.swift b/Tuist/ProjectDescriptionHelpers/TargetDependency+Extension.swift new file mode 100644 index 0000000..b57e567 --- /dev/null +++ b/Tuist/ProjectDescriptionHelpers/TargetDependency+Extension.swift @@ -0,0 +1,19 @@ +import ProjectDescription + +extension TargetDependency { + public static func external(_ dependency: DoriDependency) -> TargetDependency { + .external(name: dependency.name) + } +} + +public enum DoriDependency: String { + case alamofire + case composableArchitecture + case kakaoSDKCommon + case kakaoSDKAuth + case kakaoSDKUser + + var name: String { + rawValue + } +} diff --git a/Workspace.swift b/Workspace.swift index d486dc8..218d486 100644 --- a/Workspace.swift +++ b/Workspace.swift @@ -9,6 +9,6 @@ import ProjectDescription import ProjectDescriptionHelpers let workspace = Workspace( - name: DoriManifest.projectName, + name: Environment.projectName, projects: DoriLayer.allCases.map(\.projectPath) )