- 설정 화면 신규(첫 화면 헤더 톱니 진입): 화면 모드(시스템/라이트/다크) 선택, '기간 지난 데이터 삭제'로 만료된 기록 일괄 정리(매장 무관, 확인 후 실행) - Theme 색상을 라이트/다크 자동 전환 동적 색으로 전환, 기본값은 기존과 동일한 다크 - 유통기한에 시각(시:분) 선택 입력: 날짜 선택 후 '시간 추가' 버튼으로만 노출돼 기본 사용성(사진→날짜→저장)은 그대로 유지, 유제품 등 필요 시에만 사용 - ProductRecord.hasTime 플래그 추가(기본 false, 라이트웨이트 마이그레이션), 시각 포함 기록은 날짜 상세 그리드에 시각 배지 표시 - .gitignore에 Xcode 빌드 산출물/파생 데이터/SwiftPM 상태 추가해 상태 오염 방지 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J2HQuA6RPtkh45QTMN83i2
97 lines
3.0 KiB
Swift
97 lines
3.0 KiB
Swift
import SwiftUI
|
|
import SwiftData
|
|
|
|
@main
|
|
struct ExpirannerApp: App {
|
|
private let container: ModelContainer
|
|
|
|
init() {
|
|
let schema = Schema([Store.self, ProductRecord.self])
|
|
do {
|
|
container = try ModelContainer(for: schema)
|
|
} catch {
|
|
// 이전 버전 저장소와 호환되지 않으면 저장소를 초기화하고 다시 시도한다.
|
|
print("저장소 열기 실패, 데이터를 재설정합니다: \(error)")
|
|
Self.removeDefaultStoreFiles()
|
|
do {
|
|
container = try ModelContainer(for: schema)
|
|
} catch {
|
|
fatalError("SwiftData 저장소를 만들 수 없습니다: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func removeDefaultStoreFiles() {
|
|
let dir = URL.applicationSupportDirectory
|
|
for name in ["default.store", "default.store-shm", "default.store-wal"] {
|
|
try? FileManager.default.removeItem(at: dir.appending(path: name))
|
|
}
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
RootView()
|
|
.tint(Theme.accent)
|
|
}
|
|
.modelContainer(container)
|
|
}
|
|
}
|
|
|
|
/// 시스템 런치 스크린에서 자연스럽게 이어지는 인앱 스플래시.
|
|
private struct RootView: View {
|
|
@AppStorage(appearanceStorageKey) private var appearanceRaw = AppearanceMode.dark.rawValue
|
|
@State private var showSplash = true
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
StoreListView()
|
|
if showSplash {
|
|
SplashView()
|
|
.transition(.opacity)
|
|
.zIndex(1)
|
|
}
|
|
}
|
|
.preferredColorScheme(AppearanceMode(rawValue: appearanceRaw)?.colorScheme)
|
|
.task {
|
|
try? await Task.sleep(for: .seconds(1.0))
|
|
withAnimation(.easeOut(duration: 0.4)) {
|
|
showSplash = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct SplashView: View {
|
|
@State private var appeared = false
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Theme.bg.ignoresSafeArea()
|
|
VStack(spacing: 18) {
|
|
Image("LaunchIcon")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 110, height: 110)
|
|
.scaleEffect(appeared ? 1 : 0.9)
|
|
.shadow(color: Theme.accent.opacity(0.25), radius: 30)
|
|
VStack(spacing: 6) {
|
|
Text("Expiranner")
|
|
.font(.system(size: 26, weight: .bold, design: .rounded))
|
|
.foregroundStyle(Theme.textPrimary)
|
|
Text("유통기한 점검")
|
|
.font(.footnote.weight(.semibold))
|
|
.foregroundStyle(Theme.accent)
|
|
.kerning(1)
|
|
}
|
|
.opacity(appeared ? 1 : 0)
|
|
.offset(y: appeared ? 0 : 8)
|
|
}
|
|
}
|
|
.onAppear {
|
|
withAnimation(.spring(duration: 0.6)) {
|
|
appeared = true
|
|
}
|
|
}
|
|
}
|
|
}
|