Skip to content

Commit b0ea489

Browse files
authored
♻️ [Refactor] UMCFoundation 정비: 하드닝·공용 유틸 이식·FlexibleDecoding 통일·xcconfig 시크릿 주입 (#938)
* docs: 이슈 생성 규칙 및 Type/우선순위/Projects 자동화 문서화 - git-workflow.md: `이슈 생성 규칙` 섹션 추가 (템플릿·라벨·이슈 Type 매핑표) - Type은 생성 직후 REST PATCH로 설정 (gh 2.83.1 `--type` 미지원) - Priority/Projects는 project 스코프 필요 — iOS 보드 #3, 한글 `우선순위` 필드 안내 - CLAUDE.md: 레퍼런스 인덱스의 Git Workflow 항목에 이슈 규칙 반영 * docs: 이슈 생성 시 보드·우선순위·네이티브 Priority/Effort 기본 설정 명시 - 이슈 생성 = 제목/라벨/Type + 보드#3·우선순위 + 네이티브 Priority·Effort 기본 채움 - 보드 `우선순위`(한글) ↔ 네이티브 `Priority`(영어)를 같은 우선순위 레벨로 일치 규칙 추가 - 네이티브 이슈 Fields(베타) 설정 방식(setIssueFieldValue) 및 스킬 4-4·4-5 참조 반영 * 📄 Docs: Tuist 파일별 이관 매핑 문서 추가 레거시 AppProduct 766개 Swift 파일을 UMCApp Tuist 모듈로 옮길 때의 파일별 목적지를 정리. 확정 필요 항목은 개발 에이전트 분석으로 목적지 확정 후 반영. * ♻️ [Refactor] UMCFoundation 로케일 버그·포매터 캐싱·FoundationModels 가드·헤더 정리 * ✨ [Feat] UMCFoundation 공용 유틸 이식(Date+KST/Calendar·Error+Cancellation·String) + 테스트 * ✨ [Feat] xcconfig 기반 시크릿/환경(BASE_URL Debug·Release) 주입 * ♻️ [Refactor] 앱 Notification.Name 정의 단일 파일(Notification+Names) 통합 * ♻️ [Refactor] FlexibleDecoding 단일 소스 통일(절삭 정규화·swallow 통일)·Date 중복 제거 - Activity 내부 ext + Notice 5개 private ext 삭제 → UMCFoundation 정규 헬퍼로 통일 - 숫자→String 은 정수 절삭('1'), optional-string 은 bad-type swallow 로 정규화 - CLAUDE.md 규칙 #3 canonical 네이밍(decodeIntFlexible*) 정합 - Date+HourMinutes 중복 제거 → Foundation toHourMinutes() 사용
1 parent f296fa3 commit b0ea489

41 files changed

Lines changed: 974 additions & 828 deletions

Some content is hidden

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

UMCApp/Core/Foundation/Sources/Enums/MemberStatus.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// MemberStatus.swift
3-
// CoreEnum
3+
// UMCFoundation
44
//
55

66
import Foundation
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// Error+Cancellation.swift
3+
// UMCFoundation
4+
//
5+
// Created by euijjang97 on 7/5/26.
6+
//
7+
// Swift Concurrency 취소를 판별하기 위한 공용 헬퍼.
8+
// Task 취소는 실패가 아니므로 에러 Alert/로깅 대상에서 제외해야 합니다.
9+
//
10+
11+
import Foundation
12+
13+
public extension Error {
14+
15+
/// 이 에러가 "취소"에 해당하는지 여부.
16+
///
17+
/// `CancellationError`(구조적 동시성 취소)와 `URLError(.cancelled)`(네트워크 요청 취소)를
18+
/// 모두 취소로 간주합니다. ViewModel에서 취소된 작업을 사용자 에러로 표시하지 않도록
19+
/// 분기할 때 사용합니다.
20+
///
21+
/// ```swift
22+
/// do {
23+
/// try await useCase.execute()
24+
/// } catch {
25+
/// guard !error.isCancellation else { return }
26+
/// errorHandler.handle(error, context: ...)
27+
/// }
28+
/// ```
29+
var isCancellation: Bool {
30+
if self is CancellationError {
31+
return true
32+
}
33+
if let urlError = self as? URLError, urlError.code == .cancelled {
34+
return true
35+
}
36+
return false
37+
}
38+
}

UMCApp/Core/Foundation/Sources/Error/Handler/ErrorHandler.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
//
77

88
import Foundation
9-
import FoundationModels
109
import os.log
10+
#if canImport(FoundationModels)
11+
import FoundationModels
12+
#endif
1113

1214
/// 앱 전역에서 발생하는 에러를 중앙 집중식으로 처리하는 핸들러.
1315
///
@@ -193,9 +195,11 @@ public final class ErrorHandler {
193195
}
194196

195197
// 9. LanguageModelSession.GenerationError → DomainError 변환
198+
#if canImport(FoundationModels)
196199
if let generationError = error as? LanguageModelSession.GenerationError {
197200
return .domain(.custom(message: describeGenerationError(generationError)))
198201
}
202+
#endif
199203

200204
// 10. 알 수 없는 에러
201205
return .unknown(message: error.localizedDescription)
@@ -212,6 +216,7 @@ public final class ErrorHandler {
212216
}
213217
}
214218

219+
#if canImport(FoundationModels)
215220
private func describeGenerationError(_ error: LanguageModelSession.GenerationError) -> String {
216221
switch error {
217222
case .exceededContextWindowSize:
@@ -236,6 +241,7 @@ public final class ErrorHandler {
236241
return "AI 처리 중 문제가 발생했습니다."
237242
}
238243
}
244+
#endif
239245

240246
private func describeDecodingError(_ error: DecodingError) -> String {
241247
switch error {

UMCApp/Core/Foundation/Sources/Error/Handler/Notification+Error.swift

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// Date+Calendar.swift
3+
// UMCFoundation
4+
//
5+
// Created by euijjang97 on 7/5/26.
6+
//
7+
// 캘린더 UI(월 그리드, 요일 헤더 등) 구성에 필요한 날짜 계산 유틸리티.
8+
// KST 기준 그레고리력(`Calendar.kstGregorian`)을 사용합니다.
9+
//
10+
11+
import Foundation
12+
13+
public extension Date {
14+
15+
// MARK: - Month
16+
17+
/// KST 기준 해당 월의 첫째 날 00:00 시각.
18+
var startOfMonth: Date {
19+
let calendar = Calendar.kstGregorian
20+
let components = calendar.dateComponents([.year, .month], from: self)
21+
return calendar.date(from: components) ?? self
22+
}
23+
24+
/// KST 기준 해당 월의 마지막 날 00:00 시각.
25+
var endOfMonth: Date {
26+
let calendar = Calendar.kstGregorian
27+
let start = startOfMonth
28+
return calendar.date(byAdding: DateComponents(month: 1, day: -1), to: start) ?? self
29+
}
30+
31+
/// KST 기준 해당 월에 포함된 모든 날짜(각 날의 00:00)를 순서대로 반환합니다.
32+
///
33+
/// 월간 캘린더 그리드를 그릴 때 사용합니다.
34+
func datesInMonth() -> [Date] {
35+
let calendar = Calendar.kstGregorian
36+
guard let range = calendar.range(of: .day, in: .month, for: self) else {
37+
return []
38+
}
39+
40+
let start = startOfMonth
41+
return range.compactMap { day in
42+
calendar.date(byAdding: .day, value: day - 1, to: start)
43+
}
44+
}
45+
46+
// MARK: - Comparison
47+
48+
/// KST 기준 같은 날(연·월·일)인지 여부.
49+
func isSameDay(_ other: Date) -> Bool {
50+
Calendar.kstGregorian.isDate(self, inSameDayAs: other)
51+
}
52+
}
53+
54+
// MARK: - Weekday Symbols
55+
56+
public extension Date {
57+
58+
/// 요일 심볼 표기 스타일.
59+
enum WeekdaySymbolStyle {
60+
/// 짧은 형태 (예: "일", "월")
61+
case short
62+
/// 매우 짧은 형태 (한국어에서는 `short`와 동일하게 동작)
63+
case veryShort
64+
}
65+
66+
/// 한국어 로케일 기준 요일 심볼 배열(일요일 시작)을 반환합니다.
67+
///
68+
/// - Parameter style: 심볼 표기 스타일.
69+
/// - Returns: `["일", "월", "화", "수", "목", "금", "토"]` 형태의 배열.
70+
static func weekDaySymbols(style: WeekdaySymbolStyle = .short) -> [String] {
71+
var calendar = Calendar(identifier: .gregorian)
72+
calendar.locale = Locale(identifier: "ko_KR")
73+
74+
switch style {
75+
case .short:
76+
return calendar.shortWeekdaySymbols
77+
case .veryShort:
78+
return calendar.veryShortWeekdaySymbols
79+
}
80+
}
81+
}

UMCApp/Core/Foundation/Sources/Extensions/DateFormatter.swift renamed to UMCApp/Core/Foundation/Sources/Extensions/Date+Formatting.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ private extension Date {
8888

8989
static let hourMinuteFormatter: Foundation.DateFormatter = {
9090
let formatter = Foundation.DateFormatter()
91-
formatter.locale = Locale(identifier: "ko_KR_POSIX")
91+
formatter.locale = Locale(identifier: "en_US_POSIX")
9292
formatter.timeZone = .current
9393
formatter.dateFormat = "HH:mm"
9494
return formatter
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// Date+KST.swift
3+
// UMCFoundation
4+
//
5+
// Created by euijjang97 on 7/5/26.
6+
//
7+
// KST(Asia/Seoul) 타임존 경계를 다루기 위한 유틸리티.
8+
// 서버는 UTC 기준으로 시각을 저장하지만, 화면 표시와 "하루 종일" 판정은
9+
// 한국 로컬 기준이어야 하므로 KST 경계 계산을 이 파일로 모읍니다.
10+
//
11+
12+
import Foundation
13+
14+
// MARK: - TimeZone / Calendar
15+
16+
public extension TimeZone {
17+
/// 한국 표준시(Asia/Seoul). 식별자 조회 실패 시 현재 타임존으로 폴백합니다.
18+
static let kst: TimeZone = .init(identifier: "Asia/Seoul") ?? .current
19+
}
20+
21+
public extension Calendar {
22+
/// KST 기준 그레고리력 캘린더.
23+
static let kstGregorian: Calendar = {
24+
var calendar = Calendar(identifier: .gregorian)
25+
calendar.timeZone = .kst
26+
return calendar
27+
}()
28+
}
29+
30+
// MARK: - Date + KST 경계
31+
32+
public extension Date {
33+
34+
/// KST 기준 그 날의 시작(00:00:00) 시각.
35+
var kstStartOfDay: Date {
36+
Calendar.kstGregorian.startOfDay(for: self)
37+
}
38+
39+
/// KST 기준 그 날의 끝(다음 날 00:00:00 직전) 시각.
40+
var kstEndOfDay: Date {
41+
let calendar = Calendar.kstGregorian
42+
let start = calendar.startOfDay(for: self)
43+
return calendar.date(byAdding: DateComponents(day: 1, second: -1), to: start) ?? self
44+
}
45+
46+
/// KST 기준 그 날의 시작 시각을 UTC ISO8601 문자열로 반환합니다.
47+
///
48+
/// 서버에 "해당 날짜 00:00(KST)" 이후를 조회하는 파라미터로 사용합니다.
49+
var kstStartOfDayUTCISO8601: String {
50+
ServerDateTimeConverter.toUTCDateTimeString(kstStartOfDay)
51+
}
52+
53+
/// KST 기준 그 날의 끝 시각을 UTC ISO8601 문자열로 반환합니다.
54+
var kstEndOfDayUTCISO8601: String {
55+
ServerDateTimeConverter.toUTCDateTimeString(kstEndOfDay)
56+
}
57+
}
58+
59+
// MARK: - ClosedRange<Date>
60+
61+
public extension ClosedRange where Bound == Date {
62+
63+
/// KST 기준으로 이 구간이 하루 전체(00:00 ~ 23:59:59)를 덮는지 여부.
64+
///
65+
/// 종일 일정(all-day) 판정에 사용합니다.
66+
var isAllDayInKST: Bool {
67+
lowerBound == lowerBound.kstStartOfDay && upperBound == lowerBound.kstEndOfDay
68+
}
69+
}

0 commit comments

Comments
 (0)