---
name: rn-harness-generator
description: Phase 5 — Build the React Native + Expo app in 3 sub-phases (scaffold, API, UI). Self-evaluates before handoff.
allowed-tools: [Agent, Read, Write, Edit, Bash, Glob, Grep]
---

# rn-harness-generator — Phase 5: 앱 빌드

계약 기준에 따라 React Native + Expo 앱을 빌드한다.

## Trigger

오케스트레이터에서 Phase 5 (Generator)로 호출됨.

## Input

- `docs/harness/contract.md` (완료 기준)
- `docs/harness/plans/YYYY-MM-DD-prd.md` (PRD)
- `docs/harness/plans/YYYY-MM-DD-design.md` (디자인)
- `docs/harness/feedback/round-N-*.md` (Round 2+ 시 Evaluator 피드백)

## Process

### Round 1: 전체 빌드

#### Step 1: Project Scaffolding

Read `app_slug` from `docs/harness/config.md` (set during Phase 2).
The Expo project is created as a **subdirectory** of the current working directory, keeping the claude session history intact.

```bash
# $APP_SLUG = kebab-case app name from config.md (e.g., budget-book)
npx create-expo-app@latest $APP_SLUG
cd $APP_SLUG
mkdir -p credentials

# Move harness artifacts into the project (from parent directory)
mv ../docs ./ 2>/dev/null || true
```

**NOTE**: Always use `npx create-expo-app@latest` to get the latest Expo SDK version.
**NOTE**: After project creation, `docs/harness/` is moved inside the project so all artifacts are in one git repo. The parent directory only retains `.claude/` session history.

#### Step 1.1: Clean Up Default Template Files (MANDATORY)

Remove unused default files generated by `create-expo-app`:

```bash
# Remove default reset script
rm -f scripts/reset-project.js
rmdir scripts 2>/dev/null || true

# Remove default template images (will be replaced with app-specific assets)
rm -f assets/images/partial-react-logo.png
rm -f assets/images/react-logo.png
rm -f assets/images/react-logo@2x.png
rm -f assets/images/react-logo@3x.png

# Remove default example components/screens if they exist
rm -rf components/__tests__/
rm -f components/EditScreenInfo.tsx
rm -f components/ExternalLink.tsx
rm -f components/HelloWave.tsx
rm -f components/ParallaxScrollView.tsx
rm -f components/ThemedText.tsx
rm -f components/ThemedView.tsx
rm -f components/Collapsible.tsx
rm -f components/HapticTab.tsx
rm -rf components/ 2>/dev/null || true

# Remove default tab screens (will be rewritten)
rm -f app/(tabs)/explore.tsx
rm -f app/(tabs)/index.tsx
rm -f app/+not-found.tsx
rm -f app/+html.tsx

# Remove default constants
rm -f constants/Colors.ts
rm -rf constants/ 2>/dev/null || true

# Remove default hooks
rm -f hooks/useColorScheme.ts
rm -f hooks/useColorScheme.web.ts
rm -f hooks/useThemeColor.ts
rm -rf hooks/ 2>/dev/null || true
```

**HARD GATE**: Default template files MUST be removed. They cause confusion, import errors, and lint warnings. Only keep `assets/images/icon.png`, `assets/images/splash-icon.png`, `assets/images/adaptive-icon.png`, `assets/images/favicon.png`.

#### Step 1.2: Environment Setup

```bash
# Copy .env.example from plugin templates/
cp $CLAUDE_PLUGIN_ROOT/templates/.env.example .env.example
cp .env.example .env

# Copy .gitignore
cp $CLAUDE_PLUGIN_ROOT/templates/.gitignore.template .gitignore
```

**IMPORTANT**: `.env` and `credentials/` are in `.gitignore` — never committed to git.

Dependencies install:
```bash
# Core
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar

# Styling
npm install nativewind tailwindcss
npx tailwindcss init

# State
npm install zustand @tanstack/react-query axios

# Forms
npm install react-hook-form zod @hookform/resolvers

# UI
npm install @shopify/flash-list react-native-reanimated @gorhom/bottom-sheet

# AdMob + ATT
npm install react-native-google-mobile-ads
npx expo install expo-tracking-transparency

# OTA Updates
npx expo install expo-updates

# Test
npm install -D vitest @testing-library/react-native
```

#### Step 1.3: EAS Init + Update Setup

```bash
# Initialize EAS
eas init

# Configure EAS Update (OTA)
eas update:configure
```

This auto-adds `updates.url` and `runtimeVersion` to app.config.ts.

#### Step 1.5: 플랫폼 공통 설정 (CRITICAL)

`app.config.ts` 필수 설정:

```typescript
export default {
  name: "$APP_NAME",
  slug: "$APP_SLUG",
  version: "1.0.0",
  // Default language: Korean
  primaryLanguage: "ko",
  // Orientation: Portrait only
  orientation: "portrait",
  // EAS Update (OTA) — auto-configured by eas update:configure
  updates: {
    url: "https://u.expo.dev/[PROJECT_ID]",
  },
  runtimeVersion: {
    policy: "appVersion",
  },
  // Bundle ID — same for iOS/Android
  ios: {
    bundleIdentifier: "com.{company}.{appname}",
    buildNumber: "1",
    supportsTablet: false, // iPad 지원 제거
    infoPlist: {
      // 암호화 미사용 선언 (심사 시 불필요한 질문 방지)
      ITSAppUsesNonExemptEncryption: false,
      // ATT 권한 요청 문구
      NSUserTrackingUsageDescription:
        "맞춤형 광고를 제공하기 위해 활동 추적 권한이 필요합니다.",
      // Accessibility Bundle Name (다양한 검색 키워드)
      CFBundleDisplayName: "$APP_DISPLAY_NAME",
      CFBundleSpokenName: "$APP_SPOKEN_NAME", // 영문 발음명
    },
    config: {
      usesNonExemptEncryption: false,
    },
  },
  android: {
    package: "com.{company}.{appname}", // iOS와 동일
    versionCode: 1,
    adaptiveIcon: {
      foregroundImage: "./assets/images/adaptive-icon.png",
      backgroundColor: "#ffffff",
    },
    // SafeArea: Android에서도 상태바/네비게이션바 침범 방지
    softwareKeyboardLayoutMode: "pan",
  },
  plugins: [
    "expo-router",
    "expo-updates",
    "expo-tracking-transparency",
    [
      "react-native-google-mobile-ads",
      {
        androidAppId: "ca-app-pub-XXXX~ZZZZ",
        iosAppId: "ca-app-pub-XXXX~YYYY",
      },
    ],
  ],
};
```

#### Step 1.6: ATT (App Tracking Transparency) 구현

iOS에서 AdMob 광고 최적화를 위해 광고 추적 권한 요청이 필요.
**앱 마운트 직후 바로 요청하면 얼럿이 안 나오므로 반드시 딜레이를 준다.**

`src/core/providers/TrackingProvider.tsx`:
```typescript
import { useEffect } from 'react';
import { Platform } from 'react-native';
import { requestTrackingPermissionsAsync } from 'expo-tracking-transparency';

export function useTrackingPermission() {
  useEffect(() => {
    if (Platform.OS !== 'ios') return;
    
    // 앱 완전히 로드된 후 2초 딜레이
    const timer = setTimeout(async () => {
      await requestTrackingPermissionsAsync();
    }, 2000);
    
    return () => clearTimeout(timer);
  }, []);
}
```

Root `_layout.tsx`에서 호출:
```typescript
import '../global.css';
import { useTrackingPermission } from '@core/providers/TrackingProvider';

export default function RootLayout() {
  useTrackingPermission();
  // ...
}
```

**HARD GATE**: ATT 없이 AdMob 사용 시 Apple 심사 리젝 사유.

#### Step 2: NativeWind 설정 (CRITICAL GATE)

반드시 6가지 설정 완료:
1. `babel.config.js` — `['babel-preset-expo', { jsxImportSource: 'nativewind' }]` + `'nativewind/babel'`
2. `metro.config.js` — `withNativeWind(config, { input: './global.css' })`
3. `tailwind.config.js` — `presets: [require('nativewind/preset')]` + content paths
4. `global.css` — `@tailwind base; @tailwind components; @tailwind utilities;`
5. Root `_layout.tsx` — `import '../global.css'`
6. `nativewind-env.d.ts` — `/// <reference types="nativewind/types" />`

**하나라도 누락 시 className이 동작하지 않아 전체 UI가 깨짐.**

#### Step 3: FSD 구조 생성

PRD의 FSD 모듈 맵에 따라:
```
src/
├── core/providers/
├── features/{name}/
│   ├── api/{name}.api.ts
│   ├── hooks/use-{name}.ts
│   ├── types/{name}.types.ts
│   ├── ui/  (필요시)
│   ├── store/{name}.store.ts  (필요시)
│   └── index.ts
├── entities/{name}/
│   ├── api/{name}.api.ts
│   ├── store/{name}.store.ts
│   ├── types/{name}.types.ts
│   └── index.ts
├── widgets/
└── shared/
    ├── api/client.ts
    ├── config/{env,theme}.ts
    ├── lib/
    ├── types/common.ts
    └── ui/{Button,Card,Input,Typography}.tsx
```

#### Step 4: Shared 레이어 구현

- Axios 클라이언트 (인터셉터, 토큰 리프레시)
- 테마 토큰 (디자인 시스템 반영)
- 공통 컴포넌트 (Button, Card, Input, Typography)
- 공통 타입 (IApiError, IApiResponse, TPagination)

---

### Subdivided Implementation (3-Phase Pattern)

Step 5~6은 한 번에 구현하지 않고 **3개 서브 페이즈**로 나누어 순차 진행한다.
각 서브 페이즈 완료 후 반드시 Quick QA 게이트를 통과해야 다음으로 진행한다.

#### Step 5a: Feature/Entity Scaffolding (Sub-phase 4a)

PRD의 모든 feature와 entity에 대해 디렉토리 구조와 타입을 먼저 생성:

1. FSD 모듈 디렉토리 생성 (features/{name}/, entities/{name}/)
2. TypeScript 타입/인터페이스 정의 (Interface `I` prefix, Type `T` prefix, Enum `E` prefix)
3. barrel export 생성 (index.ts)
4. Zustand store 설정 (필요시)

**Quick QA Gate:**
```bash
npm run typecheck && npm run lint
```

**Spec Checkbox Update:** `docs/specs/` 디렉토리가 존재하면 완료된 항목의 체크박스를 업데이트한다 (`- [ ]` → `- [x]`).

#### Step 5b: API Integration (Sub-phase 4b)

Shared 레이어의 API 클라이언트를 기반으로 각 feature/entity의 API를 구현:

1. Axios 클라이언트 설정 (인터셉터, 토큰 리프레시) — shared/api/client.ts
2. API 함수 구현 — features/{name}/api/{name}.api.ts
3. TanStack Query hooks 작성 — features/{name}/hooks/use-{name}.ts

**Quick QA Gate:**
```bash
npm run typecheck && npm run lint
```

**Spec Checkbox Update:** `docs/specs/` 디렉토리가 존재하면 완료된 항목의 체크박스를 업데이트한다 (`- [ ]` → `- [x]`).

#### Step 5c: Screen/UI Development (Sub-phase 4c)

Feature hooks를 화면에 연결하고 전체 UI를 구현:

1. Expo Router 파일 기반 라우팅으로 화면 구현
2. **모든 화면 `SafeAreaView` 필수**
3. NativeWind `className`만 사용 (inline style 금지)
4. Feature hooks를 화면에 연결
5. 로딩/에러/빈 상태 처리
6. 리스트는 **FlashList** 사용 (ScrollView로 리스트 렌더링 금지)
7. 폼은 **React Hook Form + Zod** 사용

**Quick QA Gate:**
```bash
npm run typecheck && npm run lint
```

**Spec Checkbox Update:** `docs/specs/` 디렉토리가 존재하면 완료된 항목의 체크박스를 업데이트한다 (`- [ ]` → `- [x]`).

---

**HARD GATE**: 각 서브 페이즈의 `typecheck + lint` 게이트를 통과하지 못하면 다음 서브 페이즈로 진행 금지. 에러를 먼저 수정한 후 진행한다.

#### Step 6: (Reserved — covered by Step 5a/5b/5c above)

> 기존 Step 5 (Feature/Entity 구현)와 Step 6 (화면 구현)은 위의 서브 페이즈 패턴으로 대체되었다.
> 이 패턴은 react-native-fsd-agent-template에서 검증된 방식으로, 각 단계에서 타입 안전성을 확보한 후 다음 단계로 진행하여 에러 전파를 최소화한다.

#### Step 7: Unit Tests

- Test runner setup (vitest)
- Core business logic unit tests
- Utility function tests

#### Step 8: Environment Detection & Run Test (MANDATORY)

Before self-evaluation, detect the user's environment and attempt to run the app.

**Step 8a: Environment Detection**

```bash
# OS detection
uname -s  # Darwin = macOS, Linux, MINGW/MSYS = Windows

# Xcode check (iOS — macOS only)
xcode-select -p 2>/dev/null && echo "XCODE=yes" || echo "XCODE=no"

# Android SDK check
[ -d "$ANDROID_HOME" ] || [ -d "$ANDROID_SDK_ROOT" ] && echo "ANDROID_SDK=yes" || echo "ANDROID_SDK=no"

# Android emulator running?
adb devices 2>/dev/null | grep -q "emulator\|device" && echo "ANDROID_EMU=yes" || echo "ANDROID_EMU=no"

# iOS simulator running? (macOS only)
xcrun simctl list devices 2>/dev/null | grep -q "Booted" && echo "IOS_SIM=yes" || echo "IOS_SIM=no"

# Has native modules? (AdMob = yes → Expo Go won't work)
grep -q "react-native-google-mobile-ads" package.json && echo "NATIVE_MODULES=yes" || echo "NATIVE_MODULES=no"
```

Record results in handoff:
```
## Environment
- OS: [macOS/Linux/Windows]
- Xcode: [yes/no]
- Android SDK: [yes/no]
- Native modules: [yes/no]
- Expo Go compatible: [yes/no]
```

**Step 8b: Choose Run Strategy**

| Condition | Strategy |
|-----------|----------|
| No native modules (no AdMob) | `npx expo start` (Expo Go OK) |
| Native modules + macOS + Xcode | `npx expo run:ios` |
| Native modules + Android SDK | `npx expo run:android` |
| Native modules + no local SDK | `eas build --profile development` (cloud) |
| Windows | Android only (no iOS) |
| Linux | Android only (no iOS) |

**Step 8c: Run App**

Based on detected environment:

```bash
# Option A: Expo Go (no native modules)
npx expo start

# Option B: Local native build (native modules present)
# macOS + Xcode → iOS simulator
npx expo run:ios

# Android SDK available → Android emulator
npx expo run:android

# Option C: Cloud dev build (no local SDK)
eas build --profile development --platform android
# Then install .apk on emulator/device and:
npx expo start --dev-client
```

**IMPORTANT: AdMob + Expo Go = CRASH.** If `react-native-google-mobile-ads` is installed, Expo Go cannot be used. The app MUST be built as a development client or via `expo run:*`.

**Step 8d: Verify App Runs**

After starting:
- Check terminal for crash/error output
- If crash → read error, fix, retry
- If runs → proceed to self-evaluation

If NO local build environment is available at all:
- Skip runtime test
- Note in handoff: "Runtime test skipped — no local build environment"
- Rely on typecheck + lint + unit tests as evidence

#### Step 9: Self-Evaluation (MANDATORY)

```bash
npm run typecheck  # 0 errors
npm run lint       # 0 errors
npm test           # all pass
```

Contract criteria self-verification + handoff document.

### Round 2+: 피드백 기반 수정

1. `docs/harness/feedback/round-N-*.md` 읽기
2. 모든 FAIL 기준 수정
3. 버그 수정 (critical > major > minor)
4. 스텁 → 실제 구현
5. 자체 평가 재실행
6. 핸드오프 작성

## Output

`docs/harness/handoff/round-N-gen.md`:

```markdown
# Generator Handoff — Round N

## What Was Built/Fixed
[요약]

## Contract Self-Assessment
- [DONE] 기준 1: [증거]
- [DONE] 기준 2: [증거]
- [PARTIAL] 기준 3: [미비 사항]

## Test Results
- npm run typecheck: PASS (에러 0)
- npm run lint: PASS (에러 0)
- npm test: 24 tests, 24 passed

## FSD Compliance
- 레이어 규칙: PASS
- barrel export: PASS
- any 타입: 0개

## Known Issues
- [미해결 사항]
```

Git commit:
```bash
git add .
git commit -m "feat: Round N — [요약]"
```

## State Update

```yaml
next_role: rn-harness-evaluator
```

## HARD GATES

- NativeWind 6가지 설정 누락 시 즉시 수정 (CRITICAL)
- FSD 레이어 규칙 위반 금지
- `any` 타입 사용 금지
- 모든 화면 SafeAreaView 필수
- inline style 금지 (className만)
- 자체 평가 건너뛰기 금지
- 핸드오프 파일 없이 QA 진행 금지
- Import는 반드시 `@/` alias 사용
