mycode/myApp/Expiranner/IOS/YearMonthView.swift
songyc macbook 9902d44848 feat(expiranner): rebuild as store-based expiry photo checker
- 매장 선택 → 연/월 → 월 캘린더(날짜별 제품 사진 썸네일) → 날짜 상세 플로우로 전면 재작성
- 스캔 플로우: 촬영 → 재촬영/사용 → 그 달 날짜만 선택 → 저장 후 카메라 자동 복귀 (손전등 지원)
- 날짜 상세 다중 선택 삭제 (전체 선택/해제 + 일괄 삭제 확인)
- 다크 전용 민트 테마, SwiftData 모델(Store ↔ ProductRecord, 원본+썸네일 분리 저장)
- 밝은 민트 앱 아이콘(SVG 원본 포함) + 런치 스크린/인앱 스플래시
- CLAUDE.md 새 구조 기준으로 재작성

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 15:53:14 +09:00

147 lines
4.9 KiB
Swift

import SwiftUI
import SwiftData
/// : 1~12 .
struct YearMonthView: View {
let store: Store
@Query private var allRecords: [ProductRecord]
@State private var year: Int
init(store: Store) {
self.store = store
_year = State(initialValue: KoreanCalendar.calendar.component(.year, from: .now))
}
private struct MonthRoute: Hashable {
let year: Int
let month: Int
}
private var storeRecords: [ProductRecord] {
allRecords.filter { $0.store === store }
}
///
private var countsByMonth: [Int: Int] {
var counts: [Int: Int] = [:]
for record in storeRecords {
let c = KoreanCalendar.calendar.dateComponents([.year, .month], from: record.expiryDate)
if c.year == year, let month = c.month {
counts[month, default: 0] += 1
}
}
return counts
}
var body: some View {
ZStack {
Theme.bg.ignoresSafeArea()
ScrollView {
VStack(spacing: 24) {
yearPicker
monthGrid
yearSummary
}
.padding(20)
}
}
.navigationTitle(store.name)
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: MonthRoute.self) { route in
MonthCalendarView(store: store, year: route.year, month: route.month)
}
}
// MARK: -
private var yearPicker: some View {
HStack {
yearButton(systemName: "chevron.left") { year -= 1 }
Spacer()
Text(verbatim: "\(year)")
.font(.system(size: 26, weight: .bold, design: .rounded))
.foregroundStyle(Theme.textPrimary)
.contentTransition(.numericText())
Spacer()
yearButton(systemName: "chevron.right") { year += 1 }
}
.padding(.top, 8)
}
private func yearButton(systemName: String, action: @escaping () -> Void) -> some View {
Button {
withAnimation(.snappy) { action() }
Haptics.tap()
} label: {
Image(systemName: systemName)
.font(.body.weight(.semibold))
.foregroundStyle(Theme.textPrimary)
.frame(width: 40, height: 40)
.background(Circle().fill(Theme.surface))
.overlay(Circle().strokeBorder(Theme.stroke, lineWidth: 1))
}
}
// MARK: -
private var monthGrid: some View {
let columns = Array(repeating: GridItem(.flexible(), spacing: 12), count: 3)
let now = KoreanCalendar.calendar.dateComponents([.year, .month], from: .now)
return LazyVGrid(columns: columns, spacing: 12) {
ForEach(1...12, id: \.self) { month in
NavigationLink(value: MonthRoute(year: year, month: month)) {
monthCard(month: month, isCurrent: now.year == year && now.month == month)
}
.buttonStyle(.plain)
}
}
}
private func monthCard(month: Int, isCurrent: Bool) -> some View {
let count = countsByMonth[month] ?? 0
return VStack(alignment: .leading, spacing: 0) {
Text(verbatim: "\(month)")
.font(.system(.title3, design: .rounded).weight(.bold))
.foregroundStyle(count > 0 ? Theme.textPrimary : Theme.textSecondary)
Spacer(minLength: 18)
if count > 0 {
Text(verbatim: "\(count)")
.font(.caption.weight(.bold))
.foregroundStyle(Theme.onAccent)
.padding(.horizontal, 9)
.padding(.vertical, 4)
.background(Capsule().fill(Theme.accent))
} else {
Text("기록 없음")
.font(.caption2)
.foregroundStyle(Theme.textSecondary.opacity(0.6))
}
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: 96)
.background(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.fill(Theme.surface)
)
.overlay(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.strokeBorder(isCurrent ? Theme.accent.opacity(0.8) : Theme.stroke, lineWidth: isCurrent ? 1.5 : 1)
)
}
// MARK: -
private var yearSummary: some View {
let total = countsByMonth.values.reduce(0, +)
return Group {
if total > 0 {
Text(verbatim: "\(year)년에 기록한 제품 \(total)")
.font(.footnote)
.foregroundStyle(Theme.textSecondary)
}
}
}
}