mycode/myApp/Expiranner/CLAUDE.md
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

5.1 KiB

Expiranner Project Rules

1. 프로젝트 개요

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

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

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

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            // 자정으로 정규화
    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

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_*)으로 관리.