diff --git a/AGENTS.md b/AGENTS.md index 1db1fef..20c86f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,34 +1,34 @@ # Paxo — AI 에이전트 작업 규칙 -> **작업 시작 전 반드시 `git pull`.** 규칙 문서가 자주 바뀐다. +> **작업 시작 전 반드시 `git pull`.** Paxo는 화면 속 문제를 단축키로 캡처하면 AI가 정답과 해설을 알려주는 **macOS 메뉴바 앱**이다. 저장소 이름이 `iOS`지만 iOS 앱이 아니다. 배포 타깃은 macOS 14+. -이 파일은 저장소 전역 규칙이다. 하위 디렉토리에 더 구체적인 `AGENTS.md`가 있고, -에이전트는 트리에서 **가장 가까운 파일**을 읽는다. +본 문서는 저장소의 전역 규칙(Global Rule)을 정의한다. +하위 디렉터리에 위치한 AGENTS.md가 우선 적용되며, AI 에이전트는 작업 중인 위치에서 가장 가까운 규칙 파일을 참조해야 한다. -- `Paxo/AGENTS.md` — Swift 앱 코드 (밟기 쉬운 지뢰 모음) -- `proxy-vercel/AGENTS.md` — API 프록시 계약 +- `Paxo/AGENTS.md` — Swift 앱 코드 컨벤션 및 주요 주의사항 (Anti-patterns) +- `proxy-vercel/AGENTS.md` — API 프록시 서버 스펙(Contract) -공통 문서: +공통 참조 문서: -- `docs/architecture.md` — 앱 ↔ 프록시 ↔ Gemini ↔ StoreKit 연결 -- `docs/domain.md` — 기능 · 과금 용어 -- `docs/decisions.md` — 팀 결정 기록 (날짜순) +- `docs/architecture.md` — 앱 ↔ 프록시 ↔ Gemini ↔ StoreKit 시스템 구성도 +- `docs/domain.md` — 핵심 도메인 기능 및 과금 관련 용어 사전 +- `docs/decisions.md` — 팀의 주요 기술적 결정 사항 -## 셋업 +## 1. 초기 설정 (Setup) -클론 직후 반드시 1회. **이 파일이 없으면 빌드가 실패한다.** +저장소 클론 직후 반드시 1회 실행해야 한다. ```sh cp Config/Secrets.example.swift Paxo/Secrets.swift ``` -`Paxo/Secrets.swift`는 gitignore 대상이다. 절대 커밋하지 않는다. -템플릿을 `Paxo/` 안에 복사본으로 남기면 `Secrets` 중복 선언으로 빌드가 깨진다. +- `Paxo/Secrets.swift`는 gitignore 대상이다. 절대 커밋하지 않는다. +- 템플릿 파일(Secrets.example.swift)을 Paxo/ 디렉터리 내부에 복사본으로 남겨둘 경우, Secrets 구조체 중복 선언(Duplicate declaration) 오류로 인해 빌드가 깨지므로 주의한다. -## 명령 +## 2. 주요 명령어 (Commands) 빌드: @@ -51,33 +51,32 @@ xcrun swift-format lint --recursive --strict Paxo PaxoTests xcrun swift-format format --in-place --recursive Paxo PaxoTests ``` -## 코드 스타일 +## 3. 코드 스타일 및 컨벤션 -- **식별자는 영어, 주석과 사용자 노출 문구는 한국어.** 예외 없다. -- 주석은 *왜*를 적는다. API를 재진술하지 않는다. 밀도는 코드 20줄당 1줄 정도. -- import는 알파벳순. `swift-format`의 `OrderedImports` 규칙으로 CI에서 강제된다. -- 접근 제어는 `private`만 명시하고 `internal`은 생략한다. `public`/`fileprivate`은 쓰지 않는다. -- 모든 참조 타입은 `final`. -- 강제 언래핑(`!`) 금지. 현재 코드베이스에 0개다. -- `print`/`os_log`/`Logger` 추가 금지. 로깅 인프라가 없고, 도입은 별도 논의 대상이다. -- 상태 없는 유틸리티는 case 없는 `enum` 네임스페이스로 만든다 (`Prompts`, `KeychainHelper`). +- 명명 규칙: 모든 식별자(변수, 함수, 클래스 등)는 영어로, 주석과 사용자 노출 UI 문구는 한국어로 작성한다. 예외는 허용하지 않는다. +- 주석: 코드의 '동작'을 그대로 설명하지 않고 '의도(Why)'를 작성한다. 주석 밀도는 코드 20줄당 1줄 내외로 유지한다. +- Import 정렬: 알파벳순으로 정렬한다. `swift-format`의 `OrderedImports` 규칙에 의해 CI 파이프라인에서 강제(Enforce)된다. +- 접근 제어자: private만 명시적으로 작성하고 `internal은` 생략한다. `public` 및 `fileprivate`은 사용하지 않는다. +- 참조 타입: 모든 참조 타입(Class)은 `final`로 선언한다. +- 안전성: 강제 언래핑(`!`)은 엄격히 금지한다. +- 로깅: `print`, `os_log`, `Logger` 등의 추가를 금지한다. 현재 로깅 인프라가 없으며, 시스템 도입은 추후 별도로 논의한다. +- 유틸리티 객체: 상태(State)를 가지지 않는 유틸리티 함수 묶음은 case가 없는 `enum` 네임스페이스를 활용해 구현한다. (예: `Prompts`, `KeychainHelper`) -## 중복을 만들지 않는다 +## 4. 중복 코드 생성 방지 (DRY) -새 유틸리티나 헬퍼를 만들기 전에 **기존 것을 먼저 검색한다.** -비슷한 함수가 이미 있으면 새로 쓰지 말고 그것을 쓰거나 확장한다. -AI 생성 코드가 조용히 중복을 쌓는 것이 이 저장소의 가장 큰 품질 리스크다. +신규 유틸리티나 헬퍼 함수를 작성하기 전, 반드시 기존에 구현된 코드를 먼저 검색한다. +유사한 로직이 존재한다면 새로 작성하지 않고 기존 코드를 재사용하거나 확장한다. AI 에이전트가 코드베이스를 파악하지 못하고 조용히 중복 코드를 쌓아 올리는 것은 본 프로젝트의 가장 큰 품질 리스크다. -## 커밋과 PR +## 5. 브랜치 전략 및 PR 규칙 -- 브랜치 접두사: `feat/`, `fix/`, `docs/`, `chore/`, `style/`, `test/` -- 커밋: Conventional Commits 접두사 + **한국어 제목** + `-` 불릿 본문 -- **AI 사용 여부를 커밋에 표기하지 않는다.** 초안을 누가 썼든 커밋한 사람이 전적으로 책임진다 -- 기능 브랜치는 `develop`에서 만들고 PR도 `develop`으로 보낸다 (Squash 머지). `main`은 릴리즈 PR만 받는다 (Merge 머지) -- `develop` · `main` 직접 푸시 금지. PR과 CI 통과가 필수다 -- PR을 올리기 전에 빌드와 테스트를 **실제로 돌려서** 통과를 확인한다 +- 브랜치 네이밍: `feat/`, `fix/`, `docs/`, `chore/`, `style/`, `test/` 접두사를 사용한다. +- 커밋 메시지: `Conventional Commits 접두사 + 한국어 요약 제목 + (필요시) '-' 불릿을 활용한 본문` 형태로 작성한다. +- 책임 소재: 커밋 메시지에 AI 사용 여부를 표기하지 않는다. 코드의 초안 작성자와 무관하게, 해당 코드를 커밋한 담당자가 결과물에 대한 모든 책임을 진다. +- 워크플로우: 기능 개발 브랜치는 `develop`에서 분기하며, PR 또한 `develop`을 타깃으로 생성한다 (Squash Merge). `main` 브랜치는 릴리스용 PR만 수용한다 (Regular Merge). +- 직접 푸시 금지: `develop` 및 `main` 브랜치에 대한 Direct Push를 금지한다. 반드시 PR 생성 후 CI를 통과해야 한다. +- 사전 검증: PR을 생성하기 전, 로컬 환경에서 직접 빌드 및 테스트를 실행하여 정상 동작을 확인해야 한다. -## 절대 하지 말 것 +## 6. 엄격한 금지 사항 (Do Not's) - `Paxo.xcodeproj/project.pbxproj`의 파일 목록 수동 편집 — `Paxo/`는 파일시스템 동기화 그룹이라 손댈 필요가 없다 - `Paxo/Secrets.swift` 커밋 @@ -85,12 +84,20 @@ AI 생성 코드가 조용히 중복을 쌓는 것이 이 저장소의 가장 - `docs/private/` 커밋 — 비공개 저장소 `paxo-app/internal`의 clone이다. 내용을 공개 저장소로 옮기지 않는다 - `node_modules/` 커밋 -**이 저장소는 공개다.** 커밋 전에 시크릿이 섞였는지 확인한다. +- `Paxo.xcodeproj/project.pbxproj` 수동 편집 금지: `Paxo/` 디렉터리는 파일 시스템과 자동 동기화되도록 설정되어 있으므로 프로젝트 파일을 수동으로 수정할 필요가 없다. +- `Paxo/Secrets.swift` 커밋 금지. +- `docs/hansung/` 커밋 금지: 비공개 사업계획서 및 신청서가 포함되어 있다. +- `docs/private/` 커밋 금지: 사내 비공개 저장소(`paxo-app/internal`)의 서브모듈/클론본이다. 보안 내용을 퍼블릭 레포지토리로 유출하지 않는다. +- `node_modules/` 커밋 금지. -## 릴리스 +> 주의: 현재 저장소는 퍼블릭(Public) 저장소다. 커밋 전 API 키, 토큰 등 시크릿 정보가 포함되지 않았는지 반드시 교차 검증한다. -전체 절차는 비공개 저장소 `paxo-app/internal`의 `app-store-submission.md`(로컬에선 `docs/private/`)에 있다. 제출 전 함정 3가지만 여기 적는다. +## 릴리스 체크리스트 -1. `MARKETING_VERSION`이 아직 `0.1.0`이다 — 출시 빌드는 `1.0.0` -2. 공유 스킴의 StoreKit Configuration을 **None**으로 되돌려야 한다. 안 그러면 실제 결제가 동작하지 않는다 -3. `DEVELOPMENT_TEAM = L3JLLU88WG`가 실제 배포 인증서의 팀과 일치하는지 확인한다 +전체 배포 프로세스는 사내 비공개 저장소 문서(`docs/private/app-store-submission.md`)를 따른다. 배포 직전 가장 빈번하게 발생하는 3가지 크리티컬 이슈를 아래에 명시한다. + +- 버전 확인: `MARKETING_VERSION`이 0.1.0 상태로 남아있는지 확인한다. 실제 출시 빌드는 1.0.0 (또는 그 이상)이어야 한다. + +- StoreKit 설정 복구: 공유 스킴(Scheme)에 설정된 StoreKit Configuration을 반드시 None으로 원복해야 한다. 해당 설정이 남아있으면 프로덕션 환경에서 실제 결제가 동작하지 않는다. + +- 인증서 팀 매칭: 프로젝트 세팅의 `DEVELOPMENT_TEAM = L3JLLU88WG` 값이 실제 배포용 App Store Connect 팀 ID와 정확히 일치하는지 확인한다. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 3b62afb..103b8e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,18 +1,18 @@ @AGENTS.md -## Claude Code 전용 +# Claude Code 전용 설정 -위 `AGENTS.md`가 팀 공통 규칙이다. Codex도 같은 파일을 읽는다. -아래는 Claude Code에만 해당하는 내용이다. +상단의 AGENTS.md는 팀 공통 규칙이며, Codex 등 다른 AI 에이전트도 해당 파일을 동일하게 참조한다. +본 항목은 Claude Code 환경에만 추가로 적용되는 전용 규칙 및 기능이다. -### 스킬 +## 1. 커스텀 스킬 (명령어) -- `/release-check` — App Store 제출 전 검사 항목을 순서대로 확인 -- `/proxy-change` — 클라이언트와 프록시를 동시에 고쳐야 할 때의 절차 -- `/weekly-report` — 커밋 기록으로 주간 보고서 초안 생성 +- `/release-check` — App Store 배포 전 필수 점검 항목들을 순서대로 교차 검증한다. +- `/proxy-change` — 클라이언트 앱과 프록시 서버의 API 스펙(Contract)을 동시에 수정해야 할 경우, 준수해야 할 표준 절차를 실행한다. +- `/weekly-report` — 최근 커밋 기록을 바탕으로 주간 업무 보고서 초안을 자동 생성한다. -### 작업 방식 +## 2. 작업 방식 (Workflow) -- 파일 3개 이상을 건드리거나 프록시 계약을 바꾸는 변경은 **플랜 모드로 시작한다.** -- 빌드를 실제로 돌리기 전에 "고쳤다"고 보고하지 않는다. -- 프롬프트 템플릿은 `docs/prompts/`에 있다. 잘 된 프롬프트는 개인 것이 아니라 여기 쌓는다. +- 플랜 모드(Plan Mode) 필수: 파일 3개 이상을 수정하거나 프록시 API 스펙을 변경하는 등 영향 범위가 큰 작업은, 코드를 작성하기 전 반드시 플랜 모드로 시작하여 작업 계획부터 수립한다. +- 섣부른 완료 보고 금지: 코드를 수정했더라도 실제로 로컬 빌드 및 테스트를 실행하여 정상 동작을 확인하기 전까지는 절대 작업이 완료되었다고 단언(보고)하지 않는다. +- 프롬프트 문서화: 기준 프롬프트 템플릿은 `docs/prompts/` 디렉터리에 관리된다. 유용하게 쓰인 프롬프트는 개인의 노하우로 남겨두지 않고, 반드시 해당 경로에 문서화하여 팀과 공유한다. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7089df..c408bef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,77 +1,81 @@ -# 기여 가이드 +# 기여 가이드 (Contributing Guide) -AI 에이전트용 규칙은 [`AGENTS.md`](AGENTS.md)에 있다. 사람이 처음 합류할 때 필요한 것만 여기 적는다. +AI 에이전트 전용 규칙은 [`AGENTS.md`](AGENTS.md)를 참조한다. 본 문서는 팀에 새로 합류한 개발자가 초기 셋업 및 기여를 위해 알아야 할 필수 사항만 다룬다. -## 처음 한 번 +## 1. 초기 셋업 (Initial Setup) ```sh git clone https://github.com/paxo-app/iOS.git cd iOS -cp Config/Secrets.example.swift Paxo/Secrets.swift # 없으면 빌드 실패 -# 만든 파일의 appToken 에 팀에서 받은 토큰 값을 채운다 (비우면 AI 호출이 401) -open Paxo.xcodeproj +cp Config/Secrets.example.swift Paxo/Secrets.swift # 누락 시 빌드 실패 +# 생성된 파일의 appToken 필드에 발급받은 팀 토큰 값을 입력한다 (비워둘 경우 AI 호출 401 에러) +open Paxo. ``` -Xcode에서 Signing & Capabilities → Team을 본인 계정으로 바꾸고 Run. -첫 캡처 시 **화면 기록 권한**을 허용한 뒤 앱을 재실행해야 한다 (macOS TCC 특성). +1. Xcode의 **Signing & Capabilities → Team**을 본인 계정으로 변경한 후 앱을 실행(Run)한다. +2. macOS TCC(투명성, 동의 및 제어) 정책에 따라, 최초 캡처 시 '화면 기록 권한'을 허용한 후 **반드시 앱을 완전히 종료하고 재실행**해야 정상 동작한다. -⚠️ **프록시에는 이미 `APP_TOKEN` 검증이 켜져 있다.** `Paxo/Secrets.swift`의 `appToken`을 -비워두면 빌드·실행은 되지만 AI 호출이 전부 401로 실패한다. 토큰 값은 팀에 요청한다. -팀 테스트용 토큰은 프로덕션과 분리된 `APP_TOKEN_PREV` 슬롯을 쓴다. +⚠️ **주의: 프록시 서버에는 이미 `APP_TOKEN` 검증이 활성화되어 있다.** +`Paxo/Secrets.swift` 내의 `appToken` 값을 비워두면 앱 빌드와 실행은 성공하지만, AI API 호출 시 전부 401(Unauthorized) 에러가 발생한다. 토큰 값은 팀 내 담당자에게 요청한다. (팀 내부 테스트용 토큰은 프로덕션과 분리된 `APP_TOKEN_PREV` 슬롯을 사용한다.) -팀원이라면 운영 규칙과 온보딩 문서가 있는 비공개 저장소도 받는다: `git clone https://github.com/paxo-app/internal.git docs/private` → `docs/private/README.md`부터 본다. +> **정식 팀원 온보딩** +> 정규 팀원인 경우 운영 규칙과 온보딩 문서가 포함된 사내 비공개 저장소도 클론해야 한다. 클론 후 `docs/private/README.md`를 최우선으로 확인한다. +> `git clone [https://github.com/paxo-app/internal.git](https://github.com/paxo-app/internal.git) docs/private` -## 작업 흐름 +## 2. 작업 흐름 (Workflow) ```sh git fetch origin -git switch -c feat/무엇을-한다 origin/develop -# 작업 +git switch -c feat/feature-name origin/develop +# 코드 작업 진행 xcodebuild -project Paxo.xcodeproj -scheme Paxo -destination 'platform=macOS' \ CODE_SIGNING_ALLOWED=NO test xcrun swift-format lint --recursive --strict --configuration .swift-format Paxo PaxoTests git push -u origin HEAD gh pr create --base develop + ``` -`develop`과 `main`은 보호돼 있다. 직접 푸시할 수 없고 PR이 필요하다. +`develop` 및 `main` 브랜치는 보호(Protected)되어 있으므로 직접 푸시(Direct Push)가 불가하며 반드시 PR을 거쳐야 한다. -- **`develop`** — 기능 PR을 **Squash**로 머지한다. 리뷰어 승인은 필수가 아니므로 CI가 초록이면 본인이 머지한다. 단 과금 · 프록시 · 릴리즈 설정 파일을 건드리면 CODEOWNERS 승인이 필요하다 -- **`main`** — 릴리즈 PR(develop → main)만 **Merge**로 머지한다. 승인 1명이 필요하다. Squash하면 다음 릴리즈 PR에 이미 나간 변경이 다시 뜬다 +* **`develop`**: 신규 기능 PR은 **Squash and Merge** 방식으로 병합한다. 일반적인 변경 사항은 리뷰어 승인 없이 CI를 통과(Green)하면 작성자가 직접 머지할 수 있다. 단, **과금, 프록시, 릴리스 설정 파일 등 핵심(위험) 경로 수정 시에는 반드시 CODEOWNERS의 승인이 필요**하다. +* **`main`**: 릴리스 PR(`develop` → `main`)만 허용하며, **Create a merge commit (Regular Merge)** 방식으로 병합한다. 배포 전 리뷰어 최소 1명의 승인이 필수다. (Squash 병합 시 커밋 히스토리가 꼬여 다음 릴리스 PR에 이전 변경 사항이 중복 노출되므로 절대 금지한다.) -## 커밋 +## 3. 커밋 메시지 컨벤션 -Conventional Commits 접두사 + 한국어 제목. +`Conventional Commits` 표준 접두사 + **한국어 제목** 형태로 작성한다. -``` +```text feat: 캡처 영역을 마지막 선택으로 기억 - 재캡처 시 이전 영역을 기본값으로 제시 - 화면이 바뀌면 무시하고 전체 화면으로 폴백 + ``` -**AI가 초안을 썼는지는 적지 않는다.** 커밋한 사람이 전적으로 책임진다. -자세한 내용은 [`docs/ai-usage-rules.md`](docs/ai-usage-rules.md). +**커밋 메시지에 AI 사용 여부나 초안 작성 내역을 표기하지 않는다.** +코드의 초안을 누가 작성했든, 이를 커밋(반영)한 담당자가 최종 결과물에 대한 모든 책임을 진다. 상세 원칙은 [`docs/ai-usage-rules.md`](docs/ai-usage-rules.md)를 참고한다. -## 자주 걸리는 것 +## 4. 트러블슈팅 (Troubleshooting) -| 증상 | 원인 | -|---|---| -| `cannot find 'Secrets' in scope` | `Paxo/Secrets.swift`를 안 만들었다 | -| AI 호출이 401 | `Paxo/Secrets.swift`의 `appToken`이 비었거나 값이 틀렸다 | -| `Secrets` 중복 선언 | 템플릿을 `Paxo/` 안에 복사했다. `Config/`에 둬야 한다 | -| 캡처 시 권한 오류 반복 | 권한 허용 후 앱을 재실행해야 한다 | -| 결과 패널이 캡처 이미지에 찍힘 | `ScreenCapturer`의 PID 필터나 sleep을 건드렸다 | -| 뷰에서 런타임 크래시 | 주입받지 않는 `@EnvironmentObject`를 선언했다 — [`Paxo/AGENTS.md`](Paxo/AGENTS.md) 표 확인 | +| 증상 | 원인 및 해결 | +| --- | --- | +| `cannot find 'Secrets' in scope` | `Paxo/Secrets.swift` 파일이 생성되지 않음 | +| AI API 호출 시 401 에러 발생 | `Paxo/Secrets.swift` 내 `appToken` 값이 누락되었거나 불일치함 | +| `Secrets` 중복 선언 빌드 에러 | 템플릿 원본을 `Paxo/` 내부에 중복으로 복사함 (템플릿은 `Config/` 에 유지해야 함) | +| 캡처 시 화면 기록 권한 오류 반복 | 시스템 권한 허용 후 앱을 완전히 종료(Quit)하고 재실행하지 않음 | +| 캡처 이미지에 결과 패널이 찍힘 | `ScreenCapturer`의 PID 필터 로직이나 sleep 타이밍을 잘못 수정함 | +| 뷰 진입 시 런타임 크래시 발생 | 상위 뷰에서 주입하지 않은 `@EnvironmentObject`를 선언함. 상세 의존성은 [`Paxo/AGENTS.md`](Paxo/AGENTS.md) 표 참조 | -## 지표 +## 5. 프로젝트 지표 (Metrics) ```sh scripts/ax-metrics.sh 30 + ``` -PR 리드타임과 CI 통과율을 집계한다. 체감이 아니라 숫자로 본다. +PR 리드 타임(Lead Time)과 CI 통과율 등 프로젝트 건전성을 집계한다. 팀의 퍼포먼스는 막연한 체감이 아닌 **실제 데이터(숫자)를 기반으로 평가하고 개선**한다. -## 라이선스 +## 6. 라이선스 (License) -기여한 코드는 저장소의 [MIT 라이선스](LICENSE)를 따른다. +본 저장소에 기여한 모든 코드는 [MIT 라이선스](LICENSE) 정책을 따른다. diff --git a/Paxo/AGENTS.md b/Paxo/AGENTS.md index a6f4637..9f551f2 100644 --- a/Paxo/AGENTS.md +++ b/Paxo/AGENTS.md @@ -1,72 +1,55 @@ -# Paxo/ — Swift 앱 코드 +# Paxo/ — Swift 앱 코드 작성 가이드 -전역 규칙은 저장소 루트의 `AGENTS.md`를 따른다. 여기에는 **이 디렉토리에서 실제로 밟게 되는 함정**만 적는다. -구조나 타입 목록은 코드를 읽으면 알 수 있으므로 적지 않는다. +> 전역 규칙은 저장소 루트의 `AGENTS.md`를 참조한다. 본 문서에는 `Paxo/` 디렉터리 내 코드 작업 시 **실제로 밟기 쉬운 크리티컬한 함정(Gotchas)**만 기록한다. +> 코드베이스를 읽으면 파악할 수 있는 단순 구조나 타입 목록은 생략한다. -## 아키텍처 관례 +## 1. 아키텍처 및 코딩 컨벤션 -- 상태 관리는 `@MainActor final class ... ObservableObject` + `@Published`다. **`@Observable`이 아니다.** - 새 코드도 기존 방식을 따른다. 섞으면 주입 경로가 깨진다. -- `AppState.shared` 싱글턴이 앱 상태의 단일 원본이고, `StoreManager`는 `AppState.store`로 소유된다. -- 도메인 에러는 `enum: LocalizedError` + **한국어 `errorDescription`**으로 만든다 (`GeminiError`, `CaptureError` 참고). -- 파일 배치: import → 주 타입의 `///` 문서 주석 → 타입 → `private` 헬퍼 → 파일 맨 아래 에러 enum. +* **상태 관리**: `@MainActor final class ... ObservableObject`와 `@Published` 속성 래퍼를 사용한다. (**Swift 5.9의 `@Observable` 매크로는 사용하지 않는다.**) 신규 코드 또한 기존 방식을 엄격히 따른다. 섞어 쓸 경우 의존성 주입 경로가 깨진다. +* **단일 원본 (Single Source of Truth)**: `AppState.shared` 싱글턴 객체가 앱 상태의 단일 원본 역할을 수행하며, `StoreManager`는 `AppState.store` 프로퍼티에 의해 소유된다. +* **에러 핸들링**: 도메인 에러는 `LocalizedError`를 채택한 `enum`으로 정의하고, `errorDescription`은 **반드시 한국어**로 작성한다 (`GeminiError`, `CaptureError` 참조). +* **파일 구조 표준**: `import` 구문 → 주 타입의 `///` 문서 주석 → 주 타입 정의 → `private` 헬퍼 메서드 → 파일 최하단 에러 `enum` 배치 순서를 엄수한다. -## 빌드 시스템 +## 2. 빌드 시스템 주의사항 -**`Paxo/`는 `PBXFileSystemSynchronizedRootGroup`이다.** -이 디렉토리에 `.swift` 파일을 넣으면 자동으로 컴파일 대상이 된다. +**`Paxo/` 디렉터리는 `PBXFileSystemSynchronizedRootGroup`으로 설정되어 있다.** 즉, 해당 디렉터리에 `.swift` 파일을 생성하면 자동으로 컴파일 대상에 포함된다. -- `project.pbxproj`의 `Sources` 빌드 페이즈는 의도적으로 비어 있다. **채우려 하지 말 것.** -- 파일을 빌드에서 제외할 방법이 없다. 그래서 시크릿 템플릿이 `Config/`에 있다. -- 예제·백업 `.swift` 파일을 이 디렉토리에 두면 중복 선언으로 빌드가 깨진다. -- **소스가 아닌 파일도 자동으로 앱 번들 Resources에 들어간다.** 이 디렉토리의 `.md`는 - `EXCLUDED_SOURCE_FILE_NAMES = "*.md"` 빌드 설정으로 막아뒀다. 다른 확장자의 내부 파일을 - 여기 두면 사용자와 심사원에게 배포되므로, 넣기 전에 번들에 들어가는지 확인한다. +* `project.pbxproj`의 `Sources` 빌드 페이즈는 의도적으로 비워두었다. **수동으로 파일을 추가하지 않는다.** +* 특정 파일을 빌드 대상에서 개별적으로 제외할 방법이 없으므로, 시크릿 템플릿 등 제외가 필요한 파일은 `Config/` 디렉터리에 둔다. +* 예제 코드나 백업용 `.swift` 파일을 해당 디렉터리에 방치하면 중복 선언 오류로 빌드가 깨진다. +* **소스 코드가 아닌 일반 파일도 앱 번들의 `Resources`에 자동으로 포함된다.** 현재 `.md` 파일만 `EXCLUDED_SOURCE_FILE_NAMES = "*.md"` 설정으로 방어해 둔 상태다. 다른 확장자의 비공개 파일을 잘못 추가하면 사용자와 App Store 심사관에게 그대로 노출되므로, 각별히 주의한다. -## 크래시로 이어지는 것 +## 3. 런타임 크래시 유발 요인 (의존성 주입) -**`@EnvironmentObject`는 호스팅 뷰마다 선택적으로 주입된다.** +각 호스팅 뷰에는 필요한 객체만 **선택적으로 주입**된다. -| 뷰 | 주입받는 것 | -|---|---| +| 뷰 (View) | 주입 객체 (`@EnvironmentObject`) | +| --- | --- | | `MenuContentView`, `SettingsView` | `appState` + `store` | -| `ResultView`, `ToastView`, `OnboardingView` | `appState` **만** | -| `PaywallView` | `store` **만** | +| `ResultView`, `ToastView`, `OnboardingView` | `appState` **만 주입** | +| `PaywallView` | `store` **만 주입** | -오른쪽 열에 없는 것을 `@EnvironmentObject`로 선언하면 **런타임에 크래시한다.** -필요하면 해당 컨트롤러(`ResultPanelController`, `ToastController` 등)의 주입 지점을 함께 고쳐야 한다. +**위 표에 명시되지 않은 객체를 뷰에서 `@EnvironmentObject`로 선언할 경우 런타임 크래시가 발생한다.** 의존성 추가가 불가피하다면 해당 컨트롤러(`ResultPanelController`, `ToastController` 등)의 뷰 초기화/주입 로직을 함께 수정해야 한다. -## 제거하면 안 되는 것 +## 4. 로직 삭제 및 수정 엄금 (Do Not Remove) -- `AppState.hotkey`의 `didSet`에 있는 `isRevertingHotkey` 플래그 — 단축키 등록 실패 시 되돌리는데, - 이 플래그가 없으면 `didSet`이 무한 재귀한다. `launchAtLogin`의 `isSyncingLaunchAtLogin`도 같다. -- `ScreenCapturer`의 **PID 필터와 150ms sleep 두 가지 모두** — 자기 창을 캡처에서 빼는 장치다. - 하나만 빼도 결과 패널이 캡처 이미지 안에 찍힌다. +* `AppState.hotkey`의 `didSet`에 포함된 `isRevertingHotkey` 플래그: 단축키 등록 실패 시 상태를 롤백하는 역할을 하며, 해당 플래그가 없으면 무한 재귀(Infinite Recursion)에 빠진다. `launchAtLogin`의 `isSyncingLaunchAtLogin` 플래그도 동일한 이유로 유지해야 한다. +* `ScreenCapturer`의 **PID 필터링과 150ms Sleep 로직**: 둘 모두 앱 자신의 윈도우(패널)를 캡처 영역에서 제외하기 위한 필수 방어 장치다. 하나라도 누락되면 캡처된 이미지 안에 결과 패널이 찍혀 나오는 버그가 발생한다. -## 알아야 사고를 피하는 것 +## 5. 도메인 특수성 및 기술적 함정 (Gotchas) -- **캡처 결과는 JPEG다.** PNG를 가정하지 않는다. `compressionFactor 0.82`, 최대 2000px로 축소한다. -- **좌표계가 2개다.** AppKit은 좌하단 원점, 디스플레이 로컬은 좌상단 원점이다. - `ScreenCapturer`에서 변환하고, `PanelPosition.origin`은 AppKit 기준을 가정한다. 다중 모니터 버그의 단골 원인. -- **`LSUIElement = true`**라 Dock 아이콘이 없다. 창을 띄우려면 `NSApp.activate(ignoringOtherApps:)`, - `.nonactivatingPanel`, `orderFrontRegardless()`가 필요하다. `SettingsLink`조차 활성화 제스처를 덧붙여야 열린다. -- **화면 기록 권한(TCC)은 2회 실행이 필요하다.** `CGRequestScreenCaptureAccess()`는 요청만 하고 `false`를 반환한다. - 첫 캡처는 항상 실패하는 것이 정상이다. Xcode에서 재빌드하면 권한이 조용히 풀릴 수 있다. -- **`imageCache`는 인메모리 8개 한도이고 영속화하지 않는다.** 이전 실행의 히스토리 항목은 - 해설을 재생성할 수 없다. 버그가 아니라 설계다. -- 단축키는 Carbon `RegisterEventHotKey`를 쓴다. 손쉬운 사용 권한이 필요 없고 MAS 심사를 통과하기 때문이다. - 다른 API로 바꾸지 말 것. +* **이미지 포맷**: 캡처 결과물은 항상 **JPEG** 포맷이다 (PNG 가정 금지). 용량 최적화를 위해 `compressionFactor 0.82`, 최대 2000px로 리사이징(축소)한다. +* **이중 좌표계**: AppKit은 좌하단(Bottom-Left)이 원점이고, 디스플레이 로컬 좌표계는 좌상단(Top-Left)이 원점이다. `ScreenCapturer`에서 이를 변환하여 사용하며, `PanelPosition.origin`은 AppKit 기준을 가정한다. 이는 다중 모니터 대응 시 가장 잦은 버그 발생 원인이므로 주의한다. +* **백그라운드 실행 (`LSUIElement = true`)**: Dock 아이콘이 없는 백그라운드 앱이므로 창을 화면에 띄우려면 `NSApp.activate(ignoringOtherApps:)`, `.nonactivatingPanel`, `orderFrontRegardless()` 등의 명시적 호출이 필수다. `SettingsLink` 조차 활성화 제스처를 덧붙여야 정상 작동한다. +* **화면 기록 권한(TCC) 정책**: 권한 승인 플로우 특성상 **앱을 2회 실행해야 정상 작동**한다. `CGRequestScreenCaptureAccess()`는 권한 요청만 띄우고 즉시 `false`를 반환하므로 최초 캡처 시도는 항상 실패하는 것이 시스템 정상 스펙이다. (Xcode 재빌드 시 권한이 조용히 말소될 수 있음에 유의한다.) +* **이미지 캐시 무상태성**: `imageCache`는 인메모리 상에서 최대 8개까지만 유지되며 디스크에 영속화(Persist)하지 않는다. 과거 히스토리 항목에서 해설을 재생성할 수 없는 것은 버그가 아니라 의도된 설계다. +* **단축키 API**: 손쉬운 사용(Accessibility) 권한 요구를 피하고 Mac App Store 심사를 통과하기 위해 Carbon의 `RegisterEventHotKey` API를 사용한다. 절대 다른 API로 교체하지 않는다. -## 과금 로직 — 건드리기 전에 읽을 것 +## 6. 과금 비즈니스 로직 (수정 전 필독) -무료 사용량 차감 규칙이 비대칭이고, 이건 매출에 직결된다. +무료 사용량 차감 규칙은 비대칭적이며, 이는 서비스 매출과 직결되는 핵심 경로(Critical Path)다. -- 차감은 **정답 호출이 성공한 뒤에만** 일어난다. -- 해설 생성이 실패해도 **재차감하지 않는다.** 정답은 이미 받았기 때문이다. -- 페이월은 **캡처 전에** 검사한다. 캡처 후가 아니다. -- 토스트 모드는 **의도적으로 해설을 생성하지 않는다.** - -## App Store 심사 제약 - -포지셔닝은 "풀이·해설 학습 도우미"다. **해설은 접을 수는 있어도 제거할 수 없다.** -빠른 채점 모드도 해설을 숨기는 게 아니라 클릭 뒤로 접어두는 것뿐이다. 이 성질을 깨는 변경은 심사에서 문제가 된다. +* 무료 횟수는 **API 정답 호출이 성공한 직후에만** 1회 차감한다. +* 해설 생성(2차 호출)이 실패하더라도 **횟수를 재차감하지 않는다.** (정답을 이미 제공받았기 때문) +* 무료 횟수 소진 여부 및 페이월(Paywall) 표시는 **캡처를 실행하기 전**에 사전 검증한다. +* 토스트 모드(Toast Mode)에서는 의도적으로 해설 API를 호출하지 않도록 제어해야 한다. diff --git a/README.md b/README.md index a91aee7..ba1c754 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,46 @@ # Paxo -화면 속 문제를 단축키 한 번으로 캡처하면 AI가 정답과 해설을 알려주는 macOS 메뉴바 학습 도우미. +> **macOS 메뉴바 기반 AI 학습 도우미** +> 화면 속 문제를 단축키 한 번으로 캡처하면 AI가 정답과 해설을 제공합니다. -## 사용법 -1. 메뉴바의 Paxo 아이콘 클릭 또는 전역 단축키 **⌥⌘S** (설정에서 변경 가능) -2. 풀이할 문제 영역을 드래그로 선택 (Esc 취소) — 설정에서 **전체 화면** 모드로 바꾸면 드래그 없이 즉시 캡처 -3. 정답이 먼저 뜨고, 해설이 이어서 표시됨 -4. **빠른 채점 모드** (설정): 정답만 먼저 크게 표시, 해설은 "해설 보기" 클릭 시 생성 — 문제집 셀프 채점용 -5. **결과 표시** (설정): 창 위치 7곳 중 선택, 또는 **토스트 모드**(정답만 2~10초 떴다 사라짐, 해설 생성 안 함) +## 1. 핵심 기능 및 사용법 -## 개발 셋업 +1. **캡처 (Capture)**: 메뉴바 아이콘 클릭 또는 전역 단축키 `⌥⌘S` (설정 변경 가능) +2. **영역 선택 (Selection)**: 문제 영역을 드래그하여 지정 (`Esc` 취소). 설정에서 **전체 화면** 모드 활성화 시 드래그 없이 즉시 캡처. +3. **결과 노출 (Result)**: 정답이 먼저 화면에 표시되고, 이어서 해설이 스트리밍된다. +4. **빠른 채점 모드 (Fast Grading)**: 설정에서 켤 수 있으며, 정답만 크게 강조 표시하고 해설은 "해설 보기" 클릭 시 Lazy-load 방식으로 생성한다 (문제집 셀프 채점용). +5. **토스트 모드 (Toast Mode)**: 정답만 토스트 팝업으로 2~10초간 노출된 후 사라지며 해설은 아예 생성하지 않는다. +6. **결과 패널 위치**: 화면 내 7가지 프리셋 중 원하는 위치를 지정할 수 있다. -> 팀 작업 흐름과 기여 방법은 [CONTRIBUTING.md](CONTRIBUTING.md), -> AI 에이전트 규칙은 [AGENTS.md](AGENTS.md), AI 사용 원칙은 [docs/ai-usage-rules.md](docs/ai-usage-rules.md) 참고. +## 2. 프로젝트 구조 -- 요구사항: macOS 14+, Xcode 16+ -- **시크릿 파일 생성 (클론 직후 1회 필수)** — `Paxo/Secrets.swift`는 Git에 올라가지 않으므로 직접 만들어야 한다: - - ```sh - cp Config/Secrets.example.swift Paxo/Secrets.swift - ``` - - 이 파일이 없으면 `Secrets` 심볼을 못 찾아 빌드가 실패한다. - 배포된 프록시에는 이미 `APP_TOKEN`이 설정돼 있다. 토큰을 빈 값으로 두면 빌드·실행은 되지만 - AI 호출이 전부 401로 실패한다. 값은 팀에서 받는다 (아래 '프록시 보안' 참고). - 템플릿을 `Paxo/` 안에 복사본으로 남겨두지 말 것 — 이 디렉토리는 파일시스템 동기화로 - 자동 컴파일되므로 `Secrets`가 중복 선언되어 빌드가 깨진다. -- `Paxo.xcodeproj` 열기 → Signing & Capabilities에서 Team 선택 → Run -- 첫 캡처 시 **화면 기록 권한** 허용 필요 (시스템 설정 → 개인정보 보호 및 보안 → 화면 및 시스템 오디오 녹음). 권한 허용 후 앱 재실행 필요할 수 있음 -- AI 연결: - - **프록시 (기본)**: 앱에 내장된 Vercel 프록시(`https://paxo-proxy.vercel.app`)를 통해 호출한다. 앱에 Gemini 키가 없다. 배포 방법은 [proxy-vercel/README.md](proxy-vercel/README.md) 참고 - - **직접 호출 (개발 빌드 전용)**: DEBUG 빌드에서만, 설정에서 프록시 URL을 비우고 Gemini API 키를 넣으면 직접 호출. 릴리즈 빌드에는 이 경로가 컴파일되지 않음 - -## 구독 테스트 (로컬, App Store Connect 불필요) - -1. Xcode에서 Product → Scheme → Edit Scheme… → Run → Options -2. **StoreKit Configuration**에서 `Config/Paxo.storekit` 선택 -3. 실행하면 무료 하루 3회 소진 시 페이월이 뜨고, 테스트 결제(실제 과금 없음)로 Pro 흐름 확인 가능 -4. 테스트 거래 초기화: Xcode → Debug → StoreKit → Manage Transactions - -실제 출시 시에는 App Store Connect에 동일한 product ID(`com.hyeseong.Paxo.pro.monthly` / `.yearly`)로 -구독 상품을 만들고, 스킴의 StoreKit Configuration을 None으로 되돌리면 된다. - -## 구조 - -``` +```text Paxo/ - PaxoApp.swift 앱 진입점 (MenuBarExtra + Settings) - AppState.swift 상태 머신: 캡처 → 정답 → 해설, 히스토리 - HotkeyManager.swift 전역 단축키 ⌥⌘S (Carbon, MAS 허용 API) - DefaultConfig.swift 프록시 URL 등 내장 기본 설정 - Secrets.swift 로컬 시크릿 (Git 제외 — Config/Secrets.example.swift 복사해서 생성) + PaxoApp.swift 앱 진입점 (MenuBarExtra, Settings) + AppState.swift 상태 머신 (캡처 → 정답 → 해설, 히스토리 관리) + HotkeyManager.swift 전역 단축키 `⌥⌘S` 처리 (Carbon, MAS 허용 API) + DefaultConfig.swift 내장 기본 설정 (프록시 URL 등) + Secrets.swift 로컬 시크릿 키 관리 (Git 제외됨) Capture/ - SelectionOverlay.swift 드래그 영역 선택 오버레이 - ScreenCapturer.swift ScreenCaptureKit 캡처 (샌드박스 OK) + SelectionOverlay.swift 캡처 영역 지정용 드래그 오버레이 + ScreenCapturer.swift ScreenCaptureKit 연동 캡처 로직 (App Sandbox 지원) AI/ - GeminiService.swift 2단계 호출: 정답(빠름) → 해설(필요 시) - Prompts.swift - UI/ 메뉴바 팝업 / 결과 패널 / 토스트 / 설정 / 페이월 / 마크다운 렌더러 - Store/ StoreKit 2 구독 + 무료 사용량 추적 - Storage/ 히스토리 JSON, 키체인 -PaxoTests/ 순수 로직 테스트 (Swift Testing) -Config/ Info.plist, entitlements (샌드박스), StoreKit 테스트 설정, 시크릿 템플릿 -proxy-vercel/ Vercel API 프록시 (기본 — 키 보호 + 토큰·본문 검증 + 사용량 제한) -proxy/ Cloudflare Worker 프록시 (백업 — Gemini 지역 차단 이슈로 강등) -docs/ 아키텍처 · 도메인 용어 · 결정 기록 · AI 사용 원칙 · 프롬프트 라이브러리 -.claude/skills/ Claude Code 스킬 (release-check, proxy-change, weekly-report) -.github/workflows/ CI — 빌드·테스트·포맷·금지파일 검사 -``` + GeminiService.swift AI API 연동 (정답 1차 호출 → 해설 2차 호출) + Prompts.swift 시스템 프롬프트 관리 + UI/ 메뉴바 팝업 / 패널 / 토스트 / 페이월 / 마크다운 렌더러 + Store/ StoreKit 2 연동 (구독 및 무료 횟수 관리) + Storage/ 로컬 히스토리 JSON 및 키체인 관리 +PaxoTests/ 순수 비즈니스 로직 테스트 (Swift Testing 프레임워크) +Config/ Info.plist, App Sandbox Entitlements, StoreKit 설정, 시크릿 템플릿 +proxy-vercel/ Vercel 기반 주 프록시 (Gemini API 키 보호, 토큰 검증, 사용량 제어) +proxy/ [Legacy] Cloudflare Worker 기반 백업 프록시 +docs/ 아키텍처, 도메인 용어, 의사결정 기록 등 문서화 +.claude/skills/ Claude Code 전용 커스텀 스킬 +.github/workflows/ CI 파이프라인 설정 (빌드, 테스트, 린트, 금지 파일 검사) -## 로드맵 - -- [x] v0.1 — 캡처(영역/전체 화면) → 정답+해설, 빠른 채점 모드, 토스트 모드, 결과 위치 선택, 히스토리 -- [x] API 프록시 서버 (proxy-vercel/, 배포 완료 — 토큰·본문 검증 포함) -- [x] StoreKit 2 구독 — 무료 하루 3회 → Pro(월간/연간, 7일 무료 체험) 무제한 -- [x] 단축키 변경, 복사 버튼, 로그인 시 자동 실행, 다중 모니터 대응 -- [ ] App Store Connect에 구독 상품 생성 (product ID 동일하게) -- [ ] 서버 측 구독 검증 — StoreKit 영수증(JWS)을 프록시에서 확인, 클라이언트 카운트 대체 -- [x] 앱 아이콘 (Assets.xcassets — 원본 스크립트로 재생성 가능) + 온보딩(환영·권한·시작 3단계) -- [x] AX 인프라 — AGENTS.md/CLAUDE.md, CI 게이트, 테스트 타깃, 프롬프트 라이브러리 -- [ ] App Store 제출 (제출 전 `vercel env add APP_TOKEN production` 강제화 — 아래 '프록시 보안' 참고) -- [ ] v2 — 오답노트 자동 축적, 복습 알림(SRS) - -## 프록시 보안 (APP_TOKEN) - -앱은 프록시에 `x-paxo-token` 헤더로 공유 토큰을 보내고, 프록시는 `APP_TOKEN` 환경변수가 설정돼 있으면 이를 검증한다. - -- 토큰 값은 `Paxo/Secrets.swift`의 `appToken`과 Vercel `APP_TOKEN` 환경변수가 **일치**해야 한다. - 이 저장소는 공개되어 있으므로 `Paxo/Secrets.swift`는 `.gitignore` 대상이며, 커밋되는 것은 - 값이 빈 템플릿(`Config/Secrets.example.swift`)뿐이다. -- **배포 순서 (401 사고 방지)**: 프록시 배포(토큰 미설정 → 무중단) → 앱 빌드 배포 → `vercel env add APP_TOKEN production` 입력 → `npx vercel --prod` 재배포 → 이때부터 토큰 없는 요청 401. -- 로테이션: 새 토큰을 `APP_TOKEN`, 기존을 `APP_TOKEN_PREV`로 두면 구버전 앱도 한동안 동작. -- 팀 테스트용 토큰은 `APP_TOKEN_PREV` 슬롯에 둔다. 프로덕션 토큰과 분리돼 있어, 유출되거나 - 테스트가 끝나면 `APP_TOKEN_PREV`만 지우고 재배포해 출시 앱에 영향 없이 회수할 수 있다. -- 배포 직후 검증은 몇 초 기다린다. `vercel --prod` 반환 시점과 별칭이 새 배포로 - 전환되는 시점 사이에 짧은 지연이 있어, 바로 확인하면 아직 옛 배포가 응답해 401이 난다. - 값이 틀린 걸로 오해하기 쉬우니 `sleep 10` 후 재확인한다. -- 검증 curl은 `x-paxo-device`를 일부러 UUID가 아닌 값으로 보낸다. `400 bad device id`가 뜨면 성공 - (토큰 검사를 통과하고 다음 단계에서 걸린 것). Gemini를 호출하지 않아 비용도 들지 않는다. - 배포 직통 URL(`paxo-proxy-xxxx.vercel.app`)은 Deployment Protection 때문에 별도로 401이 나므로 - 반드시 별칭(`paxo-proxy.vercel.app`)으로 확인한다. -- 바이너리에서 추출 가능한 공유 시크릿이므로 완전한 방어는 아니다. 드라이브바이 어뷰징 차단용이며, 정식 방어(기기별 내구성 한도·영수증 검증)는 v2 과제. - -## App Store 메모 - -- 샌드박스 필수 — 현재 entitlements 구성 완료 (`app-sandbox` + `network.client`) -- 화면 캡처(ScreenCaptureKit + TCC)와 전역 단축키(RegisterEventHotKey)는 MAS 심사 허용 범위 -- 구독은 Apple IAP 의무 (Small Business Program 수수료 15%) -- 포지셔닝: "풀이·해설 학습 도우미" — 해설은 항상 접근 가능해야 함 (빠른 채점 모드도 해설을 숨기지 않고 접어둘 뿐) - -## 브랜치 - -- `develop` — 기본 브랜치. 기능 PR은 여기로 보내고 Squash로 머지한다 -- `main` — 출시본. 릴리즈 PR(develop → main)만 Merge로 머지한다 +``` -## 라이선스 +## 3. License -[MIT](LICENSE) +이 프로젝트는 [MIT 라이선스](https://www.google.com/search?q=LICENSE&utm_source=gemini)를 따릅니다. diff --git a/docs/ai-usage-rules.md b/docs/ai-usage-rules.md index f491a88..be85fbe 100644 --- a/docs/ai-usage-rules.md +++ b/docs/ai-usage-rules.md @@ -1,56 +1,41 @@ -# AI 사용 규칙 +# AI 활용 가이드라인 -4인이 10인처럼 움직이는 것이 목표고, 수단은 각자 AI를 잘 쓰는 것이 아니라 -**팀의 작업 흐름 자체를 AI 전제로 설계하는 것**이다. +우리의 목표는 4명의 인원으로 10명분의 성과를 내는 것이다. +이를 위해 단순히 개인의 AI 활용 능력을 높이는 것을 넘어, 팀의 업무 프로세스 자체를 AI 중심으로 재설계한다. ## 3원칙 -### 1. 초안은 AI가 +### 1. 모든 초안은 AI가 작성한다 -백지에서 시작하지 않는다. 사람은 검토하고 결정한다. -PR 설명, 커밋 메시지, 릴리스 노트, 버그 재현 코드, 회의록 요약은 전부 초안부터 AI가 만든다. +어떤 업무든 백지상태에서 시작하지 않는다. AI가 먼저 초안을 작성하고, 사람은 이를 검토하고 결정하는 역할에 집중한다. +PR 설명, 커밋 메시지, 릴리스 노트, 버그 재현 코드, 회의록 요약 등은 모두 AI를 활용해 초안을 만든다. -### 2. 프롬프트는 자산 +### 2. 프롬프트는 문서화한다 -잘 된 프롬프트는 개인 것이 아니다. +잘 작동하는 프롬프트는 개인의 노하우로만 두지 않고 팀 전체와 공유한다. +동일한 프롬프트를 두 번 이상 사용했다면 즉시 문서화하여 남긴다. - 개발용 → `docs/prompts/` (버전 관리 대상) - 마케팅·리서치·회의록 → 노션 프롬프트 DB -두 번 이상 같은 프롬프트를 입력했다면 그때가 저장할 시점이다. -### 3. 책임은 사람 +### 3. 최종 책임은 사람이 진다 -**커밋한 사람이 전적으로 책임진다.** 초안을 누가 썼는지는 묻지 않고, 커밋에 표기하지도 않는다. -"AI가 그렇게 썼다"는 해명이 되지 않는다. - -숫자, 출처, 외부 API 동작, 법적 문구는 **반드시 담당자가 직접 확인한 뒤** 반영한다. +모든 결과물의 최종 책임은 커밋(반영)한 담당자에게 있다. +코드나 문서의 초안을 누가 썼는지는 묻지 않으며, 특히 숫자, 출처, 외부 API 동작, 법적 문구 등은 반드시 담당자가 직접 팩트 체크를 진행한 후 반영한다. ## 결과물 검토 체크리스트 -머지 전에 스스로 확인한다. PR 템플릿에도 같은 항목이 있다. - -- [ ] 빌드와 테스트를 **직접 돌렸다.** AI가 "통과할 것"이라고 말한 것을 믿지 않았다 -- [ ] 코드에 들어간 **숫자와 문자열을 직접 확인**했다 (한도, 가격, URL, 제품 ID) -- [ ] **기존 유틸리티를 검색**했다. 비슷한 게 이미 있는데 새로 만들지 않았다 -- [ ] 외부 문서를 인용했다면 **원문을 열어봤다** -- [ ] 프록시 계약을 건드렸다면 **클라이언트와 서버 양쪽**을 고쳤다 -- [ ] 시크릿이나 비공개 문서가 섞이지 않았다 (저장소는 **공개**) - -## 알아둘 함정 - -이 규칙은 이론이 아니라 실제 실패 사례에서 나왔다. - -| 사례 | 무슨 일이 있었나 | 우리의 대응 | -|---|---|---| -| METR | AI 쓴 개발자가 19% 느려졌는데 본인은 빨라졌다고 느꼈다 | 체감 대신 PR 리드타임을 실측한다 | -| GitClear | 중복 코드 8배, 리팩터링 39.9% 감소 | "기존 유틸 검색"을 체크리스트에 넣었다 | -| DORA 2025 | AI 도입률 25%↑마다 배포 안정성 7.2%↓ | 속도보다 CI 게이트와 테스트를 먼저 깔았다 | -| 여기어때 | 에이전트마다 코드 스타일이 파편화됐다 | AGENTS.md 단일 원본 + swift-format CI 강제 | -| AWS 현장 실험 | 도구만 바꾼 팀은 성과가 없었다 (50개 중 25개만 개선) | 흐름을 바꿨다 — main 직접 커밋에서 PR 필수로 | +- [ ] AI의 예측을 맹신하지 않고, 직접 빌드 및 테스트를 수행했다. +- [ ] 코드에 포함된 숫자와 문자열(한도, 가격, URL, 제품 ID 등)을 직접 검증했다. +- [ ] 기존 유틸리티 코드를 검색하여 중복 개발을 방지했다. +- [ ] 외부 문서를 인용하거나 참고한 경우, 반드시 원문을 직접 확인했다. +- [ ] 프록시 컨트랙트(또는 API)를 수정한 경우, 클라이언트와 서버 양쪽 모두에 반영했다. +- [ ] 공개(Public) 저장소에 시크릿 키나 비공개 문서가 포함되지 않았는지 확인했다. ## 도구별 설정 -- **Claude Code** — `CLAUDE.md`를 읽는다. `@AGENTS.md`로 공통 규칙을 가져온 뒤 Claude 전용 항목을 덧붙였다 -- **Codex** — `AGENTS.md`를 직접 읽는다. 하위 디렉토리의 `AGENTS.md`는 그쪽 파일을 만질 때 적용된다 -- 규칙을 바꿀 때는 **`AGENTS.md`를 고친다.** `CLAUDE.md`는 파생 파일이다 +- **Claude Code** : CLAUDE.md 파일을 읽는다. @AGENTS.md를 통해 공통 규칙을 임포트하고, Claude 전용 설정만 추가로 정의한다. +- **Codex** : AGENTS.md를 직접 읽는다. 하위 디렉터리에 위치한 AGENTS.md는 해당 디렉터리 내의 파일을 수정할 때 우선 적용된다. +- **공통 규칙 변경**: 팀의 규칙을 변경할 때는 반드시 AGENTS.md를 수정한다. (CLAUDE.md는 파생 파일이므로 직접 수정하지 않는다) +- \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md index a7cf8a1..2cac371 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,55 +1,56 @@ # 아키텍처 -> 상태: 초안 — 킥오프 후 전원 검증. AI 리뷰가 읽는 문서다. 코드와 어긋나면 기록성 이슈로 바로 고친다. - ## 구성 요소 | 구성 요소 | 위치 | 역할 | | --- | --- | --- | -| 메뉴바 앱 | `Paxo/` | 전역 단축키 → 화면 캡처 → 프록시 호출 → 결과 표시. macOS 14+, SwiftUI + AppKit | -| API 프록시 | `proxy-vercel/` | Gemini API 키를 서버에 두고 요청을 대신 보낸다. Vercel `iad1` 리전 | -| Gemini API | 외부 | 정답 · 해설 생성. 모델은 프록시가 정한다 | -| StoreKit 2 · App Store | 외부 | Pro 구독(월간 · 연간) 결제와 권한 확인 | -| 레거시 프록시 | `proxy/` | Cloudflare Worker 백업. 신규 작업하지 않는다 | +| 메뉴바 앱 | `Paxo/` | 전역 단축키 → 화면 캡처 → 프록시 호출 → 결과 표시. (macOS 14+, SwiftUI + AppKit) | +| API 프록시 | `proxy-vercel/` | 서버에 Gemini API 키를 보관하고 클라이언트 대신 요청을 수행한다. (Vercel iad1 리전) | +| Gemini API | 외부 | 정답 및 해설 생성. 사용할 모델은 프록시 서버에서 결정한다. | +| StoreKit 2 · App Store | 외부 | Pro 요금제(월간/연간) 결제 처리 및 구독 상태 검증. | +| 레거시 프록시 | `proxy/` | Cloudflare Worker 기반의 백업용 프록시. 신규 개발은 진행하지 않는다. | -## 한 번의 풀이 흐름 +## 단일 풀이 흐름 (Solve Flow) ``` -단축키 ⌥⌘S - → AppState.beginSolve 무료 사용량 · Pro 확인 (부족하면 캡처 전에 페이월) - → SelectionOverlay 영역 선택 (전체 화면 모드면 생략) - → ScreenCapturer ScreenCaptureKit, JPEG q0.82, 최대 2000px - → GeminiService POST {프록시}/generate — 정답 요청 - → proxy-vercel/api/generate 헤더 검증 → 본문을 허용 형태로 재조립 → Gemini - → 결과 패널 · 토스트 정답 표시 - → (필요 시) 해설 요청 같은 경로로 한 번 더 +단축키 ⌥⌘S 입력 + → AppState.beginSolve 무료 제공량 및 Pro 구독 여부 확인 (한도 초과 시 캡처 전 결제 화면 노출) + → SelectionOverlay 캡처 영역 선택 (전체 화면 모드일 경우 생략) + → ScreenCapturer ScreenCaptureKit 사용 (JPEG q0.82, 최대 2000px) + → GeminiService POST {프록시}/generate — 프록시 서버에 정답 요청 + → proxy-vercel/api/generate 요청 헤더 검증 → 본문을 허용된 스펙으로 재조립 후 Gemini 호출 + → 결과 패널 및 토스트 UI에 정답 표시 + → (필요 시) 해설 요청 동일한 경로로 해설 추가 요청 ``` -- 정답과 해설은 따로 요청한다. 정답을 먼저 보여주기 위해서다 -- 무료 사용량은 정답 요청이 성공한 뒤에만 차감한다. 해설 실패는 다시 차감하지 않는다 -- 토스트 모드는 해설을 요청하지 않는다 +- 정답과 해설은 각각 별도의 API 요청으로 처리한다. +- 무료 횟수는 '정답 요청'이 성공했을 때만 차감한다. 해설 요청 실패 시에는 추가로 차감하지 않는다. +- 토스트 모드에서는 해설을 요청하지 않는다. + +## 앱 ↔ 프록시 API 스펙 (Contract) -## 앱 ↔ 프록시 계약 +핵심 요약만 기재하며, 상세 스펙은 proxy-vercel/AGENTS.md를 정본으로 삼는다. -요약만 적는다. 정본은 `proxy-vercel/AGENTS.md`. +- 엔드포인트: POST /generate +- 필수 헤더: x-paxo-device (UUID), x-paxo-token +- 본문(Body): 텍스트 파트 1개, 이미지 파트 1개만 허용한다. 프록시 서버에서 허용된 필드만 추출해 재조립하므로, 이외의 필드는 전송해도 무시된다. +- 변경 규칙: API 스펙 변경 시 Paxo/AI/GeminiService.swift(클라이언트)와 proxy-vercel/api/generate.js(서버)를 반드시 동일한 PR에서 수정한다. 배포는 프록시 서버가 선행되어야 한다. -- `POST /generate`, 헤더 `x-paxo-device`(UUID) · `x-paxo-token` -- 본문은 텍스트 파트 1개 + 이미지 파트 1개만 허용한다. 프록시가 허용 목록으로 다시 조립하므로 다른 필드는 전달되지 않는다 -- 계약을 바꾸면 `Paxo/AI/GeminiService.swift`와 `proxy-vercel/api/generate.js`를 **같은 PR**에서 고치고, 배포는 프록시가 먼저다 -## 데이터가 있는 곳 +## 데이터 저장소 -| 데이터 | 위치 | +| 데이터 | 저장 위치 및 방식 | | --- | --- | | 설정 · 단축키 · 무료 사용량 | UserDefaults | -| 풀이 기록 (최대 100개, 이미지 제외) | 앱 컨테이너의 `history.json` | -| 캡처 이미지 | 메모리에만 최근 8개. 디스크에 쓰지 않는다 | +| 최근 풀이 기록 | 앱 컨테이너 내 `history.json` (최대 100개, 이미지는 제외) | +| 캡처 이미지 | 최근 8장만 메모리에 유지. 디스크에 저장(I/O)하지 않는다. | | 구독 상태 | StoreKit 2 `Transaction.currentEntitlements` | -| Gemini API 키 | 프록시 환경변수. 앱에는 없다 | +| Gemini API 키 | Gemini API 키,프록시 서버의 환경 변수로 관리. 클라이언트 앱에는 절대 포함하지 않는다. | + -## 배포 +## 배포 파이프라인 -| 대상 | 방법 | 실행 | +| 대상 | 배포 방식 | 실행 주체 | | --- | --- | --- | -| 프록시 | Vercel. 프리뷰는 자동, 프로덕션은 사람이 실행 | 과금 · 프록시 가디언 | -| 앱 | develop → main 릴리즈 PR, 태그, Xcode Archive → App Store Connect | 사람만 | +| 프록시 | Vercel (Preview는 자동 배포, Production은 수동 배포) | 과금/프록시 담당자 | +| 앱 | develop → main 릴리스 PR 머지 → 태그 생성 → Xcode Archive → App Store Connect | 담당자 직접 실행 (수동) | diff --git a/docs/domain.md b/docs/domain.md index 11353b6..a848588 100644 --- a/docs/domain.md +++ b/docs/domain.md @@ -1,39 +1,34 @@ # 도메인 용어 · 규칙 -> 상태: 뼈대 — 킥오프에서 전원 보강. AI 리뷰가 읽는 문서다. 새 용어가 생기면 기록성 이슈로 한 줄 추가한다. +> 새로운 도메인 용어가 추가되면 기록용 이슈(Record Issue)를 생성하고 본 문서에 즉각 업데이트한다. -## 사용자에게 보이는 기능 +## 사용자 대면 기능 (Client-side) -| 용어 | 뜻 | 코드 | +| 용어 | 정의 및 규칙 | 코드 위치 | | --- | --- | --- | -| 캡처 | 화면 일부(영역) 또는 전체를 찍는 것. 전역 단축키 기본값 ⌥⌘S | `Paxo/Capture/` | -| 정답 | 캡처한 문제의 답. 먼저 빠르게 보여준다 | `Paxo/AI/` | -| 해설 | 풀이 과정. 정답 뒤에 이어서 만든다. **접을 수는 있어도 없앨 수는 없다** (App Store 심사 포지셔닝) | `Paxo/AI/` | -| 빠른 채점 모드 | 정답만 크게 보여주고, 해설은 "해설 보기"를 누를 때 만든다 | 설정 | -| 토스트 모드 | 정답만 몇 초 떴다 사라진다. 해설을 만들지 않는다 | `ToastController` | -| 결과 위치 | 결과 창이 뜨는 위치. 7곳 중 하나 | `PanelPosition` | -| 히스토리 | 지난 풀이 기록. 최대 100개, 이미지는 저장하지 않는다 | `HistoryStore` | -| 온보딩 | 첫 실행 3단계 (환영 · 화면 기록 권한 · 시작) | `OnboardingView` | +| 캡처 (Capture) | 화면 일부(영역) 또는 전체를 찍는 것. 전역 단축키 기본값 ⌥⌘S | `Paxo/Capture/` | +| 정답 (Answer) | 캡처한 문제의 도출된 답. 사용자에게 가장 먼저 빠르게 노출된다. | `Paxo/AI/` | +| 해설 (Explanation) | 문제 풀이 과정. 정답 하단에 이어서 생성된다. | `Paxo/AI/` | +| 빠른 채점 모드 | 정답만 강조하여 노출하며, 해설은 사용자가 "해설 보기"를 클릭할 때만 생성(Lazy Load)하는 모드. | 설정 (Settings) | +| 토스트 모드 | 정답이 일정 시간 동안 토스트 알림으로 표시된 후 사라지는 모드. 해설은 생성하지 않는다. | `ToastController` | +| 결과 위치 | 결과 패널이 렌더링되는 화면 내 위치 (총 7가지 프리셋 중 택 1). | `PanelPosition` | +| 히스토리 (History) | 과거 문제 풀이 기록. 최대 100개까지 보관하며, 캡처된 원본 이미지는 저장하지 않는다. | `HistoryStore` | +| 온보딩 (Onboarding) | 앱 최초 실행 시 제공되는 3단계 안내 플로우 (환영 → 화면 기록 권한 요청 → 시작). | `OnboardingView` | -## 과금 -| 용어 | 뜻 | -| --- | --- | -| 무료 사용량 | 하루 3회. 정답 요청이 성공했을 때만 1회 차감한다 | -| Pro | 월간 · 연간 구독, 무제한. 7일 무료 체험 | -| 페이월 | 무료 사용량을 다 쓰면 **캡처 전에** 뜬다 | - -## 개발 용어 +## 과금 정책 (Billing) | 용어 | 뜻 | | --- | --- | -| 계약 | 앱과 프록시가 주고받는 요청 형태. 양쪽을 같은 PR에서 고친다 | -| 위험 경로 | 과금 · 프록시 · 릴리즈 설정 파일. 머지 전에 가디언 승인이 필요하다 | -| 기록성 / 규칙성 | 문서 변경의 두 종류. 기록성(사실 · 경로)은 바로 머지, 규칙성(남을 구속)은 가디언 승인 | -| 참조 구현 | "새로 만들 때 이걸 보고 만든다"로 지정한 기존 코드. 각 AGENTS.md에 경로를 적는다 | +| 무료 제공량 | 일일 기본 제공 횟수(3회). API를 통한 '정답 요청'이 성공한 경우에만 1회 차감된다. | +| Pro 플랜 | 기능 무제한 사용이 가능한 유료 구독 플랜(월간/연간). 최초 7일간 무료 체험을 제공한다. | +| 페이월 (Paywall) | 무료 제공 횟수 소진 시 노출되는 결제 유도 화면. 반드시 화면 캡처가 실행되기 전에 표시되어야 한다.| -## 킥오프에서 채울 것 +## 개발 및 정책 용어 -- [ ] 주요 사용자 (수험생 · 대학생 · 자격증 준비생 등)와 대표 사용 장면 -- [ ] 잘 푸는 문제 유형과 한계 (손글씨 · 도형 · 긴 지문 등) -- [ ] 오답 · 실패 · 한도 초과 때 사용자에게 보여주는 문구 원칙 +| 용어 | 정의 및 규칙 | +| --- | --- | +| API 스펙 (Contract) | 앱(클라이언트)과 프록시 서버 간의 API 요청/응답 형태. 스펙 변경 시 반드시 양쪽 코드를 동일한 PR에서 수정한다. | +| 위험 경로 (Critical Path)| 과금, 프록시, 릴리스 설정 등 핵심 로직과 관련된 파일 경로. 해당 코드 수정 시 머지(Merge) 전 반드시 가디언(지정된 책임 리뷰어)의 승인이 필요하다. | +| 기록성 / 규칙성 변경 | 문서 업데이트의 2가지 분류. 기록성 (사실·경로): 리뷰 없이 즉시 머지 가능. 규칙성 (행동 제약): 반드시 가디언 승인 후 머지. | +| 레퍼런스 코드 (Reference) | 신규 기능 개발 시 기준이 되는 기존 코드. 각 디렉터리의 AGENTS.md에 해당 경로를 명시하여 참조하도록 한다. | diff --git a/proxy-vercel/AGENTS.md b/proxy-vercel/AGENTS.md index db5fb5c..34b56ed 100644 --- a/proxy-vercel/AGENTS.md +++ b/proxy-vercel/AGENTS.md @@ -1,69 +1,82 @@ -# proxy-vercel/ — API 프록시 (기본 경로) +# proxy-vercel/ — API 프록시 서버 (주요 경로) -전역 규칙은 저장소 루트의 `AGENTS.md`를 따른다. +> 전역 규칙은 저장소 루트의 `AGENTS.md`를 참조한다. -앱은 Gemini를 직접 호출하지 않는다. 이 프록시가 API 키를 쥐고 대신 호출한다. -릴리스 빌드에는 직접 호출 경로가 아예 컴파일되지 않는다. +클라이언트(앱)는 Gemini API를 직접 호출하지 않는다. 본 프록시 서버가 API 키를 은닉한 상태로 요청을 위임(Delegate)받아 처리한다. (릴리스 빌드에는 클라이언트의 직접 호출 경로 자체가 컴파일되지 않는다.) -## 가장 중요한 규칙 — 계약은 양쪽을 동시에 고친다 +## 1. 최우선 규칙: API 스펙(Contract) 변경의 동기화 -**프록시는 요청 본문을 통과시키지 않고 화이트리스트로 재조립한다** (`api/generate.js`의 `buildUpstreamBody`). +**프록시 서버는 클라이언트의 요청 본문(Body)을 그대로 전달(Bypass)하지 않으며, 허용된 필드만 추출하여 재조립(Build Up)한다.** (`api/generate.js` 내 `buildUpstreamBody` 참조) -허용되는 것은 정확히 이것뿐이다. +현재 허용(Whitelist)되는 페이로드 스펙은 정확히 다음과 같다. -- `text` 파트 1개 (8000자 이하) -- `inline_data` 파트 1개 (`image/jpeg` 또는 `image/png`, base64 6,000,000자 이하) +* `text` 파트 1개 (최대 8,000자 제한) +* `inline_data` 파트 1개 (`image/jpeg` 또는 `image/png`, Base64 인코딩 기준 최대 6,000,000자 제한) -`generationConfig`, `systemInstruction`, `safetySettings`, `tools`, 추가 파트, 두 번째 텍스트 파트는 -**구조적으로 전달이 불가능하다.** 클라이언트에서 넣으면 400이 돌아온다. +`generationConfig`, `systemInstruction`, `safetySettings`, `tools`, 추가 파트(Parts), 다중 텍스트 파트 등은 **구조적으로 전달이 불가능하다.** 클라이언트가 해당 필드를 포함해 요청하더라도 프록시 단에서 무시되거나 HTTP 400 에러를 반환한다. -따라서 `Paxo/AI/GeminiService.swift`의 요청 형태를 바꾸려면 `api/generate.js`를 **같은 PR에서 함께** 고쳐야 한다. -스트리밍 전환도 마찬가지로 양쪽 변경이 필요하다. +따라서 `Paxo/AI/GeminiService.swift`의 요청 형태를 변경하려면 반드시 `api/generate.js`의 파싱 로직을 **동일한 PR에서 함께 수정**해야 한다. (예: 스트리밍 응답으로의 전환 시에도 양측 코드의 동시 수정이 필수다.) -## 요청·응답 계약 +## 2. API 요청 및 응답 규격 -``` +### 요청 (Request) + +```http POST {proxyURL}/generate - content-type: application/json - x-paxo-device: - x-paxo-token: <공유 시크릿> +Content-Type: application/json +x-paxo-device: +x-paxo-token: <공유 시크릿(Shared Secret)> + +{ + "contents": [ + { + "parts": [ + { "text": "..." }, + { "inline_data": { "mime_type": "image/jpeg", "data": "" } } + ] + } + ] +} -{"contents":[{"parts":[{"text":"..."}, - {"inline_data":{"mime_type":"image/jpeg","data":""}}]}]} ``` -응답: 200과 429만 그대로 전달하고, 나머지 상태는 전부 502로 정규화한다. -업스트림 에러 본문은 클라이언트에 노출하지 않는다. -에러 형태는 Gemini와 동일한 `{error:{code,message}}` — 클라이언트가 파서 하나로 처리하기 위해서다. +### 응답 (Response) + +* HTTP 200(성공)과 429(Rate Limit) 상태 코드만 클라이언트에 그대로 전달한다. +* 그 외 업스트림(Gemini)에서 발생한 모든 에러 상태는 **HTTP 502(Bad Gateway)로 정규화(Normalize)하여 반환**한다. +* 보안을 위해 업스트림의 상세 에러 본문은 클라이언트에 노출하지 않는다. +* 클라이언트가 단일 파서로 에러를 처리할 수 있도록, 에러 반환 포맷은 Gemini 원본과 동일한 `{ "error": { "code", "message" } }` 구조를 유지한다. + +## 3. APP_TOKEN 무중단 배포 순서 -## APP_TOKEN 배포 순서 +아래 배포 순서 미준수 시 기존 프로덕션 버전 앱에서 401(Unauthorized) 에러가 대량 발생한다. -순서를 어기면 기존 사용자 앱이 401을 맞는다. +1. **프록시 1차 배포**: `APP_TOKEN` 환경 변수 없이 배포한다. (토큰 검증 로직이 우회되어 기존 앱의 무중단 서비스 가능) +2. **클라이언트 배포**: 신규 토큰 값이 하드코딩된 앱 빌드를 릴리스한다. +3. **프록시 환경 변수 주입**: Vercel 콘솔 또는 CLI를 통해 토큰을 등록한다 (`vercel env add APP_TOKEN production`). +4. **프록시 2차 배포(적용)**: `npx vercel --prod` 명령어로 재배포한다. **이 시점부터 토큰이 없거나 불일치하는 클라이언트 요청은 401 에러로 차단된다.** -1. `APP_TOKEN` 없이 프록시 배포 (검증 생략 = 무중단) -2. 토큰이 들어간 앱 빌드 배포 -3. `vercel env add APP_TOKEN production` -4. `npx vercel --prod` 재배포 — 이때부터 토큰 없는 요청이 401 +*토큰 로테이션(Rotation)*: 신규 토큰은 `APP_TOKEN`에, 기존 토큰은 `APP_TOKEN_PREV`에 할당하면 구버전 앱의 하위 호환성을 일정 기간 유지할 수 있다. 이 값은 클라이언트의 `Paxo/Secrets.swift` 내 `appToken` 속성과 정확히 일치해야 한다. -로테이션은 새 토큰을 `APP_TOKEN`, 기존 것을 `APP_TOKEN_PREV`에 두면 구버전 앱도 한동안 동작한다. -값은 `Paxo/Secrets.swift`의 `appToken`과 정확히 일치해야 한다. +## 4. 환경 변수 (Environment Variables) -## 환경변수 +| 변수명 | 필수 여부 | 기본값 | 설명 | +| --- | --- | --- | --- | +| `GEMINI_API_KEY` | **필수** | — | 누락 시 서버 구동 불가 (HTTP 500 에러 발생) | +| `APP_TOKEN` | 선택 | 없음 | 설정된 경우에만 클라이언트 토큰 검증 로직이 활성화됨 | +| `APP_TOKEN_PREV` | 선택 | 없음 | 구버전 하위 호환 및 토큰 로테이션 목적의 예비 슬롯 | +| `MODEL` | 선택 | `gemini-2.5-flash` | 호출할 업스트림 LLM 모델 지정 | -| 이름 | 필수 | 기본값 | -|---|---|---| -| `GEMINI_API_KEY` | **필수** — 없으면 500 | — | -| `APP_TOKEN` | 선택 — 설정 시에만 검증 | 없음 | -| `APP_TOKEN_PREV` | 선택 — 로테이션용 | 없음 | -| `MODEL` | 선택 | `gemini-2.5-flash` | +## 5. 코드 스타일 (Code Convention) -## 코드 스타일 +* **언어 및 포맷팅**: 순수 JavaScript (ESM 기준). 쌍따옴표(`""`), 2 Space 들여쓰기, 문장 끝 세미콜론(`;`) 사용을 엄수한다. +* **명명 규칙**: 모듈 레벨 상수는 `SCREAMING_SNAKE_CASE` 포맷을 사용한다. +* **함수 선언**: 헬퍼 함수는 호이스팅(Hoisting)이 가능하도록 `function` 키워드로 선언하며, 화살표 함수(`=>`)는 범위가 제한적인 작은 콜백이나 익명 함수에만 제한적으로 사용한다. +* **주석**: 로직의 동작 방식(How)이 아닌 '의도(Why)'를 한국어로 작성한다. (본 프로젝트는 TypeScript가 아니므로 타입 정보보다 의도 전달이 더 중요하다.) -ESM, 큰따옴표, 2칸 들여쓰기, 세미콜론 항상. 모듈 상수는 SCREAMING_SNAKE. -헬퍼는 호이스팅되는 `function` 선언으로, 작은 지역 함수만 화살표 함수로. -주석은 한국어로 *왜*를 적는다. TypeScript가 아니다. +## 6. [레거시] proxy/ (Cloudflare Worker) 디렉터리 정책 -## proxy/ (Cloudflare Worker)는 폐기된 백업이다 +`proxy/` 디렉터리는 과거 Cloudflare Worker 기반의 백업용 프록시 코드를 담고 있으나, **현재는 폐기(Deprecated)되었다.** -Gemini가 Workers 요청을 지역 차단하는 문제로 강등됐다. Vercel은 `iad1` 리전에 고정해 이를 피한다. -신규 작업은 이 디렉토리에만 한다. `proxy/`는 본문 검증이 없고 에러 형태도 다르다. +* **폐기 사유**: Gemini API가 Cloudflare Worker 대역의 요청을 지역(Region) 기반으로 차단하는 이슈가 발생하여 운영 환경에서 배제되었다. 현재의 `proxy-vercel/`은 Vercel `iad1` 리전으로 요청을 고정하여 이 문제를 우회한다. +* **유지 보수 금지**: `proxy/` 디렉터리는 본문 재조립 검증 로직이 누락되어 있으며 에러 정규화 포맷도 다르다. **향후 모든 프록시 관련 신규 개발 및 수정은 반드시 본 `proxy-vercel/` 디렉터리에서만 진행한다.** diff --git a/proxy-vercel/README.md b/proxy-vercel/README.md index 3722099..d7b2b3c 100644 --- a/proxy-vercel/README.md +++ b/proxy-vercel/README.md @@ -1,56 +1,69 @@ -# Paxo 프록시 — Vercel 버전 +# proxy-vercel/ — Vercel 기반 API 프록시 서버 -Cloudflare Workers 버전(`proxy/`)은 아웃바운드 IP가 간헐적으로 Gemini 미지원 지역을 경유해 -**"User location is not supported" 오류가 랜덤하게 발생**하는 문제가 있다 (실측 4~8/8 실패). -이 버전은 **미국 리전(iad1) 고정 실행**이라 해당 문제가 구조적으로 발생하지 않는다. +기존 Cloudflare Workers 버전(`proxy/`)의 아웃바운드 IP가 간헐적으로 Gemini 미지원 지역을 경유하여 "User location is not supported" 에러가 발생하는 치명적 결함을 해결하기 위해 도입된 아키텍처다. +본 Vercel 버전은 **실행 리전을 미국(`iad1`)으로 강제 고정**하여 해당 문제를 구조적으로 원천 차단한다. -앱 수정은 불필요 — 요청/응답 형식과 경로(`/generate`, `/privacy`)가 워커 버전과 동일하다. +> **클라이언트 하위 호환성**: 라우팅 경로(`/generate`, `/privacy`) 및 요청/응답 스펙이 기존 Worker 버전과 100% 동일하므로, 프록시 전환에 따른 앱(클라이언트) 코드 수정은 불필요하다. -## 배포 (약 5분) +## 1. 배포 파이프라인 (Deployment) -```bash -cd proxy-vercel +소요 시간은 약 5분 내외이며, Vercel CLI를 통해 진행한다. -# 1. Vercel 로그인 (계정 없으면 무료 가입, 브라우저 인증) +```bash +# 1. Vercel CLI 로그인 (계정이 없을 경우 브라우저 창에서 무료 가입/인증 진행) npx vercel login -# 2. 프로젝트 배포 (질문에는 기본값으로 Enter, 프로젝트명: paxo-proxy) +# 2. 프로젝트 최초 배포 (프롬프트 질의 시 모두 기본값(Enter) 사용, 프로젝트명: paxo-proxy) npx vercel --prod -# 3. Gemini API 키 환경변수 등록 +# 3. Gemini API 키 환경 변수 주입 npx vercel env add GEMINI_API_KEY production -# → 키 입력 후, 반영을 위해 한 번 더 배포 + +# 4. 환경 변수 적용을 위한 운영(Production) 환경 재배포 npx vercel --prod + ``` -배포 완료 시 출력되는 URL(예: `https://paxo-proxy-xxxx.vercel.app`)을 -`Paxo/DefaultConfig.swift`의 `proxyURL`에 넣으면 끝. +배포 완료 후 콘솔에 출력되는 운영 URL(예: `[https://paxo-proxy-xxxx.vercel.app](https://paxo-proxy-xxxx.vercel.app)`)을 클라이언트 프로젝트의 `Paxo/DefaultConfig.swift` 내 `proxyURL` 값으로 설정한다. + +## 2. 보안 정책 및 APP_TOKEN 강제화 + +App Store 심사 제출 전, API 어뷰징을 방지하기 위해 클라이언트 검증 로직을 반드시 강제화(Enforce)해야 한다. `api/generate.js`는 다음 3가지 보안 검증을 수행한다. -## 보안 (APP_TOKEN) — 제출 전 강제화 +1. **토큰 검증 (`x-paxo-token`)**: 서버 측 `APP_TOKEN` (및 로테이션용 `APP_TOKEN_PREV`) 환경 변수가 설정된 경우에만 일치 여부를 검사한다. **미설정 상태에서는 무조건 통과(Bypass)** 시키므로 무중단 배포에 활용된다. +2. **디바이스 검증 (`x-paxo-device`)**: UUID 포맷 준수 여부를 검사하며, 메모리 기반의 일일 베스트 에포트(Best-effort) 사용량 제한을 적용한다. +3. **페이로드 화이트리스트**: 요청 본문을 분해 후 조립하여 `text` 파트 1개, `inline_data` (jpeg/png) 파트 1개만 허용한다. 클라이언트가 변조한 `generationConfig`나 `safetySettings` 등은 원천 차단된다. -`api/generate.js`는 다음을 검증한다: -- `x-paxo-token` — `APP_TOKEN`(+로테이션용 `APP_TOKEN_PREV`) 환경변수가 설정돼 있으면 강제. **미설정이면 통과**(무중단 배포용) -- `x-paxo-device` — UUID 형식 필수 + 베스트에포트 인메모리 일일 제한 -- 본문 재구성 검증 — text 1개 + inline_data(jpeg/png) 1개만 화이트리스트로 조립. generationConfig·safetySettings 등 주입 차단 +### ⚠️ 무중단 보안 적용 순서 (Critical) + +**순서를 위반할 경우 구버전 앱에서 401(Unauthorized) 에러가 대량 발생하므로 절대 엄수한다.** + +1. 프록시 최초 배포 (토큰 환경 변수 **미설정** 상태) +2. 클라이언트(앱) 코드에 토큰 값을 주입하여 앱 배포 (`DefaultConfig.appToken`) +3. 프록시 서버에 토큰 환경 변수 등록 및 재배포 수행 ```bash -# 앱(DefaultConfig.appToken)과 동일한 토큰을 환경변수로 등록 후 강제화 +# 앱에 내장된 토큰과 동일한 값을 프록시에 등록 후 검증 로직 활성화 npx vercel env add APP_TOKEN production npx vercel --prod + ``` -배포 순서는 반드시: **프록시 배포(토큰 미설정) → 앱 배포(토큰 전송) → APP_TOKEN 등록·재배포**. -순서를 어기면 토큰을 안 보내는 앱이 401을 받는다. +## 3. 헬스 체크 (Health Check) -## 확인 +프록시 서버 정상 구동 여부 및 권한 제어 로직을 아래 명령어로 검증한다. ```bash -curl -s https://<배포주소>/privacy | head -5 # 200 HTML -curl -s -X POST https://<배포주소>/generate -d '{}' # (강제화 후) 401 또는 400 +# 1. 서버 구동 및 정상 라우팅 확인 (200 OK, HTML 반환) +curl -s https://<배포주소>/privacy | head -5 + +# 2. 보안 토큰 검증 로직 작동 확인 (APP_TOKEN 강제화 이후 401 또는 400 반환) +curl -s -X POST https://<배포주소>/generate -d '{}' + ``` -## 참고 +## 4. 기술 스펙 및 v2 로드맵 -- 요청 본문 한도 4.5MB — 앱이 캡처를 최대 2000px JPEG로 압축해 전송하므로 여유 충분 -- 인메모리 제한은 warm instance 한정. 정식 기기별 내구성 한도는 `checkDurableLimit()` 자리에 Upstash Redis 연동 예정 (v2) -- 모델 변경: Vercel 대시보드 → 환경변수 `MODEL` 설정 후 재배포 +* **페이로드 한도**: Vercel의 요청 본문 한도는 4.5MB다. 앱에서 캡처 이미지를 최대 2000px의 JPEG 포맷으로 압축(0.82)하여 전송하므로 해당 제한 내에서 안정적으로 동작한다. +* **Rate Limit 한계점**: 현재 구현된 인메모리(In-memory) 사용량 제한은 컨테이너가 웜(Warm) 상태일 때만 유지되는 임시 조치다. 향후 v2에서는 `checkDurableLimit()` 위치에 Upstash Redis를 연동하여 영속적인 기기별 Rate Limit을 구축할 예정이다. +* **LLM 모델 교체**: 호출 대상 모델을 변경하려면 Vercel 대시보드 또는 CLI를 통해 `MODEL` 환경 변수(예: `gemini-2.5-pro`)를 수정하고 재배포한다.