Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git branch:*)",
"Bash(git ls-files:*)",
"Bash(git check-ignore:*)",
"Bash(xcodebuild:*)",
"Bash(xcrun swift-format:*)",
"Bash(gh pr view:*)",
"Bash(gh pr list:*)",
"Bash(gh run list:*)",
"Bash(gh run view:*)",
"Bash(plutil -lint:*)"
],
"deny": [
"Read(./Paxo/Secrets.swift)",
"Read(./.env)",
"Read(./**/.env.*)",
"Read(./proxy-vercel/.vercel/**)",
"Read(./proxy/.wrangler/**)",
"Read(./docs/hansung/**)",
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(gh pr merge:*)",
"Bash(npx vercel --prod:*)"
]
}
}
48 changes: 48 additions & 0 deletions .claude/skills/proxy-change/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: proxy-change
description: GeminiService의 요청 형태나 프록시 API를 바꿀 때의 절차. 클라이언트와 서버를 같은 PR에서 함께 고치도록 강제한다. 모델 변경, 프롬프트 파라미터 추가, 스트리밍 전환에 쓴다.
---

# 클라이언트 · 프록시 동시 변경

## 왜 이 스킬이 있나

프록시는 요청 본문을 통과시키지 않고 **화이트리스트로 재조립한다.**
클라이언트만 고치면 조용히 400이 돌아온다. 로컬에서는 DEBUG 직접 호출 경로 때문에
동작하는 것처럼 보일 수도 있어 더 위험하다.

## 절차

### 1. 양쪽을 먼저 읽는다

- `Paxo/AI/GeminiService.swift` — 요청 조립, 응답 파싱, 에러 매핑
- `proxy-vercel/api/generate.js` — `buildUpstreamBody`의 화이트리스트
- `proxy-vercel/AGENTS.md` — 계약 요약

### 2. 무엇이 막히는지 확인한다

현재 통과 가능한 것은 `text` 파트 1개와 `inline_data` 파트 1개뿐이다.
`generationConfig`, `systemInstruction`, `safetySettings`, `tools`, 추가 파트는 전달되지 않는다.

추가하려는 필드가 여기 걸리면 **프록시를 먼저 고쳐야 한다.**

### 3. 순서대로 바꾼다

1. `proxy-vercel/api/generate.js`의 화이트리스트를 넓힌다
2. 로컬에서 프록시를 돌려 확인하거나, 프리뷰 배포로 확인한다
3. `GeminiService.swift`를 바꾼다
4. 앱을 빌드해 실제 호출이 200을 받는지 확인한다

### 4. 배포 순서를 지킨다

**프록시를 먼저 배포하고 앱을 나중에 배포한다.** 반대로 하면 구버전 프록시가 새 요청을 거부한다.

### 5. 같은 PR에 넣는다

두 변경을 나누면 중간 상태에서 앱이 깨진다. PR 템플릿의 해당 체크박스를 확인한다.

## 스트리밍으로 바꾸려면

지금은 `:generateContent` 일회성 요청이다. `:streamGenerateContent`로 바꾸려면
프록시의 응답 전달 방식과 클라이언트의 파싱을 **둘 다** 다시 써야 한다.
"정답 먼저, 해설 나중"은 스트리밍이 아니라 요청 2회로 구현돼 있다는 점을 먼저 이해할 것.
69 changes: 69 additions & 0 deletions .claude/skills/release-check/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
name: release-check
description: App Store 제출 전 검사. 버전 번호, 서명 팀, StoreKit 설정, APP_TOKEN, 번들 내용물을 순서대로 확인한다. 제출·아카이브·릴리스 준비를 말할 때 쓴다.
---

# 제출 전 검사

전체 절차는 비공개 저장소의 `docs/private/app-store-submission.md`다 (`paxo-app/internal`). 이 스킬은 **코드에서 기계적으로 확인 가능한 것**만 본다.

## 1. 버전

```sh
grep -n "MARKETING_VERSION\|CURRENT_PROJECT_VERSION" Paxo.xcodeproj/project.pbxproj
```

`MARKETING_VERSION`이 `0.1.0`이면 아직 출시 버전이 아니다. 사용자에게 올릴 값을 물어본다.
재제출이라면 `CURRENT_PROJECT_VERSION`도 올려야 한다.

## 2. StoreKit 설정 — 가장 흔한 사고

```sh
grep -n "StoreKitConfigurationFileReference" Paxo.xcodeproj/xcshareddata/xcschemes/Paxo.xcscheme
```

**출시 아카이브 전에는 반드시 제거하거나 None으로 바꿔야 한다.** 남아 있으면 실제 결제가 동작하지 않는다.
공유 스킴이라 팀 전체에 영향을 준다.

## 3. 서명 팀

```sh
grep -n "DEVELOPMENT_TEAM" Paxo.xcodeproj/project.pbxproj
security find-identity -v -p codesigning
```

`DEVELOPMENT_TEAM` 값과 배포 인증서의 팀 ID가 다르면 아카이브가 실패한다.
배포용(Apple Distribution) 인증서가 아예 없으면 그것부터 만들어야 한다.

## 4. 프록시 토큰

```sh
npx vercel env ls
```

`APP_TOKEN`이 프로덕션에 설정돼 있는지 확인한다. 설정 순서를 지키지 않으면 기존 앱이 401을 맞는다.
순서는 `proxy-vercel/AGENTS.md` 참고.

## 5. 번들 내용물

```sh
xcodebuild -project Paxo.xcodeproj -scheme Paxo -configuration Release \
-destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build
```

빌드된 `Paxo.app`에 내부 문서나 시크릿이 들어가지 않았는지 확인한다.

```sh
find <경로>/Paxo.app -iname '*.md' -o -iname '*.swift'
```

## 6. 빌드와 테스트

```sh
xcodebuild -project Paxo.xcodeproj -scheme Paxo -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO test
```

## 보고 형식

각 항목을 통과/실패로 표시하고, **실패한 것만** 어떻게 고치는지 설명한다.
사용자 승인 없이 버전 번호나 스킴을 고치지 않는다.
40 changes: 40 additions & 0 deletions .claude/skills/weekly-report/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
name: weekly-report
description: 커밋과 PR 기록으로 주간 보고서 초안을 만든다. 주간 회고, 진행 상황 공유, 팀 보고가 필요할 때 쓴다.
---

# 주간 보고서 초안

## 자료 수집

```sh
git log --since='7 days ago' --format='%h %an %s' --no-merges
git log --since='7 days ago' --format='%b' --no-merges
gh pr list --state merged --limit 30 --json number,title,mergedAt,author
gh run list --limit 20 --json conclusion,headBranch,createdAt
```

## 작성 형식

```
## 이번 주 한 일
(사람이 아니라 성과 단위로 묶는다. 커밋 나열 금지)

## 지표
- 머지된 PR: N건
- CI 통과율: N%
- develop 빌드 실패: N회

## 다음 주
(README 로드맵의 미완료 항목과 대조해서 제안한다)

## 막힌 것
(커밋 메시지나 PR 본문에서 드러난 것만. 없으면 "없음")
```

## 규칙

- **커밋을 그대로 나열하지 않는다.** 여러 커밋을 하나의 성과로 묶는다
- 숫자는 위 명령으로 실제 집계한 값만 쓴다. 추정하지 않는다
- "막힌 것"을 지어내지 않는다. 근거가 없으면 없다고 쓴다
- 한국어로 쓴다
19 changes: 19 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## 무엇을 바꿨나

<!-- 한두 줄. 이슈가 있으면 링크 -->

## 어떻게 확인했나

<!-- 실제로 돌린 명령이나 재현 절차. "빌드됨"만 쓰지 말 것 -->

## 체크리스트

- [ ] 빌드와 테스트를 **직접 돌려** 통과를 확인했다
- [ ] 새 유틸리티를 만들기 전에 **기존 것을 검색**했다
- [ ] 코드에 들어간 숫자·문구·외부 정보를 **직접 검증**했다 (AI 초안을 그대로 믿지 않았다)
- [ ] 프록시 계약(`GeminiService` ↔ `api/generate.js`)을 건드렸다면 **양쪽 다** 고쳤다
- [ ] 시크릿이나 비공개 문서가 섞이지 않았다 (저장소는 **공개**)

<!--
초안을 AI가 썼는지는 적지 않는다. 커밋한 사람이 전적으로 책임진다.
-->
86 changes: 86 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: CI

on:
pull_request:
push:
branches: [develop, main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

env:
XCODEBUILD_FLAGS: >-
-project Paxo.xcodeproj
-scheme Paxo
-destination platform=macOS
CODE_SIGNING_ALLOWED=NO

jobs:
guard:
name: 커밋 금지 파일 검사
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: 시크릿·비공개 파일이 추적되는지 검사
run: |
fail=0
check() {
if git ls-files --error-unmatch "$1" >/dev/null 2>&1; then
echo "::error file=$1::$1 은(는) 커밋하면 안 되는 파일입니다"
fail=1
fi
}
check Paxo/Secrets.swift
for p in $(git ls-files 'docs/hansung/*' 'docs/private/*' '*/node_modules/*' 'node_modules/*' '*.vercel/*' '*.wrangler/*'); do
echo "::error file=$p::$p 은(는) 커밋하면 안 되는 경로입니다"
fail=1
done
if [ "$fail" -ne 0 ]; then
echo "저장소가 공개입니다. .gitignore 를 확인하고 git rm --cached 로 제거하세요."
exit 1
fi
echo "✓ 금지 파일 없음"

- name: 템플릿이 Paxo/ 안에 복사돼 있지 않은지 검사
run: |
if git ls-files 'Paxo/Secrets*.swift' | grep -q 'example'; then
echo "::error::Secrets 템플릿이 Paxo/ 안에 있으면 중복 선언으로 빌드가 깨집니다"
exit 1
fi
echo "✓ 템플릿 위치 정상"

format:
name: swift-format
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: 포맷 검사
run: xcrun swift-format lint --recursive --strict --configuration .swift-format Paxo PaxoTests

build:
name: 빌드 및 테스트
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: 시크릿 템플릿 생성
run: cp Config/Secrets.example.swift Paxo/Secrets.swift

- name: 빌드
run: xcodebuild $XCODEBUILD_FLAGS -configuration Debug build

- name: 테스트
run: xcodebuild $XCODEBUILD_FLAGS test

- name: 앱 번들에 내부 문서가 섞이지 않았는지 검사
run: |
app=$(find ~/Library/Developer/Xcode/DerivedData -name 'Paxo.app' -type d | head -1)
if [ -z "$app" ]; then echo "::error::빌드 산출물을 찾지 못했습니다"; exit 1; fi
if find "$app" -iname '*.md' | grep .; then
echo "::error::내부 마크다운이 앱 번들에 포함됐습니다"
exit 1
fi
echo "✓ 번들 정상"
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ node_modules/
# Vercel
.vercel

# 로컬 시크릿
# 로컬 시크릿 — Config/Secrets.example.swift를 복사해서 만든다
Paxo/Secrets.swift

# 비공개 문서 — 사업계획서·신청서, 비공개 저장소 paxo-app/internal의 clone
docs/hansung
docs/private

# AI 도구 로컬 설정 (팀 공유는 .claude/settings.json)
.claude/settings.local.json
CLAUDE.local.md
26 changes: 26 additions & 0 deletions .swift-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"version": 1,
"lineLength": 120,
"indentation": { "spaces": 4 },
"respectsExistingLineBreaks": true,
"maximumBlankLines": 1,
"lineBreakBeforeEachArgument": false,
"lineBreakBeforeControlFlowKeywords": false,
"prioritizeKeepingFunctionOutputTogether": true,
"indentConditionalCompilationBlocks": false,
"rules": {
"AlwaysUseLowerCamelCase": true,
"AmbiguousTrailingClosureOverload": true,
"NeverForceUnwrap": true,
"NeverUseImplicitlyUnwrappedOptionals": true,
"OrderedImports": true,
"UseLetInEveryBoundCaseVariable": true,
"UseShorthandTypeNames": true,
"UseSingleLinePropertyGetter": true,
"ReturnVoidInsteadOfEmptyTuple": true,
"NoLeadingUnderscores": false,
"AllPublicDeclarationsHaveDocumentation": false,
"AlwaysUseLiteralForEmptyCollectionInit": false,
"NoBlockComments": false
}
}
Loading
Loading