# 삼성화재 계정 인테이크 M4 — notes

계획서 `.claude/plans/samsung-account-intake-m4.plan.md` · receipt `.claude/receipts/mccp-plan-codex/samsung-account-intake-m4.json`
· 리뷰 기록 `.claude/reviews/plan-review-samsung-account-intake-m4.md`

## 계획 창 (2026-09-17)

### 사용자 결정 2건 (계획 저작 전)

| 질문 | 선택 | 계획서 반영 |
|---|---|---|
| 프리뷰와 확정 사이에 파싱 결과를 서버에 보관하는가 (PRD Open Question) | **보관 안 함** — 확정(M5) 때 같은 파일·암호를 다시 올려 그 시점 DB 로 재판정 | UI3 · §NOT Building |
| 행 선택 체크박스를 어느 마일스톤에 두는가 | **M5** — M4 는 읽기 전용 표 | UI4 · §NOT Building |

### 라운드 표

층위는 CLAUDE.md §2 체크리스트 번호다. 계획서 본문에 실린 것은 fan-out 판정과 디자인 critique 까지이고, L2 패널
판정은 계획서가 동결된 뒤라 여기에만 있다.

| 라운드 | A | B | 층위 | 흡수 | 기각 |
|---|---|---|---|---|---|
| fan-out (Phase 2.5) | architect · security | test · explorer | 2·3·6·8 | 7 (서버 tsc 서명 대조 · jest 실패 식별자 대조 · 조회 호출 자리 `callers` · 로그 it 2건 · 규칙 충돌 단정 · DTO 상속 근거 · select 키 단정) | 14 — 계획서 §Fan-out 판정 |
| 저자 자가 1패스 | — | — | 3·5 | 3 (prettier 줄바꿈에 앵커가 깨지는 자리 · 등록 의도 경고 오탐 Risks · `#8` mustPass 가 의심까지 단정하면 통과 불가) | — |
| 디자인 critique R0 | Assessment A P1 2 · P2 7 · P3 4 | Assessment B(`impeccable detect`) 0건 | — | 9 (Task 4) | 5 |
| 디자인 critique R1 | 0건 | R0 유지(코드 무변경) | — | — | — → CONVERGED |
| L2 패널 R1 (`multi-agent`, 1/1) | architect pass(0) · security pass(LOW 1) | test pass(LOW 2) · invariant pass(MEDIUM 1 · LOW 1) | 2·3·5·8 | 게이트 스크립트 1 + 구현 창 인수 3 | 1 |

L2 판정 `converged` — `L1 + L2 quorum satisfied (4/3 responses, 4 distinct roles); L3 not fired`. 단일패스 완화는
일어나지 않았다(`single_pass_reason` 없음). receipt 봉인 후 `validate --command mccp:prp-implement --decision
samsung-account-intake-m4` 가 `ok:true` 다. 게이트 wall-clock 110초(리뷰 기록 Measurement).

### L2 지적 판정

| 관점·등급 | 지적 | 층위 | 판정 | 반영 |
|---|---|---|---|---|
| invariant MEDIUM | Task 1 이 관측 2 의 prettier 변경을 "되돌리거나 별도 커밋" 으로 허용하는데, 별도 커밋하면 서버 HEAD 가 `SRV_BASE` 에서 벗어나 `#1` 이 영원히 붉다. 고치려면 계획서 `SRV_BASE` 를 바꿔야 하고 그러면 receipt 가 stale 이다 | 5 | 구현 창 인수 | 아래 인수 1 — **되돌리기만 쓴다**. 계획서는 `plan_hash` 결박이라 고치지 않는다 |
| invariant LOW | `#8` 리포지터리 축의 짝(`agencies:` it)이 가짜 리포지터리를 써서, 사본이 실제로 쓰였는지를 증명하지 못한다 | 8 | ABSORBED(gates.mjs) | `counterfactual` 이 리포지터리 it 의 실패문에 `agc_id` 가 있는지 본다. 합성 보고서로 실측 — 실패문에 `agc_id` 가 있으면 `#8 ok`, `TypeError` 로 죽은 보고서는 `#8 FAIL: 리포지터리 사본의 붉음에 agc_id 가 없다` 로 rc=1 |
| test LOW | 배선 it 이 형식 검사의 **호출**만 보고 결과가 판정을 가르는지는 안 본다 | 2 | 구현 창 인수 | 아래 인수 2 |
| test LOW | 규칙 충돌 단정이 기존 it 안에 접혀 있어 지워도 게이트가 초록이다 | 2 | REJECTED(기존 공시) | 계획서 §NOT Building `단정 내용` · `#10` 등록부 |
| security LOW | 기업 제한 없는 조회라 삼성 외 기업도 `others[]`·`otherAgencyCodes` 에 코드·이름으로 나올 수 있다는 사실이 계획서에 없다 | — | 구현 창 인수 | 아래 인수 3 — 의도된 동작이다(오선택 방어는 모든 기업을 본다). 노출 대상은 allowlist 관리자뿐 |

### 구현 창 인수 — 계획서 문구 안에서 할 것

계획서는 `plan_hash` 결박이라 고치지 않는다. 아래는 계획서의 it 이름·계약을 바꾸지 않는 선에서 채운다.

1. **Task 1 의 prettier 변경은 되돌린다** — `git -C insuniverse-server checkout -- apps/api/src/modules/fc/agency/samsung-intake-xlsx.ts`
   (내용은 HEAD 를 prettier 에 통과시킨 것과 바이트 동일, 의미 변경 없음 — 계획서 관측 2). 별도 커밋은 `SRV_BASE` 를 깨므로
   쓰지 않는다. 이 창이 만든 변경이 아니므로 **되돌리기 전에 사람이 확인한다** — 편집기 저장 시 포맷일 가능성이 높다.
2. **`wiring: 판정의 형식 검사는 기존 계정 업로드와 같은 규칙을 쓴다`** 는 호출 단정에 더해, 비밀번호 검사 spy 를
   `mockReturnValueOnce(false)` 로 한 번 뒤집어 합성 픽스처의 신규 생성 후보 행이 `create-password-invalid` 로 바뀌는지
   단정한다 — 결과가 판정을 가르는지를 본다.
3. **리포지터리 머리 주석에 "삼성 외 기업의 회원도 매칭에 나온다" 를 한 줄 더한다** — 오선택 방어가 모든 기업을 보는 것이
   의도라는 사실을 코드 자리에 남긴다.

### 게이트 운용 이탈

- **decision 슬러그** — 명령 본문 리터럴(PRD 경로) 대신 계획서 경로로 유도했다(`samsung-account-intake-m4`). PRD 슬러그
  원장이 M1 의 2라운드로 소진돼 있었다(2/1). 계획서 §계획 게이트 운용 메모에도 적었다.
- **L2 패널 프롬프트** — 플러그인 `scripts/workflows/plan-review.js` 를 스크래치패드로 복사해 `buildPrompt` 의
  `RETURN CONTRACT` 앞에 CLAUDE.md §0 리뷰 수용 규약과 §3 불변식 표를 넣은 사본으로 돌렸다(M3 사본과 주입 내용 동일 확인).
  플러그인 원본은 무수정(B1).
- **fan-out 주입** — 원문의 `파일:줄` 인용을 `파일#L줄` 로만 바꿨다(L1 C6). 첫 주입 스크립트가 `String.replace` 의 문자열
  치환을 써서 원문 안의 `` $` `` 가 치환 패턴으로 해석돼 계획서 앞부분이 복제됐다(L1 `C7_DUPLICATE_SECTION` 11건) —
  원본 앞부분과 꼬리를 잘라 복원하고 함수 치환으로 다시 넣었다. 복원 후 L1 `converged`.
- **디자인 critique** — plan 단계라 브라우저 시각화 없이 degraded 로 돌았고, 게이트 안이라 질문 단계를 생략했다.
  스냅샷 저장도 생략했다 — 저장 경로가 `insuniverse-admin` 서브모듈 안이라 `#1` clean 조건을 깬다.

### 착수 전에 사람이 볼 것

- **서버 서브모듈의 prettier 변경**(관측 2) — 인수 1 대로 되돌릴지 확인한다. 그대로 두면 `#1` 이 기준선을 뜨지 않는다.
- **이 창 산출물 커밋** — 계획서 · 게이트 · PRD 4행 · 리뷰 기록 · receipt · 이 notes. 기준 커밋이 커밋보다 앞에 잡히면
  `#7` 이 그 구간을 표 밖으로 읽는다(Task 1). M3 는 아직 push 전이다(M3 notes).
- **M3 `#7` 붉음 3경로**(M3 notes §사람에게 넘기는 것 1)는 이 창과 무관하게 열려 있다.

## Codex Implementation Review

- 호출: `node /home/madsc/.claude/plugins/cache/mccp/mccp/1.33.6/scripts/lib/codex-invoke.js adversarial-review` (fail-closed Bash wrapper, v0.2.2)
- 라운드 수: 1
- **> Codex skipped per MCCP_CODEX_DISABLED=1** — `codex-policy.js seal` 이 `codex_disabled=true` 로 봉인했고 wrapper 가
  spawn 전에 `classification=disabled` 로 short-circuit 했다(`durationMs=1`). R2 는 존재하지 않는다. receipt 는
  `codex_verdict=skipped` 로 기록한다.
- 합치 결론: **Codex 미발화.** 이 절의 판정은 전부 `security-reviewer`(아래)와 저자 자가 1패스의 것이다.
- 라운드 원장: `mccp-implement-codex__samsung-account-intake-m4` — cap 1, pin 축 `single-pass+codex-disabled`.
- Codex session 참조: 없음 (미발화)
- 이 절을 계획서가 아니라 notes 에 두는 이유 — 계획서에 붙이면 `plan_hash` 가 바뀌어 plan receipt 가 stale 이 된다(M2 실측).

### 2.5.2 — implement-time 결정 10건 (Codex 에 물으려 했던 초점)

1. 기업 없음 오류는 계획서의 신규 `E0820` 대신 기존 `NOT_FOUND_AGENCY`(E0204 '존재하지 않는 기업입니다.') 재사용 — `admin.constant.ts` 무변경
2. `getIntakeMembersByLoginIds` select 7키(memLoginId · agcId · agcCorporateCode · agcCorporateName · memIsResigned · subState · subDisableCompletedAt), memType 필터 없음
3. 형식 안 고유 아이디 0개면 조회 생략, `IN` 분할 없음(`ponytail:`)
4. 같은 기업 안 한 아이디가 둘 이상이면 첫 행
5. 응답 `{ intent, sheetName, rowCount, rows, summary{counts, selected, others, mismatchSuspected} }` — 삼성 외 기업도 `others` 에 나온다
6. 정규화는 숫자만인 값에 `s` 접두, 형식 검사는 `AgencyFcService` private 메서드 대괄호 접근
7. `SamsungIntakePreviewDto extends SamsungIntakeParseDto` + `agencyCode @IsIn`
8. `GET agencies` · `POST preview`, 클래스 레벨 가드 상속, 업로드 크기 상한 없음(M3 parse 와 동일)
9. 조회 실패(DB 예외)는 잡지 않고 전역 필터로 흘린다
10. 프런트 `IntakePreview.tsx` 단일 컴포넌트 — 마운트 시 기업 목록, 라디오 초기 null, FormData, 텍스트 노드 렌더
- 판정 함수 시그니처는 계획서의 `agcId` 대신 `agency{agcId, code, name}` 을 받는다 — 선택 기업 매칭이 0건일 때도 요약에
  코드·이름을 실어야 해서다(이탈, 보고서에 싣는다)

### Security Reviewer

`Task(subagent_type=mccp:security-reviewer)` — "review proposed implementation". 코드 작성 **전**에 위 결정 10건을
계획서 · notes · 선례 소스(컨트롤러 · DTO · xlsx 판독기 · 리포지터리 · `pino.ts` · 전역 예외 필터 · `fc.module.ts`)와 대조했다.

**CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 3.**

리뷰어가 **틀리지 않았다고 확인한 축**: `inArray` 파라미터화(SQL 인젝션 없음) · 클래스 레벨 가드 상속과 allowlist
fail-closed · select 에 비밀번호·이름·연락처 없음 · `NOT_FOUND_AGENCY` 재사용 · `pino.ts` redact 가 필드 이름 기준 전역 ·
`FcModule` export 가 `FcService` 뿐(관측 20) · `#7` 이 남은 반사실 사본을 표 밖 변경으로 잡음.

### YAGNI Triage

| Finding | Severity | Verdict | Why |
|---|---|---|---|
| S1 결정 9 — 비HTTP 예외는 전역 필터(`all-exception.filter.ts:34` `logger.error(exception)`)가 원문째 로그에 남기고, 드라이버 예외는 실패한 SQL 을 값째 싣는다 — 수천 건의 로그인아이디가 로그로 간다. M3 판독기는 같은 원칙으로 내부 예외를 버린다 | MEDIUM | ACCEPT_NOW | drizzle 0.33 은 예외를 감싸지 않는다(실측 — `Failed query` 문자열 0건). 서비스에서 조회만 catch 해 드라이버 코드와 건수만 로그에 남기고 `INTERNAL_SERVER_ERROR` 로 다시 던진다. spec 에 거부 경로 it 1건을 더한다(필수 목록 밖 추가 it) |
| S2 결정 4 — "첫 행" 이 `ORDER BY` 없이 비결정적이다 | LOW | REJECT_YAGNI | 로그인아이디는 기업 안에서 유일하다(기존 `checkDuplicate` 가 기업 안 중복을 막는다). 정확성 경로가 사실상 없다 |
| S3 결정 2 — 기업 제한 없는 조회가 기업 스코프 리포지터리에 얹힌다 | LOW | 기존 흡수 | fan-out F3 · 머리 주석 · `#5` `callers` · export 경계(실측 재확인) |
| S4 결정 5·6 — `others`·`otherAgencyCodes` 가 기업 교차 계정 존재 조회기가 된다 | LOW | 기존 판정 | L2 security LOW 인수 3 — 의도된 동작. 도달 계정은 최고관리자 공용 계정뿐(2026-09-17 결정) |

- **2.5.5 GATE-STOP 판단** — CRITICAL·HIGH 0건. 정지하지 않는다. 2.5.4 (a) 조건 불충족이라 R2 없음(Codex 비활성이라 애초에 없다).
- Deferred to backlog: 0건
- Open Questions: 없음

### Design Review

`impeccable-detect --mode implement` → `skill_available=true` · `design_signal=false` · `silent_skip=true`
(`reason=no-signal`). 렌더 표면(`IntakePreview.tsx`)은 `insuniverse-admin` 서브모듈 안이라 부모 `git diff` 에는
gitlink 로만 보인다(서브모듈 diff 미탐). receipt 에 `--impeccable-silent-skip` 으로 남긴다. 계획 창 디자인 critique 는
R1 `CONVERGED`(계획서 §Design Critique) — Task 4 문구가 그 반영분이고, 표시 동작은 라이브 1회로 본다.

### 이 리뷰가 안 본 것

- **코드가 아직 없다.** 위 판정은 전부 *제안된 설계*에 대한 것이다.
- 범위 밖으로 분리 — 가드 사슬 전반 · 기존 4경로 · 복호화 내부 · 업로드 크기 상한 · `IN` 분할 · 프런트 test 인프라 · 기업별 세분 ACL(최고관리자 단일 계정 결정).

## 구현 완주 기록 (2026-09-17)

기준 커밋 `6c34b879` · 서버 `a4bb84fa` · admin `69d9f6b9`. 서브모듈 두 벌 모두 **미커밋**이다(UI12).

### 착수 전 사람 확인 2건

| 질문 | 답 | 조치 |
|---|---|---|
| 서버 `samsung-intake-xlsx.ts` 의 prettier 줄바꿈 변경을 되돌리는가 | 되돌린다 | `git show HEAD:… \| npx prettier` 와 바이트 동일을 다시 확인한 뒤 `checkout` |
| 계획 창 산출물을 지금 커밋하는가 | 커밋한다 | `6c34b87` docs(samsung-intake) — 6파일, push 없음 |

리뷰 절을 notes 에 붙이면 추적 파일이 더러워져 `#1` 이 기준선을 거부한다 — 절을 패치로 떼어 두고 기준선을 뜬 뒤 다시 적용했다.

### 라운드 표

| 라운드 | A | B | 층위 | 흡수 | 기각 |
|---|---|---|---|---|---|
| implement 2.5.5 | security-reviewer (MEDIUM 1 · LOW 3) | Codex 비활성 | 1·2 | 1 — S1 조회 실패 로그 | 3 — YAGNI Triage |
| 라이브 1회 | 사람 손 대신 스크립트 구동 | — | 6 | 1 — 미매칭 판정 라벨이 현재 상태 문구와 같았다 | — |

### 게이트 `#1`~`#10` — 전건 초록 (rc=0, 2분)

라벨 변경 뒤 마지막 완주 출력이다(첫 완주도 전건 초록이었다).

```
#1 ok  srv=a4bb84fa · adm=69d9f6b9 · base=6c34b879 · jest{suites:4,tests:27} tsc{srv:4파일/19서명,adm:633서명}
#2 ok  실패 테스트 27건 전건 기준선 안(식별자 대조) · 신규 실패 0
#3 ok  서버 tsc rc=1 · 서명 19건 전건 기준선(19건) 이하 · 범위(fc·에러 상수) 0건
#4 ok  judge 축 13건 통과 · 전건 단정 1건 이상
#5 ok  preview 축 13건 통과 · 전건 단정 1건 이상
#5 ok  getIntakeMembersByLoginIds 출현 3파일 — 정의·인테이크 서비스·spec 뿐
#6 ok  프런트 tsc rc=2 · M4 파일 3종 프로그램 실림 · 에러 0 · 서명 633건 전건 기준선(633건) 이하
#7 ok  기준 커밋 대비 변경 12경로 전건 표 안(표 21행) · 자격증명 첨부 의심 0건 · PRD 4행 status=in-progress
#8 ok  가드 우회·기업 조건·의심 무력화 사본으로 붉음 3건(판별) · 초록 3건(독립 축)
#9 ok  filePassword 2곳 전건 FormData append(IntakePreview 포함) · preview(FormData) · agencies() · 응답 타입에 password 값 키 0 · 기업 초기값 null·기본값 0 · 의도 null·암호 빈 문자열 유지 · innerHTML 0 · 브라우저 저장소 0
#10 ok  사각지대 8축 — insuniverse-admin 에 test 인프라가 없다 — 프리뷰 표·경고·필터는 tsc·정적 계약·라이브 1회뿐 / 리포지터리 SQL 은 jest 에서 실행되지 않는다 — 렌더한 WHERE 모양만 본다. 실 MySQL 실행은 라이브 1회 / 운영 분포 대조(조사 §E12 관측 5)는 게이트 밖이다 — 운영 DB 접근은 사람이 승인한다 / 프리뷰와 확정 사이 DB 괴리는 M5 의 재판정 몫이다 — M4 는 아무것도 보관하지 않는다 / spec 단정 내용은 이름·계수까지 — 내용은 반사실 세 축과 리뷰가 본다 / 대량 행(수천 행) 응답 시간과 화면 렌더는 라이브 1회 관측뿐이다 / baseline.json 변조는 게이트가 막지 않는다 / 서브모듈 push 와 부모 포인터 bump 미수행 · §NOT Building 이름 대조 8/8
```

### 반사실 (흡수 매 건)

| 흡수 | 뒤집은 것 | 결과 |
|---|---|---|
| S1 조회 실패 로그 | 서비스에서 `.catch(…)` 블록 1개를 뺀 사본 | 새 it `preview: 조회가 실패하면 로그인아이디 없이 코드와 건수만 로그에 남긴다` 붉음 → 복원 후 22/22 초록 |
| 가드 우회 · 기업 조건 · 의심 무력화 | 게이트 `#8` | 붉음 3 · 초록 3 · 리포지터리 붉음 실패문에 `agc_id` |

### 라이브 1회 — 격리 MySQL · 부팅한 서버 · 관리자 화면

- **DB**: docker `mysql:8.0.42`(127.0.0.1:33307). stg 덤프를 쓰지 않고 이 조회가 닿는 4표(`tn_member` · `tn_agency` ·
  `tn_subscribe` · `tc_common`)만 스키마 열 이름·형 그대로 최소 DDL 로 만들었다 — stg 에 닿지 않았다. 기업 `sf`(21) ·
  `s1`(30) · `s3`(34) · `s2`(38), 합성 회원 5,503 · 구독 5,503. 전부 지어낸 값.
- **서버**: `nest build` → preload 로 `listen(7200)` 을 7202 로 돌렸다 — 7200 은 다른 워크트리(`alimtalk-redirect-whitescreen`)
  세션 서버가 쓰고 있어 건드리지 않았다. 본 체크아웃 `.env` 복사 + env 덮기(DB · `REDIS_URL=` · 로컬 `JWT_SECRET` ·
  `SAMSUNG_INTAKE_ADMIN_IDS=990001` · 알림·메일·결제·웹훅 자격 빈 값). `FCM_PRIVATE_KEY` 를 비우면 부팅이 죽어 그 칸은 뒀다.
- **프런트**: vite 5289 · `REACT_APP_BACKEND_HOST=http://localhost:7202` · `sessionStorage.ins` · `_aaT` 쿠키 주입.
  playwright-core 스크립트로 구동(토큰을 대화에 남기지 않으려고 MCP 대신).
- 끝나고 서버·vite·컨테이너를 내리고 두 `.env` · 서명 토큰 · DB 암호 · 서버 로그(요청 로그가 쿠키 JWT 를 싣는다)를 `shred` 했다.

| 확인 | 결과 |
|---|---|
| `agencies` — allowlist · 밖 · 무토큰 | `[s1 합성기업하나, s2 합성기업둘, s3 합성기업셋]` (`sf` 없음) · 403 · 401 |
| 합성 픽스처 등록·s2 (실 MySQL) | `s00000001` 이미 사용중(사용·구독) · `s00000002` 되살리기(중지·해지 되살릴 수 있음) · `s00000003` 신규 생성(다른 기업 s1) · selected 2/1 · others s1 1/1 · 의심 거짓 |
| 합성 픽스처 삭제·s2 | 중지 · 이미 중지 · 미매칭 |
| 합성 픽스처 등록·s1 | 의심 **참** — selected s1 1/1 · others s2 2/1 |
| `agencyCode=sf` · 파일 없음 · 틀린 암호 · allowlist 밖 | 400 `E0003 입력값오류(agencyCode)` · 400 `E0804` · 400 `E0817` · 403 |
| 대량 5,000행(스크래치 합성 암호 xlsx) 등록·s2 · s1 | API 283ms · 220ms, 응답 약 957KB · 판정 계수 신규 998 · 되살리기 1,000 · 이미 사용중 3,000 · 중복 2 · 접두 추가 50 / s1 선택 시 의심 참 |
| 조회 실패(표 이름을 잠시 바꿈) | 500 `IE9999`, 로그 한 줄 `intake member lookup failed: code=ER_NO_SUCH_TABLE ids=3` |
| 서버 로그 전체 | 합성·대량·틀린 암호 · 합성 비밀번호 · 로그인아이디 문자열 0건, multipart 본문 13건 전부 `"body":{}` |
| 화면 — 읽기 전 · 읽은 뒤 | 프리뷰 영역 없음 · 기업 라디오 3개(checked 0), 미리보기 **닫힘** + "대상 기업을 고르면 미리 볼 수 있습니다." |
| 기업(s2) 고른 뒤 클릭 직후 | 열림 → `불러오는 중…` · 기업 라디오 잠김, 217ms |
| 결과 | 경고 없음 · 매칭 표 `s2(선택) 2행 1행` / `s1 1행 1행` · 묶음 계수 · 기준 줄 `판정 기준 기업: s2 · 합성기업둘` · 행 표 3행 |
| 필터 `변경 없음` | `aria-pressed=true`, 1행 |
| 기업을 s1 로 바꿈 | 결과 즉시 사라짐 → 미리보기 → 경고 `role=alert` + 기준 줄 `… — 기업 선택 의심` + 매칭 표 s1(선택) 맨 위 |
| 암호 · 의도 · 파일을 바꿈 | 셋 다 프리뷰 영역 사라짐, 다시 읽으면 기업 선택 초기화(checked 0) |
| 등록·s2 에서 필터 `검토 필요` | "이 묶음에 해당하는 행이 없습니다." |
| 대량 5,000행 화면 | 클릭→표 313ms · 페이지 50행 · 페이지 번호 1~10 묶음 · 2페이지 이동 23ms · `검토 필요` 필터에서 중복 2행 + 페이지 1 로 돌아감 |
| 기업 목록 실패(응답 가로채기 500) | 서버 message 그대로 + `다시 시도` → 라디오 3개 복귀 |
| 0행 응답(응답 가로채기) | "읽은 데이터 행이 없습니다." |
| 브라우저 저장소 | local 0 · session 은 주입한 `ins` 뿐 |

- **라이브가 바꾼 것 1건** — 삭제 미매칭 행이 "현재 상태" 와 "판정" 칸에 같은 문구 "선택 기업에 없음" 을 두 번 보였다. 판정 라벨을
  it 이름의 용어 "미매칭" 으로 바꿨다. **라벨 변경 뒤 화면은 다시 보지 않았다** — 상수 한 줄이고 `#6`·`#9` 재완주로만 확인했다.
- **라이브가 가린 것** — 로그인 화면을 거치지 않았다. DB 는 4표 최소 DDL 이라 인덱스·제약·데이터 분포가 stg 와 다르다(응답 시간은
  하한으로만 읽는다). 대량 합성 파일의 행 번호는 안내 행 뒤 빈 행이 없어 3부터다. 콘솔 500 3건은 없는 표(`/admin/menu/my` ·
  `/admin/alarm`)와 가로챈 응답이다.
- **스크래치 부수물** — cwd 가 admin 서브모듈일 때 mccp 훅 추적 2건이 `insuniverse-admin/.claude/state/` 에 쌓였다(이 세션 것). 지웠다.

### 운영 분포 대조 — (미검증)

Task 5 의 선택 항목이다. 운영 리더 접근은 매번 사람이 승인한다(UI15) — 이 창에서 요청하지 않았다.

### 설계 게이트 3.6·3.7

`impeccable-detect --mode implement` → `design_signal=false · reason=no-signal`(서브모듈 diff 미탐). 3.6 은 트리거 미충족으로
건너뛰었고 2.5.5c 캡처가 없어 3.7 도 no-op 이다. 화면 판단은 계획 창 critique(CONVERGED)와 위 라이브 표가 진다.

### 계획서와 달라진 것

1. **`E0820` 신설 대신 기존 `NOT_FOUND_AGENCY`(E0204)** — 같은 뜻의 상수가 이미 있다. `admin.constant.ts` 무변경.
2. **`judgeIntakeRows` 가 `agcId` 대신 `agency{agcId, code, name}`** — 선택 기업 매칭이 0건이어도 요약에 코드·이름이 실린다.
3. **조회 실패를 서비스에서 잡는다**(S1) — 필수 it 목록 밖에 it 1건을 더했다.
4. **M3 폼 주석의 `dangerouslySetInnerHTML` 문자열을 풀어 썼다** — `#9` 가 주석까지 세어 "쓰지 않는다" 주석에 붉었다.
5. **판정 라벨 `not-found` = "미매칭"** — 라이브 관측.

### 사람에게 넘기는 것

1. 서브모듈 두 벌 커밋 → 부모 포인터 bump · 이 창 산출물 커밋 → push (UI12). 게이트는 커밋 **전** 상태로 돌았다 — 서브모듈을 커밋하면
   HEAD 가 움직여 `#1` 이 그 뒤로 실패한다(M3 와 같다).
2. 운영 분포 대조(선택) — 운영 리더 접근 승인 여부.
3. PRD 4행은 `in-progress` 그대로 뒀다 — push·포인터 bump 가 §Acceptance 의 마지막 행이다.

## 로컬 code-review 흡수 (2026-09-17)

`/mccp:code-review` 로컬 모드 — 커밋 전 변경분(서버 8 · admin 3 · notes). CRITICAL 0 · HIGH 1(규칙표 함수 50행) · MEDIUM 1 ·
LOW 4. 사용자 지시 "모두 수용" — 전건 반영하고 게이트를 커밋 **전** 상태로 다시 완주했다.

### 라운드 표

| 라운드 | A | B | 층위 | 흡수 | 기각 |
|---|---|---|---|---|---|
| 로컬 code-review | 저자(규칙표 7범주) | — | 1·2 | 6 — 아래 표 | 0 |

| 지적 | 조치 | 증거 |
|---|---|---|
| HIGH 함수 50행 초과 — `judgeIntakeRows` 130 · `IntakePreview` 236 | 서버는 같은 파일 안 `validLoginIdOf` 7 · `verdictOf` 25 · `judgeRow` 47 · `summarize` 37 · `judgeIntakeRows` 24 로 나눴다. 프런트는 같은 파일 안 `useAgencies` · `AgencyPicker` · `AgencyMatchTable` · `VerdictCounts` · `GroupFilter` · `VerdictTable` · `PreviewResult` · `previewForm` 로 나눠 최장 `IntakePreview` 50 · `PreviewResult` 49. 새 파일은 `#7` 표 밖이라 만들지 않았다 | 서버 — judge 13 · preview 13 it 초록 · `#8` 의심 무력화 사본 판별 유지. 프런트 — 아래 DOM 대조 |
| MEDIUM 프런트 새 코드 테스트 없음 · 라벨 변경 뒤 화면 미확인 | admin 에 test 인프라가 없고 INV1(신규 의존 0)이라 spec 을 더하지 않았다. 대신 가짜 API 로 화면을 띄워 리팩터 전후 DOM 을 대조했다(아래) — 라벨 "미매칭" 을 스크린샷으로 확인 | 스크래치 하네스, 이력 밖 |
| LOW DB 로그인아이디를 바이트 비교 — collation 이 맞춘 대소문자·뒤 공백 행을 JS 가 버린다 | `matchKey` = `trimEnd().toLowerCase()` 로 DB 쪽을 접어 대조. 옛 데이터 실재는 **(미검증)** — DB 를 보지 않았다 | 새 it `judge: DB 로그인아이디가 대소문자·뒤 공백만 달라도 같은 아이디로 대조한다` |
| LOW `restore-subscription` 이 M1 경로로 복원 못 하는 `none`·`expired` 도 "반영 예정" | M4 는 계획서 Task 2 규칙 4 그대로 둔다. **M5 인계** — 사용중 회원의 구독 `none`·`expired` 는 M1 `resign` 이 `no-subscription`·`expired` 로 건너뛰는 축이다. M5 가 신규 구독 생성으로 반영할지, 묶음을 검토 필요로 옮길지 정한다 | 이 절 |
| LOW 죽은 분기 `m.agcId === null` | 리포지터리 select 의 `agcId` 를 결합한 `tnAgency.agcId` 로 바꿔 타입이 null 을 싣지 않게 하고 분기를 지웠다 | 새 단정(리포지터리 it) · select 를 되돌리면 서버 tsc 가 `samsung-intake.service.ts` 에 TS2322 |
| LOW 미리보기 결과에 `role="status"` 없음 | 늘 DOM 에 있는 `sr-only` 알림 영역 — 결과가 오면 "미리보기 N행을 불러왔습니다." | DOM 대조에서 이 요소만 달라짐 |

### 반사실

| 흡수 | 뒤집은 것 | 결과 |
|---|---|---|
| 대소문자·뒤 공백 대조 | 서비스 사본 `matchKey` 를 항등 함수로 | 새 judge it 붉음(`toEqual` 차이) |
| 기업 키 출처 | 리포지터리 사본 `agcId: tnMember.agcId` | 리포지터리 it 붉음 — `Expected "tn_agency.agc_id" · Received "tn_member.agc_id"` |
| (두 사본 한 실행) | moduleNameMapper 로 사본 2벌 | 23건 중 붉음 2 · 초록 21, 원본 43/43 초록 · 사본 정리 확인 |
| 프런트 리팩터 | 쪼개기 전 DOM 기준본 14장면(읽기 · 기업 선택 · 요청 중 · 결과 · 2페이지 · 묶음 둘 · 기업 전환 · 의심 · 암호 변경 · 기업 목록 실패·재시도 · 0행 · 미리보기 실패) | 기준본 두 번 뜬 결과 같음(스크린샷이 남기는 `style=""` 만 정규화) → 리팩터 뒤 14/14 동일 → 알림 영역 추가 뒤 13장면이 그 요소만 다름(요소를 빼면 0) |

- **리포지터리 단정은 열 객체를 직접 비교하지 않는다** — 실패하면 drizzle 열의 순환 참조가 `jest --json` 보고서를 깨뜨려
  (`Converting circular structure to JSON`) `#4`·`#5` 가 "보고서를 못 읽었다" 로 오진한다(사본 첫 실행 실측). 렌더한 SQL 문자열로 비교한다.
- **리팩터 중 스스로 잡은 결함 1건** — `preview` 의 조회 아이디 중복 제거를 객체 `Set` 으로 옮겨 적어 중복이 안 걷히는 형태였다.
  테스트 전에 고쳤다. 기존 spec 은 픽스처에 중복이 없어 이 자리를 보지 않는다(보고만 — it 추가 안 함).

### 게이트 `#1`~`#10` — 전건 초록 (rc=0, 134초)

```
#1 ok  srv=a4bb84fa · adm=69d9f6b9 · base=6c34b879 · jest{suites:4,tests:27} tsc{srv:4파일/19서명,adm:633서명}
#2 ok  실패 테스트 27건 전건 기준선 안(식별자 대조) · 신규 실패 0
#3 ok  서버 tsc rc=2 · 서명 19건 전건 기준선(19건) 이하 · 범위(fc·에러 상수) 0건
#4 ok  judge 축 13건 통과 · 전건 단정 1건 이상
#5 ok  preview 축 13건 통과 · 전건 단정 1건 이상
#5 ok  getIntakeMembersByLoginIds 출현 3파일 — 정의·인테이크 서비스·spec 뿐
#6 ok  프런트 tsc rc=2 · M4 파일 3종 프로그램 실림 · 에러 0 · 서명 633건 전건 기준선(633건) 이하
#7 ok  기준 커밋 대비 변경 12경로 전건 표 안(표 21행) · 자격증명 첨부 의심 0건 · PRD 4행 status=in-progress
#8 ok  가드 우회·기업 조건·의심 무력화 사본으로 붉음 3건(판별) · 초록 3건(독립 축)
#9 ok  filePassword 2곳 전건 FormData append(IntakePreview 포함) · preview(FormData) · agencies() · 응답 타입에 password 값 키 0 · 기업 초기값 null·기본값 0 · 의도 null·암호 빈 문자열 유지 · innerHTML 0 · 브라우저 저장소 0
#10 ok  사각지대 8축 — (축 목록은 §구현 완주 기록 의 출력과 같아 생략) · §NOT Building 이름 대조 8/8
```

- 서버 tsc rc 가 앞 완주의 1 에서 2 로 바뀌었다. 서명은 19건 그대로라 `#3` 은 rc 를 보지 않는다 — 원인은 보지 않았다.
- 리뷰 중 cwd 가 admin 서브모듈로 옮겨가 훅 추적 1건이 `insuniverse-admin/.claude/state/` 에 쌓였다(이 세션 id 확인). `#7` 이 잡기 전에 지웠다.
  서버 서브모듈의 `.claude/state/hook-trace/` 3건은 다른 세션 id 라 두었다(gitignore).

### 사람에게 넘기는 것 (갱신)

1. 서브모듈 두 벌 커밋과 부모 포인터 bump 는 이 절과 함께 커밋했다. **push 는 하지 않았다**(UI12) — 커밋으로 서브모듈 HEAD 가
   움직여 `#1` 은 이 뒤로 실패한다(M3 와 같다).
2. 운영 분포 대조(선택)와 로그인아이디 대소문자·뒤 공백 옛 데이터 실재 — 둘 다 운영 리더 접근 승인 사항.
3. M5 인계 — 위 `restore-subscription` 의 `none`·`expired` 반영 방식.
