songyc macbook ceb0ab8e16 feat(expiranner): 기록 날짜·시간 수정 + 매장별 지난 데이터 정리 + 주 시작 요일 설정
- RecordEditView 신규: 저장된 기록의 유통기한을 그래픽 달력 + '시간 추가'로 수정.
  날짜 상세 그리드의 길게 누르기 메뉴와 전체화면 뷰어 상단 버튼 양쪽에서 진입
- 설정 '데이터 관리'를 매장별로 분리: 지난 기록이 있는 매장만 행으로 나열해
  개별 삭제, 2개 매장 이상일 때만 '모든 매장에서 삭제' 행 노출(각각 확인 대화상자)
- 주 시작 요일 설정(일요일/월요일): 월 캘린더와 촬영 플로우 미니 달력의
  그리드·요일 기호·주말 색이 모두 따라감 (기본은 기존과 같은 일요일 시작)
- CLAUDE.md를 현재 동작(설정 화면, hasTime, 동적 색)에 맞게 갱신

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2HQuA6RPtkh45QTMN83i2
2026-07-28 02:29:36 +09:00

6.2 KiB

Expiranner Project Rules

1. 프로젝트 개요

  • 이름: Expiranner — 매장별 유통기한 점검 도구 (개인용, 배포 없음)
  • 루트: Expiranner/ (모든 작업은 이 루트에서 수행)
  • 소스 폴더: IOS/ — pbxproj가 파일시스템 동기화 그룹(PBXFileSystemSynchronizedRootGroup)이라 이 폴더에 Swift 파일을 넣기만 하면 빌드에 포함된다. Expiranner 하위 폴더를 새로 만들지 말 것.
  • 프레임워크: SwiftUI + SwiftData (Core Data 금지)
  • 타깃: iOS 17+, iPhone 중심
  • 언어: 모든 UI 텍스트와 로그는 한국어
  • 외관: 기본 다크, 설정에서 시스템/라이트/다크 선택(AppearanceMode, @AppStorage(appearanceStorageKey)). Theme 색은 전부 Color.dynamic(light:dark:) 동적 색.
  • 디자인 토큰: Theme.swift (다크 기준 배경 #0B0E13, 카드 #161B25, 민트 액센트 #3BE39F, rounded 폰트 디자인)

2. 핵심 플로우 (이 동작이 앱의 전부)

  1. 매장 선택 (StoreListView): 앱 첫 화면. 매장 목록 표시, +로 추가, 컨텍스트 메뉴로 이름 변경/삭제(기록 연쇄 삭제).
  2. 연/월 선택 (YearMonthView): 연도 좌우 이동 + 1~12월 그리드, 월별 기록 개수 배지.
  3. 월 캘린더 (MonthCalendarView): 날짜 셀마다 그 날짜가 유통기한인 제품 사진 썸네일 + 개수 배지. 좌우 화살표로 월 이동. 오늘 날짜 민트 테두리.
  4. 날짜 상세 (DayDetailView): 시트로 그 날짜의 사진 그리드, D-day 칩(D-n / 오늘까지 / n일 지남), 시각 포함 기록엔 시각 배지. 탭하면 전체화면 뷰어(PhotoViewerView, 스와이프 + 날짜·시간 수정 + 삭제). 사진 길게 눌러 컨텍스트 메뉴로도 날짜·시간 수정(RecordEditView) 가능. [선택] 버튼으로 다중 선택 모드 → 전체 선택/해제 + 일괄 삭제(확인 대화상자).
  5. 스캔 플로우 (CaptureFlowView, 전체화면): FAB(카메라 버튼) → 촬영 → [재촬영]/[사용하기] → 현재 보고 있는 달의 날짜만 선택 가능한 미니 달력 → [저장하기] → 성공 햅틱 + 토스트 → 자동으로 카메라 복귀. X로 캘린더 복귀. 손전등 토글 지원. 날짜 선택 후 나타나는 '시간 추가' 버튼으로 시각(시:분)까지 선택 저장 가능(기본은 날짜만 — 이 흐름을 침해하지 말 것).
  6. 로딩 화면: 시스템 런치 스크린(루트 Info.plistUILaunchScreenLaunchBackground 색 + LaunchIcon 이미지) → 인앱 스플래시(ExpirannerApp.swiftSplashView, 약 1초 후 페이드아웃)로 이어짐. 런치 스크린과 스플래시의 글리프 크기(110pt)를 맞춰 끊김이 없게 유지할 것.
  7. 설정 (SettingsView, 첫 화면 헤더 톱니로 진입): 화면 모드(시스템/라이트/다크), 주 시작 요일(일/월, @AppStorage(weekStartStorageKey) — 달력 그리드 전체가 따름), 데이터 관리(매장별 '기간 지난 데이터 삭제' + 2개 매장 이상일 때만 전체 삭제 행).

3. 데이터 모델 (Models.swift)

@Model final class Store {
    var name: String
    var createdAt: Date
    @Relationship(deleteRule: .cascade, inverse: \ProductRecord.store)
    var records: [ProductRecord] = []
}

@Model final class ProductRecord {
    var expiryDate: Date            // hasTime=false면 자정 정규화, true면 시각 포함
    var hasTime: Bool = false       // 유통기한에 시각(시:분) 포함 여부
    var createdAt: Date
    @Attribute(.externalStorage) var imageData: Data  // 최대 1600px JPEG
    var thumbnailData: Data         // 최대 360px JPEG (캘린더/그리드용)
    var store: Store?
}
  • 뷰에서는 @Query로 전체를 받고 $0.store === store로 메모리 필터링한다 (Predicate로 관계 비교하지 말 것 — iOS 17 호환성 문제 회피).
  • 스키마 변경 시 ExpirannerApp.init의 저장소 초기화 폴백이 기존 데이터를 지우고 재생성한다.

4. 파일 구성 (IOS/)

  • ExpirannerApp.swift — 엔트리, ModelContainer(실패 시 저장소 리셋 폴백)
  • Models.swift — Store, ProductRecord
  • Theme.swift — 색상/카드 스타일/햅틱(Haptics)/달력 유틸(KoreanCalendar)/이미지 다운스케일
  • StoreListView.swift, YearMonthView.swift, MonthCalendarView.swift, DayDetailView.swift
  • CaptureFlowView.swift — 촬영→확인→날짜선택(+시간 추가)→저장 상태 머신
  • CameraService.swift — AVCaptureSession 래퍼(@MainActor) + CameraPreview
  • SettingsView.swift — 화면 모드 / 주 시작 요일 / 매장별 지난 데이터 정리
  • RecordEditView.swift — 저장된 기록의 유통기한(날짜·시간) 수정 시트

5. 코딩 규칙

  • 연도 등 숫자를 Text에 넣을 때 반드시 Text(verbatim:) 사용 (천 단위 구분 방지: "2,026년" 버그).
  • 날짜 계산은 전부 KoreanCalendar.calendar (gregorian + ko_KR) 사용.
  • 새 모듈 심볼 사용 시 명시적 import 필요 (MEMBER_IMPORT_VISIBILITY 켜져 있음 — 예: @Publishedimport Combine).
  • 저장/삭제 후 try? context.save() + 햅틱 호출이 관례.
  • 불필요한 메타데이터 필드, 복잡한 설정 화면, 알림 기능 등을 임의로 추가하지 말 것.

6. 빌드 / 검증

xcodebuild -project Expiranner.xcodeproj -scheme Expiranner \
  -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.5' \
  -configuration Debug build CODE_SIGNING_ALLOWED=NO
  • 시뮬레이터에서는 카메라가 동작하지 않으므로 스캔 플로우는 실기기에서 확인.
  • 앱 아이콘 원본: 루트의 AppIcon.svg(밝은 민트 배경) → qlmanage -t -s 1024로 렌더 → PIL로 알파 제거 → IOS/Assets.xcassets/AppIcon.appiconset/AppIcon.png.
  • 런치 글리프(LaunchIcon.imageset, 투명 배경 필수): qlmanage가 SVG를 흰 배경으로 렌더하는 경우가 있으므로 PIL로 도형을 직접 그려 생성했다(2x=220px, 3x=330px). 루트 LaunchIcon.svg는 도형 좌표 원본.
  • GENERATE_INFOPLIST_FILE=YES + INFOPLIST_FILE=Info.plist 병합 구성 — 루트 Info.plist에는 UILaunchScreen만 두고 나머지 키는 빌드 설정(INFOPLIST_KEY_*)으로 관리.