- 매장 선택 → 연/월 → 월 캘린더(날짜별 제품 사진 썸네일) → 날짜 상세 플로우로 전면 재작성 - 스캔 플로우: 촬영 → 재촬영/사용 → 그 달 날짜만 선택 → 저장 후 카메라 자동 복귀 (손전등 지원) - 날짜 상세 다중 선택 삭제 (전체 선택/해제 + 일괄 삭제 확인) - 다크 전용 민트 테마, SwiftData 모델(Store ↔ ProductRecord, 원본+썸네일 분리 저장) - 밝은 민트 앱 아이콘(SVG 원본 포함) + 런치 스크린/인앱 스플래시 - CLAUDE.md 새 구조 기준으로 재작성 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
147 lines
4.9 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|
|
}
|