# TechLog Backend 정합 설계 ## 상태 - 승인일: 2026-08-17 - 기준선: `tech-log-frontend` `main` (UI 이식 완료 상태) - 원본 요구: `/home/donghyeon/workspace/tech-log-alignment-design/01-tech-log-frontend-alignment-design.md` - Canonical 계약: `/home/donghyeon/workspace/tech-log-design-package/contracts/openapi/studio-v1.yaml` - Specification Version `2.0.0` - 계약 digest `sha256:85a65004f29880334b9a0a3b54089450a898f1a815f679b2985b87ed8723df5b` - 설계 패키지 revision `0ec5582` - 결정: 현재 Public/Studio UI 기준선을 고정하고, Studio 계약·전송 경계와 Asset capability를 canonical 계약에 정합시킨다. Public 조회의 HTTP 전환은 이 사이클에서 제외한다. ### Task 12 완료 상태 (2026-08-18) 12개 Task 전부 `feature/techlog-backend-alignment`에 커밋됐다. 아래는 §완료 조건의 12개 항목을 Task 12 게이트 실행(전체 로그는 `.superpowers/sdd/2026-08-17-techlog-backend-alignment/task-12-report.md`)과 Task 1–11이 기록한 구현 상태를 근거로 판정한 결과다. | # | 완료 조건 | 판정 | 근거 | |---|---|---|---| | 1 | 현재 Public UI·라우트가 변경되지 않는다 | 충족 | `check:architecture`, `check:registries` PASS; Public 화면 테스트(`public-document-screens.test.tsx` 등) 무변경 통과 | | 2 | 현재 Studio 작업 흐름이 변경되지 않는다 | 충족 | 기본 `MOCK`에서 `test:tech-log`(36 files/303 tests) 전부 PASS; `tech-log-studio-workflow.spec.ts` chromium 2/2 PASS | | 3 | Studio 계약이 canonical에서 생성되고 digest 고정·drift 게이트 동작 | 충족 | `check:tech-log-contract`: "in sync: @tech-log/studio-contract@2.0.0 (ce2e748), 19 operations" | | 4 | `StudioGateway` 전체 operation이 HTTP 어댑터로 구현·MSW 검증 | 충족 | `test:unit`/`test:integration`의 HTTP·MSW 계약 스위트 PASS (환경 요인 실패 1건 제외, 아래 참고) | | 5 | WorkingCopy 저장이 Public Projection을 변경하지 않는다 | 충족 | `studio-publication-flow.test.tsx`, `public-document-screens.test.tsx` PASS | | 6 | Validation/Preview/Publish가 version·dependency revision으로 묶인다 | 충족 | `studio-validation-preview.test.tsx` PASS | | 7 | Publication Event/Snapshot 조회 가능, 과거 Snapshot 불변 | 충족 | `studio-publication-flow.test.tsx` PASS | | 8 | Image/SVG 업로드 + Asset 기반 evidence 삽입 | 충족 | Task 10/11 Asset Picker·업로드 다이얼로그·Asset Library; `test:tech-log` 내 asset 관련 스위트 PASS | | 9 | `READY` Asset만 Preview/Publish에 사용, `QUARANTINED` 미노출 | 충족 | Task 6/9가 구현; 관련 렌더러·게이트 테스트 PASS | | 10 | 23개 오류 코드 + idempotency/version 충돌 구분 | 충족 | `error-classification.test.ts` 등 PASS | | 11 | 프론트가 Backend 도메인 Aggregate를 복제하지 않는다 | 충족 | `check:architecture` PASS (415 modules, 전 import 해석, 12개 회귀 fixture PASS) | | 12 | 런타임 스위치 `MOCK`/`HTTP` 전환, 기본 `MOCK`에서 기존 parity 스위트 전부 통과 | 충족 | 위 1·2 근거 + 수동 확인: `TECH_LOG_STUDIO_SOURCE=HTTP`에서 Backend 부재 시 Studio 쉘은 정상 렌더되고 패널은 "작업 흐름을 불러오지 못했습니다" 인라인 오류로 우아하게 저하됨(백지·미처리 예외 없음) | 명시적 비완료 항목 — 계획대로 이번 사이클에 포함되지 않는다: - **실행 중 Backend와의 실응답 대조**: 이 환경에 Backend가 없다. Task 12 수동 확인은 "Backend 부재 시 우아한 오류 상태"까지만 검증했고, 실제 Backend 응답과의 대조는 Backend Studio 구현 완료 후 별도로 수행한다. - **Public 조회의 HTTP 전환**: 범위에서 명시적으로 제외됐다(§범위 "제외 — Public 조회의 HTTP 전환"). `adapters/static/public-query.ts`는 이번 사이클에서 손대지 않았고, `public-v1.yaml` 기준 별도 spec/plan 사이클로 수행한다. 완료 조건 자체는 아니지만, 게이트 실행 중 확인된 사전 존재(pre-existing) 또는 환경적(environmental) 이슈: - `tests/unit/ci-artifact-contract.test.ts` 16개 테스트가 `bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted` 샌드박스 제약으로 실패한다. 브랜치 분기점 `9e5fbd1`에서도 동일하게 재현되는 환경 문제이며 이번 작업과 무관하다. - `test:coverage`는 위 환경 실패 때문에 vitest가 non-zero로 종료해 `check-risk-coverage.ts`까지 도달하지 못한다(vitest 기본값 `coverage.reportOnFailure: false`). 그 파일만 제외한 진단 실행에서는 `src/application/policies/compatibility.ts`(re-export전용, 계측 가능한 statement 0개)와 `reference-http-gateway.ts`(statements 86.95%/branches 85%, 임계값 90%) 2건이 걸리는데, 둘 다 병합 지점(`9e5fbd1`) 이후 이 브랜치가 건드리지 않은 파일이다. - `test:visual`(chromium)에서 130개 중 18개가 실패한다. Studio 문서/미리보기/게시 화면과 일부 Public Case 화면의 스냅샷 높이가 커졌다(예: 1440×2706 → 1440×2999) — evidence figure가 이제 실제 backend Asset 크기로 렌더되기 때문으로 보이며(Task 9), 골든 스냅샷 갱신 여부는 리뷰 판단이 필요해 이번 Task에서 임의로 갱신하지 않았다. - `test:e2e`/`test:a11y`는 chromium에서 전부 통과하고, firefox/webkit 실패는 이 환경의 브라우저 의존성 문제다(firefox: Pretendard 폰트의 "name records not sorted" 경고를 strict 콘솔 검사가 실패로 잡음; webkit: 호스트에 필요한 시스템 라이브러리 없음 — `playwright install-deps` 필요). 전체 명령·원문 출력은 `.superpowers/sdd/2026-08-17-techlog-backend-alignment/task-12-report.md`에 기록했다. ## 목적 현재 TechLog 프론트엔드는 Studio를 세션 수명 `MockStudioGateway`로, Public을 정적 동기 catalog로 구동한다. 이 설계는 다음을 달성한다. 1. Studio HTTP 계약을 canonical `studio-v1.yaml` 단일 출처에서 생성하고, 두 계약이 다시 갈라지지 못하게 빌드로 막는다. 2. `StudioGateway`의 모든 operation을 canonical 계약 기준 HTTP 어댑터로 구현한다. 3. Backend에 이미 존재하는 Asset/Image/SVG capability를 프론트 편집 흐름에 연결한다. 4. 위 전부를 실행 중인 Backend 없이 완료하고, Backend가 완성되면 런타임 스위치만으로 대조할 수 있게 한다. 현재 Public UI 라우트·화면 구성과 Studio의 `작업본 → 편집 → 저장 → 검증 → Public Preview → 게시/재게시 → 게시 기록/Snapshot` 흐름은 변경하지 않는다. ## Source of Truth 우선순위 | 영역 | Source of Truth | |---|---| | Public 화면·라우트·사용 흐름 | 현재 `tech-log-frontend` | | Studio 화면 구조·사용 흐름 | 현재 `tech-log-frontend` | | Studio HTTP 계약 | `studio-v1.yaml` (단일 canonical) | | Asset lifecycle·불변 조건 | `studio-v1.yaml` | | 전송·오류·재시도·관측 규약 | 현재 프론트 플랫폼 (`src/contracts/external-contract-runtime.ts`) | UI는 Backend 도메인 구조를 그대로 노출하지 않는다. 프론트의 `WorkingCopy`는 Backend Aggregate가 아니라 **편집 계약**이다. ## 확인된 사실과 정정 착수 전 검증에서 원본 요구 문서 및 설계 패키지 README와 실제 코드가 어긋나는 지점을 확인했다. 아래는 구현 기준으로 채택하는 정정이다. ### 이미 충족된 항목 - 설계 패키지 README는 프론트 격차로 "`RecordKind`에 `PROJECT_DECISION` 없음"을 든다. **이미 충족돼 있다** — `contracts/studio/studio-api.openapi.yaml:236`, `contracts/studio/generated.ts:200`. README가 지칭하는 대상은 구 `techlog-studio-frontend` 저장소다. - `application/ports/studio-gateway.ts`의 operation 집합·필터·`IdempotentOptions`는 원본 요구 §4와 이미 일치한다. 포트 재설계가 아니라 구현체 교체가 필요하다. ### 실재하는 격차 - 프론트 계약에 `X-CSRF-TOKEN` 선언이 없다 (canonical 6회 참조, 프론트 0회). canonical은 모든 mutating operation에 CSRF를 요구한다. - 프론트 계약의 `Idempotency-Key` 선언이 1회로, canonical(2회, 공용 parameter)과 구조가 다르다. - canonical에는 프론트 계약에 없는 operation이 6개 있다: `getStudioSession`, `listStudioAssets`, `uploadStudioAsset`, `getStudioAsset`, `updateStudioAsset`, `deleteStudioAsset`. canonical 총 operation은 **19개**(프론트 현재 13개). - canonical 오류 코드는 **23개**로, 원본 요구 §10의 21개에 `IDEMPOTENCY_KEY_REUSED`와 `WARNING_ACKNOWLEDGEMENT_REQUIRED`가 추가된다. 두 코드 모두 필요하다 — 전자는 §10이 요구하는 "version conflict와 idempotency replay 구분"의 실제 코드이고, 후자는 이미 존재하는 `warning-acknowledgements.tsx`가 처리해야 하는 거절 사유다. **23개 전부를 채택한다.** - 원본 요구는 세션/CSRF를 다루지 않는다. canonical은 CSRF 토큰을 `getStudioSession`이 발급한다고 정한다. 아래 §"세션과 CSRF"에서 경계를 정한다. ### 플랫폼 제약 - `ContractContributionSource`의 `TEMPLATE_FIXTURE`는 타입상 `fixtureId: "REFERENCE_FEATURE_V1"`로 닫혀 있다(`external-contract-runtime.ts:186-190`). TechLog는 `EXTERNAL_PACKAGE`만 사용할 수 있다. - 플랫폼 계약 런타임은 `requestBody: "NONE" | "JSON"`만 허용하고, 그 외를 구성 시점에 거절한다(`external-contract-runtime.ts:145`, `:407`). 저수준 `client.ts`도 본문을 `JSON.stringify`로 고정한다(`client.ts:719`). **`multipart/form-data`를 표현할 수 없다.** - 기존 `browser-transfer` capability는 presigned `GET`/`PUT`과 resumable part 업로드용이다(`PresignedTransferMethod = "GET" | "PUT"`). canonical의 `POST /assets` multipart MVP 계약에 그대로 맞지 않는다. - 설계 패키지의 `MANIFEST.sha256`은 stale하다(yaml은 2026-08-17 갱신, manifest는 08-11 기준이며 `preview-v1.yaml`을 아직 나열한다). 계약 고정은 manifest가 아니라 위 §상태에 기록한 실측 digest를 기준으로 한다. ## 범위 ### 포함 | | 범위 | |---|---| | **A. 계약 정합** | canonical `studio-v1.yaml` 도입, 타입 생성 자동화, digest 고정, drift 게이트 | | **B. Studio 전송** | 계약 기여(JSON 18개 전체) + `StudioGateway` HTTP 어댑터(14개 소비) + 세션/CSRF + 오류 매핑 + 런타임 스위치 | | **C. Asset** | `StudioAssetGateway` 포트, 5개 operation(JSON 4 + multipart 1), Asset Library/Picker/Upload UI, `/studio/assets` 라우트, evidence directive 연결, alt/decorative 규칙 정정 | ### 제외 — Public 조회의 HTTP 전환 원본 요구 §11은 `adapters/static/public-query.ts`를 HTTP로 교체하도록 나열한다. 이 사이클에서 제외하고 별도 사이클로 분리한다. 근거: - `PublicContentQueries`는 전 메서드가 **동기**이고, 프레젠테이션 19개 파일 31개 호출 지점이 렌더 중 직접 호출한다. async 전환은 전 Public 화면에 loading/error/empty 상태를 도입하는 작업이다. - 방금 완료한 UI 이식의 시각·접근성 parity 기준선을 광범위하게 흔든다. - 실행 중인 Public Backend가 없어 지금 전환해도 검증할 대상이 없고 사용자 가치도 없다. - canonical `public-v1.yaml`(1,765줄)은 준비돼 있으므로, Public API가 실제로 서비스될 때 자체 spec/plan 사이클로 수행한다. 이 제외는 범위 축소가 아니라 순서 결정이다. 완료 조건에서 해당 항목을 별도로 명시한다. ## 선택한 접근 ### A. 계약 정합 — digest로 고정된 단일 출처 canonical yaml을 저장소에 vendor하고, 타입을 생성하고, **계약 기여의 `EXTERNAL_PACKAGE` provenance로 canonical revision에 암호학적으로 고정**한다. `InstalledContractPackageIdentity`(`external-contract-runtime.ts:173`)는 이미 이 목적에 맞는 필드를 요구한다. ```text packageId tech-log-studio-contract version 2.0.0 (canonical info.version, exact SemVer) digest sha256:85a65004… (canonical studio-v1.yaml의 SHA-256) runtimeProtocolVersion 1 sourceRevision 0ec5582 (설계 패키지 git revision) ``` `assertPackageIdentity`는 shape을 검증하므로 npm 레지스트리 없이 지금 사용할 수 있다. 실제 패키지 배포로 승격할 때 같은 필드를 그대로 채운다. drift 방지는 저장소 관례(`generate:*` / `check:*`)를 따르는 스크립트 한 쌍으로 강제한다. ```text generate:tech-log-contract canonical yaml → vendor 사본 + generated.ts + digest 기록 check:tech-log-contract 재생성 결과가 커밋 내용과 바이트 동일한지, digest가 계약 기여의 선언과 일치하는지 검증. 불일치 시 실패. ``` `openapi-typescript`를 devDependency로 고정한다. 현재 `generated.ts`는 이 도구로 만들어졌으나 도구도 스크립트도 저장소에 없어 재생성이 불가능하다 — 이것이 두 계약이 갈라진 근본 원인이다. `X-CSRF-TOKEN`과 공용 `Idempotency-Key` parameter는 생성 결과에 자동 반영된다. 프론트 yaml을 손으로 고치지 않는다. ### B. Studio 전송 — 플랫폼 계약 런타임 사용 계약 기여는 서비스 패키지당 하나이므로, `features/tech-log/contracts/tech-log-studio-contract-contribution.ts` 한 파일에 canonical의 **JSON operation 18개 전부**(19개 중 multipart 업로드 제외)를 선언한다. Asset의 JSON operation 4개도 같은 기여에 속한다 — 같은 `studio-v1` 패키지이기 때문이다. 이 중 `StudioGateway`가 14개를, `StudioAssetGateway`가 4개를 소비한다. 선언 형식은 `reference-feature-contract-contribution.ts`가 확립한 것을 따른다: operation별 `inputValidator`/`outputValidator`/`problemValidator`, `acceptedStatuses`, `retrySemantics`, `commandRecovery`, `commandEffect`, `projectRequest`, 그리고 byte limit·deadline·retry budget·diagnostics 이름. `retrySemantics`는 canonical의 안전성 구분을 그대로 반영한다. 조회는 `SAFE`, mutating operation은 `KEYED`이며 `commandRecovery.mode = "IDEMPOTENCY_REPLAY"`, retry budget은 0이다 — 발신된 KEYED 명령의 자동 재시도는 금지된다. `StudioGateway` 구현체는 `contractOperations` executor 위에 얹고, 포트 시그니처는 변경하지 않는다. UI는 어댑터가 mock인지 HTTP인지 알지 못한다. #### 세션과 CSRF CSRF는 전송 관심사이며 UI 관심사가 아니다. 현재 Studio에는 인증 UI가 없고(이식 시 유보), 이 사이클에서도 추가하지 않는다. 따라서 `getStudioSession`을 포트로 노출하지 않는다. HTTP 어댑터 내부가 첫 mutating 요청 전에 세션을 조회해 CSRF 토큰을 캐시하고, `csrfHeaderName`으로 헤더를 붙인다. `401`/`403`은 기존 오류 경로로 흘려보낸다. `displayName`/`roles`는 이 사이클에서 소비하지 않는다. 이 결정으로 완료 조건 "현재 Studio 작업 흐름이 변경되지 않는다"가 유지된다. #### 런타임 스위치 `config/runtime/*.json`에 스위치를 추가한다. ```text TECH_LOG_STUDIO_SOURCE: "MOCK" | "HTTP" local, development → MOCK (기본값) staging, production → HTTP ``` `createTechLogFeatureInstalledInput`이 이 값으로 gateway factory를 고른다. mock은 삭제하지 않고 test fixture 겸 fallback으로 유지한다. Backend 완성 시 `local.json` 한 줄로 대조를 시작한다. 기본값을 `MOCK`으로 두는 이유는 현재 앱과 이식 parity 테스트가 그대로 통과해야 하기 때문이다. ### C. Asset — 포트 분리와 업로드 전송 경계 `StudioAssetGateway`를 `StudioGateway`와 별도 포트로 둔다. 파일 전송과 JSON orchestration의 실패 모델이 다르고, 향후 presigned/resumable 교체가 이 포트 뒤에서 끝나야 한다. ```text StudioAssetGateway listAssets(query, options) GET /api/v1/studio/assets uploadAsset(form, options) POST /api/v1/studio/assets (multipart) getAsset(assetId, options) GET /api/v1/studio/assets/{assetId} updateAssetMetadata(assetId, cmd, o) PUT /api/v1/studio/assets/{assetId} deleteAsset(assetId, options) DELETE /api/v1/studio/assets/{assetId} ``` #### 업로드 전송 플랫폼 계약 런타임이 multipart를 표현할 수 없으므로, 업로드 한 operation만 전용 전송 seam으로 분리한다. ```text StudioAssetUploadTransport (좁은 인터페이스: form + 헤더 → 결과) └ fetch + FormData 구현체 런타임 config의 API_BASE_URL·타임아웃 재사용 CSRF·Idempotency-Key 헤더는 B와 동일 경로로 획득 오류는 B와 동일한 코드 매핑 테이블 사용 ``` 나머지 4개 JSON operation은 A/B와 같은 계약 런타임을 통과한다. 즉 계약 런타임을 우회하는 것은 **19개 중 1개**다. 이 경계를 명시적 seam으로 두는 이유는 §7.4의 교체 가능성 요구를 만족시키기 위해서다. 플랫폼에 `MULTIPART` 모드가 생기거나 presigned로 옮길 때 구현체 한 파일만 바뀐다. 플랫폼 파일(`external-contract-runtime.ts`, `client.ts`)은 template 동기화 대상이므로 이 사이클에서 수정하지 않는다. 결정 사유와 우회 범위는 `docs/reviews/adapters/`에 기록한다. #### UI 접근점 Studio primary navigation을 Asset 중심 CMS로 되돌리지 않는다. 두 접근점을 둔다. 1. **Editor contextual Asset Picker** — 작성 흐름의 기본 진입점. 선택 시 evidence directive를 삽입한다. 2. **`/studio/assets` Asset Library** — 검색·메타데이터·사용처·정리용 보조 화면. navigation에는 secondary utility link로만 노출한다. `/studio/assets`는 `tech-log-route-contract.ts`에 `layoutGroup: "STUDIO"`로 추가한다(현재 27개 → 28개). 라우트 registry 거버넌스 기준선을 함께 갱신한다. #### Evidence Figure와 Asset 연결 현재 content format directive를 유지한다. ```text :::evidence key="asset-key" alt="설명" caption="캡션" zoom="true" ``` 변경점: - 정적 `evidenceAssets` 레지스트리(`adapters/static/evidence-assets.ts`) 대신 Backend Asset의 `assetKey`를 사용한다. `assetKey`는 canonical에서 immutable이며 공개 이력 이후 재사용이 금지된 안정 key다. - Asset Picker 선택 시 directive를 자동 삽입한다. 사용자가 raw object-storage URL을 Markdown에 직접 넣지 않게 한다. - 렌더러는 API가 제공한 Asset descriptor를 resolver로 주입받는다. 렌더러 경계는 변경하지 않는다. - `QUARANTINED` Asset은 Public/Preview에 렌더링하지 않는다. - 사용자 업로드 SVG 원문을 `innerHTML`로 주입하지 않는다. 검증된 `publicPath`를 ``로 렌더링한다. 정적 `evidenceAssets`는 기존 하드코딩 Case 화면의 parity 유지를 위해 fixture로 남기고, Asset 기반 경로와 공존시킨다. #### alt / decorative 규칙 정정 현재 parser는 빈 alt를 syntax error로 차단한다. ```text parse-case-content.ts:505 if (!attributes.alt) invalid(node, "evidence alt text is required"); ``` Asset capability와 연결하면 이 위치에서는 판단할 수 없다 — 필요 여부가 Asset의 `decorative`에 달려 있다. 규칙을 옮긴다. ```text parser 빈 alt를 문법 오류로 차단하지 않는다 (구조만 검증) publish 검증 Asset decorative=false + 사용 위치 alt 비어 있음 → ERROR Asset decorative=true → alt="" 허용 ``` 이는 content format의 **의미 변경**이므로 parser/serializer round-trip 픽스처와 기존 검증 테스트를 함께 갱신한다. ## 대상 아키텍처 ```text src/features/tech-log/ ├── contracts/ │ ├── studio/ │ │ ├── studio-api.openapi.yaml canonical vendor 사본 (생성물, 수동 편집 금지) │ │ ├── generated.ts 생성물 │ │ ├── contract.ts 타입 alias (Asset 계열 추가) │ │ └── canonical-source.json digest·revision 기록 │ └── tech-log-studio-contract-contribution.ts JSON operation 18개 + EXTERNAL_PACKAGE provenance ├── application/ports/ │ ├── studio-gateway.ts 변경 없음 │ └── studio-asset-gateway.ts 신규 ├── adapters/ │ ├── http/ │ │ ├── http-studio-gateway.ts contractOperations 위 구현 │ │ ├── http-studio-asset-gateway.ts JSON 4 + 업로드 위임 │ │ ├── asset-upload-transport.ts multipart seam │ │ ├── studio-session-csrf.ts CSRF 토큰 획득·캐시 │ │ └── studio-error-mapping.ts 23 코드 → 플랫폼 FailureKind │ ├── mock/ 유지 (fixture·fallback) │ └── static/ 유지 (Public, 이 사이클 범위 외) └── presentation/studio/ ├── pages/assets-page.tsx 신규 └── components/ ├── asset-library.tsx 신규 ├── asset-picker.tsx 신규 └── asset-upload-dialog.tsx 신규 ``` 기존 유지 대상: `presentation/public/**`, `presentation/studio/pages/**`, `document-*`, `validation-*`, `public-preview-screen`, `publish-screen`, `publication-*`, `presentation/shared/public-render/**`. ## 오류·동시성 계약 23개 canonical 코드를 전부 처리한다. 매핑은 `studio-error-mapping.ts` 한 곳에 둔다. ```text AUTHENTICATION_REQUIRED STUDIO_ACCESS_DENIED DOCUMENT_NOT_FOUND VERSION_CONFLICT REQUEST_VALIDATION_FAILED VALIDATION_FAILED VALIDATION_STALE PREVIEW_NOT_FOUND PREVIEW_STALE PREVIEW_EXPIRED PUBLICATION_NOT_FOUND PUBLICATION_CONFLICT PUBLICATION_EVENT_NOT_FOUND PUBLICATION_SNAPSHOT_NOT_FOUND IDEMPOTENCY_KEY_REUSED WARNING_ACKNOWLEDGEMENT_REQUIRED ASSET_NOT_FOUND ASSET_NOT_READY ASSET_IN_USE ASSET_QUARANTINED PAYLOAD_TOO_LARGE UNSUPPORTED_MEDIA_TYPE STUDIO_UNAVAILABLE ``` 규칙: - 모든 mutation은 `Idempotency-Key`를 보낸다. 명령 하나당 새 key를 만들고, 그 명령의 안전한 재시도에만 같은 key를 재사용한다. - `VERSION_CONFLICT`(낙관적 잠금 실패)와 `IDEMPOTENCY_KEY_REUSED`(같은 key·다른 요청)를 혼동하지 않는다. 전자는 사용자 데이터 손실 없는 충돌 화면으로, 후자는 클라이언트 결함으로 다룬다. - `Idempotency-Replayed` 응답 헤더를 replay 판별에 사용한다. - workflow 상태(`publicationStatus`, `hasUnpublishedChanges`, `nextAction`)는 서버가 계산한 값을 그대로 신뢰한다. 프론트에서 재계산하지 않는다. - 업로드 상태는 `선택 실패 / 업로드 중 / 전송 실패 / READY / REJECTED / QUARANTINED / 크기 초과 / 미지원 형식`을 구분한다. 업로드 전송 성공과 서버 검증 성공을 분리한다. - Asset은 `READY`일 때만 Preview/Publish에 사용한다. ## 테스트 전략 TDD로 진행한다. 각 단위는 red → green → 게이트 순서를 지킨다. **계약(A)** - `check:tech-log-contract`가 vendor 사본·`generated.ts`·digest 불일치를 잡는다 (의도적 변조로 red 확인). - canonical 19개 operationId가 전부 덮이는지 parity 검증: 18개는 계약 기여에, `uploadStudioAsset`은 업로드 전송 seam에 존재해야 한다. 어느 쪽에도 없는 operationId가 있으면 실패한다. - `EXTERNAL_PACKAGE` identity가 `assertPackageIdentity`를 통과하고 `contractSet`에 나타나는지 확인. **전송(B)** — MSW로 canonical 응답·오류를 재현 - 불완전 draft 저장 성공, `expectedVersion` 충돌 → `VERSION_CONFLICT`. - idempotency replay(`Idempotency-Replayed: true`)와 `IDEMPOTENCY_KEY_REUSED` 구분. - workflow 전이: `INVALID → FIX_VALIDATION`, `VALID/WARNINGS → CREATE_PREVIEW`, `CURRENT → PUBLISH`, 게시 후 `NONE`, 편집 후 `VALIDATE` 복귀, expired preview 재생성. - Publication: 최초 `PUBLISHED`, 재게시 `REPUBLISHED`, 취소 `UNPUBLISHED`, 과거 Snapshot 불변성. - 23개 코드 전부의 UI 관측 가능한 처리. - CSRF 토큰 획득 실패·만료 경로. - 런타임 스위치: `MOCK`/`HTTP` 각각에서 gateway 종류가 선택되는지. **Asset(C)** - PNG/JPEG/WebP/SVG 업로드 성공, 미지원 형식 `UNSUPPORTED_MEDIA_TYPE`, 크기 초과 `PAYLOAD_TOO_LARGE`. - `decorative=false` + 빈 alt → publish ERROR / `decorative=true` + `alt=""` 허용. - `QUARANTINED` Asset의 Public·Preview 렌더링 차단. - 사용 중 Asset hard delete 차단 → `ASSET_IN_USE`, `ARCHIVED` 전환 경로. - Asset Picker가 evidence directive를 정확한 문법으로 삽입. - parser가 빈 alt를 더 이상 syntax error로 차단하지 않음 + round-trip 픽스처 갱신. **렌더러 불변** 동일 픽스처에 대해 `Instant Preview`, `Server Public Preview`, `Published Public`, `Publication Snapshot`의 semantic output이 동일해야 한다. Asset resolver도 같은 렌더러 경계로 주입한다. **회귀** 기존 이식 parity 스위트(시각·접근성·라우트·아키텍처 경계)가 전부 통과해야 한다. 기본 스위치가 `MOCK`이므로 이 스위트는 영향받지 않아야 한다. ## 위험과 완화 | 위험 | 완화 | |---|---| | 실행 중 Backend 없음 → 실응답 미검증 | canonical 계약 기준 MSW 검증. 스위치로 대조 지점을 남긴다. 완료 조건에 "실서버 대조 미포함"을 명시한다. | | canonical yaml이 계속 변경 중 (오늘도 수정됨) | digest·revision을 커밋에 고정하고 drift 게이트로 감지. canonical 갱신은 의도적 재생성 커밋으로만 반영. | | 계약 런타임 우회(업로드 1개)가 거버넌스 위반으로 보일 수 있음 | 좁은 seam으로 격리, 사유·범위를 adapter review 문서에 기록, 나머지 18개는 런타임 통과. | | content format 의미 변경(alt)이 기존 픽스처를 깨뜨림 | parser 변경과 픽스처·검증 갱신을 한 단위로 묶어 red→green으로 수행. | | 라우트 1개 추가가 registry 기준선을 깨뜨림 | 거버넌스 기준선 갱신을 같은 단위에 포함. | | `openapi-typescript` 도입이 생성물 diff를 크게 만듦 | 첫 생성 결과를 별도 커밋으로 분리해 리뷰 가능하게 한다. | ## 완료 조건 1. 현재 Public UI·라우트가 변경되지 않는다. 2. 현재 Studio 작업 흐름이 변경되지 않는다. 3. Studio 계약이 canonical `studio-v1.yaml`에서 생성되고, digest 고정과 drift 게이트가 동작한다. 4. `StudioGateway`의 모든 operation이 HTTP 어댑터로 구현되고 MSW 계약 테스트로 검증된다. 5. WorkingCopy 저장이 Public Projection을 변경하지 않는다. 6. Validation/Preview/Publish가 version과 dependency revision으로 묶인다. 7. Publication Event와 Snapshot을 현재 UI에서 조회할 수 있고 과거 Snapshot이 불변이다. 8. Image/SVG를 업로드하고 Case content에 Asset 기반 evidence로 삽입할 수 있다. 9. Asset은 `READY`일 때만 Preview/Publish에 사용된다. `QUARANTINED`는 렌더링되지 않는다. 10. 23개 오류 코드와 idempotency/version 충돌 구분이 처리된다. 11. 프론트가 Backend 도메인 Aggregate를 복제하지 않는다. 12. 런타임 스위치로 `MOCK`/`HTTP`를 전환할 수 있고, 기본 `MOCK`에서 기존 parity 스위트가 전부 통과한다. 명시적 비완료 항목: - 실행 중 Backend와의 실응답 대조. Backend Studio 구현 완료 후 별도로 수행한다. - Public 조회의 HTTP 전환. `public-v1.yaml` 기준 별도 spec/plan 사이클로 수행한다.