# 삼성화재 계정 인테이크 M5 — 확정 반영과 실패 회수 notes

계획서 `.claude/plans/samsung-account-intake-m5.plan.md` 는 `plan_hash` 로 봉인됐다(`sha256:29be106a…`). 계획 게이트 뒤의 판정·인수는
계획서가 아니라 여기에 적는다 — 계획서를 고치면 plan-codex receipt 가 stale 이 된다.

## 계획 창 결정 (2026-09-17, 사용자)

| 질문 | 결정 | 계획서 |
|---|---|---|
| 미리보기 뒤 DB 가 바뀐 행 | 그 행만 반영하지 않고 상태 변경으로 돌려준다 | UI4 |
| 실패 행 회수 (PRD Open Question) | 실패분 엑셀 + 실패 행만 다시 확정, 둘 다 | UI5 |
| 구독 만료·무이력 | 기존 구독처리 경로로 신규 구독 생성 | UI6 |
| 되살리기 계정 값 | 비밀번호만 덮어쓴다 — 없거나 형식 밖이면 유지 | UI7 |
| 행 선택 | 반영 예정 전건 기본 선택 + 의심이면 확인 체크 잠금 | UI8 |
| 제외 사유 (PRD Open Question) | 받지 않음 — 미선택 수만 | UI9 |
| 실행 방식 | 요청 안 처리 + 화면 행별 결과 + 요약 알림 | UI10 |
| seat 하한 | 수치를 보이고 한 번 더 확인 후 강행 | UI11 |

## 계획 게이트 판정

- 슬러그 `samsung-account-intake-m5`(계획서 경로) · 원장 1/1 · pin `single-pass+codex-disabled` · 모드 `multi-agent`
- L1 `converged` · L2 **4/3 `converged`**(4관점 전건 pass · HIGH 0 · MEDIUM 3 · LOW 4) · L3 미발화(Codex 비활성)
- L2 패널은 CLAUDE.md §0·§3 을 `buildPrompt` 에 주입한 스크래치 사본으로 돌렸다(M3·M4 와 같은 방식)
- 리뷰 기록 `.claude/reviews/plan-review-samsung-account-intake-m5.md` · receipt `.claude/receipts/mccp-plan-codex/samsung-account-intake-m5.json`
  (`review_verdict=converged` · `review_source=multi-agent` · design critique 3라운드 `converged`)
- `validate --command mccp:prp-implement --decision samsung-account-intake-m5` → `ok:true`
- multi-agent 승인은 cross-gate dedupe 를 채우지 않는다 — `/mccp:pr` 에서 PR-Codex 가 돈다

## 계획서 동결 뒤 나온 지적 — 구현 창 인수

계획서를 고치지 않는다. 구현 창이 흡수하고 흡수마다 반사실(수정 전 붉음 → 수정 후 초록)로 확인한다.
층위는 CLAUDE.md §2 번호다.

| # | 관점·등급 | 지적 | 층위 | 구현 창 조치 |
|---|---|---|---|---|
| P1 | security MEDIUM | 확정이 쓰는 `memId` 를 어디서 고르는지 계획서가 적지 않았다 — 조회는 기업 제한이 없어, 다른 기업의 같은 아이디 회원을 고르면 그 기업 회원이 중지·되살리기·비밀번호 변경된다 | 2 | 쓰기 대상 `memId` 는 **재판정에서 선택 기업 `agcId` 와 일치한 회원(`mine`)** 에서만 가져온다. `confirm:` 판정별 it 의 가짜 조회에 같은 아이디의 다른 기업 회원을 함께 넣고, 쓰기 인자가 선택 기업 회원 번호인지 단정한다 |
| P2 | test MEDIUM | 중지·해지 행에서 `resign` 반환 `skipped` 의 `not-found` → 실패, `no-subscription` → 반영 매핑을 보는 it 이 없다 | 2 | 중지·해지 it 에 두 사유를 넣어 행 결과를 단정한다(backlog `samsung-m1-resign-skipped-discarded` 부분 흡수의 증거) |
| P3 | invariant MEDIUM | 되살리기 행의 `resign(useAt 1)` 반환 사유 처리가 없다 — 두 인자면 늘 `not-requested` 이고 `not-found` 가 섞여도 반영으로 셀 수 있다 | 2 | 되살리기에서 `not-found` 는 행 실패, `not-requested` 는 예상된 값으로 무시(구독은 뒤 `subscribe()` 가 맡는다 — DD1). 그 밖 사유는 실패로 fail-closed. it 으로 단정 |
| P4 | test LOW | 의심 확인이 **있을 때** 쓰기가 진행되고 표지가 로그·알림에 남는 서비스 수준 it 이 없다 | 2 | `confirm:` 요약 알림 it 안에서 의심 확인 경로의 알림 문구 표지를 함께 단정한다 |
| P5 | invariant LOW | `#1` 은 기준선 파일이 있으면 clean 트리 검사를 건너뛴다 | 8 | 기존 공시(§NOT Building `baseline.json 변조`) — 조치 없음 |
| P6 | invariant LOW | `#7` 의 기대 PRD 상태를 환경변수로 바꿀 수 있다 | 7 | 기존 관례(M1~M4 같은 형태) — 완주 기록에 사용한 값을 함께 적는다 |
| P7 | security LOW | `force`·`mismatchAcknowledged` 는 서버가 확인 여부를 증명하지 못한다 | — | 계획서 DD10 이 의도로 받아들였다 — 표지 기록(P4)으로 갈음 |

## 계획 창에서 스스로 잡은 것

- 짧은 `파일:줄` 인용 12건이 mccp L1 C6 로 떨어졌다 — `파일#L줄` 과 전체 경로로 고쳤다
- fan-out architect MEDIUM 이 초판의 사실 오류를 짚었다 — 서버 `noImplicitAny: false` 라 대괄호 접근 이름은 tsc 가 못 본다
- 도구 안 인라인 서브셸은 `set -e` 가 무시돼 `#10` 음성이 FAIL 뒤 `ok` 까지 진행했다 — 셸 블록 음성은 전부 `bash` 파일 실행으로 다시 떴다

## 사람에게 넘기는 것

1. admin `IntakePreview.tsx` 의 이 창이 만들지 않은 prettier 변경(76+/19-) — 되돌리거나 따로 커밋해야 Task 1 기준선이 선다
2. PRD Risks "1 이 닫히기 전에 5 를 시작하지 않는다" — M1 코드는 브랜치에 있으나 M1 milestone-close·PR 은 아직이다
3. backlog `samsung-m1-seat-upper-bound` 를 전제 없음(seat 는 하한뿐)으로 기각했다 — 상한 규칙이 사업상 있으면 알려 달라
4. 계획 창 산출물 커밋 — 사람이 스위치를 잡는다

## Codex Implementation Review

> 계획서가 아니라 여기에 둔다 — 계획서 본문을 고치면 `plan_hash` 가 바뀌어 `mccp-plan-codex` receipt 가 stale 이 되고
> `/mccp:pr` 이 막힌다(기억: `mccp-implement-review-goes-to-notes`).

- 호출: `node ~/.claude/plugins/cache/mccp/mccp/1.33.6/scripts/lib/codex-invoke.js adversarial-review` (fail-closed Bash wrapper)
- 라운드 수: 0 — `> Codex skipped per MCCP_CODEX_DISABLED=1` (`classification=disabled` · `blocking=false` · `durationMs=0`)
- 합치 결론: Codex 는 운영자 정책으로 비활성이다. 라운드 상한은 봉인값 `cap=1 pinnedBy=single-pass+codex-disabled`
  이고 R2 는 존재하지 않는다. 구현 시점 결정은 아래 security-reviewer 와 계획 게이트 L2 4관점이 봤다.
- YAGNI Triage:

  | Finding | Severity | Verdict | Why |
  |---|---|---|---|
  | (없음) | — | — | Codex 미발화 — 판정할 finding 이 0건이다 |

- Deferred to backlog: 0
- Open Questions: (없음)
- Codex session 참조: (없음 — 미발화)

### 구현 시점 결정 (2.5.2)

계획서가 미리 못 박지 않은 자리. 아래 넷이 security-reviewer 의 초점이었다.

1. `samsung-intake-confirm.ts` 가 `IntakeVerdict`·`SamsungIntakeIntent` 를 **타입 전용 import** 로 가져온다 — 런타임 순환이 없다
   (`isolatedModules: true` 라 `import type` 필수).
2. `rejudge` 는 기업 조회를 하지 않고 **조회된 기업을 인자로 받는다** — 확정은 진행 표시를 잡기 전에 기업이 필요해서(순서 2·3),
   계획서 시그니처대로면 같은 질의를 두 번 돈다.
3. 행 실패는 `settleInBatches` 의 거부가 아니라 worker 안 `try/catch` 로 결과 행을 만든다 — 사유 조립과 비밀번호 가림이 한 자리다.
4. `maskPassword` 는 `split/join` 이다 — `new RegExp(password)` 였다면 정규식 메타문자가 든 비밀번호가 안 가려진다.

### Security Reviewer

`Task(subagent_type: "security-reviewer")` — `review proposed implementation: 확정 엔드포인트(입력 검증 · 인가 경계 ·
비밀번호 취급 · 주입/경로 · 권한 상승)`. 판정 **CRITICAL/HIGH 0건**.

| 축 | 판정 | 근거 |
|---|---|---|
| 입력 검증 | 통과 | `selections` 는 busboy 기본 1MiB 필드 한도 안(서버에 `fieldSize` 설정 0건) · `parseSelections` 가 쓰기 전 거부 · 값은 동등 비교에만 쓰여 주입면이 없다 · 전역 `ClassValidatorPipe` whitelist |
| 인가 경계 | 통과 | 클래스 레벨 가드라 `confirm` 이 자동 상속 · access spec 이 프로토타입에서 핸들러를 유도 · allowlist 가드는 빈 목록에 fail-closed |
| 비밀번호 | 통과 | 응답·엑셀 열·알림에 값 칸 없음 · 비HTTP 예외는 코드만 로그 · multipart 필드는 요청 로그에 안 실림 |
| 주입·경로 | 통과 | 파일명 조립은 닫힌 enum 두 개뿐 · exceljs 는 평문 문자열을 `Cell.Types.String` 으로만 쓴다(수식 주입 불가, `cell.js#L1062` 실측) |
| 권한 상승 | 통과 | `resign`·`subscribe` 는 내부 관리자에게 기업 경계를 안 본다 — 막는 것은 M5 설계뿐이다. 쓰기 `memId` 는 `judgeRow` 의 `mine`(선택 기업 `agcId` 일치)에서만 나오고 응답에 싣지 않아 클라이언트가 지정할 수 없다 |

구현 창 조치 — 리뷰어가 유일하게 남긴 비차단 관찰(정규식 메타문자 비밀번호가 안 가려질 수 있다)을 `outcome:` 가림 it 의
입력에 넣는다. 나머지는 계획서 DD10·§NOT Building 이 이미 판정했다.

### Design Review

`impeccable-detect --mode implement` → `skill_available=true` · `design_signal=false` · `silent_skip=true`
(`reason=no-signal`). UI 가 서브모듈이라 검출기가 diff 를 못 본다(기억: `mccp-impeccable-detect-blind-to-submodules`).
receipt 에 `--impeccable-silent-skip --impeccable-silent-skip-reason no-signal` 로 실린다. 화면 판단은 계획 창
design critique 3라운드(`converged`)와 Task 5 라이브 1회다.

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

### 게이트 `#1`~`#11` — 전건 초록 (rc=0, 1회 완주 2분 8초)

```
#1 ok  srv=e411a84b · adm=db249303 · base=30d0e099 · 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  plan 축 9건 통과 · 전건 단정 1건 이상
#5 ok  confirm 축 20건 통과 · 전건 단정 1건 이상
#5 ok  getIntakeMembersByLoginIds 출현 4파일 — 정의·인테이크 서비스·spec 두 벌뿐
#6 ok  프런트 tsc rc=2 · M5 파일 3종 프로그램 실림 · 에러 0 · 서명 633건 전건 기준선(633건) 이하
#7 ok  기준 커밋 대비 변경 12경로 전건 표 안(표 21행) · 자격증명 첨부 의심 0건 · PRD 5행 status=in-progress
#10 ok  M1 opt-in 봉인 유지(허용 호출처 0곳) · 인테이크 서비스 .resign( 2줄이 봉인 검사 범위 안
#8 ok  확정 우회·괴리 무력화·의심 확인 무력화·하한 사전 판정 제거 사본으로 붉음 4건(판별) · 초록 4건(독립 축)
#9 ok  filePassword 3곳 전건 FormData append · confirm(FormData) · 응답 타입에 password 값 키 0 · 기본 선택·재시도 미리 선택 실행 2건 ·
       확정 잠금 진리표 5행 · 배선 4곳 · seat 강행 append 1곳 · selections JSON 필드 · 실패분 Blob 내려받기 ·
       의도 null·암호 빈 문자열·기업 null 유지 · innerHTML 0 · 브라우저 저장소 0
#11 ok  사각지대 9축 (축 목록은 §NOT Building 과 이름 대조 9/9)
```

- `M5_PRD_EXPECT` 는 기본값 `in-progress` 로 돌렸다(P6 — 계획 창이 환경변수 교체 가능성을 공시했다).
- `#1` 은 이 뒤로 붉다 — 서브모듈 커밋이 HEAD 를 옮긴다(M3·M4 와 같다).

### 계획서 동결 뒤 인수(P1~P7) 반영

| # | 조치 | 반사실 |
|---|---|---|
| P1 | 쓰기 `memId` 는 `memberOf`(선택 기업 `agcId` 일치)에서만 나온다 — `samsung-intake.service.ts` 의 한 루프가 테넌트 경계다. `confirm:` 되살리기·구독 복원·중지 it 셋이 같은 아이디의 다른 기업 회원(999)을 함께 넣고 쓰기 인자에 그 번호가 없음을 단정 | 라이브에서 s1 회원 601 이 등록·삭제 확정 전후로 무변경 |
| P2 | `stop`·`cancel-subscription` 은 `assertResignApplied(res, ['no-subscription'])` — `not-found` 는 행 실패다. 중지 it 이 세 행에 두 사유를 넣어 `[applied, failed, applied]` 를 단정 | — |
| P3 | `revive` 는 `['not-requested']` 만 허용. 그 밖 사유는 fail-closed | — |
| P4 | 요약 알림 it 이 의심 확인·하한 강행 경로에서 표지 두 개를 단정 | 라이브 알림 행에 `하한 강행` 실림 |
| P5·P6 | 공시로 갈음(기존 §NOT Building · 완주 기록의 `M5_PRD_EXPECT` 값) | — |
| P7 | 계획서 DD10 의 의도 — 조치 없음 | — |

security-reviewer 의 비차단 관찰(정규식 메타문자 비밀번호)은 `outcome:` 가림 it 의 입력에 `a.*b(c)+[d]` 로 들어갔다.

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

- **DB**: docker `mysql:8.0.42`(127.0.0.1:33309, M5 전용으로 따로 띄웠다 — 다른 세션의 `abp-live-mysql` 은 건드리지
  않았다). **stg 에 닿지 않았다** — 그 컨테이너에서 `mysqldump --no-data --routines` 로 표 175·루틴 7을 떠 새 컨테이너에
  적재하고, 기업 `sf`(21)·`s1`(30)·`s3`(34)·`s2`(38) · 상품 1 · 계약 2(s2 seat 3) · 합성 회원과 구독을 넣었다. 전부 지어낸 값.
- **`--default-character-set=utf8mb4` 가 필요하다** — 없이 시드하면 `tc_common.cmn_name` 이 이중 인코딩돼 `classify` 가
  전건 `unknown-subscription-state` 로 떨어진다(이 창 실측, 한 번 걸렸다).
- **서버**: `nest build` → 엔트리는 `dist/apps/api/apps/api/src/main.js` 다(중첩 경로). preload 로 `listen(7200)` 을
  7203 으로 돌렸다 — 7200 은 다른 워크트리 세션이 쓰고 있다. 본 체크아웃 `apps/api/.env` 복사 + 덮기(DB · `REDIS_URL=` ·
  로컬 `JWT_SECRET` · `AES_SECRET` · `SAMSUNG_INTAKE_ADMIN_IDS=990001` · 외부 자격 빈 값, `FCM_PRIVATE_KEY` 는 둔다).
- **프런트**: vite **5199** — `ALLOW_ORIGINS` 에 든 포트라야 CORS 가 선다(5291 로 띄웠다가 되돌렸다).
  `_aaT` 쿠키는 도메인 `127.0.0.1` 에 달면 서버(7203)·화면(5199) 양쪽에 간다. playwright-core 스크립트로 구동.
- 끝나고 서버·vite·컨테이너를 내리고 두 `.env` · 토큰 2건 · DB 암호 · 서버 로그 · 합성 엑셀을 `shred` 했다.

| 확인 | 결과 |
|---|---|
| 접근제어 — allowlist · 밖 · 무토큰 | `session` 200 · 403 · 401 |
| 등록 확정(되살리기 + 신규 생성) | 501 `mem_is_resigned` 1→0 · 비밀번호 해시 교체 · 구독 1001 해지→구독 / 신규 2396 생성 + 구독 3393. **같은 아이디의 s1 회원 601 무변경**(P1) |
| 삭제 확정 — 하한 경고 | `{code:SEAT_BELOW_CONTRACT_WARN, data:{current:4, after:1, seat:3}}` 로 거부 · **중지 0건**(쓰기 전 차단) |
| 삭제 확정 — 강행 | `force=true` 로 3행 전건 반영 · 알림 문구에 `하한 강행` |
| 미리보기 뒤 손으로 되돌린 행 | 4행 `changed` `이미 사용중` "미리보기 뒤 판정이 달라졌습니다" · 쓰기 없음 / 5행은 반영 / 고르지 않은 6행 `unselected 1` |
| 되살리기 비밀번호 없음(UI7) | 5행 반영 + `detail` "파일에 쓸 수 있는 비밀번호가 없어 기존 비밀번호를 유지했습니다." |
| 행 실패 격리(상품 삭제) | 3행 전건 `failed` "상품 존재하지 않음" · 앞 단계(비밀번호·사용 복원)는 반영된 채 남는다 |
| 실패 회수(DD4) | 상품 복구 뒤 재판정이 3행을 `restore-subscription` 으로 잡고 확정이 전건 반영 → 사용·구독 |
| 반영 예정 밖 판정 · 기업 의심 | `already-registered` 선택 → `E0001` / s1 선택 시 `mismatch=true`, 확인 없이 확정 → `E0820` |
| 대량 7,786행 삭제 확정 | **204.3초**(26ms/행) · 전건 반영 0실패 · 응답 870KB · 미리보기는 234ms |
| 선택 필드 크기 | 7,786행 `selections` **201,333바이트 = busboy 기본 1MiB 한도의 19.2%** (계획 관측 13 의 추정 318,129B 보다 작다 — 가장 긴 판정 이름이 아니라 실제 `stop` 이라서) |
| 서버 로그 905줄 | 합성 비밀번호·파일 암호·로그인아이디 **0건** · 인테이크 요청 20건 전부 `"body":{}` · 확정 로그는 계수와 표지만 |
| 화면 — 읽기 전 | 확정 영역 없음 |
| 화면 — 미리보기(211ms) | 선택 열 · 기본 선택 반영 예정 2/2 · `이미 사용중` 행은 체크 해제 + `disabled` · "반영 예정 2행 중 2행 선택" |
| 전체 해제 / 전체 선택 | 확정 버튼 `disabled` + "반영할 행을 한 건 이상 고르세요." / 다시 열림 · 버튼 문구가 계수를 따라간다 |
| 확인창 | "s2 · 합성기업둘에 등록 1행을 반영합니다." |
| 결과 | 요약 5칸 · "고른 행이 모두 반영됐습니다" · 결과 표 1행 · `실패` 필터 "이 결과에 해당하는 행이 없습니다" |
| 기업 바꿈 | 결과·확정 영역 즉시 사라짐 |
| 기업 의심 화면 | 라벨 "선택 기업 s1 · 기업명 매칭 1행 · 가장 많이 매칭된 다른 기업 s2 3행 — 기업 선택을 확인했습니다" · 확인 전 `disabled` + 이유 · 확인 뒤 열림 |
| 실패분 내려받기 | 파일명 `삼성화재_계정반영_실패분_s2_등록.xlsx` · 머리행 6열 + 실패 2행 · 비밀번호 열·값 0건 |
| 실패 행 다시 확정 | 재판정 뒤 4·5행만 미리 선택(6행 해제) · 확정 → 2행 전건 반영 · DB 사용·구독 |
| 브라우저 저장소 | local 0 · session 은 주입한 `ins` 뿐 · 콘솔 error 0건 |

- **라이브가 바꾼 것 1건** — `IntakeConfirm.tsx` 의 `return api.confirm(form).then(…)` 을 prettier 훅이 `api` 와
  `.confirm` 사이에서 접어 게이트 `#9` 의 `api.confirm(` 검사가 재완주에서 붉어졌다. `const sent = api.confirm(form);`
  한 문장으로 붙들어 재포맷에 견디게 고쳤다. 동작은 같은 약속 사슬이라 위 표는 유효하다. 고친 뒤 `#1`~`#11` 을 다시 완주했다.
- **커밋 직전에 잡은 것 1건** — `admin.constant.ts` 는 **CRLF** 파일인데 파이썬 텍스트 쓰기가 LF 로 바꿔 놓아
  `git diff` 가 102+/98- 로 떴다(내용 변경은 4줄뿐). 프리티어가 아니라 개행이 원인이다. 바이너리 읽기·쓰기로
  다시 넣어 `+4` 로 되돌리고 게이트를 재완주했다. `#7` 은 경로만 보므로 이 유형을 못 잡는다.
- **라이브가 가린 것** — 로그인 화면을 거치지 않았다(토큰 주입). 스키마는 stg 사본이지만 데이터 분포·인덱스 통계가 달라
  소요는 **하한**으로만 읽는다. 배포 앞단(로드밸런서) 유휴 제한시간은 저장소 밖이라 204초가 실환경에서 끊기는지는
  **(미검증)** 이다. 동시 확정은 한 프로세스 안에서만 봤다(여러 태스크 배포는 §NOT Building).
- **대량 확정 소요의 함의** — 7,786행 204초는 계획서 관측 14 의 `requestTimeout` 30분 안이지만, 로드밸런서 유휴 한도가
  보통 60초라 **한 번에 수천 행을 확정하면 화면이 결과를 못 받을 수 있다**. 서버는 끝까지 반영하고 요약 알림이 남으며
  같은 파일로 다시 미리보기하면 반영분이 변경 없음으로 빠진다(DD4) — 후속 PRD 의 비동기 처리 판단 재료다.

### 운영 되살리기 모집단 — (미검증)

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

### 설계 게이트 3.6·3.7

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

### 커밋 뒤 로컬 리뷰 — `silent-failure-hunter` HIGH 2건 흡수

CLAUDE.md §4 "커밋한 뒤 리뷰어를 돌린다". 리뷰어는 P1~P7 과 겹치지 않는 두 건을 냈고 둘 다 범위 안이다
(resign `skipped` 소비 · 비밀번호 가림 · 테넌트 경계 · 행 격리는 "의도대로" 로 확인만 하고 새 지적 없음).

| # | 등급 | 지적 | 층위 | 흡수 |
|---|---|---|---|---|
| R1 | HIGH | **쓰기 뒤 부수효과가 전체 응답을 삼킨다** — `settleInBatches` 가 끝나면 DB 쓰기는 이미 커밋됐는데 `addAlarm` 이나 `buildFailuresXlsx` 가 던지면 `confirm()` 이 통째로 500 이 된다. 반영된 작업이 실패로 보고되고, 감사 로그 한 줄(DD10)과 실패 행 회수 경로(UI5)가 함께 사라진다 | 1 | 감사 로그를 알림보다 **앞**으로 옮기고, `addAlarm` 은 `.catch`, `buildFailuresXlsx` 는 `try/catch` 로 각각 가뒀다. 엑셀 실패는 그 칸만 `null` 로 낮춘다 |
| R2 | HIGH | **행 실패 로그가 진짜 버그와 예상된 예외를 구분 못 한다** — 비HttpException 은 `code=unknown` 한 줄만 남아 행 번호·기업·판정·예외 이름이 전부 없다. 요청 상관 ID 도 저장소에 0건이라 두 기업의 확정이 겹치면 사후 특정이 불가능하다 | 1 | `rowFailureReason(e, ctx, item)` 로 바꿔 `row=`·`agency=`·`verdict=`·`name=`·`code=` 를 남긴다. 전부 응답 JSON 에도 실리는 비민감 값이고 **원문·스택은 계속 버린다** |

**반사실** — 새 필수 it `confirm: 쓰기 뒤 알림·실패분 엑셀이 실패해도 결과가 돌아오고 행 실패 로그에 행·기업·판정이
남는다` 를 **먼저** 써서 수정 전 붉음을 확인하고(`1 failed`), 수정 뒤 초록(`25 passed`)을 봤다. 게이트
`SPEC_GROUPS.confirm` 에 등재해 `#5` 가 20건 → **21건**이 됐다.

흡수 뒤 `#1`~`#11` 을 다시 완주했다(rc 0). 착수 기준선은 동결값이라 서브모듈 커밋 뒤에는 `#1` 이 정의상 붉다 —
재완주는 **커밋된 포인터(`4deea121`·`f19710ca`·`54e39f9`)로 재기준한 기준선 사본**으로 돌렸고, 원본
`baseline.json` 은 건드리지 않았다.

```
#1 ok  srv=4deea121 · adm=f19710ca · base=54e39f9 · jest{suites:4,tests:27} tsc{srv:4파일/19서명,adm:633서명}
#2 ok  실패 테스트 27건 전건 기준선 안 · 신규 실패 0
#3 ok  서버 tsc · 서명 19건 전건 기준선 이하 · 범위 0건
#4 ok  plan 축 9건 · #5 ok  confirm 축 21건 · #5 ok  조회 출현 4파일
#6 ok  프런트 M5 파일 3종 실림 · 에러 0 · 서명 633건
#7 ok  기준 커밋 대비 변경 3경로 전건 표 안 · 자격증명 0 · PRD 5행 in-progress
#10 ok  M1 봉인 유지 · #8 ok  붉음 4건(판별) · 초록 4건 · #9 ok  프런트 계약 · #11 ok  사각지대 9축
```

## 브랜치 전체 로컬 리뷰 (2026-09-18) — `/mccp:code-review` 로컬 모드

`origin/main...HEAD` 20커밋. 부모 51파일(문서 10,134+) · 서버 `4d798f5..236dcb8` 19파일(4,266+) ·
admin `fb001e1..f19710c` 10파일(1,324+). **실코드는 전문을 읽었다.** M1~M5 각 창의 리뷰가 마일스톤
경계 안을 봤으므로, 이 패스는 경계를 가로지르는 자리와 병합 뒤에 남는 것을 봤다.

### 검증 실측

| 검사 | 결과 |
|---|---|
| 서버 `tsc --noEmit` | 19건 — 전건 기존 spec 파일, **이번 창 파일 0건** |
| 이번 창 spec 7스위트 | **81/81 통과** |
| 서버 전체 jest | 41통과 / 4실패(27건) — `treat/dispatch` 3 · `alimtalk` 1. 전건 창 밖이고 변경 파일을 import 하지 않음(실측) |
| admin `tsc`(참조 프로젝트 직접) | 633건 기존 붉음 · **SamsungIntake 파일 0건** |
| 픽스처 PII | `__fixtures__/samsung-intake-synthetic-encrypted.xlsx` 를 저장소 판독기로 **복호해 확인** — `합성회원일/이/삼` · `s0000000N` · `010-0000-000N`. 파일명이 아니라 내용으로 판정 |
| 지운 `.mcp.json` | Windows playwright 경로뿐, 자격증명 0건 |

판정 **APPROVE(코멘트)** — CRITICAL 0 · HIGH 0.

### 판정 표

| # | 등급 | 지적 | 층위 | 판정 |
|---|---|---|---|---|
| L1 | LOW | `forceAgain` 이 `seat.form` 에 `append("force")` 한다 — seat 모달이 두 번 뜨는 경로가 생기면 `force` 가 두 벌 실려 서버 `@IsIn(['true'])` 가 배열을 받아 400 | 1 | 흡수 — `form.set` 으로. 현재는 도달 불가(강행 뒤 서버가 같은 코드를 내지 않는다)이며 **잠재 결함**으로 기록 |
| L2 | LOW | 쓰기 뒤 `assertResignApplied` throw 로 "실패로 보고된 반영" | 1 | **기각(리뷰어 오판)** — `agencyResigned` 는 `UPDATE tn_member … WHERE mem_id IN (…)` 하나뿐이고 `getAgencySubscribe` 는 `.from(tnMember)` 다. 회원 행이 없으면 갱신도 0행이므로 `not-found` 는 정확히 "쓰기가 0행이었다" 를 뜻한다. 두 조건이 같은 조건이다 |
| L3 | LOW | 확인 모달의 "반영" 버튼에 잠금이 없다 — 바깥 버튼만 `confirmBlocked` 를 진다 | 2 | 흡수 — `disabled={busy}`. 실질 이중 제출은 `start` 가 모달을 닫아 막지만 잠금 조건이 한 자리에만 있으면 다음 편집이 두 자리를 어긋나게 둔다 |
| L4 | LOW | `admin?: any` 3자리 | 1 | 흡수 — `IntakeAdmin = { admId?: number; role?: string }` 한 자리로. 이 서비스가 읽는 칸은 `admId` 뿐이고 나머지는 `role` 만 보는 기존 메서드로 흘러간다. 기존 메서드 매개변수가 저장소 전역 `any` 라 좁힌 값이 그대로 간다(tsc 0건 확인) |
| M1 | MEDIUM | 동결 게이트가 브랜치 끝에서 안 돈다 — 병합 후 회귀망 0 | 8 | **이월** backlog `samsung-m5-frozen-gates-unrunnable`. 코드 결함이 아니라 검증 수단의 사각지대다 |
| M2 | MEDIUM | 대량 확정 204.3초가 LB 유휴 한도(통상 60초)를 넘어 UI5 회수 경로를 잃는다 | 6 | **이월** backlog `samsung-m5-bulk-confirm-lb-idle`. §NOT Building 이 비동기 처리를 공시했고 이 행은 그 사이 기간의 **운용 규칙** 부재를 가리킨다 |

### 재제기하지 않은 것 (이미 판정됨 — CLAUDE.md §3 리뷰 수용 규약)

seat **상한** 부재(관측 8 · 이월 표 REJECTED · §NOT Building), 기존 `resign` 호출처 3곳의 `skipped`
폐기(부분 흡수 공시), multer `limits` 부재(저장소 선례 10곳 전건 동일 · `selections` 는 1MiB 의 19.2%
실측), KDF 동기 130ms(`ponytail:` 주석이 상한과 이전 경로를 명시), 여러 태스크 간 동시 확정(DD8 ·
§NOT Building), 요청 로그 쿠키 헤더(backlog `server-request-log-cookie-header`).

### 흡수 뒤 재확인

서버 `tsc` 19건(수정 전과 동일) · 창 안 0건 · spec 7스위트 81/81. admin `tsc` 633건(동일) ·
SamsungIntake 0건. **게이트는 M1 사유로 재완주하지 않았다** — 재현 수단은 위 spec·tsc 다.
