188 lines
6.9 KiB
Swift
188 lines
6.9 KiB
Swift
//
|
|
// MonthCalendarView.swift
|
|
// Expiranner
|
|
//
|
|
// 월별 캘린더: 날짜 그리드 + 선택한 날의 아이템 목록
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
struct MonthCalendarView: View {
|
|
let year: Int
|
|
let month: Int
|
|
|
|
@Environment(\.modelContext) private var modelContext
|
|
@Query private var items: [ProductItem]
|
|
|
|
@State private var selectedDay: Int?
|
|
@State private var showScanner = false
|
|
@State private var editingItem: ProductItem?
|
|
|
|
private let columns = Array(repeating: GridItem(.flexible()), count: 7)
|
|
private let weekdaySymbols = ["일", "월", "화", "수", "목", "금", "토"]
|
|
|
|
init(year: Int, month: Int) {
|
|
self.year = year
|
|
self.month = month
|
|
let (start, end) = MonthCell.monthBounds(year: year, month: month)
|
|
_items = Query(
|
|
filter: #Predicate<ProductItem> { $0.date >= start && $0.date < end },
|
|
sort: \ProductItem.date
|
|
)
|
|
}
|
|
|
|
private var daysInMonth: Int {
|
|
let calendar = Calendar.current
|
|
let start = calendar.date(from: DateComponents(year: year, month: month, day: 1)) ?? Date()
|
|
return calendar.range(of: .day, in: .month, for: start)?.count ?? 30
|
|
}
|
|
|
|
/// 1일의 요일 오프셋 (일요일 시작 기준)
|
|
private var firstWeekdayOffset: Int {
|
|
let calendar = Calendar.current
|
|
let start = calendar.date(from: DateComponents(year: year, month: month, day: 1)) ?? Date()
|
|
return calendar.component(.weekday, from: start) - 1
|
|
}
|
|
|
|
private func day(of item: ProductItem) -> Int {
|
|
Calendar.current.component(.day, from: item.date)
|
|
}
|
|
|
|
private var selectedDayItems: [ProductItem] {
|
|
guard let selectedDay else { return [] }
|
|
return items.filter { day(of: $0) == selectedDay }
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack(alignment: .bottomTrailing) {
|
|
VStack {
|
|
LazyVGrid(columns: columns, spacing: 8) {
|
|
ForEach(weekdaySymbols, id: \.self) { symbol in
|
|
Text(symbol)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
ForEach(0..<firstWeekdayOffset, id: \.self) { _ in
|
|
Text("")
|
|
}
|
|
ForEach(1...daysInMonth, id: \.self) { day in
|
|
let count = items.filter { self.day(of: $0) == day }.count
|
|
Button {
|
|
selectedDay = day
|
|
} label: {
|
|
VStack(spacing: 2) {
|
|
Text("\(day)")
|
|
.fontWeight(selectedDay == day ? .bold : .regular)
|
|
Circle()
|
|
.fill(count > 0 ? .red : .clear)
|
|
.frame(width: 6, height: 6)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 4)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.fill(selectedDay == day ? Color(.systemGray4) : .clear)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
|
|
if let selectedDay {
|
|
List {
|
|
Section("\(month)월 \(selectedDay)일 저장된 항목") {
|
|
if selectedDayItems.isEmpty {
|
|
Text("저장된 항목이 없습니다")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
ForEach(selectedDayItems) { item in
|
|
Button {
|
|
editingItem = item
|
|
} label: {
|
|
HStack {
|
|
if let data = item.imageData, let image = UIImage(data: data) {
|
|
Image(uiImage: image)
|
|
.resizable()
|
|
.scaledToFill()
|
|
.frame(width: 44, height: 44)
|
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
|
}
|
|
Text(item.name)
|
|
Spacer()
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
.onDelete(perform: deleteItems)
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
} else {
|
|
Spacer()
|
|
Text("날짜를 선택하면 저장된 항목이 표시됩니다")
|
|
.foregroundStyle(.secondary)
|
|
Spacer()
|
|
}
|
|
}
|
|
|
|
FloatingScanButton {
|
|
showScanner = true
|
|
}
|
|
}
|
|
.navigationTitle("\(String(year))년 \(month)월")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.fullScreenCover(isPresented: $showScanner) {
|
|
ScannerView(year: year, month: month)
|
|
}
|
|
.sheet(item: $editingItem) { item in
|
|
ItemEditView(item: item)
|
|
}
|
|
}
|
|
|
|
private func deleteItems(at offsets: IndexSet) {
|
|
let targets = offsets.map { selectedDayItems[$0] }
|
|
for item in targets {
|
|
print("항목 삭제: \(item.name)")
|
|
modelContext.delete(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 저장된 항목의 날짜(연/월/일) 수정 뷰
|
|
struct ItemEditView: View {
|
|
@Bindable var item: ProductItem
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section("제품명") {
|
|
TextField("제품명", text: $item.name)
|
|
}
|
|
Section("날짜 이동") {
|
|
DatePicker("날짜", selection: $item.date, displayedComponents: .date)
|
|
.datePickerStyle(.graphical)
|
|
}
|
|
}
|
|
.navigationTitle("항목 수정")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("완료") {
|
|
print("항목 수정 완료: \(item.name) → \(item.date)")
|
|
dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
MonthCalendarView(year: 2026, month: 7)
|
|
}
|
|
.modelContainer(for: ProductItem.self, inMemory: true)
|
|
}
|