Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a8b08db
ci: Gemini 리뷰 prompt 정비 — 프로젝트 AGENTS.md 규칙 반영 + nitpick 차단
Roy-wonji May 19, 2026
6ec934c
feat: #6 채팅방 화면 골격 + 오디오 플레이어 공용 컴포넌트
Roy-wonji May 20, 2026
14085fa
feat: 사전 배틀 투표 API 연동 + 투표 완료 시 채팅방 이동 #4
Roy-wonji May 20, 2026
7d7246a
feat: 채팅방 오디오 progress bar 드래그/탭 시킹 + 가이드 보강 #4
Roy-wonji May 20, 2026
5031e66
feat: 오디오 자동 재생(tick) + 한 번 완청 후에만 드래그/시킹 허용 #4
Roy-wonji May 20, 2026
a5082f7
feat: 배틀 시나리오 API 연동 + 채팅방 onAppear 시 호출 #4
Roy-wonji May 20, 2026
5bf3ca8
docs: switch 기반 computed property return 명시 규칙 확장 #4
Roy-wonji May 20, 2026
8be355b
fix: 오디오 컨트롤 3버튼 수평 baseline 정렬 #4
Roy-wonji May 20, 2026
e05301b
feat: 채팅방 음원 완청 후 선택지 섹션 노출 (.pen 채팅방_선택지) #4
Roy-wonji May 20, 2026
625e65d
chore: 선택지 카드 보더를 디자인 토큰으로 교체 + 드래그 임시 항상 허용 #4
Roy-wonji May 20, 2026
09803f0
chore: 선택지 게이트 임시 완화 — 옵션 있으면 항상 노출 #4
Roy-wonji May 20, 2026
e1d93d2
chore: 로그인 요청 body JSON 디버그 로그 추가 #20
Roy-wonji May 20, 2026
7c0b9c7
style: 홈 카로셀 카드 태그 표시에 # 접두사 일관 적용 #4
Roy-wonji May 20, 2026
eb301ea
feat: 지금 뜨는 배틀 섹션 빈 데이터일 때 숨김 처리 #4
Roy-wonji May 20, 2026
8202867
feat: 홈 화면 모든 섹션 빈 데이터일 때 숨김 일관 적용 #4
Roy-wonji May 20, 2026
2c3b5c4
feat: 배틀 상세 API(/api/v1/battles/{id}) 연동 + Poll 도메인 전면 제거 #4
Roy-wonji May 20, 2026
6c05bb6
fix: 배틀 상세 응답 userVoteStatus/currentStep null 허용 #4
Roy-wonji May 20, 2026
8e5c06c
fix: 사전 투표 화면 레이아웃 깨짐 해결 #4
Roy-wonji May 20, 2026
e4bfe26
fix: 사전 투표 화면 hero/콘텐츠 overlap 구조로 단순화 #4
Roy-wonji May 20, 2026
a537315
feat: 채팅방을 시나리오 응답 데이터 기반으로 렌더링 + 사전 투표 화면 안정화 #4
Roy-wonji May 20, 2026
fe99ce3
feat: 오디오 실재생 통합 (Clean Architecture) + 채팅방 시나리오 mp3 연결 #4
Roy-wonji May 20, 2026
0fe5547
fix: 사전 투표 화면 뒤로 가기 터치가 ScrollView 로 흘러가는 문제 해결 #4
Roy-wonji May 20, 2026
02b9f53
refactor: 채팅방·사전 투표 엔티티/뷰 정비 + 화자 매핑 강화 #4
Roy-wonji May 20, 2026
3c5d5e6
feat: 오디오 진행 시점에 맞춰 채팅 메시지 자동 스크롤 #4
Roy-wonji May 20, 2026
0e73016
fix: 채팅 메시지 자동 스크롤 트리거를 currentTime 으로 변경 #4
Roy-wonji May 20, 2026
8dadd46
fix: 채팅방 메시지 id 안정화로 자동 스크롤 정상화 + 시킹 게이트 복원 #4
Roy-wonji May 20, 2026
c3203bc
feat: 채팅방 Skeleton Loader + 오디오 컨트롤 사이즈 조정 #4
Roy-wonji May 20, 2026
e193084
fix: 투표 카드 빈칸 라벨 텍스트 잘림 방지 #4
Roy-wonji May 21, 2026
6fb79ef
chore: Chat 모듈 스캐폴드 + ChatRoom CustomAlert / Skeleton 정비 #4
Roy-wonji May 21, 2026
fafb75a
refactor: ChatRoom 을 별도 Chat 모듈로 분리 + ChatCoordinator 도입 #4
Roy-wonji May 21, 2026
8dccd5d
refactor: PreVote 도 Chat 모듈로 흡수 + ChatCoordinator 가 PreVote→ChatRoom …
Roy-wonji May 21, 2026
ad0a865
chore: PreVote Skeleton 의미 단위 리팩토링 + SwiftUI Expert Skill 안내 추가 #4
Roy-wonji May 21, 2026
35154ba
fix(ci): Gemini 리뷰 actions/github-script SyntaxError 해결 #4
Roy-wonji May 21, 2026
a3fbe31
fix(test): Chat 모듈 테스트 파일 / import / struct 명 오타 정정 #4
Roy-wonji May 21, 2026
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
128 changes: 69 additions & 59 deletions .github/workflows/gemini-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,65 +119,75 @@ jobs:
responseMimeType: "application/json",
},
});
const prompt = `You are a senior iOS engineer performing a code review on a Swift 6 / SwiftUI / TCA 1.25 multi-module Clean Architecture project built with Tuist 4.

[Tone and Style Guidelines]
- Do NOT include unnecessary praise, greetings, or overly verbose explanations.
- Do NOT provide unsolicited CS insights (컴퓨터 과학적 통찰) or related interview questions.
- Provide concise, objective, and well-organized feedback suitable for immediate practical use.

CRITICAL LINE NUMBER RULES (MUST FOLLOW):
- Each diff line is annotated with [LINE N] for added lines or [CTX N] for context lines.
- You MUST only comment on [LINE N] lines (added/modified code). NEVER comment on [CTX] or [DEL] lines.
- Use the EXACT number N shown in the [LINE N] annotation. Do NOT compute line numbers yourself.
- The "code_snippet" field must contain the actual code from that [LINE N] line.

[PR Context]
Title: ${pr_title}
Description: ${pr_body}

[Project Architecture]
- Stack: Swift 6, SwiftUI, TCA 1.25, Tuist 4 (multi-module)
- Layer dependency: Presentation → Domain ← Data, Network is only referenced by Data
- Deployment target: iOS 26.0 (iPhone only)
- Source roots: Projects/App, Projects/Presentation, Projects/Domain/{Entity,UseCase,DomainInterface,DataInterface}, Projects/Data/{Model,Repository,API,Service}, Projects/Network/*, Projects/Shared/*

[Review Criteria]
1. TCA Convention: Verify @Reducer + @ObservableState usage; Action naming describes events that occurred (e.g., xxxButtonTapped, xxxResponse), NOT intended effects (e.g., performLogin, loadData); Effect is .none when no side effect and .run for async work; shared logic lives in private methods, NOT shared Actions; Effect.run must NOT capture entire @ObservableState (extract needed values first); Reducer must NOT perform CPU-intensive work (offload to Effect); Store.scope must use stored property paths only (no computed transforms); Navigation uses @Reducer enum; transient UI state (hover, focus, animation) stays in SwiftUI @State, not TCA State.
2. Module Architecture: Respect Presentation → Domain ← Data dependency direction; Network is only imported by Data; module boundaries expose protocols (DomainInterface / DataInterface); DTO-to-Entity mapping stays in Data layer.
3. SwiftUI Convention: SubViews are structs (NOT @ViewBuilder functions); use @Binding when a SubView mutates parent @State; no "View" suffix in View names (unless clarity requires it); use .frame(maxWidth/maxHeight: .infinity) instead of Spacer() for simple expansion; required props via init, optional props via ViewModifier-style functions.
4. Swift Code Quality: guard early return with shorthand optional binding (guard let value else { ... }) followed by a blank line; final class by default; private first (avoid fileprivate unless required); never force unwrap; operator line break puts operator at the start of the next line; function params line-break with closing paren on its own line; ternary for simple return/assignment only, split on '?'; [weak self] + guard let self else { return } in closures; constant groups as private enum (Metric/Font/Constant), NOT struct; empty collection literals ([] / [:]); indent 4 spaces; 120-char line limit.
5. Actionable Feedback: When improvement is needed you MUST provide a concrete Swift fix using GitHub's \`\`\`suggestion block.

[Severity Prefix]
Each comment body MUST start with a severity tag on its own line, then a blank line, then the actual comment:
- 🔴 [P1] Critical: force-unwrap crash risk, retain cycles / memory leaks, heavy or blocking work inside a Reducer, main-thread blocking
- 🟠 [P2] Major: module dependency-direction violations (e.g., Domain importing Data), Effect.run capturing entire @ObservableState, sharing logic through Actions, Store.scope with computed property, serious concurrency or error-mapping issues
- 🟡 [P3] Minor: Action naming that describes intent/effect (performLogin, loadData, setRecords), SubView written as @ViewBuilder function, Swift API Design Guideline violations on public APIs, inefficient Effect composition
- 🔵 [P4] Readability: View-suffix naming, Spacer() misuse, missing final / private, guard / ternary / line-break style violations, constant groups declared as struct instead of enum
- ⚪ [P5] Nitpick: typos, whitespace, formatting

Format: "🔴 **[P1] Critical**\\n\\nActual comment content here..."

Ignore comments and formatting-only changes. Write all review comments in Korean using Markdown, without greetings or closings.

Respond ONLY with a JSON object in this exact format:
{
"summary": "전체 리뷰 요약 (한국어, 마크다운)",
"comments": [
{
"path": "file path relative to repo root (from the b/ prefix in diff)",
"line": <exact N from [LINE N] annotation>,
"code_snippet": "the actual code content from that line",
"body": "🔴/🟠/🟡/🔵/⚪ **[P1~P5] Label**\\n\\n리뷰 코멘트 (한국어, 마크다운. 개선이 필요하면 \\\`\\\`\\\`suggestion 블록 포함)"
}
]
}
If no issues are found, return {"summary": "...", "comments": []}.

<annotated_diff>
${annotated_diff}
</annotated_diff>`;
const TRIPLE = "```";
const promptLines = [
"You are a senior iOS engineer performing a code review on a Swift 6 / SwiftUI / TCA 1.25 multi-module Clean Architecture project built with Tuist 4.",
"",
"[Tone and Style Guidelines]",
"- Do NOT include unnecessary praise, greetings, or overly verbose explanations.",
"- Do NOT provide unsolicited CS insights (컴퓨터 과학적 통찰) or related interview questions.",
"- Provide concise, objective, and well-organized feedback suitable for immediate practical use.",
"",
"CRITICAL LINE NUMBER RULES (MUST FOLLOW):",
"- Each diff line is annotated with [LINE N] for added lines or [CTX N] for context lines.",
"- You MUST only comment on [LINE N] lines (added/modified code). NEVER comment on [CTX] or [DEL] lines.",
"- Use the EXACT number N shown in the [LINE N] annotation. Do NOT compute line numbers yourself.",
"- The \"code_snippet\" field must contain the actual code from that [LINE N] line.",
"",
"[PR Context]",
"Title: " + pr_title,
"Description: " + pr_body,
"",
"[Project Architecture]",
"- Stack: Swift 6, SwiftUI, TCA 1.25, Tuist 4 (multi-module)",
"- Layer dependency: Presentation → Domain ← Data, Network is only referenced by Data",
"- Deployment target: iOS 26.0 (iPhone only)",
"- Source roots: Projects/App, Projects/Presentation, Projects/Domain/{Entity,UseCase,DomainInterface,DataInterface}, Projects/Data/{Model,Repository,API,Service}, Projects/Network/*, Projects/Shared/*",
"",
"[Review Criteria]",
"1. TCA Convention: Verify @Reducer + @ObservableState usage; Action naming describes events that occurred (e.g., xxxButtonTapped, xxxResponse), NOT intended effects (e.g., performLogin, loadData); Effect is .none when no side effect and .run for async work; shared logic lives in private methods, NOT shared Actions; Effect.run must NOT capture entire @ObservableState (extract needed values first); Reducer must NOT perform CPU-intensive work (offload to Effect); Store.scope must use stored property paths only (no computed transforms); Navigation uses @Reducer enum; transient UI state (hover, focus, animation) stays in SwiftUI @State, not TCA State.",
"2. Module Architecture: Respect Presentation → Domain ← Data dependency direction; Network is only imported by Data; module boundaries expose protocols (DomainInterface / DataInterface); DTO-to-Entity mapping stays in Data layer.",
"3. SwiftUI Convention (이 프로젝트는 AGENTS.md 규칙을 따른다 — SubView 분리는 @ViewBuilder private func 또는 private var 형태가 정식이며 위반이 아니다. SubView 를 struct 로 강제하는 의견은 절대 내지 말 것); use @Binding when a SubView mutates parent @State; no \"View\" suffix in View names (unless clarity requires it); use .frame(maxWidth/maxHeight: .infinity) instead of Spacer() for simple expansion; required props via init, optional props via ViewModifier-style functions.",
"4. Swift Code Quality: guard early return with shorthand optional binding (guard let value else { ... }) followed by a blank line; final class by default; private first (avoid fileprivate unless required); never force unwrap; operator line break puts operator at the start of the next line; function params line-break with closing paren on its own line; ternary for simple return/assignment only, split on '?'; [weak self] + guard let self else { return } in closures; constant groups as private enum (Metric/Font/Constant), NOT struct; empty collection literals ([] / [:]); indent 4 spaces; 120-char line limit.",
"5. Actionable Feedback: When improvement is needed you MUST provide a concrete Swift fix using GitHub's " + TRIPLE + "suggestion block.",
"",
"[Severity Prefix]",
"Each comment body MUST start with a severity tag on its own line, then a blank line, then the actual comment:",
"- 🔴 [P1] Critical: force-unwrap crash risk, retain cycles / memory leaks, heavy or blocking work inside a Reducer, main-thread blocking",
"- 🟠 [P2] Major: module dependency-direction violations (e.g., Domain importing Data), Effect.run capturing entire @ObservableState, sharing logic through Actions, Store.scope with computed property, serious concurrency or error-mapping issues",
"- 🟡 [P3] Minor: Action naming that describes intent/effect (performLogin, loadData, setRecords), SubView written as @ViewBuilder function, Swift API Design Guideline violations on public APIs, inefficient Effect composition",
"- 🔵 [P4] Readability: View-suffix naming, Spacer() misuse, missing final / private, guard / ternary / line-break style violations, constant groups declared as struct instead of enum",
"(P5 Nitpick 등급은 사용하지 않는다. 빈 줄·공백·trailing comma·주석 줄 같은 포맷 의견은 절대 만들지 말 것.)",
"",
"Format: \"🔴 **[P1] Critical**\\n\\nActual comment content here...\"",
"",
"Ignore the following entirely — these are NOT review issues for this project:",
"- blank lines, whitespace, trailing commas, comment-only lines (P5 nitpicks)",
"- SubView 를 struct 로 분리하라는 의견 (이 프로젝트는 AGENTS.md 의 @ViewBuilder 패턴이 정식)",
"- import 순서, 줄 단위 들여쓰기",
"- description 같은 BaseTargetType 의 정식 프로퍼티 네이밍 의견",
"",
"Only report substantive issues — bugs, architecture violations, security/concurrency risks. Do NOT generate P5 nitpicks. Write all review comments in Korean using Markdown, without greetings or closings.",
"",
"Respond ONLY with a JSON object in this exact format:",
"{",
" \"summary\": \"전체 리뷰 요약 (한국어, 마크다운)\",",
" \"comments\": [",
" {",
" \"path\": \"file path relative to repo root (from the b/ prefix in diff)\",",
" \"line\": <exact N from [LINE N] annotation>,",
" \"code_snippet\": \"the actual code content from that line\",",
" \"body\": \"🔴/🟠/🟡/🔵 **[P1~P4] Label**\\n\\n리뷰 코멘트 (한국어, 마크다운. 개선이 필요하면 " + TRIPLE + "suggestion 블록 포함)\"",
" }",
" ]",
"}",
"If no issues are found, return {\"summary\": \"...\", \"comments\": []}.",
"",
"<annotated_diff>",
annotated_diff,
"</annotated_diff>"
];
const prompt = promptLines.join("\n");
const result = await model.generateContent(prompt);
const text = result.response.text();
fs.writeFileSync("review_result.json", text);
Expand Down
102 changes: 102 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,13 +474,16 @@ public struct HomeCoordinator {
// … State / Action / handleRoute …
}

// swiftformat:disable extensionAccessControl
extension HomeCoordinator {
@Reducer
public enum HomeScreen {
case home(HomeFeature)
case preVote(PreVoteFeature)
case chatRoom(ChatRoomFeature)
}
}
// swiftformat:enable extensionAccessControl

extension HomeCoordinator.HomeScreen.State: Equatable {}

Expand All @@ -489,15 +492,99 @@ public struct HomeCoordinator {
@Reducer
public enum HomeScreen { ... } // 안 됨 (매크로 인식 / Route 추론 깨짐)
}

// ❌ 금지 — 자동 포맷터가 `public extension { enum }` 으로 바꾸도록 방치
public extension HomeCoordinator {
@Reducer
enum HomeScreen { ... } // @Reducer 매크로 확장이 internal 로 생성 → "must be declared public" 에러
}
```

규칙:
- `XScreen` 은 `@Reducer public enum`. struct 로 바꾸지 말 것
- 본체와 분리된 **별도 extension** 안에 선언
- **swiftformat 자동 변환 차단**: 해당 블록 앞뒤로 `// swiftformat:disable extensionAccessControl` / `// swiftformat:enable extensionAccessControl` 주석 페어 필수.
- 포맷터가 `public extension X { enum XScreen }` 으로 끌어올리면 `@Reducer` 매크로가 만드는 `State` / `Action` / `body` 가 internal 로 생성되어 `enum 'State' must be declared public because it matches a requirement in public protocol 'CaseReducer'` 빌드 에러가 난다.
- `extension Coordinator.XScreen.State: Equatable {}` 보조 conformance 도 같이 유지 (Route diff 비교 필요)
- 라우터 핸들러 (`routerAction`) 안에서 `state.routes.push/pop/goBack` 직접 호출은 OK, 단 `dismiss`/`submit` 같이 반복되는 종료 액션은 `.send(.view(.backAction))` 으로 일원화
- 레퍼런스: `HomeCoordinator`, `AuthCoordinator`, `MainTabCoordinator`

#### 🌐 switch 기반 computed property — 모든 case 에 `return` 명시 유지 (DomainType / BaseTargetType / DependencyKey 공통)

`switch self { ... }` 만으로 값을 돌려주는 computed property 는 **모든 case 에 `return` 키워드를 명시한다**. 적용 범위:

1. **`DomainType.url`** — `PieckeDomain` 같은 도메인 prefix 매핑
2. **`BaseTargetType` 구현체의 `urlPath` / `method` / `parameters` / `task` / `sampleData`** — 모든 Moya TargetType switch
3. **`DependencyKey.liveValue` / `testValue`** — `UnifiedDI.resolve(...) ?? Default...()` 패턴
4. 그 외 단일 표현식 switch 를 본문으로 갖는 computed property 전부

자동 포맷터의 `redundantReturn` 룰이 single-expression switch 에서 `return` 을 떼어내려 하지만, **새 case 추가 시 일부만 implicit / 일부는 explicit 으로 혼합되는 상태가 가장 깨지기 쉽다**. 새 case 추가 후엔 항상 기존 case 까지 같이 `return` 으로 정렬할 것.

```swift
// ✅ DomainType — 모든 case return
extension PieckeDomain: DomainType {
public var url: String {
switch self {
case .auth: return "api/v1/auth/"
case .profile: return "api/v1/me/"
case .home: return "api/v1/home"
case .poll: return "api/v1/poll"
case .battle: return "api/v1/battles/"
}
}
}

// ✅ BaseTargetType — urlPath / method / parameters 모두 return 명시
extension BattleService: BaseTargetType {
public var urlPath: String {
switch self {
case let .preVote(battleId, _):
return BattleAPI.preVote(battleId: battleId).description
case let .scenario(battleId):
return BattleAPI.scenario(battleId: battleId).description
}
}

public var method: Moya.Method {
switch self {
case .preVote: return .post
case .scenario: return .get
}
}

public var parameters: [String: Any]? {
switch self {
case let .preVote(_, body): return body.toDictionary
case .scenario: return nil
}
}
}

// ✅ DependencyKey — liveValue / testValue 도 return 명시
public struct BattleRepositoryDependency: DependencyKey {
public static var liveValue: BattleInterface {
return UnifiedDI.resolve(BattleInterface.self) ?? DefaultBattleRepositoryImpl()
}
public static var testValue: BattleInterface {
return UnifiedDI.resolve(BattleInterface.self) ?? DefaultBattleRepositoryImpl()
}
}

// ❌ 금지 — 포맷터가 떼어낸 implicit return 혼합
public var method: Moya.Method {
switch self {
case .preVote: .post // ← 안 됨
case .scenario: return .get // ← 혼합 상태
}
}
```

규칙:
- 새 case 추가 후 포맷터가 기존 case 의 `return` 을 떼어냈다면 **PR 전에 직접 되돌려서 일관성 유지**
- 새 도메인 case (`.battle` 등) / 새 서비스 case (`.scenario` 등) / 새 DI 키 추가 시 모두 동일하게 `return ...` 형태로 작성
- 포맷터가 반복적으로 깨면 해당 파일 또는 함수 블록에 `// swiftformat:disable redundantReturn` 디렉티브 페어 추가 검토
- 레퍼런스: `PieckeDomain`, `BattleService`, `AuthService`, `BattleRepositoryDependency`, `HomeRepositoryDependency`

### 📏 Swift 코딩 규칙 (`docs/agent/swift-coding-rules.md`)
- Swift 스타일 가이드
- 에러 처리 패턴
Expand Down Expand Up @@ -664,6 +751,21 @@ tuist generate --no-open --path Projects/Shared/DesignSystem
- `@swiftui-uikit-interop` — SwiftUI ↔ UIKit 상호 운용성 전문
- `@swift-concurrency` — Swift 6 Concurrency 및 async/await 전문

### SwiftUI 전문 가이드 스킬 — `swiftui-expert-skill`
- **출처**: [AvdLee/SwiftUI-Agent-Skill](https://github.com/AvdLee/SwiftUI-Agent-Skill) (Agent Skills 오픈 포맷)
- **로컬 설치 위치**
- Claude Code: `~/.claude/plugins/SwiftUI-Agent-Skill/`
- Codex: `~/.codex/skills/swiftui-expert-skill/`
- Cursor 도 동일 폴더를 `Plugins` 가이드대로 등록하면 됨
- **재설치 / 업데이트**
```bash
rm -rf ~/.claude/plugins/SwiftUI-Agent-Skill ~/.codex/skills/swiftui-expert-skill
git clone https://github.com/AvdLee/SwiftUI-Agent-Skill.git ~/.claude/plugins/SwiftUI-Agent-Skill
cp -R ~/.claude/plugins/SwiftUI-Agent-Skill/swiftui-expert-skill ~/.codex/skills/swiftui-expert-skill
```
- **언제 호출**: SwiftUI 상태관리(`@Observable` / 프로퍼티 래퍼 선택), 뷰 컴포지션, 리스트·내비게이션·시트, Swift Charts, 애니메이션, macOS multi-window, iOS 26+ Liquid Glass, 접근성, Instruments 트레이스 분석.
- **호출 방법**: 프롬프트에 *"swiftui-expert skill 을 사용해 ..."* 형태로 지시하거나, `.trace` 경로/녹화 요청처럼 트리거 키워드가 들어오면 자동 활성화.

### 자동 호출 키워드
다음 키워드 언급 시 **자동으로 성능 최적화 스킬 호출**:
- `ifCaseLet`, `TCA`, `Effect`, `메모리 누수`, `성능`, `최적화`
Expand Down
Loading
Loading