인입 세 경로(Wodup·InBody·Motra)의 실패를 "어느 단계·어느 줄·왜·원문은 남는가"로, 같은 파일 재시도와 신규 재인입을 구분, 큰 파일 비용을 숫자로 — v0.18.0 I01 (2026-09-10)
- 기간: 2026-09-10 (세션 2개 — 첫 세션
5f069000…이 분석·Phase 1~3 과 Phase 4 적용 도중 사용량 한도로 중단, 세션20466fdd…이 이어받아 Phase 4~6. 오너 지시 "물어보지 말고 Phase 끝까지 완주"). 계획 ID I01 / Phase 3 Step 2([리팩터링 3-2]). - 랜딩: PR #1498 (Phase 1~5 한 PR, base
release/v0.18.0, 릴리스 큐merge:request) — release/v0.18.0 병합55e9fae8(2026-09-10 02:44 UTC · 11:44 KST, 큐 run 34430574867). 마이그레이션 없음. 엣지 함수_shared/wodup-normalizer.ts·_shared/wodup-import-worker.ts·_shared/wodup-import-errors.ts변경(엣지 배포는 승격 뒤 각 환경 Deploy 가 한다). release 통합 / staging 확인 / Production 배포는 서로 다른 상태: release 통합까지, staging·Production 은 미반영(§5). 적용 릴리스 v0.18.0 예정. - 설계서: 없음 — 분석·Phase 계획·"예상 효과·개선사항" 표는 이슈 #1415 댓글(2026-09-10).
- 정본: 인입 공통 계약
src/react/features/import/importContract.ts(ImportIssue·ImportPreflightError·경로 등록표IMPORT_PATHS·한도IMPORT_LIMITS·A11 화면 상태ImportJobView·worker 실패 코드 표WODUP_WORKER_ERROR_CODES), 화면 상태 변환features/import/wodupImportView.ts·motraImportView.ts, 파서services/wodupJsonlUpload.ts·services/inbodyImport.ts, 코덱domains/codecs/motraImportCodec.ts, 서버supabase/functions/_shared/wodup-import-errors.ts(worker 실패 코드·정규화 실패/경고 코드 정본)·wodup-normalizer.ts. 문서: 이 기록, 인입 파이프라인 §경로별 책임, 자원 예산 §3-5, 성능 기준선 §6. - 도구:
scripts/performance/import/wodup-large-file.mjs(npm run perf:import -- --sessions N) — seed 로 결정적 Wodup 파일을 만들어 브라우저 사전검사·SHA-256·서버 정규화의 시간·이벤트 루프 최대 정지·힙 최고점을 잰다. 결과 JSON 은scripts/performance/evidence/(gitignore, 규격은evidence/schema.json). - 게이트:
tests/react/importContract.test.mjs(경로 등록표의 소유 모듈 실존·worker 코드 표 일치·worker 가 실제로 던지는 코드 전수),motraImportContract(Motra typed 오류·죽은 파서 사본 부재),wodupNormalizerIssues(정규화 typed 오류·경고·worker 동봉),importReplayFixtures(fixture 결정성·manifest 대조),resourceContract(한도 근거·실측값), 기존wodupJsonlUpload·wodupImportRunner·inbodyImport·importRepositoryDestinations·readImportContractHardCut재조준.npm run check전체(§5).ci:precheck-local: ci:precheck-local precheck · static 통과 · unit-1 통과 1695 passed / 0 failed / 29 conditional skip · unit-2 통과 1837 passed / 0 failed / 9 conditional skip · database 통과 db reset(마이그레이션 전체 적용): pass · schema.sql 스냅샷 --check: pass · pgTAP: pass 136파일/2625 assert · 동시 저장 세대·영수증: pass · dirty 범위·세대 병합: pass · 큰 이력 계산 격리·동시 CRUD·통계 수렴: pass · 7분 29초 (2026-09-10 02:41 UTC · 11:41 KST, head7ea8dad7). - 버그리포트: 없음(수리 건 아님).
- 계약:
ImportPort결과에WodupImportRunOutcome(job / already_imported / cancelled / detached)·errorCode·statsPending, InBody 저장소 결과{kind: imported | already_imported, workspace, rowCount}(contracts/ports/importDto.ts·writes.ts). workererror_code표는_shared/wodup-import-errors.ts가 정본이고 클라이언트 표와 테스트로 일치.
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 인입 공통 계약(이슈·단계·원문 가용성·재시도 정책)·경로 등록표·A11 화면 상태·worker 실패 코드 표 + 일치 테스트 | ✅ d49b08dd |
| Phase 2 | Wodup 클라이언트: 줄 번호가 붙은 typed 사전검사, 500줄마다 양보 + 취소, 전체 파일 SHA-256 봉인, 같은 파일 재시도/신규 재인입 구분, D09 error_code → typed issue | ✅ a7b35f3c |
| Phase 3 | InBody: 건너뛴 행·중복·시간 표기를 결과에 싣고, 이미 넣은 파일을 성공으로 위장하지 않음 | ✅ c0812df7 |
| Phase 4 | Motra 코덱 typed 오류·통계 대기, 앱의 죽은 Motra 파서 사본 제거 | ✅ 4eac83a5 |
| Phase 5 | 서버 normalizer typed 오류·경고 코드, 큰 파일 계측, replay fixture | ✅ 7ea8dad7 |
| Phase 6 | 문서(이 기록·인입 파이프라인·자원 예산·성능 기준선·총괄) → Precheck → PR → Merge Check | ✅ release/v0.18.0 병합 55e9fae8 |
1. 배경
유저 A 가 내 정보 화면에서 Wodup 파일을 올린다. 파일 2번째 줄이 JSON 이 아니면 브라우저 사전검사는 "2번째 줄을 JSON 으로 못 읽음"까지 알아내지만, 화면은 그 문구를 버리고 "파일을 가져오지 못했어요"만 보여줬다. 같은 파일을 한 번 더 올리면 새 배치가 만들어져 서버가 끝까지 처리한 뒤에야 "이미 가져온 기록"으로 실패했다. 유저 B 가 InBody CSV 를 올리면 날짜·체중을 못 읽은 행은 조용히 빠졌고, 같은 파일을 두 번 올려도 "N개 가져왔어요"라고 성공처럼 말했다. 관리자가 Motra 기록을 대리 인입할 때 서버가 "이미 넣은 세션"(22023)·"권한 없음"(42501)으로 거부하면 앱 쪽 코덱은 그 오류를 구분하지 못했다.
A05(#1406)가 인입 서버 호출을 자기 저장소·코덱으로 옮기고 D09(#1404)가 Wodup worker 를 임대·재시도·원자 완료가 있는 durable 소비자로 바꾼 뒤, 남은 것은 파서·정규화·identity·부분 실패의 표현이었다. 이 트랙은 그 표현을 세 경로가 공유하는 계약 한 벌로 모으고, 큰 파일 비용을 G04 기준선의 "인입 미측정" 자리에 숫자로 채운다.
2. 문제 제기
세 경로가 실패를 각자 throw new Error("문구")·return null 로 말했다
"어느 단계에서, 어느 줄/행이, 왜, 원문은 남는가"를 담는 공통 모양이 없었다. 화면은 문구를 버리고 일반 안내로 바꿨고(Wodup 사전검사), 건너뛴 행·이미 넣은 파일 같은 부분 결과는 성공으로 흡수됐다(InBody). Motra 코덱은 서버 오류 코드를 통째로 넘겼다.
재시도와 신규 재인입의 구분이 서버 게이트에만 있었다
브라우저는 파일 identity(해시)를 만들지 않아 같은 파일을 다시 올리면 새 배치를 만들었다. D09 worker 가 나중에 해시를 봉인하므로 "같은 파일이 진행 중인가·이미 끝났는가"를 올리기 전에 알 수 없었다. 취소·업로드 중단 상태도 없어 폴링을 멈추면 서버는 계속 도는데 화면은 그냥 끝났다.
서버 정규화가 값을 조용히 추정했다
ISO 가 아닌 날짜("June 20, 2026")는 new Date() 로 해석해 시간대 가정이 들어갔고, 숫자가 아닌 세트 값("60kg")은 소리 없이 null 이 됐다. 정규화 실패는 Error("Line 2: invalid JSON …") 문자열뿐이라 worker 의 normalization_failed 뒤에 코드·줄 번호가 남지 않았다.
큰 파일 비용이 숫자로 없었다
G04 기준선은 인입을 "근사만(저장 문 연속 호출) — I01 이 잰다"로 남겼고, 자원 계약의 parseMemoryBytes 는 unmeasured 였다. 사전검사가 표본 1만 줄을 동기 루프로 파싱해 UI 를 얼마나 막는지도 몰랐다.
앱 저장소에 호출자 0인 Motra 파서 사본이 있었다
#1463(v0.17.7)이 관리자 화면을 Barbelic-docs/admin 으로 옮긴 뒤 motraNormalizedUpload.ts·motraXlsxBlocks.ts 는 테스트와 자원 계약만 참조하는 죽은 사본이었다.
3. 해결 방안
원칙
오너 결정 항목 없음(§26). 이슈 계획의 전제대로 진행: 판정 권위는 서버 게이트(one-shot·해시 대조)에 두고 브라우저는 같은 판정을 미리 안내만 한다. D09 worker runtime(임대·재시도·완료 판단)은 손대지 않고 normalizer 의 결과 모양만 바꾼다. DB 변경 없음. CASE-055(e2e)가 실패 토스트 문구·관측 수를 정확히 대조하므로 서버 실패 문구는 종전 그대로 두고 코드별 문구는 A11 화면 상태(ImportJobView.issue)에만 싣는다.
접근
| 안 | 내용 | 판정 |
|---|---|---|
| A. 공통 typed 계약 + 경로별 적용 | features/import/importContract.ts 한 벌(단계·이슈 코드·위치·원문 가용성·재시도 정책·A11 화면 상태·무효화 대상)을 두고 세 경로의 파서·러너·코덱이 그 모양으로 결과를 낸다. 파일 해시로 재시도(같은 배치에 붙기)와 신규 재인입(거부)을 브라우저에서 구분 | 채택 — 다음에 경로가 늘어도 같은 계약을 채우면 된다 |
| B. 경로별 문구·토스트만 손질 | 각 catch 에서 문구를 골라 보여준다 | 기각 — 부분 결과·재시도 구분·취소는 해결 안 됨(땜질) |
| C. 서버에 사전검사 RPC 추가 | 브라우저 검사를 서버로 옮긴다 | 기각 — 검사 대상이 브라우저의 파일이라 마이그레이션·엣지 배포 비용 대비 이득 없음 |
4. 적용한 내용
Phase 1 — 인입 공통 계약 (d49b08dd)
src/react/features/import/importContract.ts:ImportIssue(source·stage·code·message·location(줄/행/필드)·rawAvailable·retryable),ImportPreflightError, 사전검사 코드 표IMPORT_PREFLIGHT_CODES, 경로 등록표IMPORT_PATHS(세 경로 × 단계 file→parse→validate→identity→upload→canonical→stats 의 소유 모듈·원문 가용성·재시도/재인입 정책·S10 복구 지원·통계 시점), 한도IMPORT_LIMITS, A11 화면 상태ImportJobView(phase 9종·progress·outcome·issue·serverContinues·statsPending·invalidates).supabase/functions/_shared/wodup-import-errors.ts: worker 가 배치 행error_code에 남기는 코드 18종 표(retryable·stage·httpStatus). 클라이언트 표WODUP_WORKER_ERROR_CODES와 키·retryable·stage 일치를 테스트가 대조하고, worker 소스에서 실제로 던지는 코드를 전수 추출해 표에 있는지 확인한다.
Phase 2 — Wodup 클라이언트 (a7b35f3c)
wodupJsonlUpload.ts: 사전검사 실패가 코드·단계·줄 번호를 실은ImportPreflightError로(문구는 종전과 같음). 500줄(IMPORT_LIMITS.wodup.yieldEveryLines)마다 이벤트 루프에 양보하고AbortSignal로 취소(file_read_aborted). 64MB 이하 파일은 브라우저가 전체 SHA-256 을 계산해 배치 행에 봉인(fileHashScope: full / skipped_size / unavailable) — D09 worker 의 해시 대조가 실제로 동작한다.importRepository.findWodupImportBatchByFileHashport 추가. 러너(wodupImportRunner)는 같은 해시의 배치를 찾아 완료됨 →already_imported거부(서버까지 안 감)·진행 중 → 그 배치에 붙기·실패 → 새 배치. 결과는WodupImportRunOutcome(job / already_imported / cancelled(serverContinues) / detached).- DTO 에 D09
error_code→errorCode;features/import/wodupImportView.ts가 잡 DTO 를ImportJobView로 그린다(describeWodupWorkerError로 코드별 문구).appController는 typed 문구를 그대로 보여준다.
Phase 3 — InBody (c0812df7)
inbodyImport.ts:inspectInbodyRows가 건너뛴 행(행 번호·이유date_unreadable/weight_unreadable)·중복 수·시간 표기 집계(텍스트/엑셀 일련번호/Date 셀)를 결과에 싣는다. 헤더 없음·범위 밖 값은 행·필드가 붙은 typed 오류. 시간 표기가 섞이면validate_time_zone_mixed경고(값은 바꾸지 않는다 —source_ref가measuredAt을 포함해 identity 가 흔들리므로 경고로만).profileRepository.importInbodyBodyMetrics결과{kind: "imported" | "already_imported", workspace, rowCount}— 같은 파일을 다시 올리면 "이미 가져온 파일이에요"(프로필 컨트롤러 문구 분기).
Phase 4 — Motra 코덱·죽은 파서 사본 제거 (4eac83a5)
motraImportCodec.motraImportIssue: RPC 오류를 typed issue 로 — 22023import_already_applied(이미 넣은 세션, 제거 절차 뒤에만 재인입)·22023 판정표 slug 가 카탈로그에 없음(identity_decision_invalid)·22023 행 검사(validate_row_shape)·42501not_authorized·23505identity_session_ref_duplicate; 모르는 모양은 null(호출자가 원래 오류를 그대로 다룬다). 결과 DTO 에statsPending(stats_mode !== "inline").features/import/motraImportView.ts가 결과·오류를ImportJobView로.- 앱의
motraNormalizedUpload.ts·motraXlsxBlocks.ts+ 테스트 2개 제거(정본은 Barbelic-docs/admin). 자원 계약의 인입 한도 근거를IMPORT_LIMITS로, 아키텍처 장부(coverage-inventory)의 삭제 파일 참조 정리.
Phase 5 — 서버 normalizer·계측·fixture (7ea8dad7)
_shared/wodup-import-errors.ts: 정규화 실패 코드 5종(line_invalid_json·line_source_mismatch·session_exercise_limit·exercise_set_limit·session_set_limit)·경고 접두 11종(기존 8 +date_parsed_non_iso·date_unparseable·set_value_not_numeric)·WodupNormalizeError(code·line·sessionId)._shared/wodup-normalizer.ts:throw new Error0 → 전부WodupNormalizeError(세션 한도 오류는normalizeWodupRow가 줄 번호를 붙인다). 조용한 강제 변환을 경고로: ISO 아닌 날짜를 Date 로 해석하면date_parsed_non_iso:<원문>, 못 읽으면date_unparseable, 세트의 숫자 필드에 값이 있는데 숫자가 아니면set_value_not_numeric:<필드>:<세트 id>(값은 null 그대로 — 0 이나 다른 단위로 추정하지 않는다). worker 는normalization_failed/second_pass_normalization_failed실패 상세에normalize_code·line을 동봉(문구·error_code는 종전 그대로).- 계측
scripts/performance/import/wodup-large-file.mjs(§5 표), 자원 계약clientImport에parseMemoryBytes(measured)·preflightMaxSyncBlockMs·workerNormalizeHeapPeakBytes추가. - replay fixture
tests/fixtures/import/:wodup-raw.jsonl(정상 세션·엉성한 세션(비ISO 날짜·"60kg"·"five")·종목 행) →wodup-normalized.jsonl(정규화 결과, 바이트 단위 결정성 검사),inbody.csv(유효 3·건너뜀 2·중복 1),motra-normalized.json(pgTAP 과 같은 3세션 페이로드),manifest.json(경로별 identity·원문 가용성·복구 방식·기대 결과 —IMPORT_PATHS와 대조).
주요 결정과 그 근거
- 판정 권위는 서버, 브라우저는 미리 안내. 파일 해시 대조·one-shot 게이트는 서버가 하고, 브라우저는 같은 해시의 완료 배치를 찾으면 서버까지 가지 않고
already_imported를 안내한다. 서버 게이트를 우회하는 경로는 없다. - 정규화 경고는 늘리되 값은 바꾸지 않는다. 조용한 추정을 없애는 대신 값을 고치지 않고(정본 불변, §23) 경고 코드로만 드러낸다.
warning_count가 커질 수 있다. - 서버 실패 문구는 그대로. CASE-055 가 문구·관측 수를 대조한다. 코드별 문구는
ImportJobView.issue에만 있고 화면 반영은 A11 몫. - 정규화 코드 표는 서버 파일이 정본. 클라이언트에 같은 표를 복제하지 않았다(소비자가 아직 없다 — A11 이
errorMessage의 "Line N:" 과normalize_code를 쓸 때 옮긴다).
작업 중 드러난 것
- 첫 세션은 Phase 4 적용 도중 사용량 한도로 끊겼고 워크트리에 미커밋 변경이 남았다. 이어받은 세션이 검토 후 마무리했다(인수인계 댓글·이슈 스레드).
- 이어받은 세션에서 백그라운드
npm run check와git stash를 같은 워크트리에서 겹쳐 돌려 git 인덱스가 비었다(파일은 온전).git reset으로 복구하고 검사를 다시 돌렸다 — 같은 워크트리에서 검사 중에는 git 작업을 하지 않는다. - 아키텍처 장부 검사(
check-coverage-inventory)는 기준 release 에서도 같은 항목(미분류 문서 5·크론 진입점barbelic-stats-cron-history-purge)으로 실패하는 기존 상태다 — 이 트랙 범위 밖. - 계측 첫 실행에서 사전검사 최대 정지 123ms·정규화 632ms 가 나왔으나 다른 세션의 CPU 부하 중이었고, 3회 반복에서는 47ms·0.94초로 안정됐다. 같은 PC 의 반복 최댓값을 기록했다.
- 사전검사의 최대 정지는 줄 수가 아니라 표본 문자열(최대 32MB)을 줄로 쪼개는 한 번에서 나온다 — 500줄 양보로는 그 한 번이 줄지 않는다. 표본 한도가 상한을 정하므로 그대로 두었다.
- 정규화는 파일 전체를 한 문자열로 들고 산출도 한 문자열로 만든다. 1만 세션 파일의 힙 최고점 226MB 는 Edge 함수 메모리 한도 256MB 의 88% — 줄 단위 스트리밍은 D09 runtime(청크 staging)과 같이 설계해야 하므로 후속으로 남겼다(자원 계약 note).
- 이슈 본문의 소유 경계에 적힌
src/react/controllers/motraImportController.ts는 #1463 이관으로 앱에 없다. Motra typed 결과의 앱 쪽 소비자는 저장소 port 와 A11 뿐이다.
5. 적용 결과
| 항목 | 전 → 후 |
|---|---|
| Wodup 사전검사 실패 표현 | Error("문구") → ImportPreflightError(코드 15종·단계·줄 번호), 화면 문구 일반 안내 1종 → 코드별 문구(줄 번호 포함) |
| 같은 파일(완료됨) 재업로드 시 새 배치 생성 | 1(서버가 끝까지 처리 뒤 실패) → 0(브라우저가 해시로 찾아 already_imported) · 진행 중이면 그 배치에 붙음 · 실패했으면 새 배치 |
| 브라우저가 계산하는 파일 해시 | 없음(worker 가 나중에 봉인) → 64MB 이하 전체 SHA-256 을 배치 행에 봉인(초과는 skipped_size) |
| 취소·업로드 중단 | 상태 없음 → cancelled(serverContinues 로 서버 계속 여부)·detached(폴링 상한) |
D09 error_code | 클라이언트가 읽지 않음 → DTO errorCode → ImportJobView.issue(코드 18종 문구), worker 표와 일치 테스트 |
| InBody 건너뛴 행·중복·이미 넣은 파일 | 조용히 버림·"N개 가져왔어요" → 행 번호·이유·중복 수·already_imported |
| Motra 서버 오류 | 코드 통째로 전달 → typed issue 5종 + statsPending |
| 서버 정규화 실패 | Error 문자열 → WodupNormalizeError 5종(코드·줄 번호), worker 실패 상세에 동봉 |
| 서버 정규화의 조용한 추정 | 무음 → 경고 3종(date_parsed_non_iso·date_unparseable·set_value_not_numeric), 값은 불변 |
| 앱 저장소의 죽은 Motra 파서 사본 | 파일 2 + 테스트 2 → 0 (−791줄) |
자원 계약 clientImport.parseMemoryBytes | unmeasured → 54,945,496 bytes(measured) + preflightMaxSyncBlockMs 47 + workerNormalizeHeapPeakBytes 225,966,552 |
| 새 테스트 | importContract 6·motraImportContract 3·wodupNormalizerIssues 4·importReplayFixtures 4 + 기존 파일에 사전검사·러너·InBody 검사 10건 |
| 로컬 게이트 | npm run check 3,570 중 3,532 통과·0 실패·38 건너뜀(DB 조건) · check:unused 통과 |
| 화면 동작 | 폴링 간격·상한·서버 실패 문구·CASE-055 관측 불변. 시각 변경 0 |
| release 통합 / staging / Production | release/v0.18.0 병합 55e9fae8(2026-09-10 02:44 UTC · 11:44 KST, 큐 run 34430574867) · staging 미반영 · Production 미반영 |
큰 파일 실측(Node 24, 이 PC, --expose-gc, 3회 반복 최댓값 — 브라우저가 아니라 Node 이벤트 루프로 잰 값):
| 파일 | 사전검사 | 이벤트 루프 최대 정지 | 사전검사 힙 최고 | 전체 SHA-256 | 서버 정규화(동기) | 정규화 힙 최고 |
|---|---|---|---|---|---|---|
| 2,000세션 · 2,008줄 · 4.9MB | 44~66ms | 12~21ms | 12.5MB | 4ms | 0.20~0.22초 | 52MB |
| 10,000세션 · 10,008줄 · 24.3MB(표본 1만 줄 한도) | 0.29~0.33초 | 39~47ms | 55MB | 20ms | 0.92~0.94초 | 226MB(Edge 256MB 의 88%) |
G04 예산에는 UI 정지 기준이 없어 충족 여부 대신 실측값을 남긴다. Edge 함수 CPU 한도 2초 안(0.94초), 메모리 한도는 1만 세션 파일에서 88%.
미검증: 실제 브라우저의 메인 스레드 정지·메모리(e2e CASE-055 는 시간만 본다), staging·Production 에서의 엣지 함수 동작(승격 뒤 Deploy).
6. 이번 개선으로 향상된 것
잘못된 파일이 어디서 왜 막혔는지 유저가 안다
유저 A 의 Wodup 파일 2번째 줄이 깨져 있으면 "2번째 줄을 JSON 으로 읽지 못했어요"가 그대로 보인다. InBody 파일은 "3개 가져옴·2행 건너뜀(6행 측정일, 7행 체중)"처럼 부분 결과를 말한다.
같은 파일을 다시 올려도 서버가 헛돌지 않는다
완료된 파일은 올리기 전에 "이미 가져온 파일"로 안내하고, 진행 중이던 배치에는 다시 붙는다. 실패했던 파일만 새 배치를 만든다.
서버가 추정한 값이 숨지 않는다
정규화가 날짜·숫자를 추정하거나 버릴 때마다 경고 코드가 남아 R03 복구·운영이 원문과 대조할 수 있다. 값은 바꾸지 않는다.
큰 파일 비용이 숫자로 남았다
npm run perf:import 한 번으로 같은 파일을 다시 잴 수 있고, 자원 계약이 Edge 한도와 나란히 읽힌다.
구조적으로 남는 것
- 인입 계약 1벌(
features/import/importContract.ts)과 경로 등록표 — 소유 모듈이 실제 파일인지 테스트가 지킨다. - worker 실패 코드 표·정규화 실패/경고 코드 표(
_shared/wodup-import-errors.ts) — 클라이언트 표 일치·worker 실제 코드 전수 테스트. - replay fixture 3종 + manifest — 파서가 바뀌면 fixture 결정성 테스트가 먼저 깨진다.
- 큰 파일 계측 스크립트와 증거 규격.
남은 것
- A11 인계:
ImportJobView(phase·progress·outcome·issue·serverContinues·statsPending·invalidates)를 그대로 화면 상태로 쓴다. Wodup 은wodupImportJobView(job)·러너 결과 종류, InBody 는 저장소 결과kind, Motra 는motraImportJobViewFrom/motraImportFailureViewFrom. 코드별 문구를 화면에 실을 때 CASE-055 의 문구 대조를 함께 갱신한다. 정규화 실패의normalize_code·줄 번호는 지금 worker 실패 상세(로그)에만 있다 — 배치 행에 싣는 것은 D09 와 상의. - S10/R03 인계:
tests/fixtures/import/manifest.json이 경로별 원문 가용성·복구 방식·identity 의 정본이다(Wodup 만import_replay, InBody 는 재업로드, Motra 는 변환기 재실행). - R04 인계:
npm run perf:import -- --sessions N으로 같은 파일을 재측정한다. 브라우저 실측은 e2e 에서 따로. - D09 후속: 정규화의 줄 단위 스트리밍(1만 세션 파일 힙 226MB → 세션 수와 무관하게). 청크 staging 과 같이 설계.
- Barbelic-docs/admin: 관리자 쪽 Motra 파서 사본(
admin/src/react/services/motraNormalizedUpload.ts)은 이 PR 범위 밖 — 같은ImportIssue계약을 적용하는 것은 후속. - staging 승격(full CI 1회)·QA·Production 은 릴리스 담당 절차(v0.18.0)로 진행하며 이 문서 §5 의 상태 표를 그때 갱신한다.