- 매장 선택 → 연/월 → 월 캘린더(날짜별 제품 사진 썸네일) → 날짜 상세 플로우로 전면 재작성 - 스캔 플로우: 촬영 → 재촬영/사용 → 그 달 날짜만 선택 → 저장 후 카메라 자동 복귀 (손전등 지원) - 날짜 상세 다중 선택 삭제 (전체 선택/해제 + 일괄 삭제 확인) - 다크 전용 민트 테마, SwiftData 모델(Store ↔ ProductRecord, 원본+썸네일 분리 저장) - 밝은 민트 앱 아이콘(SVG 원본 포함) + 런치 스크린/인앱 스플래시 - CLAUDE.md 새 구조 기준으로 재작성 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
96 lines
2.9 KiB
Swift
96 lines
2.9 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()
|
|
.preferredColorScheme(.dark)
|
|
.tint(Theme.accent)
|
|
}
|
|
.modelContainer(container)
|
|
}
|
|
}
|
|
|
|
/// 시스템 런치 스크린에서 자연스럽게 이어지는 인앱 스플래시.
|
|
private struct RootView: View {
|
|
@State private var showSplash = true
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
StoreListView()
|
|
if showSplash {
|
|
SplashView()
|
|
.transition(.opacity)
|
|
.zIndex(1)
|
|
}
|
|
}
|
|
.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
|
|
}
|
|
}
|
|
}
|
|
}
|