Skip to content

Commit cce412d

Browse files
committed
Merge branch 'develop' into testflight
2 parents 04e677f + 1d000da commit cce412d

141 files changed

Lines changed: 5365 additions & 941 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Keychy/Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ GEM
169169
httpclient (2.9.0)
170170
mutex_m
171171
jmespath (1.6.2)
172-
json (2.18.1)
172+
json (2.19.2)
173173
jwt (2.10.2)
174174
base64
175175
logger (1.7.0)

Keychy/Keychy.xcodeproj/project.pbxproj

Lines changed: 114 additions & 10 deletions
Large diffs are not rendered by default.

Keychy/Keychy/App/AppDelegate.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class AppDelegate: NSObject, UIApplicationDelegate {
5555
// Firebase 초기화
5656
FirebaseApp.configure()
5757

58+
// 빌드 환경 판별 (Debug/TestFlight vs App Store)
59+
Task { await BuildEnvironment.configure() }
60+
5861
// TabBar 외형 설정
5962
configureTabBarAppearance()
6063

@@ -125,8 +128,25 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
125128
) {
126129
let userInfo = response.notification.request.content.userInfo
127130

128-
// postOfficeId 추출해서 화면 이동
129-
if let postOfficeId = userInfo["postOfficeId"] as? String {
131+
// 키치 소식 푸시 (공지 / 키링 배포)
132+
if let type = userInfo["type"] as? String {
133+
switch type {
134+
case "announcement":
135+
let destination = userInfo["deepLink"] as? String ?? ""
136+
DeepLinkManager.shared.handleNewsPush(destination: destination)
137+
case "keyringEvent":
138+
if let postOfficeId = userInfo["postOfficeId"] as? String, !postOfficeId.isEmpty {
139+
// postOfficeId가 있으면 기존 collect 수령 플로우로 연결
140+
DeepLinkManager.shared.handleDeepLink(postOfficeId: postOfficeId, type: .collect)
141+
} else {
142+
DeepLinkManager.shared.handleNewsPush(destination: "보관함")
143+
}
144+
default:
145+
break
146+
}
147+
}
148+
// 기존 선물 알림 (postOfficeId 기반)
149+
else if let postOfficeId = userInfo["postOfficeId"] as? String {
130150
DeepLinkManager.shared.handleDeepLink(postOfficeId: postOfficeId, type: .notification)
131151
}
132152

Keychy/Keychy/App/KeychyApp.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import SwiftUI
9+
import Nuke
910

1011
/// Keychy 앱의 진입점
1112
/// - AppDelegate를 통해 Firebase, Push 알림 등의 초기 설정 수행
@@ -17,6 +18,18 @@ struct KeychyApp: App {
1718
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
1819

1920
init() {
21+
// Nuke DataCache 활성화 (HTTP 헤더 무시, 무조건 디스크 저장)
22+
ImagePipeline.shared = ImagePipeline(configuration: .withDataCache)
23+
24+
// 기존 StorageManager 디스크 캐시 → Nuke DataCache 마이그레이션 (1회성)
25+
let migrationKey = "didMigrateToNukeDataCache"
26+
if !UserDefaults.standard.bool(forKey: migrationKey) {
27+
let oldCacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
28+
.appendingPathComponent("StorageImageCache")
29+
try? FileManager.default.removeItem(at: oldCacheDir)
30+
UserDefaults.standard.set(true, forKey: migrationKey)
31+
}
32+
2033
// 네트워크 모니터링 시작
2134
NetworkManager.shared.startMonitoring()
2235

Keychy/Keychy/CommonModels/Keyring/Keyring.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ struct Keyring: Identifiable, Equatable, Hashable {
3535
var senderId: String?
3636
var receivedAt: Date?
3737
var hookOffsetY: CGFloat? // 템플릿에서 받아온 바디 연결 지점 Y 오프셋 (nil이면 0.0 사용)
38+
var isGyroscope: Bool // 자이로 인터랙션 사용 여부 (렌티큘러 등)
39+
var shimmerColorId: String? // 시머(광택) 색상 ID (nil이면 "silver" 기본값)
40+
var borderColorId: String? // 테두리 색상 ID (nil이면 shimmerColorId와 동일)
3841

3942
// MARK: - Firestore 변환
4043
func toDictionary() -> [String: Any] {
@@ -54,7 +57,8 @@ struct Keyring: Identifiable, Equatable, Hashable {
5457
"isPackaged": isPackaged,
5558
"isPublished": isPublished,
5659
"chainLength": chainLength,
57-
"isNew": isNew
60+
"isNew": isNew,
61+
"isGyroscope": isGyroscope
5862
]
5963

6064
// Optional 필드 처리
@@ -76,6 +80,12 @@ struct Keyring: Identifiable, Equatable, Hashable {
7680
if let hookOffsetY = hookOffsetY {
7781
dict["hookOffsetY"] = hookOffsetY
7882
}
83+
if let shimmerColorId = shimmerColorId {
84+
dict["shimmerColorId"] = shimmerColorId
85+
}
86+
if let borderColorId = borderColorId {
87+
dict["borderColorId"] = borderColorId
88+
}
7989

8090
return dict
8191
}
@@ -118,6 +128,9 @@ struct Keyring: Identifiable, Equatable, Hashable {
118128
self.chainLength = data["chainLength"] as? Int ?? 5
119129
self.isNew = data["isNew"] as? Bool ?? true
120130
self.hookOffsetY = data["hookOffsetY"] as? CGFloat
131+
self.isGyroscope = data["isGyroscope"] as? Bool ?? false // 기존 키링은 모두 false
132+
self.shimmerColorId = data["shimmerColorId"] as? String // nil이면 silver 기본값
133+
self.borderColorId = data["borderColorId"] as? String // nil이면 shimmerColorId 사용
121134

122135
// Optional 필드
123136
self.memo = data["memo"] as? String
@@ -150,7 +163,10 @@ struct Keyring: Identifiable, Equatable, Hashable {
150163
isNew: Bool = true,
151164
senderId: String? = nil,
152165
receivedAt: Date? = nil,
153-
hookOffsetY: CGFloat? = nil
166+
hookOffsetY: CGFloat? = nil,
167+
isGyroscope: Bool = false,
168+
shimmerColorId: String? = nil,
169+
borderColorId: String? = nil
154170
) {
155171
self.name = name
156172
self.bodyImage = bodyImage
@@ -174,5 +190,8 @@ struct Keyring: Identifiable, Equatable, Hashable {
174190
self.senderId = senderId
175191
self.receivedAt = receivedAt
176192
self.hookOffsetY = hookOffsetY
193+
self.isGyroscope = isGyroscope
194+
self.shimmerColorId = shimmerColorId
195+
self.borderColorId = borderColorId
177196
}
178197
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//
2+
// KeyringAppearanceColor.swift
3+
// Keychy
4+
//
5+
// Created by 길지훈 on 2026-04-01.
6+
//
7+
8+
import SwiftUI
9+
10+
// MARK: - 시머/테두리 색상 통합 모델
11+
/// 프리셋(KeyringStylePreset) + 프리셋 모드를 유지한 커스텀 틴트를 표현
12+
/// - `.preset(.silver)` → 메탈릭 기본색 (mode=0)
13+
/// - `.customTint(mode: .matrix, r, g, b)` → 매트릭스 모드(mode=4) + 커스텀 색상
14+
enum KeyringAppearanceColor: Equatable, Identifiable {
15+
case preset(KeyringStylePreset)
16+
case customTint(mode: KeyringStylePreset, r: Float, g: Float, b: Float)
17+
18+
var id: String { firestoreId }
19+
20+
// MARK: - 현재 활성 프리셋 (모드 결정용)
21+
/// `.preset(.matrix)` → `.matrix`, `.customTint(mode: .matrix, ...)` → `.matrix`
22+
var activePreset: KeyringStylePreset {
23+
switch self {
24+
case .preset(let p): return p
25+
case .customTint(let mode, _, _, _): return mode
26+
}
27+
}
28+
29+
// MARK: - Firestore 저장 ID
30+
/// 프리셋: "silver", 커스텀 틴트: "silver_D2D6E0"
31+
var firestoreId: String {
32+
switch self {
33+
case .preset(let p): return p.rawValue
34+
case .customTint(let mode, let r, let g, let b):
35+
let ir = Int(min(max(r, 0), 1) * 255)
36+
let ig = Int(min(max(g, 0), 1) * 255)
37+
let ib = Int(min(max(b, 0), 1) * 255)
38+
return String(format: "%@_%02X%02X%02X", mode.rawValue, ir, ig, ib)
39+
}
40+
}
41+
42+
// MARK: - Firestore에서 복원
43+
static func from(id: String?) -> KeyringAppearanceColor {
44+
guard let id else { return .preset(.silver) }
45+
46+
// "preset_RRGGBB" 형식 (예: "silver_D2D6E0", "matrix_00D94D")
47+
for preset in KeyringStylePreset.allCases {
48+
let prefix = preset.rawValue + "_"
49+
if id.hasPrefix(prefix) {
50+
let hex = String(id.dropFirst(prefix.count))
51+
guard hex.count == 6,
52+
let value = UInt32(hex, radix: 16) else { continue }
53+
let r = Float((value >> 16) & 0xFF) / 255.0
54+
let g = Float((value >> 8) & 0xFF) / 255.0
55+
let b = Float(value & 0xFF) / 255.0
56+
return .customTint(mode: preset, r: r, g: g, b: b)
57+
}
58+
}
59+
60+
// 프리셋 매칭 (정확한 rawValue)
61+
if let p = KeyringStylePreset(rawValue: id) {
62+
return .preset(p)
63+
}
64+
65+
// 레거시: "custom_RRGGBB" → 메탈릭 틴트로 변환
66+
if id.hasPrefix("custom_") {
67+
let hex = String(id.dropFirst(7))
68+
guard hex.count == 6,
69+
let value = UInt32(hex, radix: 16) else {
70+
return .preset(.silver)
71+
}
72+
let r = Float((value >> 16) & 0xFF) / 255.0
73+
let g = Float((value >> 8) & 0xFF) / 255.0
74+
let b = Float(value & 0xFF) / 255.0
75+
return .customTint(mode: .silver, r: r, g: g, b: b)
76+
}
77+
78+
// 레거시 하위 호환
79+
let legacyMap: [String: KeyringStylePreset] = [
80+
"gold": .silver,
81+
"roseGold": .silver,
82+
"goldHolo": .silver,
83+
"goldDuo": .silver,
84+
"roseHolo": .silver,
85+
"roseGlitter": .silver,
86+
"midnightHolo": .silver,
87+
"midnightPearl": .silver,
88+
]
89+
if let legacy = legacyMap[id] {
90+
return .preset(legacy)
91+
}
92+
93+
// "glitter_RRGGBB" 레거시 → 메탈릭 틴트로 변환
94+
if id.hasPrefix("glitter_") {
95+
let hex = String(id.dropFirst(8))
96+
guard hex.count == 6,
97+
let value = UInt32(hex, radix: 16) else {
98+
return .preset(.silver)
99+
}
100+
let r = Float((value >> 16) & 0xFF) / 255.0
101+
let g = Float((value >> 8) & 0xFF) / 255.0
102+
let b = Float(value & 0xFF) / 255.0
103+
return .customTint(mode: .silver, r: r, g: g, b: b)
104+
}
105+
106+
return .preset(.silver)
107+
}
108+
109+
// MARK: - 셰이더 Uniform 값
110+
var shaderColor: (r: Float, g: Float, b: Float) {
111+
switch self {
112+
case .preset(let p): return p.shaderColor
113+
case .customTint(_, let r, let g, let b): return (r, g, b)
114+
}
115+
}
116+
117+
/// 셰이더 mode — 프리셋 모드를 따름 (0=메탈릭, 1=홀로그램, 4=매트릭스, 5=우주)
118+
var shaderMode: Float {
119+
activePreset.shaderMode
120+
}
121+
122+
// MARK: - UI 미리보기
123+
var previewColor: UIColor {
124+
let c = shaderColor
125+
return UIColor(red: CGFloat(c.r), green: CGFloat(c.g), blue: CGFloat(c.b), alpha: 1.0)
126+
}
127+
128+
var displayName: String {
129+
activePreset.displayName
130+
}
131+
132+
/// 커스텀 틴트가 적용되었는지 여부
133+
var isCustomTint: Bool {
134+
if case .customTint = self { return true }
135+
return false
136+
}
137+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// KeyringStylePreset.swift
3+
// Keychy
4+
//
5+
// Created by 길지훈 on 2026-04-01.
6+
//
7+
8+
import SwiftUI
9+
10+
// MARK: - 키링 스타일 프리셋
11+
/// 렌티큘러 셰이더 스타일 프리셋 (시머 + 테두리 공용)
12+
/// - mode 0 = 메탈릭, 1 = 홀로그램, 2 = 얼룩, 3 = 펄스, 4 = 모자이크, 5 = 글리터
13+
enum KeyringStylePreset: String, CaseIterable, Identifiable {
14+
case silver // 메탈릭 — 쿨 실버
15+
case hologram // 홀로그램 — 순수 무지개
16+
case liquid // 얼룩 — 크롬 반사
17+
case pulse // 펄스 — 레이더 파동
18+
case matrix // 모자이크 — 엠보스 타일
19+
case cosmos // 글리터 — 반짝이 알갱이
20+
21+
var id: String { rawValue }
22+
23+
var displayName: String {
24+
switch self {
25+
case .silver: return "메탈릭"
26+
case .hologram: return "홀로그램"
27+
case .liquid: return "얼룩"
28+
case .pulse: return "펄스"
29+
case .matrix: return "모자이크"
30+
case .cosmos: return "글리터"
31+
}
32+
}
33+
34+
/// 셰이더 mode 값
35+
var shaderMode: Float {
36+
switch self {
37+
case .silver: return 0.0
38+
case .hologram: return 1.0
39+
case .liquid: return 2.0
40+
case .pulse: return 3.0
41+
case .matrix: return 4.0
42+
case .cosmos: return 5.0
43+
}
44+
}
45+
46+
/// 셰이더에 전달할 틴트 RGB
47+
var shaderColor: (r: Float, g: Float, b: Float) {
48+
switch self {
49+
case .silver: return (0.82, 0.84, 0.88)
50+
case .hologram: return (1.0, 1.0, 1.0)
51+
case .liquid: return (0.90, 0.92, 0.95) // 얼룩 크롬 실버
52+
case .pulse: return (0.45, 0.55, 0.90) // 펄스 쿨 블루
53+
case .matrix: return (0.0, 0.85, 0.30) // 모자이크 그린
54+
case .cosmos: return (0.08, 0.10, 0.28) // 글리터 네이비
55+
}
56+
}
57+
58+
/// 셀렉터 UI 미리보기 색상
59+
var previewColor: UIColor {
60+
let c = shaderColor
61+
return UIColor(red: CGFloat(c.r), green: CGFloat(c.g), blue: CGFloat(c.b), alpha: 1.0)
62+
}
63+
64+
/// 광택 효과 섹션에 표시할 프리셋
65+
static var shimmerPresets: [KeyringStylePreset] {
66+
[.silver, .hologram, .liquid, .matrix]
67+
}
68+
69+
/// 테두리 섹션에 표시할 프리셋
70+
static var borderPresets: [KeyringStylePreset] {
71+
[.silver, .hologram, .pulse, .cosmos]
72+
}
73+
}

Keychy/Keychy/CommonModels/Keyring/WidgetKeyring.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,23 @@ struct WidgetKeyring: Codable, Identifiable, Hashable {
1313
let name: String // 키링 이름
1414
let imagePath: String // App Group 내 이미지 경로
1515
let createdAt: Date // 생성일 (위젯 목록 정렬용)
16+
let isGyroscope: Bool // 렌티큘러 키링 여부
1617

17-
// 기존 데이터 호환용 (createdAt 없는 경우 .distantPast로 처리)
18+
// 기존 데이터 호환용 (createdAt/isGyroscope 없는 경우 기본값 처리)
1819
init(from decoder: Decoder) throws {
1920
let container = try decoder.container(keyedBy: CodingKeys.self)
2021
id = try container.decode(String.self, forKey: .id)
2122
name = try container.decode(String.self, forKey: .name)
2223
imagePath = try container.decode(String.self, forKey: .imagePath)
2324
createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) ?? .distantPast
25+
isGyroscope = try container.decodeIfPresent(Bool.self, forKey: .isGyroscope) ?? false
2426
}
2527

26-
init(id: String, name: String, imagePath: String, createdAt: Date) {
28+
init(id: String, name: String, imagePath: String, createdAt: Date, isGyroscope: Bool = false) {
2729
self.id = id
2830
self.name = name
2931
self.imagePath = imagePath
3032
self.createdAt = createdAt
33+
self.isGyroscope = isGyroscope
3134
}
3235
}

0 commit comments

Comments
 (0)