mycode/myApp/HaruDanim/Shared/Formatters.swift
songyc macbook c18e3c7c6f fix(review): 전체 코드 리뷰 1차 — 데이터 복구·통계 표시·기록 편집 결함 수정
'처음 보는 리뷰어' 관점 전체 검증(핵심 계산·데이터 계층·기록/통계 완료,
나머지 영역 진행 중)에서 확인된 결함 수정. 핵심 계산부(DayMath·
QuestProgress·Models)는 발견 0건.

[M] DataStore: 스토어 손상 백업 복구 직후 legacy 이관 가드(타깃 없음)가
    다시 열려 구 샌드박스 스토어(영구 잔존)가 부활 — 수개월 전 데이터로
    조용한 롤백. App Group 플래그(migration.legacyStoreImported)로 평생
    1회 보장 (§5.1 문서화)
[M] 통계 시리즈가 행동/꼬리표 이름 키 — 동명 2개면 차트가 한 선으로 합쳐
    지고 Identifiable id 충돌, 동명 꼬리표는 합산. Format.disambiguated
    (이름 (2) 형식)로 통계 탭·통계 내보내기·⑤ 위젯 3표면 통일, 꼬리표
    집계는 identity 키로 재작성 (수치는 기존과 동일, 표시·범례만 구분)
[L] SessionAlertManager: 시작→즉시 종료 연타 시 조회(await)~추가 사이
    경합으로 종료된 세션의 장시간 알림이 잔존 — 세대 카운터로 마지막
    호출만 확정
[L] 세션 편집기: 분 절사 값을 무조건 덮어써 1분 미만 세션이 메모만
    고쳐도 0초로 파괴, 시작=종료 0길이 기록은 저장돼도 어디에도 안 보임
    — 안 움직인 필드는 원본 시각(초) 보존 + 0길이 저장 차단(문구 갱신)
[L] 타임테이블 시간축: 하루 시작이 정시가 아니면(06:30) 라벨이 시만
    표기해 최대 59분 어긋남 — 분 성분 포함(HH:mm), 화면·내보내기 동일
[L] 기록·통계 필터: 제외했던 행동을 삭제하면 잔존 ID로 칩·내보내기
    필터 문구가 허위 활성 — 실재 행동 기준으로 판정
[L] 목표 편집: 시작일을 종료일 뒤로 옮기면 종료<시작 저장 가능(DatePicker
    in: 은 표시 제약만) — 저장 시 정규화

+ QuestEditor·TagViews·GoalViews·인텐트·WidgetSupport 정독 — 추가 발견 없음
+ §15-11 서브 에이전트 금지 명문화 (사용자 지시)

검증: Debug/Store 빌드, 카탈로그 missing/stale 0 (새 문구 2키 en/ja)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 21:05:16 +09:00

85 lines
3.3 KiB
Swift

//
// Formatters.swift
// Haru_Danim
//
import Foundation
enum Format {
/// 1:23:45 23:45 ( )
static func timer(_ interval: TimeInterval) -> String {
let total = Int(interval.rounded(.down))
let h = total / 3600
let m = (total % 3600) / 60
let s = total % 60
if h > 0 { return String(format: "%d:%02d:%02d", h, m, s) }
return String(format: "%d:%02d", m, s)
}
/// "2 30" / "45" / "30"
static func durationShort(_ interval: TimeInterval) -> String {
let total = Int(interval.rounded())
let h = total / 3600
let m = (total % 3600) / 60
if h > 0 {
return m > 0
? String(localized: "\(h)시간 \(m)")
: String(localized: "\(h)시간")
}
if m > 0 { return String(localized: "\(m)") }
return String(localized: "\(total)")
}
static func weekdayShort(_ weekday: Int) -> String {
let names = [
String(localized: "", comment: "요일 축약(일요일)"),
String(localized: "", comment: "요일 축약(월요일)"),
String(localized: "", comment: "요일 축약(화요일)"),
String(localized: "", comment: "요일 축약(수요일)"),
String(localized: "", comment: "요일 축약(목요일)"),
String(localized: "", comment: "요일 축약(금요일)"),
String(localized: "", comment: "요일 축약(토요일)"),
]
let index = (weekday - 1) % 7
return names[max(0, index)]
}
static func shortDate(_ date: Date) -> String {
date.formatted(.dateTime.month(.defaultDigits).day())
}
static func fullDate(_ date: Date) -> String {
date.formatted(.dateTime.year().month().day().weekday(.short))
}
static func time(_ date: Date) -> String {
date.formatted(.dateTime.hour().minute())
}
static func percent(_ ratio: Double) -> String {
"\(Int((ratio * 100).rounded()))%"
}
/// ("3.5"). String(format:)
/// · '' .
static func countAverage(_ value: Double) -> String {
let rounded = (value * 10).rounded() / 10
return String(localized: "\(rounded.formatted(.number.precision(.fractionLength(1))))",
comment: "횟수 평균 (소수 1자리)")
}
/// ··Identifiable id
/// ( · · ) /
/// id . ,
/// " (2)", " (3)" ( .
/// · )
static func disambiguated(_ names: [String]) -> [String] {
var counts: [String: Int] = [:]
return names.map { name in
let n = (counts[name] ?? 0) + 1
counts[name] = n
return n == 1 ? name : "\(name) (\(n))"
}
}
}