# QC 개선 Phase 1 최종 산출물
**프로젝트**: InfoKeyword Worker-Frontend 스키마 불일치 사고 대응
**기간**: 2026-03-04 (미팅 1~3 완료)
**팀**: 개발2팀 (토르 / 프레이야 / 헤임달 / 미미르 / 마아트)
**상태**: Phase 1 설계 완료, 구현 대기

---

## 사고 개요

**현상**: InfoKeyword Worker가 Firestore에 `snake_case` 키(`keyword_count`, `search_volume` 등)로 저장하였으나, 프론트엔드 TypeScript 인터페이스는 `camelCase` 키(`keywordCount`, `searchVolume`)를 기대하여 모든 데이터 필드가 `undefined`로 렌더링되는 프로덕션 사고 발생.

**근본 원인**: QC 파이프라인(qc_verify.py + QC-RULES.md)이 "컴포넌트 완성도"(각 컴포넌트가 내부적으로 올바른가)만 검증하도록 설계되어 있고, "컴포넌트 간 인터페이스 계약"(Worker 출력과 Frontend 입력이 일치하는가)을 검증하는 메커니즘이 전혀 없었음.

---

## 3회 미팅 요약

### 미팅 1: 현황 진단 (2026-03-04)
**참가자**: 토르, 프레이야, 헤임달, 마아트

**핵심 발견**:
- Worker 출력 Firestore 키 명세(spec) 부재 → camelCase 변환 책임자 불명확
- 프론트엔드 런타임 스키마 검증 없음 (`as KeywordData` 타입 단언 남용)
- `undefined` 렌더링이 에러가 아닌 묵음 실패(silent failure)
- qc_verify.py 4개 verifier 중 데이터 스키마를 검사하는 것 전무
- 셀프 QC 5항목이 인터페이스 검증을 요구하지 않음

**5 Whys 결론**: QC 프로세스가 "컴포넌트 간 데이터 계약 일관성"을 검증하는 단계를 요구하지 않았기 때문

### 미팅 2: 개선안 설계 (2026-03-04)
**참가자**: 토르, 프레이야, 헤임달, 미미르(신규), 마아트

**핵심 결정**:
- 기술 스택: Python=Pydantic v2, TypeScript=Zod, 공유=JSON Schema, QC=jsonschema
- camelCase 변환 책임: 프론트엔드 Zod transform 레이어 (P2 해결)
- 계약 테스트 구조: pytest(Producer) + vitest(Consumer) + JSON Schema(계약 파일)
- 4단계 구현 로드맵 수립 (Phase 1~4)
- QC-RULES.md 개정 방향: 셀프 QC 6항목 + 계약 섹션 + schema_contract verifier

**미미르 원칙**: "Agent가 이해하고 유지보수할 수 있는 수준의 단순성. 화려한 도구보다 기계적으로 작동하는 도구."

### 미팅 3: 최종 확정 + 반대 프레임 (2026-03-04)
**참가자**: 토르, 프레이야, 헤임달, 미미르, 마아트

**Devil's Advocate 검토로 발견한 5가지 약점**:

| 약점 | 제시자 | 심각도 |
|------|--------|--------|
| Pydantic 모델 구조적 강제 수단 없음 (Worker마다 만들어야 하지만 Agent가 잊으면?) | 토르 | 높음 |
| Zod ↔ JSON Schema 수동 동기화 드리프트 발생 가능 | 프레이야 | 높음 |
| 계약 테스트 우회 가능 (sample.json만 수정하면 PASS) | 헤임달 | 높음 |
| JSON Schema 자체가 틀리면 모든 테스트가 공통의 잘못된 기준을 공유 | 미미르 | 높음 |
| QC 체크리스트 항목 증가 = 준수율 감소 우려 | 마아트 | 중간 |

**약점 보완으로 설계가 강화된 부분**:
- Pydantic → BaseWorker 추상 클래스로 구조적 강제
- JSON Schema를 파생 아티팩트로 격하, Pydantic 모델이 진정한 SSoT
- schema_contract.py가 test_contract.py 존재 자체를 검증
- CODEOWNERS + Pydantic ↔ JSON Schema 필드 자동 비교
- 셀프 QC를 "항상 적용(5항목)" + "조건부 추가(1항목)" 구조로 분리

---

## 최종 확정 아키텍처

### 데이터 흐름 및 검증 레이어

```
[진정한 SSoT]
workers/{name}/models.py  (Pydantic v2 모델 — 모든 진실의 원천)
        │
        │ make export-schema (자동 생성, 수동 편집 금지)
        ▼
shared/schemas/{name}.schema.json  (파생 아티팩트, 소유: 마아트)
        │                     │
        │                     │ JSON Schema 기반으로 수동 작성
        │                     ▼
        │            shared/schemas/{name}.schema.ts
        │            (Zod 스키마 + camelCase transform)
        │
        │ jsonschema 라이브러리로 검증
        ▼
shared/schemas/{name}.sample.normal.json  (정상 케이스 Worker 실제 출력)
shared/schemas/{name}.sample.edge.json   (엣지 케이스)
        │
        ├── pytest test_contract.py  (Python 계약 테스트, CI 필수)
        └── vitest keyword-contract.test.ts  (TS 계약 테스트, CI 필수)

[QC 자동화 레이어]
qc_verify.py
    → api_health, file_check, data_integrity, test_runner (기존 유지)
    → schema_contract (신규, workers/ 디렉토리 자동 감지)
```

### 핵심 설계 원칙

1. **Pydantic = 진정한 SSoT**: JSON Schema는 Pydantic에서 자동 생성. JSON Schema가 틀릴 수 없는 구조.
2. **구조가 강제하는 준수**: BaseWorker 추상 클래스로 models.py 없는 Worker 인스턴스화 불가.
3. **계약 파일 자동 감지**: qc_verify.py가 workers/에서 models.py를 감지하면 schema_contract 자동 실행. 플래그 불필요.
4. **묵음 실패 금지**: Zod safeParse FAIL → ZodError → React Error Boundary → Sentry 알람. 잘못된 데이터의 무언 표시 불허.
5. **도구를 신뢰하려면 도구를 검증**: schema_contract.py verifier 자체를 test_schema_contract_verifier.py로 테스트.

---

## 기술 스택 결정

| 영역 | 기술 | 역할 | 선택 이유 |
|------|------|------|-----------|
| Python Worker 스키마 | **Pydantic v2** | 출력 스키마 정의 + 런타임 검증 + JSON Schema 자동 생성 | Python 생태계 표준, Agent 친화적, `model_json_schema()` 자동 문서화 |
| TypeScript Frontend 스키마 | **Zod** | Firestore 데이터 런타임 파싱 + camelCase transform + 타입 추론 | 에러 메시지 명확, safeParse, infer로 타입 안전성 |
| 공유 계약 파일 | **JSON Schema** (`.schema.json`) | 언어 독립적 계약 정의 (파생 아티팩트) | Pydantic/Zod 모두 JSON Schema 지원, 자동 생성 |
| Python 계약 검증 | **jsonschema** 라이브러리 | JSON Schema 파일 대상 Python 데이터 검증 | 표준 패키지, 추가 의존성 최소 |
| Python 계약 테스트 | **pytest** | 계약 테스트 실행 | 기존 프레임워크 재사용 |
| TypeScript 계약 테스트 | **Vitest** | 계약 테스트 실행 | 기존 프레임워크 재사용 |
| Worker 추상화 | **BaseWorker (신규)** | output_model 추상 속성 강제 | 구조적 준수 강제 |

**채택하지 않은 기술**: Pact (과도한 복잡성), humps (Zod transform으로 대체), json-schema-to-zod (신뢰성 불확실), GraphQL (대규모 구조 변경 필요)

---

## QC-RULES.md 주요 변경 사항 (v1.0 → v2.0)

### 변경 1: 셀프 QC 구조 개편 (가장 중요)

**v1.0**: 5항목 일괄 적용
**v2.0**: "항상 적용(5항목)" + "데이터 모델 변경 시 조건부 추가(1항목 6개 세부항목)"

**조건부 적용 트리거**: `workers/`, `src/types/`, `src/services/`, `shared/schemas/` 파일 변경 시

**조건부 추가 항목**:
- schema.json이 Pydantic 모델과 동기화되었는가? (`make export-schema` 실행)
- sample.normal.json이 최신 Worker 실제 출력 기반인가?
- Python 계약 테스트 재실행 (`pytest test_contract.py`)
- TypeScript 계약 테스트 재실행 (`vitest run tests/integration/`)
- camelCase transform이 Zod 스키마에 반영되었는가?

### 변경 2: qc_verify.py에 schema_contract verifier 추가

새 verifier 8개 검증 항목 (SC-1 ~ SC-8):
- SC-1~2: models.py, test_contract.py 존재 (FAIL)
- SC-3~5: schema.json, sample 파일 존재 + 검증 (FAIL)
- SC-6: Pydantic 모델 ↔ JSON Schema 필드 비교 (FAIL)
- SC-7~8: 드리프트 의심, v1 문법 탐지 (WARN)

### 변경 3: 마아트 독립 검증 강화

critical 레벨에서 계약 테스트 직접 재실행 + CODEOWNERS 리뷰 추가.

### 변경 4: 에러 처리 원칙 신설

"잘못된 데이터의 묵음 표시 불허" 원칙 명문화. Zod 파싱 실패 → Error Boundary → Sentry.

---

## 구현 파일 목록 (전체)

### Phase 1 — 이번 주 (10개 파일)

| # | 파일 경로 | 유형 | 담당 |
|---|-----------|------|------|
| 1 | `workers/base_worker.py` | 신규 | 미미르 |
| 2 | `workers/info_keyword/models.py` | 신규 | 토르 |
| 3 | `workers/info_keyword/worker.py` | 수정 | 토르 |
| 4 | `Makefile` | 신규/수정 | 토르 |
| 5 | `shared/schemas/keyword-data.schema.json` | 신규 (자동생성) | 토르 |
| 6 | `shared/schemas/keyword-data.sample.normal.json` | 신규 | 토르 |
| 7 | `shared/schemas/keyword-data.sample.edge.json` | 신규 | 헤임달 |
| 8 | `shared/schemas/keyword-data.schema.ts` | 신규 | 프레이야 |
| 9 | `frontend/src/services/keyword-service.ts` | 수정 | 프레이야 |
| 10 | `QC-RULES.md` | 수정 | 마아트 |

### Phase 2 — 다음 주 (3개 파일)

| # | 파일 경로 | 유형 | 담당 |
|---|-----------|------|------|
| 11 | `workers/info_keyword/tests/test_contract.py` | 신규 | 헤임달+토르 |
| 12 | `frontend/tests/integration/keyword-contract.test.ts` | 신규 | 헤임달+프레이야 |
| 13 | `.github/CODEOWNERS` | 신규/수정 | 미미르 |

### Phase 3 — 2~3주 내 (4개 파일)

| # | 파일 경로 | 유형 | 담당 |
|---|-----------|------|------|
| 14 | `verifiers/schema_contract.py` | 신규 | 마아트+미미르 |
| 15 | `qc_verify.py` | 수정 | 마아트 |
| 16 | `tests/test_schema_contract_verifier.py` | 신규 | 헤임달 |
| 17 | CI 설정 파일 | 수정 | 미미르+헤임달 |

### Phase 4 — 1개월 내 (장기)

| # | 내용 | 담당 |
|---|------|------|
| 18 | Firebase Emulator Suite 기반 E2E 테스트 환경 | 헤임달 |

---

## 주요 코드 패턴 레퍼런스

### BaseWorker 추상 클래스 (workers/base_worker.py)

```python
from abc import ABC, abstractmethod
from pydantic import BaseModel

class BaseWorker(ABC):
    @property
    @abstractmethod
    def output_model(self) -> type[BaseModel]:
        """서브클래스는 반드시 Pydantic 출력 모델을 정의해야 합니다."""
        ...

    def validated_output(self, result: dict) -> dict:
        """출력 dict를 Pydantic 모델로 검증 후 반환합니다."""
        return self.output_model(**result).model_dump()
```

### Pydantic 출력 모델 (workers/info_keyword/models.py)

```python
from pydantic import BaseModel, Field

class KeywordFirestoreDoc(BaseModel):
    """InfoKeyword Worker Firestore 저장 스키마.
    이 모델이 Worker-Frontend 계약의 진정한 SSoT입니다.
    변경 시: make export-schema 실행 후 Zod 스키마 동기화 필수.
    """
    keyword: str = Field(..., description="검색 키워드")
    keyword_count: int = Field(..., description="검색 결과 수", ge=0)
    search_volume: int = Field(..., description="월간 검색량", ge=0)
    difficulty_score: float = Field(..., description="키워드 난이도 0.0~1.0", ge=0.0, le=1.0)
    source: str = Field(..., description="데이터 출처 (예: google, naver)")
    task_id: str = Field(..., description="생성 Worker 태스크 ID")
    created_at: str = Field(..., description="ISO 8601 생성 시각")
    updated_at: str = Field(..., description="ISO 8601 갱신 시각")
```

### JSON Schema 자동 생성 (Makefile)

```makefile
export-schema:
    cd workers/info_keyword && python -c \
    "from models import KeywordFirestoreDoc; \
    import json; \
    print(json.dumps(KeywordFirestoreDoc.model_json_schema(), indent=2))" \
    > shared/schemas/keyword-data.schema.json
    @echo "schema.json 갱신 완료. Zod 스키마와 동기화하세요."
```

### Zod 스키마 + camelCase transform (shared/schemas/keyword-data.schema.ts)

```typescript
import { z } from 'zod'

// Firestore 원본 스키마 (snake_case, Worker 출력 그대로)
export const KeywordFirestoreDocSchema = z.object({
  keyword: z.string(),
  keyword_count: z.number().int().nonnegative(),
  search_volume: z.number().int().nonnegative(),
  difficulty_score: z.number().min(0).max(1),
  source: z.string(),
  task_id: z.string(),
  created_at: z.string(),
  updated_at: z.string(),
})

export type KeywordFirestoreDoc = z.infer<typeof KeywordFirestoreDocSchema>

// 프론트엔드 표현용 (camelCase 변환 — 책임 공백 P2 해결)
export const KeywordDisplaySchema = KeywordFirestoreDocSchema.transform((data) => ({
  keyword: data.keyword,
  keywordCount: data.keyword_count,
  searchVolume: data.search_volume,
  difficultyScore: data.difficulty_score,
  source: data.source,
  taskId: data.task_id,
  createdAt: data.created_at,
  updatedAt: data.updated_at,
}))

export type KeywordDisplay = z.infer<typeof KeywordDisplaySchema>
```

### Python 계약 테스트 핵심 (workers/info_keyword/tests/test_contract.py)

```python
def test_worker_output_matches_contract(schema):
    result = process(keyword="테스트", task_id="test-001")
    doc = KeywordFirestoreDoc(**result)           # Pydantic 검증
    jsonschema.validate(doc.model_dump(), schema) # JSON Schema 계약 검증

def test_pydantic_json_schema_sync(schema):
    pydantic_schema = KeywordFirestoreDoc.model_json_schema()
    pydantic_fields = set(pydantic_schema["properties"].keys())
    contract_fields = set(schema["properties"].keys())
    assert pydantic_fields == contract_fields
```

### TypeScript 계약 테스트 핵심 (frontend/tests/integration/keyword-contract.test.ts)

```typescript
test('모든 필드가 undefined가 아니어야 한다 (silent failure 방지)', () => {
  const result = KeywordDisplaySchema.safeParse(sampleOutput)
  expect(result.success).toBe(true)
  if (result.success) {
    Object.entries(result.data).forEach(([key, value]) => {
      expect(value, `${key} 필드가 undefined입니다`).not.toBeUndefined()
    })
  }
})
```

---

## 담당별 Action Items

### 토르 (백엔드)
- [ ] `workers/base_worker.py` 구현 (미미르와 협의)
- [ ] `workers/info_keyword/models.py` 구현
- [ ] `workers/info_keyword/worker.py` — BaseWorker 상속 + `validated_output()` 적용
- [ ] `Makefile` `export-schema` 타겟 추가
- [ ] `shared/schemas/keyword-data.schema.json` 자동 생성 (make export-schema)
- [ ] `shared/schemas/keyword-data.sample.normal.json` 작성
- [ ] `workers/info_keyword/tests/test_contract.py` 작성 (헤임달과 협의)

### 프레이야 (프론트엔드)
- [ ] `shared/schemas/keyword-data.schema.ts` 구현 (Zod + camelCase transform)
- [ ] `frontend/src/services/keyword-service.ts` — `as KeywordData` → `safeParse` 교체
- [ ] React Error Boundary 컴포넌트 추가
- [ ] `frontend/tests/integration/keyword-contract.test.ts` 작성 (헤임달과 협의)

### 헤임달 (테스터)
- [ ] `shared/schemas/keyword-data.sample.edge.json` 작성 (엣지 케이스 정의)
- [ ] `workers/info_keyword/tests/test_contract.py` 작성 (토르와 협의)
- [ ] `frontend/tests/integration/keyword-contract.test.ts` 작성 (프레이야와 협의)
- [ ] `tests/test_schema_contract_verifier.py` 작성

### 미미르 (아키텍트)
- [ ] `workers/base_worker.py` 설계 (토르와 협의)
- [ ] `.github/CODEOWNERS` 설정
- [ ] CI 파이프라인 계약 테스트 게이트 설계 + 구현 (헤임달과 협의)

### 마아트 (QC)
- [ ] `QC-RULES.md` v2.0으로 개정 (이번 미팅 최종안 기반)
- [ ] `verifiers/schema_contract.py` 구현 (미미르와 협의)
- [ ] `qc_verify.py` — schema_contract 통합 + 자동 감지 로직
- [ ] shared/schemas PR 리뷰 프로세스 수립 (schema_change_summary 템플릿)

---

## 성공 기준 (Phase 1 완료 정의)

Phase 1이 완료되었다는 것은 다음이 모두 만족될 때:

1. Worker가 `keyword_count` 대신 `keywordCount`로 저장하려 하면 → **ValidationError 즉시 발생**
2. Firestore에서 `keyword_count`(snake_case)가 오면 → **Zod가 올바르게 파싱하고 camelCase로 변환**
3. Firestore에서 예상치 못한 키가 오면 → **ZodError 발생, 묵음 실패 없음**
4. `qc_verify.py`를 실행하면 → **schema_contract verifier가 자동으로 실행됨**
5. QC-RULES.md를 읽으면 → **데이터 모델 변경 시 무엇을 해야 하는지 명확히 알 수 있음**

이 5가지가 모두 달성되면, 이번 사고와 동일한 유형의 문제는 재발하지 않습니다.

---

## 참고 문서

| 문서 | 경로 | 내용 |
|------|------|------|
| 미팅 1 현황 진단 | `memory/meetings/2026-03-04-qc-meeting1-diagnosis.md` | 사고 원인 분석, 문제점 10가지, 5 Whys |
| 미팅 2 개선안 설계 | `memory/meetings/2026-03-04-qc-meeting2-design.md` | 기술 선택, 계약 테스트 설계, 4단계 로드맵 |
| 미팅 3 최종 확정 | `memory/meetings/2026-03-04-qc-meeting3-final.md` | Devil's Advocate, 보완책, 최종 확정 계획, QC-RULES.md 전문 |
| 본 문서 (통합 요약) | `memory/meetings/2026-03-04-qc-improvement.md` | Phase 1 최종 산출물, 실행 가이드 |
