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
84 changes: 21 additions & 63 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ TCA + Domain Client + App live 조립

## 2. 문서 우선순위

1. docs/ARCHITECTURE.md
2. docs/CONVENTIONS.md
3. docs/AGENT_WORKFLOW.md
4. docs/skills.md
5. AGENTS.md
6. CLAUDE.md -> AGENTS.md
1. `docs/ARCHITECTURE.md` — 전역 구조
2. `docs/CONVENTIONS.md` — 작성 규칙(주석/테스트/형식)
3. `docs/AGENT_WORKFLOW.md` — 실행/hard gate
4. `docs/skills.md` — 스킬 인덱스
5. `AGENTS.md` — 진입 요약
6. `CLAUDE.md` → `AGENTS.md`
7. `Projects/**/README.md` — 모듈 디테일

## 3. 구조
## 3. 구조 요약 + 금지 의존

```text
Projects/
Expand Down Expand Up @@ -72,41 +73,31 @@ MoziApp → CompositionRoot → RootFeature → AppCoordinator
bootstrapping → main(Placeholder)
```

Domain/Data 는 초기 placeholder 만 둔다.

## 5. 규칙
## 5. 핵심 규칙 요약

1. Feature 모듈 분리 금지
2. Scene 직접 참조 금지
3. live 조립 App only
4. Feature 는 Domain `*Client` 만
5. scheme: Mozi-Debug / Mozi
6. bundle: com.teamMozi.debug / com.teamMozi.app

## 6. Git / PR
5. scheme: `Mozi-Debug` / `Mozi`, bundle: `com.teamMozi.debug` / `com.teamMozi.app`

- 브랜치: `Type/짧은-설명`
- 커밋: `Type: 요약`
- PR 제목: 변경 내용만
- PR 본문: `변경 요약` + 영역별 `변경 내용`
- 템플릿: `.github/pull_request_template.md`
- 워크플로 상세: [docs/AGENT_WORKFLOW.md](docs/AGENT_WORKFLOW.md)
## 6. Git / PR Hard Gate 요약

Hard gates:
- `커밋해` 전 staging/commit/push 금지
- 커밋 계획 전 로컬 린트 1회
- 커밋은 계획(제목+의도+파일) 1회 승인 후 진행
- `PR 초안` / `작성만` = 본문만
- `PR 생성해` / `PR 올려` / `올려` 전 PR 생성 금지
- PR용 push 전 `origin/dev` rebase, 충돌 시 중단

1. `커밋해` 전 staging/commit/push 금지
2. 커밋 계획 전 로컬 린트 1회: `mise exec -- swiftlint lint --strict`
3. 커밋은 계획(제목+의도+파일) 1회 승인 후 진행
4. `PR 초안` / `작성만` = 본문만, 생성 금지
5. `PR 생성해` / `PR 올려` / `올려` 전 PR 생성 금지
6. PR용 push 전 `origin/dev` rebase, 충돌 시 중단

스킬:
상세 절차: [docs/AGENT_WORKFLOW.md](docs/AGENT_WORKFLOW.md)
형식 규칙: [docs/CONVENTIONS.md](docs/CONVENTIONS.md)
스킬: [docs/skills.md](docs/skills.md)

- worktree: `$mozi-worktree-bootstrap`
- 커밋: `$mozi-commit`
- PR: `$mozi-pr`
- 스킬 목록: [docs/skills.md](docs/skills.md)
- cleanup: `$mozi-worktree-cleanup`

## 7. 명령

Expand All @@ -116,36 +107,3 @@ mise exec -- tuist generate --no-open
open Mozi.xcworkspace
xcodebuild -workspace Mozi.xcworkspace -scheme Mozi-Debug -destination 'generic/platform=iOS Simulator' build
```

## 8. Worktree

상세 절차. 강제 규칙은 위 `0. Worktree Hard Gate` 를 따른다.
새 git worktree 로 작업할 때는 첫 진입 시 아래 스크립트를 실행한다.
일회성 수동 세팅이 아니라 이 경로를 기본으로 쓴다.

```bash
./scripts/setup-worktree.sh
```

스크립트가 하는 일:

1. detached HEAD 이면 로컬 브랜치 생성 또는 전환
- `MOZI_WORKTREE_BRANCH` 가 있으면 그 이름 사용
- 없으면 Codex worktree id 기준 `codex/<id>` (예: `codex/f8d0`)
- 이미 브랜치에 있으면 유지
2. 다른 worktree/main 에서 `Config/Debug.xcconfig`, `Config/Release.xcconfig` 복사
3. `mise trust` + `mise install`
4. `tuist generate`
5. `Mozi-Debug` 빌드 검증 (로컬 `.derivedData`)

옵션:

- `--skip-build`: 빌드 검증 생략
- `--force-generate`: workspace 가 있어도 generate 재실행
- `MOZI_CONFIG_SOURCE=/path/to/main`: config 복사 소스 강제 지정
- `MOZI_WORKTREE_BRANCH=feat/foo`: detached HEAD 일 때 생성/전환할 브랜치 지정

참고:

- Codex worktree 세션은 기본적으로 detached HEAD 로 열린다.
- 이 스크립트가 임시 브랜치(`codex/<id>`)를 만든 뒤, 작업 주제가 정해지면 `Type/짧은-설명` 으로 rename 하거나 새로 판다.
1 change: 0 additions & 1 deletion CLAUDE.md

This file was deleted.

109 changes: 109 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# AGENTS

Mozi 작업 시 에이전트 진입점.

## 0. Worktree Hard Gate

Mozi worktree 세션에서는 **어떤 응답/작업보다 먼저** ready 여부를 확인한다.

ready 가 아니면 즉시 실행:

```bash
./scripts/setup-worktree.sh
```

ready 판정(모두 만족해야 ready):

1. detached HEAD 가 아님
2. `Config/Debug.xcconfig` 존재
3. `Config/Release.xcconfig` 존재
4. `Mozi.xcworkspace` 존재

규칙:

- ready 가 아니면 setup 없이 본 요청을 시작하지 않는다
- setup 후 한 줄로 결과(브랜치/workspace/config)를 보고한 뒤 본 요청을 처리한다
- 이미 ready 면 setup 을 다시 돌리지 않는다
- 사용자가 명시적으로 다른 옵션을 주면 그때만 `--skip-build` 등을 사용한다
- 스킬: `$mozi-worktree-bootstrap` (`.codex/skills/mozi-worktree-bootstrap`)

## 1. 프로젝트 한 줄

```text
Tuist multi-project iOS 앱
세로 계층은 모듈, 가로 Feature는 폴더
TCA + Domain Client + App live 조립
```

## 2. 문서 우선순위

1. `docs/ARCHITECTURE.md` — 전역 구조
2. `docs/CONVENTIONS.md` — 작성 규칙(주석/테스트/형식)
3. `docs/AGENT_WORKFLOW.md` — 실행/hard gate
4. `docs/skills.md` — 스킬 인덱스
5. `AGENTS.md` — 진입 요약
6. `CLAUDE.md` → `AGENTS.md`
7. `Projects/**/README.md` — 모듈 디테일

## 3. 구조 요약 + 금지 의존

```text
Projects/
Shared/{Util,DesignSystem,Logger}
ThirdParty/{ThirdParty,ThirdPartyUI,ThirdPartyCore}
Domain/
Core/{Network,Storage}
Data/
Feature/
App/
```

금지:

```text
Feature → Data / Core* / ThirdPartyCore
Domain → Data / Core* / Feature
Data → Feature
```

## 4. 현재 앱 흐름

```text
MoziApp → CompositionRoot → RootFeature → AppCoordinator
bootstrapping → main(Placeholder)
```

## 5. 핵심 규칙 요약

1. Feature 모듈 분리 금지
2. Scene 직접 참조 금지
3. live 조립 App only
4. Feature 는 Domain `*Client` 만
5. scheme: `Mozi-Debug` / `Mozi`, bundle: `com.teamMozi.debug` / `com.teamMozi.app`

## 6. Git / PR Hard Gate 요약

- `커밋해` 전 staging/commit/push 금지
- 커밋 계획 전 로컬 린트 1회
- 커밋은 계획(제목+의도+파일) 1회 승인 후 진행
- `PR 초안` / `작성만` = 본문만
- `PR 생성해` / `PR 올려` / `올려` 전 PR 생성 금지
- PR용 push 전 `origin/dev` rebase, 충돌 시 중단

상세 절차: [docs/AGENT_WORKFLOW.md](docs/AGENT_WORKFLOW.md)
형식 규칙: [docs/CONVENTIONS.md](docs/CONVENTIONS.md)
스킬: [docs/skills.md](docs/skills.md)

- worktree: `$mozi-worktree-bootstrap`
- 커밋: `$mozi-commit`
- PR: `$mozi-pr`
- cleanup: `$mozi-worktree-cleanup`

## 7. 명령

```bash
mise install
mise exec -- tuist generate --no-open
open Mozi.xcworkspace
xcodebuild -workspace Mozi.xcworkspace -scheme Mozi-Debug -destination 'generic/platform=iOS Simulator' build
```
2 changes: 1 addition & 1 deletion Projects/App/Sources/DI/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ enum Dependencies {
_ values: inout DependencyValues,
infra: InfraContainer
) {
// 초기 domain client 없음. 이후 factory 등록 지점.
// TODO: Domain Client factory 등록
_ = infra
_ = values
}
Expand Down
30 changes: 30 additions & 0 deletions Projects/Core/Network/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# CoreNetwork

## 책임
- HTTP 요청 파이프라인, endpoint 모델, 네트워크 에러, 인증 토큰 refresh 유틸

## 의존
- 허용: SharedUtils, SharedLogger, ThirdPartyCore
- 금지: Domain/Feature service-specific flow

## 내부 규칙
- SharedLogger category: `.network`
- service-specific domain flow 금지
- authed 요청의 401 refresh 는 single-flight
- refresh 성공 후 generation 이 바뀌었으면 중복 refresh 없이 재시도

## 주요 진입점
- `DefaultNetworkClient`
- `APIEndpoint`
- `NetworkError`
- token provider/refresher 포트

## 테스트 포인트
- request building
- 401 refresh/retry/single-flight
- error mapping
- URL sanitize logging

## 관련 문서
- [ARCHITECTURE.md](../../../docs/ARCHITECTURE.md)
- [CONVENTIONS.md](../../../docs/CONVENTIONS.md)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ final class DefaultNetworkClientReviewFixTests: XCTestCase {
super.tearDown()
}

func test_authedHTTPURL_rejectsAuthorization() async {
func test_인증_HTTP_URL이면_authorization_거부() async {
URLProtocolStub.requestHandler = { _ in
XCTFail("request should not be sent")
return .init(statusCode: 200, headers: [:], data: Data())
Expand Down Expand Up @@ -36,7 +36,7 @@ final class DefaultNetworkClientReviewFixTests: XCTestCase {
}
}

func test_delayed401_afterRefresh_doesNotRefreshTwice() async throws {
func test_refresh_이후_늦은_401은_재refresh하지_않음() async throws {
final class RequestCounter: @unchecked Sendable {
private let lock = NSLock()
private var count = 0
Expand All @@ -60,7 +60,7 @@ final class DefaultNetworkClientReviewFixTests: XCTestCase {
)
}
if index == 2 {
// late 401 for a request that started with the old token generation
// 이전 토큰 generation 으로 시작한 요청의 늦은 401
Thread.sleep(forTimeInterval: 0.25)
return .init(
statusCode: 401,
Expand Down
20 changes: 10 additions & 10 deletions Projects/Core/Network/Tests/DefaultNetworkClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ final class DefaultNetworkClientTests: XCTestCase {
super.tearDown()
}

func test_request_buildsMethodPathQueryHeadersAndBody() async throws {
func test_요청이_메서드_경로_쿼리_헤더_바디를_구성() async throws {
URLProtocolStub.requestHandler = { request in
XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.url?.path, "/api/auth/login/kakao")
Expand Down Expand Up @@ -38,7 +38,7 @@ final class DefaultNetworkClientTests: XCTestCase {
XCTAssertEqual(response, OkPayload(ok: true))
}

func test_voidRequest_succeedsOn2xxAndIgnoresBody() async throws {
func test_void_요청은_2xx면_성공하고_바디를_무시() async throws {
URLProtocolStub.requestHandler = { _ in
.init(statusCode: 204, headers: [:], data: Data(#"{"ignored":true}"#.utf8))
}
Expand All @@ -50,7 +50,7 @@ final class DefaultNetworkClientTests: XCTestCase {
try await client.request(TestEndpoint(path: "/api/auth/logout", method: .post))
}

func test_requestDecodable_emptyBody_throwsDecodingFailed() async {
func test_디코딩_요청_빈_바디면_decodingFailed() async {
URLProtocolStub.requestHandler = { _ in
.init(statusCode: 204, headers: [:], data: Data())
}
Expand All @@ -73,7 +73,7 @@ final class DefaultNetworkClientTests: XCTestCase {
}
}

func test_authedRequest_attachesBearerToken() async throws {
func test_인증_요청에_Bearer_토큰_첨부() async throws {
URLProtocolStub.requestHandler = { request in
XCTAssertEqual(
request.value(forHTTPHeaderField: "Authorization"),
Expand All @@ -96,7 +96,7 @@ final class DefaultNetworkClientTests: XCTestCase {
let _: OkPayload = try await client.request(TestEndpoint())
}

func test_authed401_refreshesOnceAndRetries() async throws {
func test_인증_401이면_한_번만_refresh_후_재시도() async throws {
final class CodeQueue: @unchecked Sendable {
private let lock = NSLock()
private var codes = [401, 200]
Expand Down Expand Up @@ -135,7 +135,7 @@ final class DefaultNetworkClientTests: XCTestCase {
XCTAssertEqual(URLProtocolStub.requests.count, 2)
}

func test_concurrent401_singleFlightRefresh() async throws {
func test_동시_401이면_refresh_single_flight() async throws {
final class RequestCounter: @unchecked Sendable {
private let lock = NSLock()
private var requestIndex = 0
Expand All @@ -151,7 +151,7 @@ final class DefaultNetworkClientTests: XCTestCase {
let counter = RequestCounter()
URLProtocolStub.requestHandler = { _ in
let requestIndex = counter.next()
// first 3 are original 401s, next 3 are retries 200
// 앞 3개는 원래 401, 다음 3개는 재시도 200
if requestIndex <= 3 {
return .init(
statusCode: 401,
Expand All @@ -166,7 +166,7 @@ final class DefaultNetworkClientTests: XCTestCase {
let baseURL = try XCTUnwrap(URL(string: "https://api.example.invalid"))
let provider = StubTokenProvider(token: "access-token")
let refresher = StubTokenRefresher()
// Keep refresh in-flight long enough for concurrent 401s to join single-flight.
// 동시 401이 single-flight refresh 에 합류할 만큼 refresh 를 지연한다.
await refresher.setDelayNanoseconds(200_000_000)
let client = DefaultNetworkClient.authed(
configuration: NetworkConfiguration(baseURL: baseURL),
Expand All @@ -185,7 +185,7 @@ final class DefaultNetworkClientTests: XCTestCase {
XCTAssertEqual(URLProtocolStub.requests.count, 6)
}

func test_refreshFailure_mapsToUnauthorized() async {
func test_refresh_실패면_unauthorized_매핑() async {
URLProtocolStub.requestHandler = { _ in
.init(statusCode: 401, headers: [:], data: Data(#"{"message":"expired"}"#.utf8))
}
Expand Down Expand Up @@ -216,7 +216,7 @@ final class DefaultNetworkClientTests: XCTestCase {
}
}

func test_retryStill401_mapsToUnauthorized() async {
func test_재시도해도_401이면_unauthorized_매핑() async {
URLProtocolStub.requestHandler = { _ in
.init(statusCode: 401, headers: [:], data: Data(#"{"message":"expired"}"#.utf8))
}
Expand Down
2 changes: 1 addition & 1 deletion Projects/Core/Network/Tests/NetworkErrorMappingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ final class NetworkErrorMappingTests: XCTestCase {
super.tearDown()
}

func test_statusCodeMapping() async {
func test_상태코드가_네트워크_에러로_매핑() async {
let cases: [(Int, NetworkError)] = [
(400, .badRequest(message: "bad")),
(401, .unauthorized),
Expand Down
Loading