diff --git a/.swiftlint.yml b/.swiftlint.yml
index 467ebdb..ab13113 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -1,37 +1,35 @@
-# Mozi SwiftLint draft
-# 기준: StyleShare 계열 가이드 + 현재 레포(SwiftUI/TCA)
-# 상태: 초안. 세부 룰은 추후 조정 가능
+# Mozi SwiftLint
+# 기준: 안전성 우선 + SwiftUI/TCA/DesignSystem 실무 균형
+# strict CI를 유지하되, 스타일 노이즈는 완화
disabled_rules:
- # 초안 단계 노이즈 완화
+ # 포맷/스타일 노이즈
- trailing_whitespace
- todo
- multiple_closures_with_trailing_closure
- trailing_comma
- file_length
- # 포맷터 없이 과도한 스타일 강제 완화
- opening_brace
- statement_position
+ - sorted_imports
+ - nesting
opt_in_rules:
+ # 안전성
- force_unwrapping
- implicitly_unwrapped_optional
- - closure_spacing
- unowned_variable_capture
+ - weak_delegate
+ # 실용 품질
- private_over_fileprivate
- empty_enum_arguments
- - weak_delegate
- first_where
- contains_over_filter_count
- contains_over_filter_is_empty
- overridden_super_call
- toggle_bool
- - closure_body_length
- - sorted_imports
- - vertical_whitespace_closing_braces
- modifier_order
- redundant_nil_coalescing
- - unused_import
# 안전성
force_unwrapping: error
@@ -39,7 +37,7 @@ implicitly_unwrapped_optional: error
force_cast: warning
force_try: warning
-# 한 줄 길이: 초안 단계 노이즈 완화를 위해 상향
+# 가독성: 너무 타이트하지 않게 완화
line_length:
warning: 120
error: 140
@@ -47,17 +45,14 @@ line_length:
ignores_urls: true
function_body_length:
- warning: 60
- error: 100
+ # --strict promotes warnings to errors, so warning is the practical gate
+ warning: 90
+ error: 120
type_body_length:
warning: 250
error: 350
-file_length:
- warning: 400
- error: 600
-
# 테스트 코드의 짧은 타입명(OK 등) 허용을 위해 최소 길이 완화
type_name:
min_length:
@@ -67,7 +62,7 @@ type_name:
warning: 50
error: 60
-# 테스트 코드의 짧은 식별자(a/b/c 등) 허용을 위해 최소 길이 완화
+# 디자인 토큰 shade 이름(_50, _300)과 짧은 식별자 허용
identifier_name:
min_length:
warning: 1
@@ -75,6 +70,7 @@ identifier_name:
max_length:
warning: 50
error: 60
+ allowed_symbols: "_"
excluded:
- id
- x
@@ -86,11 +82,18 @@ identifier_name:
- to
- at
- vc
-
-nesting:
- type_level:
- warning: 2
- error: 3
+ - _0
+ - _50
+ - _100
+ - _200
+ - _300
+ - _400
+ - _500
+ - _600
+ - _700
+ - _800
+ - _900
+ - _1000
cyclomatic_complexity:
warning: 12
diff --git a/Projects/App/Sources/DI/AppBootstrap.swift b/Projects/App/Sources/DI/AppBootstrap.swift
index 4117fac..af778cf 100644
--- a/Projects/App/Sources/DI/AppBootstrap.swift
+++ b/Projects/App/Sources/DI/AppBootstrap.swift
@@ -1,10 +1,12 @@
import Foundation
+import SharedDesignSystem
import SharedLogger
import ThirdParty
enum AppBootstrap {
@MainActor
static func run() {
+ _ = DesignSystemFontRegistration.registerIfNeeded()
let infra = InfraContainer.live()
prepareDependencies {
Dependencies.register(&$0, infra: infra)
diff --git a/Projects/Shared/DesignSystem/Project.swift b/Projects/Shared/DesignSystem/Project.swift
index b2d7d62..c9da7ca 100644
--- a/Projects/Shared/DesignSystem/Project.swift
+++ b/Projects/Shared/DesignSystem/Project.swift
@@ -6,5 +6,6 @@ let project = ProjectFactory.framework(
dependencies: [
.sharedUtils,
],
- resources: ["Resources/**"]
+ resources: ["Resources/**"],
+ includesTests: true
)
diff --git a/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Contents.json b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/Contents.json b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_apple.imageset/Apple.svg b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_apple.imageset/Apple.svg
new file mode 100644
index 0000000..f689d28
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_apple.imageset/Apple.svg
@@ -0,0 +1,10 @@
+
diff --git a/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_apple.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_apple.imageset/Contents.json
new file mode 100644
index 0000000..f3dc5cd
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_apple.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "filename" : "Apple.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true,
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_kakao.imageset/Contents.json b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_kakao.imageset/Contents.json
new file mode 100644
index 0000000..3bf37ca
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_kakao.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "filename" : "Kakao.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true,
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_kakao.imageset/Kakao.svg b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_kakao.imageset/Kakao.svg
new file mode 100644
index 0000000..e4b9c21
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Resources/Assets.xcassets/Icon/Social/icon_social_kakao.imageset/Kakao.svg
@@ -0,0 +1,10 @@
+
diff --git a/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-Bold.otf b/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-Bold.otf
new file mode 100644
index 0000000..8e5e30a
Binary files /dev/null and b/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-Bold.otf differ
diff --git a/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-Medium.otf b/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-Medium.otf
new file mode 100644
index 0000000..0575069
Binary files /dev/null and b/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-Medium.otf differ
diff --git a/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-Regular.otf b/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-Regular.otf
new file mode 100644
index 0000000..08bf4cf
Binary files /dev/null and b/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-Regular.otf differ
diff --git a/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-SemiBold.otf b/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-SemiBold.otf
new file mode 100644
index 0000000..e7e36ab
Binary files /dev/null and b/Projects/Shared/DesignSystem/Resources/Fonts/Pretendard-SemiBold.otf differ
diff --git a/Projects/Shared/DesignSystem/Sources/Access/CGFloat+DesignSystem.swift b/Projects/Shared/DesignSystem/Sources/Access/CGFloat+DesignSystem.swift
new file mode 100644
index 0000000..a85e5a5
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Access/CGFloat+DesignSystem.swift
@@ -0,0 +1,61 @@
+import CoreGraphics
+
+public extension CGFloat {
+ static let ds = DesignSystemMetrics()
+}
+
+public struct DesignSystemMetrics: Sendable {
+ public let radius = RadiusMetrics()
+ public let spacing = SpacingMetrics()
+ public let border = BorderMetrics()
+ public let controlHeight = ControlHeightMetrics()
+ public let dim = DimMetrics()
+
+ public init() {}
+}
+
+public struct RadiusMetrics: Sendable {
+ public let none = SemanticNumber.Radius.none
+ public let xsm = SemanticNumber.Radius.xsm
+ public let sm = SemanticNumber.Radius.sm
+ public let md = SemanticNumber.Radius.md
+ public let lg = SemanticNumber.Radius.lg
+ public let xxlg = SemanticNumber.Radius.xxlg
+ public let full = SemanticNumber.Radius.full
+
+ public init() {}
+}
+
+public struct SpacingMetrics: Sendable {
+ public let xs = SemanticNumber.Spacing.xs
+ public let sm = SemanticNumber.Spacing.sm
+ public let md = SemanticNumber.Spacing.md
+ public let lg = SemanticNumber.Spacing.lg
+ public let xl = SemanticNumber.Spacing.xl
+ public let xxl = SemanticNumber.Spacing.xxl
+
+ public init() {}
+}
+
+public struct BorderMetrics: Sendable {
+ public let thin = SemanticNumber.Border.thin
+ public let thick = SemanticNumber.Border.thick
+
+ public init() {}
+}
+
+public struct ControlHeightMetrics: Sendable {
+ public let sm = SemanticNumber.ControlHeight.sm
+ public let md = SemanticNumber.ControlHeight.md
+ public let lg = SemanticNumber.ControlHeight.lg
+
+ public init() {}
+}
+
+public struct DimMetrics: Sendable {
+ public let light = SemanticNumber.Dim.light
+ public let `default` = SemanticNumber.Dim.default
+ public let heavy = SemanticNumber.Dim.heavy
+
+ public init() {}
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Access/Color+DesignSystem.swift b/Projects/Shared/DesignSystem/Sources/Access/Color+DesignSystem.swift
new file mode 100644
index 0000000..40b8801
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Access/Color+DesignSystem.swift
@@ -0,0 +1,212 @@
+import SwiftUI
+
+public extension Color {
+ static let ds = DesignSystemColors()
+}
+
+public struct DesignSystemColors: Sendable {
+ public let button = ButtonColors()
+ public let text = TextColors()
+ public let background = BackgroundColors()
+ public let border = BorderColors()
+ public let dim = DimColors()
+ public let social = SocialColors()
+
+ public init() {}
+}
+
+public struct ButtonColors: Sendable {
+ public let primary = ButtonPrimaryColors()
+ public let secondary = ButtonSecondaryColors()
+ public let outlined = ButtonOutlinedColors()
+ public let text = ButtonTextColors()
+
+ public init() {}
+}
+
+public struct ButtonPrimaryColors: Sendable {
+ public let background = ButtonPrimaryBackgroundColors()
+ public let content = ButtonPrimaryContentColors()
+
+ public init() {}
+}
+
+public struct ButtonPrimaryBackgroundColors: Sendable {
+ public let `default` = SemanticColor.Button.Primary.background.default.color
+ public let pressed = SemanticColor.Button.Primary.background.pressed.color
+ public let disabled = SemanticColor.Button.Primary.background.disabled.color
+
+ public init() {}
+}
+
+public struct ButtonPrimaryContentColors: Sendable {
+ public let `default` = SemanticColor.Button.Primary.content.default.color
+ public let pressed = SemanticColor.Button.Primary.content.pressed.color
+ public let disabled = SemanticColor.Button.Primary.content.disabled.color
+
+ public init() {}
+}
+
+public struct ButtonSecondaryColors: Sendable {
+ public let background = ButtonSecondaryBackgroundColors()
+ public let content = ButtonSecondaryContentColors()
+
+ public init() {}
+}
+
+public struct ButtonSecondaryBackgroundColors: Sendable {
+ public let `default` = SemanticColor.Button.Secondary.background.default.color
+ public let pressed = SemanticColor.Button.Secondary.background.pressed.color
+ public let disabled = SemanticColor.Button.Secondary.background.disabled.color
+
+ public init() {}
+}
+
+public struct ButtonSecondaryContentColors: Sendable {
+ public let `default` = SemanticColor.Button.Secondary.content.default.color
+ public let pressed = SemanticColor.Button.Secondary.content.pressed.color
+ public let disabled = SemanticColor.Button.Secondary.content.disabled.color
+
+ public init() {}
+}
+
+public struct ButtonOutlinedColors: Sendable {
+ public let background = ButtonOutlinedBackgroundColors()
+ public let content = ButtonOutlinedContentColors()
+ public let border = ButtonOutlinedBorderColors()
+
+ public init() {}
+}
+
+public struct ButtonOutlinedBackgroundColors: Sendable {
+ public let `default` = SemanticColor.Button.Outlined.background.default.color
+ public let pressed = SemanticColor.Button.Outlined.background.pressed.color
+ public let disabled = SemanticColor.Button.Outlined.background.disabled.color
+
+ public init() {}
+}
+
+public struct ButtonOutlinedContentColors: Sendable {
+ public let `default` = SemanticColor.Button.Outlined.content.default.color
+ public let pressed = SemanticColor.Button.Outlined.content.pressed.color
+ public let disabled = SemanticColor.Button.Outlined.content.disabled.color
+
+ public init() {}
+}
+
+public struct ButtonOutlinedBorderColors: Sendable {
+ public let `default` = SemanticColor.Button.Outlined.border.default.color
+ public let pressed = SemanticColor.Button.Outlined.border.pressed.color
+ public let disabled = SemanticColor.Button.Outlined.border.disabled.color
+
+ public init() {}
+}
+
+public struct ButtonTextColors: Sendable {
+ public let background = ButtonTextBackgroundColors()
+ public let content = ButtonTextContentColors()
+
+ public init() {}
+}
+
+public struct ButtonTextBackgroundColors: Sendable {
+ public let `default` = SemanticColor.Button.Text.background.default.color
+ public let pressed = SemanticColor.Button.Text.background.pressed.color
+ public let disabled = SemanticColor.Button.Text.background.disabled.color
+
+ public init() {}
+}
+
+public struct ButtonTextContentColors: Sendable {
+ public let `default` = SemanticColor.Button.Text.content.default.color
+ public let pressed = SemanticColor.Button.Text.content.pressed.color
+ public let disabled = SemanticColor.Button.Text.content.disabled.color
+
+ public init() {}
+}
+
+public struct TextColors: Sendable {
+ public let neutral = TextNeutralColors()
+ public let primary = TextPrimaryColors()
+ public let secondary = TextSecondaryColors()
+
+ public init() {}
+}
+
+public struct TextNeutralColors: Sendable {
+ public let black = SemanticColor.Text.Neutral.black.color
+ public let darker = SemanticColor.Text.Neutral.darker.color
+ public let dark = SemanticColor.Text.Neutral.dark.color
+ public let basic = SemanticColor.Text.Neutral.basic.color
+ public let light = SemanticColor.Text.Neutral.light.color
+ public let lighter = SemanticColor.Text.Neutral.lighter.color
+ public let white = SemanticColor.Text.Neutral.white.color
+
+ public init() {}
+}
+
+public struct TextPrimaryColors: Sendable {
+ public let basic = SemanticColor.Text.Primary.basic.color
+ public let dark = SemanticColor.Text.Primary.dark.color
+ public let darker = SemanticColor.Text.Primary.darker.color
+
+ public init() {}
+}
+
+public struct TextSecondaryColors: Sendable {
+ public let basic = SemanticColor.Text.Secondary.basic.color
+ public let darker = SemanticColor.Text.Secondary.darker.color
+
+ public init() {}
+}
+
+public struct BackgroundColors: Sendable {
+ public let black = SemanticColor.Background.black.color
+ public let grayDarker = SemanticColor.Background.grayDarker.color
+ public let grayDark = SemanticColor.Background.grayDark.color
+
+ public init() {}
+}
+
+public struct BorderColors: Sendable {
+ public let primary = BorderPrimaryColors()
+ public let neutral = BorderNeutralColors()
+
+ public init() {}
+}
+
+public struct BorderPrimaryColors: Sendable {
+ public let basic = SemanticColor.Border.Primary.basic.color
+ public let dark = SemanticColor.Border.Primary.dark.color
+ public let darker = SemanticColor.Border.Primary.darker.color
+
+ public init() {}
+}
+
+public struct BorderNeutralColors: Sendable {
+ public let lighter = SemanticColor.Border.Neutral.lighter.color
+ public let light = SemanticColor.Border.Neutral.light.color
+ public let basic = SemanticColor.Border.Neutral.basic.color
+ public let dark = SemanticColor.Border.Neutral.dark.color
+ public let darker = SemanticColor.Border.Neutral.darker.color
+
+ public init() {}
+}
+
+public struct DimColors: Sendable {
+ public let light = SemanticColor.Dim.light.color
+ public let `default` = SemanticColor.Dim.default.color
+ public let heavy = SemanticColor.Dim.heavy.color
+
+ public init() {}
+}
+
+public struct SocialColors: Sendable {
+ public let kakao = SemanticColor.Social.kakao.color
+ public let kakaoContent = SemanticColor.Social.kakaoContent.color
+ public let appleBackground = SemanticColor.Social.appleBackground.color
+ public let appleBorder = SemanticColor.Social.appleBorder.color
+ public let appleContent = SemanticColor.Social.appleContent.color
+
+ public init() {}
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Access/Font+DesignSystem.swift b/Projects/Shared/DesignSystem/Sources/Access/Font+DesignSystem.swift
new file mode 100644
index 0000000..d1dd648
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Access/Font+DesignSystem.swift
@@ -0,0 +1,64 @@
+import SwiftUI
+
+public extension Font {
+ static let ds = DesignSystemFonts()
+}
+
+public struct DesignSystemFonts: Sendable {
+ public let heading = HeadingFonts()
+ public let title = TitleFonts()
+ public let body = BodyFonts()
+ public let label = LabelFonts()
+ public let button = ButtonFonts()
+
+ public init() {}
+}
+
+public struct HeadingFonts: Sendable {
+ public var large32Bold: Font { TextStyle.ds.heading.large32Bold.font }
+ public var large32Semibold: Font { TextStyle.ds.heading.large32Semibold.font }
+ public var medium28Bold: Font { TextStyle.ds.heading.medium28Bold.font }
+ public var medium28Semibold: Font { TextStyle.ds.heading.medium28Semibold.font }
+
+ public init() {}
+}
+
+public struct TitleFonts: Sendable {
+ public var large24Bold: Font { TextStyle.ds.title.large24Bold.font }
+ public var large24Semibold: Font { TextStyle.ds.title.large24Semibold.font }
+ public var medium22Bold: Font { TextStyle.ds.title.medium22Bold.font }
+ public var medium22Semibold: Font { TextStyle.ds.title.medium22Semibold.font }
+ public var small20Bold: Font { TextStyle.ds.title.small20Bold.font }
+ public var small20Semibold: Font { TextStyle.ds.title.small20Semibold.font }
+ public var xsmall18Bold: Font { TextStyle.ds.title.xsmall18Bold.font }
+ public var xsmall18Semibold: Font { TextStyle.ds.title.xsmall18Semibold.font }
+
+ public init() {}
+}
+
+public struct BodyFonts: Sendable {
+ public var large18Semibold: Font { TextStyle.ds.body.large18Semibold.font }
+ public var large18Regular: Font { TextStyle.ds.body.large18Regular.font }
+ public var medium16Semibold: Font { TextStyle.ds.body.medium16Semibold.font }
+ public var medium16Regular: Font { TextStyle.ds.body.medium16Regular.font }
+ public var small14Semibold: Font { TextStyle.ds.body.small14Semibold.font }
+ public var small14Regular: Font { TextStyle.ds.body.small14Regular.font }
+
+ public init() {}
+}
+
+public struct LabelFonts: Sendable {
+ public var large16Medium: Font { TextStyle.ds.label.large16Medium.font }
+ public var medium14Medium: Font { TextStyle.ds.label.medium14Medium.font }
+ public var small12Medium: Font { TextStyle.ds.label.small12Medium.font }
+
+ public init() {}
+}
+
+public struct ButtonFonts: Sendable {
+ public var sm14Semibold: Font { TextStyle.ds.button.sm14Semibold.font }
+ public var md16Semibold: Font { TextStyle.ds.button.md16Semibold.font }
+ public var lg18Semibold: Font { TextStyle.ds.button.lg18Semibold.font }
+
+ public init() {}
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButton.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButton.swift
new file mode 100644
index 0000000..477e1c7
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButton.swift
@@ -0,0 +1,76 @@
+import SwiftUI
+
+public struct DesignButton: View {
+ private let title: String
+ private let variant: DesignButtonVariant
+ private let size: DesignButtonSize
+ private let isEnabled: Bool
+ private let isFullWidth: Bool
+ private let leadingIcon: Image?
+ private let trailingIcon: Image?
+ private let action: () -> Void
+
+ public init(
+ _ title: String,
+ variant: DesignButtonVariant = .primary,
+ size: DesignButtonSize = .md,
+ isEnabled: Bool = true,
+ isFullWidth: Bool = false,
+ leadingIcon: Image? = nil,
+ trailingIcon: Image? = nil,
+ action: @escaping () -> Void
+ ) {
+ self.title = title
+ self.variant = variant
+ self.size = size
+ self.isEnabled = isEnabled
+ self.isFullWidth = isFullWidth
+ self.leadingIcon = leadingIcon
+ self.trailingIcon = trailingIcon
+ self.action = action
+ }
+
+ public var body: some View {
+ Button(action: action) {
+ labelContent
+ }
+ .buttonStyle(
+ DesignButtonChromeStyle(
+ variant: variant,
+ size: size,
+ isFullWidth: isFullWidth
+ )
+ )
+ .disabled(!isEnabled)
+ }
+
+ private var labelContent: some View {
+ let textStyle = size.textStyle
+
+ // Width/height ownership is intentionally in DesignButtonChromeStyle
+ // so padding/frame order stays consistent for full-width layout.
+ return HStack(spacing: size.contentGap) {
+ if let leadingIcon {
+ leadingIcon
+ .resizable()
+ .renderingMode(.template)
+ .scaledToFit()
+ .frame(width: size.iconSize, height: size.iconSize)
+ }
+
+ Text(title)
+ .font(textStyle.font)
+ .kerning(textStyle.letterSpacing)
+ .lineSpacing(textStyle.additionalLineSpacing)
+ .lineLimit(1)
+
+ if let trailingIcon {
+ trailingIcon
+ .resizable()
+ .renderingMode(.template)
+ .scaledToFit()
+ .frame(width: size.iconSize, height: size.iconSize)
+ }
+ }
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButtonSize.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButtonSize.swift
new file mode 100644
index 0000000..1b7d745
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButtonSize.swift
@@ -0,0 +1,64 @@
+import CoreGraphics
+
+public enum DesignButtonSize: Sendable {
+ case sm
+ case md
+ case lg
+
+ public var height: CGFloat {
+ switch self {
+ case .sm:
+ SemanticNumber.ControlHeight.sm
+ case .md:
+ SemanticNumber.ControlHeight.md
+ case .lg:
+ SemanticNumber.ControlHeight.lg
+ }
+ }
+
+ public var horizontalPadding: CGFloat {
+ switch self {
+ case .sm:
+ 12
+ case .md:
+ 16
+ case .lg:
+ 20
+ }
+ }
+
+ public var cornerRadius: CGFloat {
+ switch self {
+ case .sm, .md:
+ SemanticNumber.Radius.xsm
+ case .lg:
+ SemanticNumber.Radius.sm
+ }
+ }
+
+ public var iconSize: CGFloat {
+ switch self {
+ case .sm:
+ 14
+ case .md:
+ 16
+ case .lg:
+ 18
+ }
+ }
+
+ public var textStyle: TextStyle {
+ switch self {
+ case .sm:
+ TextStyle.ds.button.sm14Semibold
+ case .md:
+ TextStyle.ds.button.md16Semibold
+ case .lg:
+ TextStyle.ds.button.lg18Semibold
+ }
+ }
+
+ public var contentGap: CGFloat {
+ SemanticNumber.Spacing.sm
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButtonStyle.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButtonStyle.swift
new file mode 100644
index 0000000..66dee3c
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButtonStyle.swift
@@ -0,0 +1,212 @@
+import CoreGraphics
+import SwiftUI
+
+enum DesignButtonInteractionState: Sendable {
+ case `default`
+ case pressed
+ case disabled
+}
+
+struct DesignButtonResolvedStyle: Equatable, Sendable {
+ let background: TokenColor
+ let content: TokenColor
+ let border: TokenColor?
+ let borderWidth: CGFloat
+}
+
+enum DesignButtonStyleResolver {
+ static func resolve(
+ variant: DesignButtonVariant,
+ state: DesignButtonInteractionState
+ ) -> DesignButtonResolvedStyle {
+ switch variant {
+ case .primary:
+ resolvePrimary(state: state)
+ case .secondary:
+ resolveSecondary(state: state)
+ case .outlined:
+ resolveOutlined(state: state)
+ case .text:
+ resolveText(state: state)
+ }
+ }
+
+ private static func resolvePrimary(
+ state: DesignButtonInteractionState
+ ) -> DesignButtonResolvedStyle {
+ switch state {
+ case .default:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Primary.background.default,
+ content: SemanticColor.Button.Primary.content.default,
+ border: nil,
+ borderWidth: 0
+ )
+ case .pressed:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Primary.background.pressed,
+ content: SemanticColor.Button.Primary.content.pressed,
+ border: nil,
+ borderWidth: 0
+ )
+ case .disabled:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Primary.background.disabled,
+ content: SemanticColor.Button.Primary.content.disabled,
+ border: nil,
+ borderWidth: 0
+ )
+ }
+ }
+
+ private static func resolveSecondary(
+ state: DesignButtonInteractionState
+ ) -> DesignButtonResolvedStyle {
+ switch state {
+ case .default:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Secondary.background.default,
+ content: SemanticColor.Button.Secondary.content.default,
+ border: nil,
+ borderWidth: 0
+ )
+ case .pressed:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Secondary.background.pressed,
+ content: SemanticColor.Button.Secondary.content.pressed,
+ border: nil,
+ borderWidth: 0
+ )
+ case .disabled:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Secondary.background.disabled,
+ content: SemanticColor.Button.Secondary.content.disabled,
+ border: nil,
+ borderWidth: 0
+ )
+ }
+ }
+
+ private static func resolveOutlined(
+ state: DesignButtonInteractionState
+ ) -> DesignButtonResolvedStyle {
+ switch state {
+ case .default:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Outlined.background.default,
+ content: SemanticColor.Button.Outlined.content.default,
+ border: SemanticColor.Button.Outlined.border.default,
+ borderWidth: SemanticNumber.Border.thin
+ )
+ case .pressed:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Outlined.background.pressed,
+ content: SemanticColor.Button.Outlined.content.pressed,
+ border: SemanticColor.Button.Outlined.border.pressed,
+ borderWidth: SemanticNumber.Border.thin
+ )
+ case .disabled:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Outlined.background.disabled,
+ content: SemanticColor.Button.Outlined.content.disabled,
+ border: SemanticColor.Button.Outlined.border.disabled,
+ borderWidth: SemanticNumber.Border.thin
+ )
+ }
+ }
+
+ private static func resolveText(
+ state: DesignButtonInteractionState
+ ) -> DesignButtonResolvedStyle {
+ switch state {
+ case .default:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Text.background.default,
+ content: SemanticColor.Button.Text.content.default,
+ border: nil,
+ borderWidth: 0
+ )
+ case .pressed:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Text.background.pressed,
+ content: SemanticColor.Button.Text.content.pressed,
+ border: nil,
+ borderWidth: 0
+ )
+ case .disabled:
+ DesignButtonResolvedStyle(
+ background: SemanticColor.Button.Text.background.disabled,
+ content: SemanticColor.Button.Text.content.disabled,
+ border: nil,
+ borderWidth: 0
+ )
+ }
+ }
+}
+
+enum DesignButtonInteractionStateResolver {
+ static func resolve(isEnabled: Bool, isPressed: Bool) -> DesignButtonInteractionState {
+ if !isEnabled {
+ return .disabled
+ }
+ return isPressed ? .pressed : .default
+ }
+}
+
+struct DesignButtonChromeStyle: ButtonStyle {
+ let variant: DesignButtonVariant
+ let size: DesignButtonSize
+ let isFullWidth: Bool
+
+ func makeBody(configuration: Configuration) -> some View {
+ // Nested content reads @Environment(\.isEnabled) so parent .disabled
+ // and DesignButton(isEnabled:) both select disabled tokens.
+ ChromeContent(
+ configuration: configuration,
+ variant: variant,
+ size: size,
+ isFullWidth: isFullWidth
+ )
+ }
+}
+
+private struct ChromeContent: View {
+ let configuration: ButtonStyle.Configuration
+ let variant: DesignButtonVariant
+ let size: DesignButtonSize
+ let isFullWidth: Bool
+
+ @Environment(\.isEnabled) private var isEnabled
+
+ var body: some View {
+ let style = DesignButtonStyleResolver.resolve(
+ variant: variant,
+ state: DesignButtonInteractionStateResolver.resolve(
+ isEnabled: isEnabled,
+ isPressed: configuration.isPressed
+ )
+ )
+
+ // Layout ownership lives here:
+ // label → horizontal padding → expand/height → chrome.
+ // Padding must come before maxWidth expansion so full-width
+ // buttons do not request parent-width + padding.
+ configuration.label
+ .foregroundStyle(style.content.color)
+ .padding(.horizontal, size.horizontalPadding)
+ .frame(
+ maxWidth: isFullWidth ? .infinity : nil,
+ minHeight: size.height,
+ maxHeight: size.height
+ )
+ .background(style.background.color)
+ .clipShape(RoundedRectangle(cornerRadius: size.cornerRadius, style: .continuous))
+ .overlay {
+ if let border = style.border {
+ RoundedRectangle(cornerRadius: size.cornerRadius, style: .continuous)
+ .strokeBorder(border.color, lineWidth: style.borderWidth)
+ }
+ }
+ .contentShape(RoundedRectangle(cornerRadius: size.cornerRadius, style: .continuous))
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButtonVariant.swift b/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButtonVariant.swift
new file mode 100644
index 0000000..92a05ec
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Components/Button/DesignButtonVariant.swift
@@ -0,0 +1,6 @@
+public enum DesignButtonVariant: Sendable {
+ case primary
+ case secondary
+ case outlined
+ case text
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Components/Social/SocialLoginButton.swift b/Projects/Shared/DesignSystem/Sources/Components/Social/SocialLoginButton.swift
new file mode 100644
index 0000000..98b3534
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Components/Social/SocialLoginButton.swift
@@ -0,0 +1,106 @@
+import SwiftUI
+
+public struct SocialLoginButton: View {
+ private let provider: SocialLoginProvider
+ private let action: () -> Void
+
+ public init(
+ provider: SocialLoginProvider,
+ action: @escaping () -> Void
+ ) {
+ self.provider = provider
+ self.action = action
+ }
+
+ public var body: some View {
+ Button(action: action) {
+ labelContent
+ }
+ .buttonStyle(
+ SocialLoginButtonChromeStyle(
+ background: provider.background,
+ content: provider.content,
+ border: provider.border
+ )
+ )
+ }
+
+ private var labelContent: some View {
+ let textStyle = TextStyle.ds.button.lg18Semibold
+
+ return HStack(spacing: provider.contentGap) {
+ providerIcon
+ .resizable()
+ .renderingMode(.template)
+ .scaledToFit()
+ .frame(
+ width: provider.iconSize.width,
+ height: provider.iconSize.height
+ )
+
+ Text(provider.title)
+ .font(textStyle.font)
+ .kerning(textStyle.letterSpacing)
+ .lineSpacing(textStyle.additionalLineSpacing)
+ .lineLimit(1)
+ }
+ }
+
+ private var providerIcon: Image {
+ switch provider {
+ case .kakao:
+ SharedDesignSystemAsset.iconSocialKakao.swiftUIImage
+ case .apple:
+ SharedDesignSystemAsset.iconSocialApple.swiftUIImage
+ }
+ }
+}
+
+private struct SocialLoginButtonChromeStyle: ButtonStyle {
+ let background: TokenColor
+ let content: TokenColor
+ let border: TokenColor?
+
+ func makeBody(configuration: Configuration) -> some View {
+ // Full-width social CTAs expand first, then paint chrome.
+ configuration.label
+ .foregroundStyle(content.color)
+ .frame(
+ maxWidth: .infinity,
+ minHeight: SocialLoginButtonMetrics.height,
+ maxHeight: SocialLoginButtonMetrics.height
+ )
+ .background(background.color)
+ .clipShape(
+ RoundedRectangle(
+ cornerRadius: SocialLoginButtonMetrics.cornerRadius,
+ style: .continuous
+ )
+ )
+ .overlay {
+ if let border {
+ RoundedRectangle(
+ cornerRadius: SocialLoginButtonMetrics.cornerRadius,
+ style: .continuous
+ )
+ .strokeBorder(
+ border.color,
+ lineWidth: SocialLoginButtonMetrics.borderWidth
+ )
+ }
+ }
+ .contentShape(
+ RoundedRectangle(
+ cornerRadius: SocialLoginButtonMetrics.cornerRadius,
+ style: .continuous
+ )
+ )
+ .opacity(configuration.isPressed ? 0.88 : 1)
+ }
+}
+
+private enum SocialLoginButtonMetrics {
+ static let height: CGFloat = SemanticNumber.ControlHeight.lg
+ static let cornerRadius: CGFloat = SemanticNumber.Radius.sm
+ static let borderWidth: CGFloat = SemanticNumber.Border.thin
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Components/Social/SocialLoginProvider.swift b/Projects/Shared/DesignSystem/Sources/Components/Social/SocialLoginProvider.swift
new file mode 100644
index 0000000..64a8dc2
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Components/Social/SocialLoginProvider.swift
@@ -0,0 +1,60 @@
+import CoreGraphics
+
+public enum SocialLoginProvider: Sendable {
+ case kakao
+ case apple
+
+ public var title: String {
+ switch self {
+ case .kakao:
+ "카카오로 시작하기"
+ case .apple:
+ "Apple로 로그인"
+ }
+ }
+
+ var background: TokenColor {
+ switch self {
+ case .kakao:
+ SemanticColor.Social.kakao
+ case .apple:
+ SemanticColor.Social.appleBackground
+ }
+ }
+
+ var content: TokenColor {
+ switch self {
+ case .kakao:
+ SemanticColor.Social.kakaoContent
+ case .apple:
+ SemanticColor.Social.appleContent
+ }
+ }
+
+ var border: TokenColor? {
+ switch self {
+ case .kakao:
+ nil
+ case .apple:
+ SemanticColor.Social.appleBorder
+ }
+ }
+
+ var contentGap: CGFloat {
+ switch self {
+ case .kakao:
+ 12
+ case .apple:
+ 9
+ }
+ }
+
+ var iconSize: CGSize {
+ switch self {
+ case .kakao:
+ CGSize(width: 18, height: 18)
+ case .apple:
+ CGSize(width: 15, height: 18)
+ }
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Components/Text/DesignText.swift b/Projects/Shared/DesignSystem/Sources/Components/Text/DesignText.swift
new file mode 100644
index 0000000..e0caf1e
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Components/Text/DesignText.swift
@@ -0,0 +1,33 @@
+import SwiftUI
+
+public struct DesignText: View {
+ private let text: String
+ private let style: TextStyle
+ private let color: Color
+ private let alignment: TextAlignment
+ private let lineLimit: Int?
+
+ public init(
+ _ text: String,
+ style: TextStyle,
+ color: Color = Color.ds.text.neutral.white,
+ alignment: TextAlignment = .leading,
+ lineLimit: Int? = nil
+ ) {
+ self.text = text
+ self.style = style
+ self.color = color
+ self.alignment = alignment
+ self.lineLimit = lineLimit
+ }
+
+ public var body: some View {
+ Text(text)
+ .font(style.font)
+ .kerning(style.letterSpacing)
+ .lineSpacing(style.additionalLineSpacing)
+ .foregroundStyle(color)
+ .multilineTextAlignment(alignment)
+ .lineLimit(lineLimit)
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Foundation/Color+Hex.swift b/Projects/Shared/DesignSystem/Sources/Foundation/Color+Hex.swift
new file mode 100644
index 0000000..ae53d5c
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Foundation/Color+Hex.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+
+extension Color {
+ init(hex: UInt32, alpha: Double = 1.0) {
+ let red = Double((hex >> 16) & 0xFF) / 255.0
+ let green = Double((hex >> 8) & 0xFF) / 255.0
+ let blue = Double(hex & 0xFF) / 255.0
+ self.init(.sRGB, red: red, green: green, blue: blue, opacity: alpha)
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Foundation/FontRegistration.swift b/Projects/Shared/DesignSystem/Sources/Foundation/FontRegistration.swift
new file mode 100644
index 0000000..082a7b7
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Foundation/FontRegistration.swift
@@ -0,0 +1,67 @@
+import CoreText
+import Foundation
+
+public enum DesignSystemFontRegistration {
+ private static let lock = NSLock()
+ // Protected by `lock`; Swift 6 requires an explicit escape hatch for mutable static state.
+ nonisolated(unsafe) private static var didRegister = false
+
+ private static let fileNames = [
+ "Pretendard-Regular",
+ "Pretendard-Medium",
+ "Pretendard-SemiBold",
+ "Pretendard-Bold",
+ ]
+
+ @discardableResult
+ public static func registerIfNeeded() -> Bool {
+ lock.lock()
+ defer { lock.unlock() }
+
+ if didRegister {
+ return true
+ }
+
+ var success = true
+ for fileName in fileNames {
+ success = registerFont(named: fileName) && success
+ }
+
+ didRegister = success
+ return success
+ }
+
+ private static func registerFont(named fileName: String) -> Bool {
+ let extensions = ["otf", "ttf"]
+ let bundle = resourceBundle
+
+ guard
+ let url = extensions
+ .compactMap({ bundle.url(forResource: fileName, withExtension: $0) })
+ .first
+ else {
+ return false
+ }
+
+ var error: Unmanaged?
+ if CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error) {
+ return true
+ }
+
+ guard let cfError = error?.takeRetainedValue() else {
+ return false
+ }
+
+ let nsError = cfError as Error as NSError
+ // Already present in the process is fine for retries / multi-entry bootstrap.
+ return nsError.domain == (kCTFontManagerErrorDomain as String)
+ && (
+ nsError.code == CTFontManagerError.alreadyRegistered.rawValue
+ || nsError.code == CTFontManagerError.duplicatedName.rawValue
+ )
+ }
+
+ private static var resourceBundle: Bundle {
+ SharedDesignSystemResources.bundle
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Placeholder.swift b/Projects/Shared/DesignSystem/Sources/Placeholder.swift
deleted file mode 100644
index 532ecd0..0000000
--- a/Projects/Shared/DesignSystem/Sources/Placeholder.swift
+++ /dev/null
@@ -1 +0,0 @@
-public enum SharedDesignSystemPlaceholder {}
diff --git a/Projects/Shared/DesignSystem/Sources/Preview/DesignSystemGallery.swift b/Projects/Shared/DesignSystem/Sources/Preview/DesignSystemGallery.swift
new file mode 100644
index 0000000..05fc8f1
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Preview/DesignSystemGallery.swift
@@ -0,0 +1,298 @@
+#if DEBUG
+import SwiftUI
+
+/// DEBUG-only visual catalog for SharedDesignSystem tokens/components.
+struct DesignSystemGallery: View {
+ @State private var selectedSection: GallerySection? = .colors
+
+ var body: some View {
+ NavigationSplitView {
+ List(GallerySection.allCases, selection: $selectedSection) { section in
+ Text(section.title)
+ .foregroundStyle(Color.ds.text.neutral.white)
+ .tag(section)
+ }
+ .navigationTitle("Design System")
+ .scrollContentBackground(.hidden)
+ .background(Color.ds.background.black)
+ } detail: {
+ Group {
+ if let selectedSection {
+ sectionDetail(selectedSection)
+ } else {
+ ContentUnavailableView(
+ "Select a section",
+ systemImage: "sidebar.left",
+ description: Text("Choose a design system section from the list.")
+ )
+ .foregroundStyle(Color.ds.text.neutral.basic)
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(Color.ds.background.black)
+ }
+ .preferredColorScheme(.dark)
+ }
+
+ @ViewBuilder
+ private func sectionDetail(_ section: GallerySection) -> some View {
+ switch section {
+ case .colors:
+ GalleryColorsSection()
+ case .typography:
+ GalleryTypographySection()
+ case .buttons:
+ GalleryButtonsSection()
+ case .social:
+ GallerySocialSection()
+ }
+ }
+}
+
+// MARK: - Section Model
+
+private enum GallerySection: String, CaseIterable, Identifiable, Hashable {
+ case colors
+ case typography
+ case buttons
+ case social
+
+ var id: String { rawValue }
+
+ var title: String {
+ switch self {
+ case .colors: "Colors"
+ case .typography: "Typography"
+ case .buttons: "Buttons"
+ case .social: "Social"
+ }
+ }
+}
+
+// MARK: - Section Screens
+
+private struct GalleryColorsSection: View {
+ var body: some View {
+ GallerySectionScaffold(
+ title: "Colors",
+ subtitle: "Semantic color tokens used by Feature"
+ ) {
+ VStack(alignment: .leading, spacing: CGFloat.ds.spacing.md) {
+ colorRow(
+ title: "Text",
+ swatches: [
+ ("white", Color.ds.text.neutral.white),
+ ("basic", Color.ds.text.neutral.basic),
+ ("dark", Color.ds.text.neutral.dark),
+ ("primary", Color.ds.text.primary.basic),
+ ("secondary", Color.ds.text.secondary.basic),
+ ]
+ )
+
+ colorRow(
+ title: "Background",
+ swatches: [
+ ("black", Color.ds.background.black),
+ ("grayDarker", Color.ds.background.grayDarker),
+ ("grayDark", Color.ds.background.grayDark),
+ ]
+ )
+
+ colorRow(
+ title: "Button Primary BG",
+ swatches: [
+ ("default", Color.ds.button.primary.background.default),
+ ("pressed", Color.ds.button.primary.background.pressed),
+ ("disabled", Color.ds.button.primary.background.disabled),
+ ]
+ )
+
+ colorRow(
+ title: "Social Colors",
+ swatches: [
+ ("kakao", Color.ds.social.kakao),
+ ("appleBG", Color.ds.social.appleBackground),
+ ("appleBorder", Color.ds.social.appleBorder),
+ ]
+ )
+ }
+ }
+ }
+
+ private func colorRow(title: String, swatches: [(String, Color)]) -> some View {
+ VStack(alignment: .leading, spacing: CGFloat.ds.spacing.xs) {
+ DesignText(title, style: TextStyle.ds.label.medium14Medium, color: Color.ds.text.neutral.basic)
+
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: CGFloat.ds.spacing.sm) {
+ ForEach(Array(swatches.enumerated()), id: \.offset) { _, item in
+ VStack(spacing: CGFloat.ds.spacing.xs) {
+ RoundedRectangle(cornerRadius: CGFloat.ds.radius.xsm, style: .continuous)
+ .fill(item.1)
+ .frame(width: 56, height: 56)
+ .overlay {
+ RoundedRectangle(cornerRadius: CGFloat.ds.radius.xsm, style: .continuous)
+ .strokeBorder(
+ Color.ds.border.neutral.basic.opacity(0.4),
+ lineWidth: CGFloat.ds.border.thin
+ )
+ }
+
+ DesignText(
+ item.0,
+ style: TextStyle.ds.label.small12Medium,
+ color: Color.ds.text.neutral.light,
+ alignment: .center
+ )
+ .frame(width: 64)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+private struct GalleryTypographySection: View {
+ var body: some View {
+ GallerySectionScaffold(
+ title: "Typography",
+ subtitle: "Pretendard text styles from TextStyle.ds"
+ ) {
+ VStack(alignment: .leading, spacing: CGFloat.ds.spacing.sm) {
+ DesignText("Heading / large32Bold", style: TextStyle.ds.heading.large32Bold)
+ DesignText("Title / large24Bold", style: TextStyle.ds.title.large24Bold)
+ DesignText("Title / medium22Semibold", style: TextStyle.ds.title.medium22Semibold)
+ DesignText(
+ "Body / medium16Regular",
+ style: TextStyle.ds.body.medium16Regular,
+ color: Color.ds.text.neutral.basic
+ )
+ DesignText(
+ "Body / small14Regular",
+ style: TextStyle.ds.body.small14Regular,
+ color: Color.ds.text.neutral.basic
+ )
+ DesignText(
+ "Label / medium14Medium",
+ style: TextStyle.ds.label.medium14Medium,
+ color: Color.ds.text.primary.basic
+ )
+ DesignText("Button / lg18Semibold", style: TextStyle.ds.button.lg18Semibold)
+ }
+ }
+ }
+}
+
+private struct GalleryButtonsSection: View {
+ var body: some View {
+ GallerySectionScaffold(
+ title: "DesignButton",
+ subtitle: "Variants, sizes, full-width, disabled, icons"
+ ) {
+ VStack(alignment: .leading, spacing: CGFloat.ds.spacing.md) {
+ buttonVariantRow(title: "Primary", variant: .primary)
+ buttonVariantRow(title: "Secondary", variant: .secondary)
+ buttonVariantRow(title: "Outlined", variant: .outlined)
+ buttonVariantRow(title: "Text", variant: .text)
+
+ DesignText(
+ "Full width / disabled",
+ style: TextStyle.ds.label.small12Medium,
+ color: Color.ds.text.neutral.basic
+ )
+
+ DesignButton("Full Width Primary", variant: .primary, size: .lg, isFullWidth: true) {}
+ DesignButton(
+ "Disabled Primary",
+ variant: .primary,
+ size: .md,
+ isEnabled: false,
+ isFullWidth: true
+ ) {}
+ DesignButton(
+ "With Icons",
+ variant: .secondary,
+ size: .md,
+ leadingIcon: Image(systemName: "checkmark.circle.fill"),
+ trailingIcon: Image(systemName: "chevron.right")
+ ) {}
+ }
+ }
+ }
+
+ private func buttonVariantRow(title: String, variant: DesignButtonVariant) -> some View {
+ VStack(alignment: .leading, spacing: CGFloat.ds.spacing.sm) {
+ DesignText(title, style: TextStyle.ds.label.medium14Medium, color: Color.ds.text.neutral.basic)
+
+ HStack(spacing: CGFloat.ds.spacing.sm) {
+ DesignButton("sm", variant: variant, size: .sm) {}
+ DesignButton("md", variant: variant, size: .md) {}
+ DesignButton("lg", variant: variant, size: .lg) {}
+ }
+ }
+ }
+}
+
+private struct GallerySocialSection: View {
+ var body: some View {
+ GallerySectionScaffold(
+ title: "SocialLoginButton",
+ subtitle: "Kakao / Apple fixed brand buttons"
+ ) {
+ VStack(spacing: CGFloat.ds.spacing.sm) {
+ SocialLoginButton(provider: .kakao) {}
+ SocialLoginButton(provider: .apple) {}
+ }
+ }
+ }
+}
+
+// MARK: - Shared Scaffold
+
+private struct GallerySectionScaffold: View {
+ let title: String
+ let subtitle: String
+ @ViewBuilder let content: Content
+
+ var body: some View {
+ ScrollView {
+ VStack(alignment: .leading, spacing: CGFloat.ds.spacing.lg) {
+ VStack(alignment: .leading, spacing: CGFloat.ds.spacing.xs) {
+ DesignText(title, style: TextStyle.ds.title.large24Bold)
+ DesignText(
+ subtitle,
+ style: TextStyle.ds.body.medium16Regular,
+ color: Color.ds.text.neutral.basic
+ )
+ }
+
+ content
+ .padding(CGFloat.ds.spacing.md)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(Color.ds.background.grayDarker)
+ .clipShape(RoundedRectangle(cornerRadius: CGFloat.ds.radius.md, style: .continuous))
+ }
+ .padding(CGFloat.ds.spacing.lg)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .background(Color.ds.background.black)
+ .navigationTitle(title)
+ .navigationBarTitleDisplayMode(.inline)
+ }
+}
+
+#Preview("Design System Gallery") {
+ DesignSystemGalleryPreviewHost()
+}
+
+/// Preview-only host that registers fonts without making Gallery itself do bootstrap work.
+private struct DesignSystemGalleryPreviewHost: View {
+ var body: some View {
+ DesignSystemGallery()
+ .onAppear {
+ _ = DesignSystemFontRegistration.registerIfNeeded()
+ }
+ }
+}
+#endif
diff --git a/Projects/Shared/DesignSystem/Sources/Tokens/Primitive/PrimitiveColor.swift b/Projects/Shared/DesignSystem/Sources/Tokens/Primitive/PrimitiveColor.swift
new file mode 100644
index 0000000..485b67c
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Tokens/Primitive/PrimitiveColor.swift
@@ -0,0 +1,65 @@
+import SwiftUI
+
+struct TokenColor: Equatable, Sendable {
+ let hexRGB: UInt32
+ let alpha: Double
+
+ init(hexRGB: UInt32, alpha: Double = 1.0) {
+ self.hexRGB = hexRGB
+ self.alpha = alpha
+ }
+
+ var color: Color { Color(hex: hexRGB, alpha: alpha) }
+}
+
+enum PrimitiveColor {
+ enum Primary {
+ static let _50 = TokenColor(hexRGB: 0xF0E89A)
+ static let _100 = TokenColor(hexRGB: 0xEDE58C)
+ static let _200 = TokenColor(hexRGB: 0xEBE17D)
+ static let _300 = TokenColor(hexRGB: 0xE9DE6F)
+ static let _400 = TokenColor(hexRGB: 0xD2C864)
+ static let _500 = TokenColor(hexRGB: 0xBAB259)
+ static let _600 = TokenColor(hexRGB: 0xA39B4E)
+ static let _700 = TokenColor(hexRGB: 0x8C8543)
+ static let _800 = TokenColor(hexRGB: 0x5D592C)
+ static let _900 = TokenColor(hexRGB: 0x464321)
+ static let _1000 = TokenColor(hexRGB: 0x2F2C16)
+ }
+
+ enum Secondary {
+ static let _100 = TokenColor(hexRGB: 0xF8FBEF)
+ static let _200 = TokenColor(hexRGB: 0xEFF7D6)
+ static let _300 = TokenColor(hexRGB: 0xE5F2BD)
+ static let _400 = TokenColor(hexRGB: 0xD3E98B)
+ static let _500 = TokenColor(hexRGB: 0xD0E77C)
+ static let _600 = TokenColor(hexRGB: 0xB9CF6F)
+ static let _700 = TokenColor(hexRGB: 0xA2B661)
+ static let _800 = TokenColor(hexRGB: 0x8B9E54)
+ static let _900 = TokenColor(hexRGB: 0x748546)
+ }
+
+ enum Neutral {
+ static let _0 = TokenColor(hexRGB: 0xFFFFFF)
+ static let _100 = TokenColor(hexRGB: 0xE5E5E5)
+ static let _200 = TokenColor(hexRGB: 0xCCCCCC)
+ static let _300 = TokenColor(hexRGB: 0xB2B2B2)
+ static let _400 = TokenColor(hexRGB: 0x999999)
+ static let _500 = TokenColor(hexRGB: 0x808080)
+ static let _600 = TokenColor(hexRGB: 0x666666)
+ static let _700 = TokenColor(hexRGB: 0x4D4D4D)
+ static let _800 = TokenColor(hexRGB: 0x333333)
+ static let _900 = TokenColor(hexRGB: 0x1A1A1A)
+ static let _1000 = TokenColor(hexRGB: 0x000000)
+ }
+
+ enum System {
+ static let blue = TokenColor(hexRGB: 0x5297FF)
+ static let red = TokenColor(hexRGB: 0xF96767)
+ static let orange = TokenColor(hexRGB: 0xFB923C)
+ }
+
+ enum Social {
+ static let kakaoYellow = TokenColor(hexRGB: 0xFEE500)
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Tokens/Primitive/PrimitiveNumber.swift b/Projects/Shared/DesignSystem/Sources/Tokens/Primitive/PrimitiveNumber.swift
new file mode 100644
index 0000000..498f822
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Tokens/Primitive/PrimitiveNumber.swift
@@ -0,0 +1,39 @@
+import CoreGraphics
+
+enum PrimitiveNumber {
+ enum Radius {
+ static let none: CGFloat = 0
+ static let xsm: CGFloat = 4
+ static let sm: CGFloat = 8
+ static let md: CGFloat = 12
+ static let lg: CGFloat = 16
+ static let xxlg: CGFloat = 24
+ static let full: CGFloat = 9999
+ }
+
+ enum Spacing {
+ static let xs: CGFloat = 4
+ static let sm: CGFloat = 8
+ static let md: CGFloat = 12
+ static let lg: CGFloat = 16
+ static let xl: CGFloat = 24
+ static let xxl: CGFloat = 32
+ }
+
+ enum Border {
+ static let thin: CGFloat = 1
+ static let thick: CGFloat = 2
+ }
+
+ enum ControlHeight {
+ static let sm: CGFloat = 32
+ static let md: CGFloat = 40
+ static let lg: CGFloat = 48
+ }
+
+ enum Dim {
+ static let light: CGFloat = 0.2
+ static let `default`: CGFloat = 0.4
+ static let heavy: CGFloat = 0.6
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Tokens/Semantic/SemanticColor.swift b/Projects/Shared/DesignSystem/Sources/Tokens/Semantic/SemanticColor.swift
new file mode 100644
index 0000000..1ca3163
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Tokens/Semantic/SemanticColor.swift
@@ -0,0 +1,141 @@
+import Foundation
+
+/// Clear backgrounds for outlined/text default states are represented as
+/// `TokenColor(hexRGB: 0x000000, alpha: 0)` so they stay testable as TokenColor.
+enum SemanticColor {
+ enum Button {
+ enum Primary {
+ static let background = Background()
+ static let content = Content()
+
+ struct Background {
+ let `default` = PrimitiveColor.Primary._300
+ let pressed = PrimitiveColor.Primary._600
+ let disabled = PrimitiveColor.Primary._50
+ }
+
+ struct Content {
+ let `default` = PrimitiveColor.Neutral._1000
+ let pressed = PrimitiveColor.Neutral._1000
+ let disabled = PrimitiveColor.Neutral._500
+ }
+ }
+
+ enum Secondary {
+ static let background = Background()
+ static let content = Content()
+
+ struct Background {
+ let `default` = PrimitiveColor.Secondary._500
+ let pressed = PrimitiveColor.Secondary._800
+ let disabled = PrimitiveColor.Secondary._200
+ }
+
+ struct Content {
+ let `default` = PrimitiveColor.Neutral._1000
+ let pressed = PrimitiveColor.Neutral._1000
+ let disabled = PrimitiveColor.Neutral._500
+ }
+ }
+
+ enum Outlined {
+ static let background = Background()
+ static let content = Content()
+ static let border = Border()
+
+ struct Background {
+ let `default` = TokenColor(hexRGB: 0x000000, alpha: 0)
+ let pressed = PrimitiveColor.Primary._900
+ let disabled = PrimitiveColor.Primary._800
+ }
+
+ struct Content {
+ let `default` = PrimitiveColor.Primary._300
+ let pressed = PrimitiveColor.Primary._300
+ let disabled = PrimitiveColor.Primary._700
+ }
+
+ struct Border {
+ let `default` = PrimitiveColor.Primary._300
+ let pressed = PrimitiveColor.Primary._300
+ let disabled = PrimitiveColor.Primary._700
+ }
+ }
+
+ enum Text {
+ static let background = Background()
+ static let content = Content()
+
+ struct Background {
+ let `default` = TokenColor(hexRGB: 0x000000, alpha: 0)
+ let pressed = PrimitiveColor.Primary._900
+ let disabled = PrimitiveColor.Primary._800
+ }
+
+ struct Content {
+ let `default` = PrimitiveColor.Primary._300
+ let pressed = PrimitiveColor.Primary._300
+ let disabled = PrimitiveColor.Primary._700
+ }
+ }
+ }
+
+ enum Text {
+ enum Neutral {
+ static let black = PrimitiveColor.Neutral._1000
+ static let darker = PrimitiveColor.Neutral._800
+ static let dark = PrimitiveColor.Neutral._700
+ static let basic = PrimitiveColor.Neutral._500
+ static let light = PrimitiveColor.Neutral._300
+ static let lighter = PrimitiveColor.Neutral._100
+ static let white = PrimitiveColor.Neutral._0
+ }
+
+ enum Primary {
+ static let basic = PrimitiveColor.Primary._300
+ static let dark = PrimitiveColor.Primary._500
+ static let darker = PrimitiveColor.Primary._700
+ }
+
+ enum Secondary {
+ static let basic = PrimitiveColor.Secondary._500
+ static let darker = PrimitiveColor.Secondary._800
+ }
+ }
+
+ enum Background {
+ static let black = PrimitiveColor.Neutral._1000
+ static let grayDarker = PrimitiveColor.Neutral._900
+ static let grayDark = PrimitiveColor.Neutral._800
+ }
+
+ enum Border {
+ enum Primary {
+ static let basic = PrimitiveColor.Primary._300
+ static let dark = PrimitiveColor.Primary._500
+ static let darker = PrimitiveColor.Primary._700
+ }
+
+ enum Neutral {
+ static let lighter = PrimitiveColor.Neutral._100
+ static let light = PrimitiveColor.Neutral._300
+ static let basic = PrimitiveColor.Neutral._500
+ static let dark = PrimitiveColor.Neutral._700
+ static let darker = PrimitiveColor.Neutral._800
+ }
+ }
+
+ enum Social {
+ static let kakao = PrimitiveColor.Social.kakaoYellow
+ static let kakaoContent = PrimitiveColor.Neutral._1000
+ static let appleBackground = PrimitiveColor.Neutral._0
+ static let appleBorder = PrimitiveColor.Neutral._1000
+ static let appleContent = PrimitiveColor.Neutral._1000
+ }
+
+ enum Dim {
+ static let light = TokenColor(hexRGB: 0x000000, alpha: Double(PrimitiveNumber.Dim.light))
+ static let `default` = TokenColor(hexRGB: 0x000000, alpha: Double(PrimitiveNumber.Dim.default))
+ static let heavy = TokenColor(hexRGB: 0x000000, alpha: Double(PrimitiveNumber.Dim.heavy))
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Tokens/Semantic/SemanticNumber.swift b/Projects/Shared/DesignSystem/Sources/Tokens/Semantic/SemanticNumber.swift
new file mode 100644
index 0000000..0a8b413
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Tokens/Semantic/SemanticNumber.swift
@@ -0,0 +1,39 @@
+import CoreGraphics
+
+enum SemanticNumber {
+ enum Radius {
+ static let none = PrimitiveNumber.Radius.none
+ static let xsm = PrimitiveNumber.Radius.xsm
+ static let sm = PrimitiveNumber.Radius.sm
+ static let md = PrimitiveNumber.Radius.md
+ static let lg = PrimitiveNumber.Radius.lg
+ static let xxlg = PrimitiveNumber.Radius.xxlg
+ static let full = PrimitiveNumber.Radius.full
+ }
+
+ enum Spacing {
+ static let xs = PrimitiveNumber.Spacing.xs
+ static let sm = PrimitiveNumber.Spacing.sm
+ static let md = PrimitiveNumber.Spacing.md
+ static let lg = PrimitiveNumber.Spacing.lg
+ static let xl = PrimitiveNumber.Spacing.xl
+ static let xxl = PrimitiveNumber.Spacing.xxl
+ }
+
+ enum Border {
+ static let thin = PrimitiveNumber.Border.thin
+ static let thick = PrimitiveNumber.Border.thick
+ }
+
+ enum ControlHeight {
+ static let sm = PrimitiveNumber.ControlHeight.sm
+ static let md = PrimitiveNumber.ControlHeight.md
+ static let lg = PrimitiveNumber.ControlHeight.lg
+ }
+
+ enum Dim {
+ static let light = PrimitiveNumber.Dim.light
+ static let `default` = PrimitiveNumber.Dim.default
+ static let heavy = PrimitiveNumber.Dim.heavy
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Tokens/Typography/TextStyle.swift b/Projects/Shared/DesignSystem/Sources/Tokens/Typography/TextStyle.swift
new file mode 100644
index 0000000..c23380f
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Tokens/Typography/TextStyle.swift
@@ -0,0 +1,47 @@
+import CoreGraphics
+import SwiftUI
+
+public struct TextStyle: Sendable, Equatable {
+ public let fontName: String
+ public let size: CGFloat
+ public let lineHeight: CGFloat
+ public let letterSpacingEm: CGFloat
+
+ public init(
+ fontName: String,
+ size: CGFloat,
+ lineHeight: CGFloat,
+ letterSpacingEm: CGFloat
+ ) {
+ self.fontName = fontName
+ self.size = size
+ self.lineHeight = lineHeight
+ self.letterSpacingEm = letterSpacingEm
+ }
+
+ public var font: Font {
+ Font.custom(fontName, size: size)
+ }
+
+ public var letterSpacing: CGFloat {
+ size * letterSpacingEm
+ }
+
+ public var additionalLineSpacing: CGFloat {
+ max(0, lineHeight - size)
+ }
+}
+
+public extension TextStyle {
+ static let ds = DesignSystemTextStyles()
+}
+
+public struct DesignSystemTextStyles: Sendable {
+ public let heading = HeadingTextStyles()
+ public let title = TitleTextStyles()
+ public let body = BodyTextStyles()
+ public let label = LabelTextStyles()
+ public let button = ButtonTextStyles()
+
+ public init() {}
+}
diff --git a/Projects/Shared/DesignSystem/Sources/Tokens/Typography/Typography.swift b/Projects/Shared/DesignSystem/Sources/Tokens/Typography/Typography.swift
new file mode 100644
index 0000000..69504d7
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Sources/Tokens/Typography/Typography.swift
@@ -0,0 +1,177 @@
+import CoreGraphics
+
+enum DesignSystemFontName {
+ static let regular = "Pretendard-Regular"
+ static let medium = "Pretendard-Medium"
+ static let semibold = "Pretendard-SemiBold"
+ static let bold = "Pretendard-Bold"
+}
+
+public struct HeadingTextStyles: Sendable {
+ public let large32Bold = TextStyle(
+ fontName: DesignSystemFontName.bold,
+ size: 32,
+ lineHeight: 40,
+ letterSpacingEm: -0.01
+ )
+ public let large32Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 32,
+ lineHeight: 40,
+ letterSpacingEm: -0.01
+ )
+ public let medium28Bold = TextStyle(
+ fontName: DesignSystemFontName.bold,
+ size: 28,
+ lineHeight: 36,
+ letterSpacingEm: -0.01
+ )
+ public let medium28Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 28,
+ lineHeight: 36,
+ letterSpacingEm: -0.01
+ )
+
+ public init() {}
+}
+
+public struct TitleTextStyles: Sendable {
+ public let large24Bold = TextStyle(
+ fontName: DesignSystemFontName.bold,
+ size: 24,
+ lineHeight: 32,
+ letterSpacingEm: -0.01
+ )
+ public let large24Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 24,
+ lineHeight: 32,
+ letterSpacingEm: -0.01
+ )
+ public let medium22Bold = TextStyle(
+ fontName: DesignSystemFontName.bold,
+ size: 22,
+ lineHeight: 30,
+ letterSpacingEm: -0.01
+ )
+ public let medium22Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 22,
+ lineHeight: 30,
+ letterSpacingEm: -0.01
+ )
+ public let small20Bold = TextStyle(
+ fontName: DesignSystemFontName.bold,
+ size: 20,
+ lineHeight: 28,
+ letterSpacingEm: -0.01
+ )
+ public let small20Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 20,
+ lineHeight: 28,
+ letterSpacingEm: -0.01
+ )
+ public let xsmall18Bold = TextStyle(
+ fontName: DesignSystemFontName.bold,
+ size: 18,
+ lineHeight: 26,
+ letterSpacingEm: -0.01
+ )
+ public let xsmall18Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 18,
+ lineHeight: 26,
+ letterSpacingEm: -0.01
+ )
+
+ public init() {}
+}
+
+public struct BodyTextStyles: Sendable {
+ public let large18Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 18,
+ lineHeight: 30,
+ letterSpacingEm: 0
+ )
+ public let large18Regular = TextStyle(
+ fontName: DesignSystemFontName.regular,
+ size: 18,
+ lineHeight: 30,
+ letterSpacingEm: 0
+ )
+ public let medium16Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 16,
+ lineHeight: 28,
+ letterSpacingEm: 0
+ )
+ public let medium16Regular = TextStyle(
+ fontName: DesignSystemFontName.regular,
+ size: 16,
+ lineHeight: 28,
+ letterSpacingEm: 0
+ )
+ public let small14Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 14,
+ lineHeight: 24,
+ letterSpacingEm: 0
+ )
+ public let small14Regular = TextStyle(
+ fontName: DesignSystemFontName.regular,
+ size: 14,
+ lineHeight: 24,
+ letterSpacingEm: 0
+ )
+
+ public init() {}
+}
+
+public struct LabelTextStyles: Sendable {
+ public let large16Medium = TextStyle(
+ fontName: DesignSystemFontName.medium,
+ size: 16,
+ lineHeight: 20,
+ letterSpacingEm: 0.02
+ )
+ public let medium14Medium = TextStyle(
+ fontName: DesignSystemFontName.medium,
+ size: 14,
+ lineHeight: 20,
+ letterSpacingEm: 0.02
+ )
+ public let small12Medium = TextStyle(
+ fontName: DesignSystemFontName.medium,
+ size: 12,
+ lineHeight: 16,
+ letterSpacingEm: 0.02
+ )
+
+ public init() {}
+}
+
+public struct ButtonTextStyles: Sendable {
+ public let sm14Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 14,
+ lineHeight: 14,
+ letterSpacingEm: 0
+ )
+ public let md16Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 16,
+ lineHeight: 16,
+ letterSpacingEm: 0
+ )
+ public let lg18Semibold = TextStyle(
+ fontName: DesignSystemFontName.semibold,
+ size: 18,
+ lineHeight: 18,
+ letterSpacingEm: 0
+ )
+
+ public init() {}
+}
diff --git a/Projects/Shared/DesignSystem/Tests/Components/DesignButtonMetricsTests.swift b/Projects/Shared/DesignSystem/Tests/Components/DesignButtonMetricsTests.swift
new file mode 100644
index 0000000..ebb8611
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Tests/Components/DesignButtonMetricsTests.swift
@@ -0,0 +1,53 @@
+import XCTest
+@testable import SharedDesignSystem
+
+final class DesignButtonMetricsTests: XCTestCase {
+ func test_size_metrics() {
+ XCTAssertEqual(DesignButtonSize.sm.height, 32)
+ XCTAssertEqual(DesignButtonSize.sm.horizontalPadding, 12)
+ XCTAssertEqual(DesignButtonSize.sm.cornerRadius, 4)
+ XCTAssertEqual(DesignButtonSize.sm.iconSize, 14)
+
+ XCTAssertEqual(DesignButtonSize.md.height, 40)
+ XCTAssertEqual(DesignButtonSize.md.horizontalPadding, 16)
+ XCTAssertEqual(DesignButtonSize.md.cornerRadius, 4)
+ XCTAssertEqual(DesignButtonSize.md.iconSize, 16)
+
+ XCTAssertEqual(DesignButtonSize.lg.height, 48)
+ XCTAssertEqual(DesignButtonSize.lg.horizontalPadding, 20)
+ XCTAssertEqual(DesignButtonSize.lg.cornerRadius, 8)
+ XCTAssertEqual(DesignButtonSize.lg.iconSize, 18)
+ }
+
+ func test_primary_disabled_uses_semantic_tokens() {
+ let style = DesignButtonStyleResolver.resolve(variant: .primary, state: .disabled)
+ XCTAssertEqual(style.background.hexRGB, PrimitiveColor.Primary._50.hexRGB)
+ XCTAssertEqual(style.content.hexRGB, PrimitiveColor.Neutral._500.hexRGB)
+ }
+
+ func test_outlined_pressed_background() {
+ let style = DesignButtonStyleResolver.resolve(variant: .outlined, state: .pressed)
+ XCTAssertEqual(style.background.hexRGB, PrimitiveColor.Primary._900.hexRGB)
+ XCTAssertEqual(style.border?.hexRGB, PrimitiveColor.Primary._300.hexRGB)
+ }
+
+ func test_disabled_state_resolution() {
+ // Covers parent .disabled / DesignButton(isEnabled: false) → disabled tokens.
+ XCTAssertEqual(
+ DesignButtonInteractionStateResolver.resolve(isEnabled: false, isPressed: false),
+ .disabled
+ )
+ XCTAssertEqual(
+ DesignButtonInteractionStateResolver.resolve(isEnabled: false, isPressed: true),
+ .disabled
+ )
+ XCTAssertEqual(
+ DesignButtonInteractionStateResolver.resolve(isEnabled: true, isPressed: true),
+ .pressed
+ )
+ XCTAssertEqual(
+ DesignButtonInteractionStateResolver.resolve(isEnabled: true, isPressed: false),
+ .default
+ )
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Tests/Components/SocialLoginProviderTests.swift b/Projects/Shared/DesignSystem/Tests/Components/SocialLoginProviderTests.swift
new file mode 100644
index 0000000..31091ec
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Tests/Components/SocialLoginProviderTests.swift
@@ -0,0 +1,20 @@
+import XCTest
+@testable import SharedDesignSystem
+
+final class SocialLoginProviderTests: XCTestCase {
+ func test_kakao_copy_and_colors() {
+ XCTAssertEqual(SocialLoginProvider.kakao.title, "카카오로 시작하기")
+ XCTAssertEqual(SocialLoginProvider.kakao.background.hexRGB, PrimitiveColor.Social.kakaoYellow.hexRGB)
+ XCTAssertEqual(SocialLoginProvider.kakao.content.hexRGB, PrimitiveColor.Neutral._1000.hexRGB)
+ XCTAssertNil(SocialLoginProvider.kakao.border)
+ XCTAssertEqual(SocialLoginProvider.kakao.contentGap, 12)
+ }
+
+ func test_apple_copy_and_colors() {
+ XCTAssertEqual(SocialLoginProvider.apple.title, "Apple로 로그인")
+ XCTAssertEqual(SocialLoginProvider.apple.background.hexRGB, PrimitiveColor.Neutral._0.hexRGB)
+ XCTAssertEqual(SocialLoginProvider.apple.content.hexRGB, PrimitiveColor.Neutral._1000.hexRGB)
+ XCTAssertEqual(SocialLoginProvider.apple.border?.hexRGB, PrimitiveColor.Neutral._1000.hexRGB)
+ XCTAssertEqual(SocialLoginProvider.apple.contentGap, 9)
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Tests/Foundation/FontRegistrationTests.swift b/Projects/Shared/DesignSystem/Tests/Foundation/FontRegistrationTests.swift
new file mode 100644
index 0000000..9ef2ef7
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Tests/Foundation/FontRegistrationTests.swift
@@ -0,0 +1,29 @@
+import XCTest
+@testable import SharedDesignSystem
+
+final class FontRegistrationTests: XCTestCase {
+ func test_font_resources_exist() {
+ let names = [
+ "Pretendard-Regular",
+ "Pretendard-Medium",
+ "Pretendard-SemiBold",
+ "Pretendard-Bold",
+ ]
+ let bundle = SharedDesignSystemResources.bundle
+
+ for name in names {
+ XCTAssertNotNil(
+ bundle.url(forResource: name, withExtension: "otf")
+ ?? bundle.url(forResource: name, withExtension: "ttf"),
+ "Missing font resource: \(name)"
+ )
+ }
+ }
+
+ func test_registerIfNeeded_is_idempotent() {
+ let first = DesignSystemFontRegistration.registerIfNeeded()
+ let second = DesignSystemFontRegistration.registerIfNeeded()
+ XCTAssertTrue(first)
+ XCTAssertTrue(second)
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Tests/Smoke/SharedDesignSystemSmokeTests.swift b/Projects/Shared/DesignSystem/Tests/Smoke/SharedDesignSystemSmokeTests.swift
new file mode 100644
index 0000000..a5529a0
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Tests/Smoke/SharedDesignSystemSmokeTests.swift
@@ -0,0 +1,9 @@
+import SharedDesignSystem
+import SwiftUI
+import XCTest
+
+final class SharedDesignSystemSmokeTests: XCTestCase {
+ func test_모듈_임포트() {
+ XCTAssertNotNil(Color.red)
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Tests/Tokens/PrimitiveColorTests.swift b/Projects/Shared/DesignSystem/Tests/Tokens/PrimitiveColorTests.swift
new file mode 100644
index 0000000..cd1e8a9
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Tests/Tokens/PrimitiveColorTests.swift
@@ -0,0 +1,16 @@
+import XCTest
+@testable import SharedDesignSystem
+
+final class PrimitiveColorTests: XCTestCase {
+ func test_primary300_hex() {
+ XCTAssertEqual(PrimitiveColor.Primary._300.hexRGB, 0xE9DE6F)
+ }
+
+ func test_neutral1000_hex() {
+ XCTAssertEqual(PrimitiveColor.Neutral._1000.hexRGB, 0x000000)
+ }
+
+ func test_brand_kakao_hex() {
+ XCTAssertEqual(PrimitiveColor.Social.kakaoYellow.hexRGB, 0xFEE500)
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Tests/Tokens/SemanticColorTests.swift b/Projects/Shared/DesignSystem/Tests/Tokens/SemanticColorTests.swift
new file mode 100644
index 0000000..7861b48
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Tests/Tokens/SemanticColorTests.swift
@@ -0,0 +1,90 @@
+import SwiftUI
+import XCTest
+@testable import SharedDesignSystem
+
+final class SemanticColorTests: XCTestCase {
+ func test_button_primary_default_maps_primary300() {
+ XCTAssertEqual(
+ SemanticColor.Button.Primary.background.default.hexRGB,
+ PrimitiveColor.Primary._300.hexRGB
+ )
+ }
+
+ func test_button_primary_disabled_content_maps_neutral500() {
+ XCTAssertEqual(
+ SemanticColor.Button.Primary.content.disabled.hexRGB,
+ PrimitiveColor.Neutral._500.hexRGB
+ )
+ }
+
+ func test_text_neutral_white_maps_neutral0() {
+ XCTAssertEqual(
+ SemanticColor.Text.Neutral.white.hexRGB,
+ PrimitiveColor.Neutral._0.hexRGB
+ )
+ }
+
+ func test_brand_kakao_maps_brand_token() {
+ XCTAssertEqual(
+ SemanticColor.Social.kakao.hexRGB,
+ PrimitiveColor.Social.kakaoYellow.hexRGB
+ )
+ XCTAssertEqual(
+ SemanticColor.Social.kakaoContent.hexRGB,
+ PrimitiveColor.Neutral._1000.hexRGB
+ )
+ XCTAssertEqual(
+ SemanticColor.Social.appleContent.hexRGB,
+ PrimitiveColor.Neutral._1000.hexRGB
+ )
+ }
+
+ func test_button_secondary_default_maps_secondary500() {
+ XCTAssertEqual(
+ SemanticColor.Button.Secondary.background.default.hexRGB,
+ PrimitiveColor.Secondary._500.hexRGB
+ )
+ }
+
+ func test_button_outlined_default_is_clear() {
+ let background = SemanticColor.Button.Outlined.background.default
+ XCTAssertEqual(background.hexRGB, 0x000000)
+ XCTAssertEqual(background.alpha, 0, accuracy: 0.0001)
+ }
+
+ func test_button_text_pressed_background_maps_primary900() {
+ XCTAssertEqual(
+ SemanticColor.Button.Text.background.pressed.hexRGB,
+ PrimitiveColor.Primary._900.hexRGB
+ )
+ }
+
+ func test_background_grayDarker_maps_neutral900() {
+ XCTAssertEqual(
+ SemanticColor.Background.grayDarker.hexRGB,
+ PrimitiveColor.Neutral._900.hexRGB
+ )
+ }
+
+ func test_border_primary_basic_maps_primary300() {
+ XCTAssertEqual(
+ SemanticColor.Border.Primary.basic.hexRGB,
+ PrimitiveColor.Primary._300.hexRGB
+ )
+ }
+
+ func test_dim_default_is_black_40_percent() {
+ let dim = SemanticColor.Dim.default
+ XCTAssertEqual(dim.hexRGB, 0x000000)
+ XCTAssertEqual(dim.alpha, 0.4, accuracy: 0.0001)
+ }
+
+ func test_public_color_ds_paths_resolve() {
+ XCTAssertNotNil(Color.ds.button.primary.background.default)
+ XCTAssertNotNil(Color.ds.text.neutral.basic)
+ XCTAssertNotNil(Color.ds.background.grayDarker)
+ XCTAssertNotNil(Color.ds.border.primary.basic)
+ XCTAssertNotNil(Color.ds.dim.default)
+ XCTAssertNotNil(Color.ds.social.kakao)
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Tests/Tokens/SemanticNumberTests.swift b/Projects/Shared/DesignSystem/Tests/Tokens/SemanticNumberTests.swift
new file mode 100644
index 0000000..f9bbdd7
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Tests/Tokens/SemanticNumberTests.swift
@@ -0,0 +1,43 @@
+import XCTest
+@testable import SharedDesignSystem
+
+final class SemanticNumberTests: XCTestCase {
+ func test_spacing_and_radius() {
+ XCTAssertEqual(SemanticNumber.Spacing.md, 12)
+ XCTAssertEqual(SemanticNumber.Radius.xsm, 4)
+ XCTAssertEqual(SemanticNumber.ControlHeight.lg, 48)
+ XCTAssertEqual(SemanticNumber.Dim.default, 0.4, accuracy: 0.0001)
+ }
+
+ func test_full_semantic_number_table() {
+ XCTAssertEqual(SemanticNumber.Radius.none, 0)
+ XCTAssertEqual(SemanticNumber.Radius.sm, 8)
+ XCTAssertEqual(SemanticNumber.Radius.md, 12)
+ XCTAssertEqual(SemanticNumber.Radius.lg, 16)
+ XCTAssertEqual(SemanticNumber.Radius.xxlg, 24)
+ XCTAssertEqual(SemanticNumber.Radius.full, 9999)
+
+ XCTAssertEqual(SemanticNumber.Spacing.xs, 4)
+ XCTAssertEqual(SemanticNumber.Spacing.sm, 8)
+ XCTAssertEqual(SemanticNumber.Spacing.lg, 16)
+ XCTAssertEqual(SemanticNumber.Spacing.xl, 24)
+ XCTAssertEqual(SemanticNumber.Spacing.xxl, 32)
+
+ XCTAssertEqual(SemanticNumber.Border.thin, 1)
+ XCTAssertEqual(SemanticNumber.Border.thick, 2)
+
+ XCTAssertEqual(SemanticNumber.ControlHeight.sm, 32)
+ XCTAssertEqual(SemanticNumber.ControlHeight.md, 40)
+
+ XCTAssertEqual(SemanticNumber.Dim.light, 0.2, accuracy: 0.0001)
+ XCTAssertEqual(SemanticNumber.Dim.heavy, 0.6, accuracy: 0.0001)
+ }
+
+ func test_public_cgfloat_ds_paths_resolve() {
+ XCTAssertEqual(CGFloat.ds.spacing.md, 12)
+ XCTAssertEqual(CGFloat.ds.radius.xsm, 4)
+ XCTAssertEqual(CGFloat.ds.controlHeight.lg, 48)
+ XCTAssertEqual(CGFloat.ds.dim.default, 0.4, accuracy: 0.0001)
+ XCTAssertEqual(CGFloat.ds.border.thin, 1)
+ }
+}
diff --git a/Projects/Shared/DesignSystem/Tests/Tokens/TypographyTests.swift b/Projects/Shared/DesignSystem/Tests/Tokens/TypographyTests.swift
new file mode 100644
index 0000000..3d65a2a
--- /dev/null
+++ b/Projects/Shared/DesignSystem/Tests/Tokens/TypographyTests.swift
@@ -0,0 +1,246 @@
+import SwiftUI
+import XCTest
+@testable import SharedDesignSystem
+
+final class TypographyTests: XCTestCase {
+ func test_body_small14_regular_metrics() {
+ let style = TextStyle.ds.body.small14Regular
+ XCTAssertEqual(style.size, 14)
+ XCTAssertEqual(style.lineHeight, 24)
+ XCTAssertEqual(style.letterSpacingEm, 0)
+ XCTAssertEqual(style.fontName, "Pretendard-Regular")
+ }
+
+ func test_heading_large32_bold_metrics() {
+ let style = TextStyle.ds.heading.large32Bold
+ XCTAssertEqual(style.size, 32)
+ XCTAssertEqual(style.lineHeight, 40)
+ XCTAssertEqual(style.letterSpacingEm, -0.01, accuracy: 0.0001)
+ XCTAssertEqual(style.fontName, "Pretendard-Bold")
+ }
+
+ func test_button_sm14_is_semibold() {
+ let style = TextStyle.ds.button.sm14Semibold
+ XCTAssertEqual(style.size, 14)
+ XCTAssertEqual(style.lineHeight, 14)
+ XCTAssertEqual(style.fontName, "Pretendard-SemiBold")
+ }
+
+ func test_label_letter_spacing() {
+ XCTAssertEqual(TextStyle.ds.label.small12Medium.letterSpacingEm, 0.02, accuracy: 0.0001)
+ }
+
+ func test_heading_and_title_full_table() {
+ assertStyle(
+ TextStyle.ds.heading.large32Bold,
+ fontName: "Pretendard-Bold",
+ size: 32,
+ lineHeight: 40,
+ letterSpacingEm: -0.01
+ )
+ assertStyle(
+ TextStyle.ds.heading.large32Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 32,
+ lineHeight: 40,
+ letterSpacingEm: -0.01
+ )
+ assertStyle(
+ TextStyle.ds.heading.medium28Bold,
+ fontName: "Pretendard-Bold",
+ size: 28,
+ lineHeight: 36,
+ letterSpacingEm: -0.01
+ )
+ assertStyle(
+ TextStyle.ds.heading.medium28Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 28,
+ lineHeight: 36,
+ letterSpacingEm: -0.01
+ )
+
+ assertStyle(
+ TextStyle.ds.title.large24Bold,
+ fontName: "Pretendard-Bold",
+ size: 24,
+ lineHeight: 32,
+ letterSpacingEm: -0.01
+ )
+ assertStyle(
+ TextStyle.ds.title.large24Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 24,
+ lineHeight: 32,
+ letterSpacingEm: -0.01
+ )
+ assertStyle(
+ TextStyle.ds.title.medium22Bold,
+ fontName: "Pretendard-Bold",
+ size: 22,
+ lineHeight: 30,
+ letterSpacingEm: -0.01
+ )
+ assertStyle(
+ TextStyle.ds.title.medium22Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 22,
+ lineHeight: 30,
+ letterSpacingEm: -0.01
+ )
+ assertStyle(
+ TextStyle.ds.title.small20Bold,
+ fontName: "Pretendard-Bold",
+ size: 20,
+ lineHeight: 28,
+ letterSpacingEm: -0.01
+ )
+ assertStyle(
+ TextStyle.ds.title.small20Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 20,
+ lineHeight: 28,
+ letterSpacingEm: -0.01
+ )
+ assertStyle(
+ TextStyle.ds.title.xsmall18Bold,
+ fontName: "Pretendard-Bold",
+ size: 18,
+ lineHeight: 26,
+ letterSpacingEm: -0.01
+ )
+ assertStyle(
+ TextStyle.ds.title.xsmall18Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 18,
+ lineHeight: 26,
+ letterSpacingEm: -0.01
+ )
+ }
+
+ func test_body_label_button_full_table() {
+ assertStyle(
+ TextStyle.ds.body.large18Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 18,
+ lineHeight: 30,
+ letterSpacingEm: 0
+ )
+ assertStyle(
+ TextStyle.ds.body.large18Regular,
+ fontName: "Pretendard-Regular",
+ size: 18,
+ lineHeight: 30,
+ letterSpacingEm: 0
+ )
+ assertStyle(
+ TextStyle.ds.body.medium16Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 16,
+ lineHeight: 28,
+ letterSpacingEm: 0
+ )
+ assertStyle(
+ TextStyle.ds.body.medium16Regular,
+ fontName: "Pretendard-Regular",
+ size: 16,
+ lineHeight: 28,
+ letterSpacingEm: 0
+ )
+ assertStyle(
+ TextStyle.ds.body.small14Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 14,
+ lineHeight: 24,
+ letterSpacingEm: 0
+ )
+ assertStyle(
+ TextStyle.ds.body.small14Regular,
+ fontName: "Pretendard-Regular",
+ size: 14,
+ lineHeight: 24,
+ letterSpacingEm: 0
+ )
+
+ assertStyle(
+ TextStyle.ds.label.large16Medium,
+ fontName: "Pretendard-Medium",
+ size: 16,
+ lineHeight: 20,
+ letterSpacingEm: 0.02
+ )
+ assertStyle(
+ TextStyle.ds.label.medium14Medium,
+ fontName: "Pretendard-Medium",
+ size: 14,
+ lineHeight: 20,
+ letterSpacingEm: 0.02
+ )
+ assertStyle(
+ TextStyle.ds.label.small12Medium,
+ fontName: "Pretendard-Medium",
+ size: 12,
+ lineHeight: 16,
+ letterSpacingEm: 0.02
+ )
+
+ assertStyle(
+ TextStyle.ds.button.sm14Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 14,
+ lineHeight: 14,
+ letterSpacingEm: 0
+ )
+ assertStyle(
+ TextStyle.ds.button.md16Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 16,
+ lineHeight: 16,
+ letterSpacingEm: 0
+ )
+ assertStyle(
+ TextStyle.ds.button.lg18Semibold,
+ fontName: "Pretendard-SemiBold",
+ size: 18,
+ lineHeight: 18,
+ letterSpacingEm: 0
+ )
+ }
+
+ func test_derived_spacing_helpers() {
+ let heading = TextStyle.ds.heading.large32Bold
+ XCTAssertEqual(heading.letterSpacing, 32 * -0.01, accuracy: 0.0001)
+ XCTAssertEqual(heading.additionalLineSpacing, 8)
+
+ let button = TextStyle.ds.button.sm14Semibold
+ XCTAssertEqual(button.letterSpacing, 0)
+ XCTAssertEqual(button.additionalLineSpacing, 0)
+
+ let label = TextStyle.ds.label.small12Medium
+ XCTAssertEqual(label.letterSpacing, 12 * 0.02, accuracy: 0.0001)
+ XCTAssertEqual(label.additionalLineSpacing, 4)
+ }
+
+ func test_public_font_ds_paths_resolve() {
+ XCTAssertNotNil(Font.ds.heading.large32Bold)
+ XCTAssertNotNil(Font.ds.title.xsmall18Semibold)
+ XCTAssertNotNil(Font.ds.body.small14Regular)
+ XCTAssertNotNil(Font.ds.label.medium14Medium)
+ XCTAssertNotNil(Font.ds.button.lg18Semibold)
+ }
+
+ private func assertStyle(
+ _ style: TextStyle,
+ fontName: String,
+ size: CGFloat,
+ lineHeight: CGFloat,
+ letterSpacingEm: CGFloat,
+ file: StaticString = #filePath,
+ line: UInt = #line
+ ) {
+ XCTAssertEqual(style.fontName, fontName, file: file, line: line)
+ XCTAssertEqual(style.size, size, file: file, line: line)
+ XCTAssertEqual(style.lineHeight, lineHeight, file: file, line: line)
+ XCTAssertEqual(style.letterSpacingEm, letterSpacingEm, accuracy: 0.0001, file: file, line: line)
+ }
+}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 079305b..665692d 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -52,6 +52,15 @@ App → 조립
- 포함: `AppInfo`, pure Foundation 헬퍼
- 제외: UI, Network, Storage, Logger, Domain, TCA helper
+### SharedDesignSystem
+
+- 포함: semantic UI tokens, typography, `DesignText` / `DesignButton` / `SocialLoginButton`
+- Feature public surface: `Color.ds`, `Font.ds`, `CGFloat.ds`, `TextStyle.ds` + 공통 컴포넌트
+- Primitive / Semantic 토큰 타입은 모듈 내부 전용. Feature는 `*.ds`와 컴포넌트만 사용
+- 컴포넌트는 Semantic 토큰을 통해 값을 해석한다
+- Pretendard 폰트 등록은 App bootstrap에서 1회 수행
+- `DesignText` lineHeight는 SwiftUI `.lineSpacing` 기반 근사 적용(고정 line box 미보장)
+
### SharedLogger
- 전역 `Logger.shared` OSLog facade
diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md
index 340b97b..9077e4e 100644
--- a/docs/CONVENTIONS.md
+++ b/docs/CONVENTIONS.md
@@ -14,6 +14,7 @@
- Scene 통신은 delegate bubble-up
- SharedUtils는 pure Foundation / App metadata 만. UI·I/O·Domain 금지
- CoreNetwork uses SharedLogger (`.network`) and never contains service-specific domain flows
+- SharedDesignSystem은 Feature에서 `Color.ds` / `Font.ds` / `CGFloat.ds` / `TextStyle.ds` 와 공통 컴포넌트만 사용. Primitive 직접 참조 금지
## Schemes