# 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.plist`의 `UILaunchScreen` — `LaunchBackground` 색 + `LaunchIcon` 이미지) → 인앱 스플래시(`ExpirannerApp.swift`의 `SplashView`, 약 1초 후 페이드아웃)로 이어짐. 런치 스크린과 스플래시의 글리프 크기(110pt)를 맞춰 끊김이 없게 유지할 것. 7. **설정** (`SettingsView`, 첫 화면 헤더 톱니로 진입): 화면 모드(시스템/라이트/다크), 주 시작 요일(일/월, `@AppStorage(weekStartStorageKey)` — 달력 그리드 전체가 따름), 데이터 관리(**매장별** '기간 지난 데이터 삭제' + 2개 매장 이상일 때만 전체 삭제 행). ## 3. 데이터 모델 (`Models.swift`) ```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` 켜져 있음 — 예: `@Published`엔 `import Combine`). - 저장/삭제 후 `try? context.save()` + 햅틱 호출이 관례. - 불필요한 메타데이터 필드, 복잡한 설정 화면, 알림 기능 등을 임의로 추가하지 말 것. ## 6. 빌드 / 검증 ```bash 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_*)으로 관리.