Files
tech-log-frontend/docs/superpowers/specs/2026-08-17-techlog-backend-alignment-design.md
T
DongHyeonkaandClaude Opus 5 813f9e16cd test: regenerate only the five goldens this branch actually changed
Attribution was measured, not inferred: the visual suite was run at the
merge-base `9e5fbd1` in a detached checkout (13 failed / 117 passed) and the
`-actual.png` each run produced was compared by SHA-256 against HEAD's. For
13 of the 18 failures the rendered bytes at HEAD and at the merge-base are
identical, so those failures belong to `main`, not here.

Regenerated (all Case-editor surfaces, all grown by the Task 10 Asset panel;
the +293px band was inspected in the new golden and is the "EVIDENCE / 본문에
Asset 삽입" panel):

  tech-log-studio-document-edit-360    360x3313  -> 360x3627
  tech-log-studio-document-edit-1440   1440x2706 -> 1440x2999
  tech-log-studio-case-editor-1440     1440x2706 -> 1440x2999
  tech-log-studio-conflict-editor-1440 1440x2706 -> 1440x2999
  tech-log-studio-dirty-leave-dialog-1440 1440x2706 -> 1440x2999

`--update-snapshots` was restricted to those five tests with `--grep`; a
blanket update would have absorbed the 13 inherited failures and destroyed
the distinction. `test:visual` now reports 13 failed / 117 passed, exactly
the merge-base's set.

Two earlier records are corrected in the spec: the five "pixel-only, not
investigated" failures are all inherited, and `TECH_LOG_STUDIO_DOCUMENT_NEW`
360px was wrongly attributed to the Asset Picker -- it fails at the
merge-base with the identical 28,413 differing pixels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 09:29:46 +09:00

33 KiB
Raw Blame History

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·revision의 단일 기록처는 src/features/tech-log/contracts/studio/canonical-source.json이다. 이 문서는 그 값을 복제하지 않는다. 승인 시점에 여기 적혀 있던 sha256:85a65004… / revision 0ec5582은 구현 중 canonical yaml이 갱신되면서 무효가 됐고, 실제로 vendor·고정된 값은 canonical-source.json이 기록한 sha256:99f54f56… / revision ce2e748이다(Task 12 표 3행의 ce2e748과 동일).
    • 계약 기여(tech-log-studio-contract-contribution.ts)는 그 파일을 import해서 EXTERNAL_PACKAGE provenance를 채우고, check:tech-log-contract가 vendor 사본·generated.ts와의 일치를 검증한다. 두 곳이 다시 갈라질 수 있는 지점은 없다.
  • 결정: 현재 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·라우트가 변경되지 않는다 충족 Public 화면 테스트(public-document-screens.test.tsx 등) 무변경 통과; case-body-renderer.tsx/evidence-figure.tsx9e5fbd1..HEAD 사이 diff가 비어 있어 렌더러 코드에 변경이 없다(check:architecture/check:registries는 import 그래프·레지스트리 정합만 보고 Public 렌더 출력을 관찰하지 않으므로 이 판정의 근거가 아니다). test:visual의 Public 스냅샷 실패는 이 브랜치가 아니라 main79e9aa8에서 물려받은 것이다(아래 §Task 12 참고)
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". 최종 fix wave에서 이 명령을 config/ci/gates.json의 FE-GATE-010과 test:all에 연결했다 — 그전까지는 손으로 칠 때만 실행돼 drift 게이트가 실질적으로 비어 있었다
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 사이클로 수행한다.
  • 성공 payload의 런타임 계약 검증: tech-log-studio-contract-contribution.ts의 18개 operation은 전부 passthrough(z.unknown()) inputValidator/outputValidator를 쓴다. 의도된 선택이고 코드에도 주석으로 남아 있다 — canonical 계약이 payload를 소유하고 generated.ts가 컴파일 시점 계약이며, 런타임 재검증은 계약 갱신 때마다 두 곳을 고치게 만든다. problemValidator는 그대로 엄격하다(23개 코드 enum + 필드 제약). 결과적으로 성공 응답의 shape 불일치로는 CONTRACT_VIOLATION이 발생할 수 없다. 이 한계는 바로 위 "실행 중 Backend와의 실응답 대조 없음"과 같은 종류의 위험이다: Backend가 없으므로 MSW는 테스트 작성자가 적은 것을 그대로 돌려주고, 그것을 canonical 스키마와 대조하는 주체가 없다. 즉 shape 회귀를 잡을 수 있는 층이 지금은 컴파일 타임 한 겹뿐이다. Backend 대조 사이클에서 (a) 실서버 응답 대조로 대체할지 (b) canonical에서 생성한 런타임 스키마로 outputValidator를 채울지 함께 판단한다.

완료 조건 자체는 아니지만, 게이트 실행 중 확인된 사전 존재(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): 최종 fix wave에서 130개 중 13개 실패로 정리했고, 남은 13개는 전부 merge-base 9e5fbd1에서 동일하게 실패한다 — 이 브랜치가 만든 시각 회귀는 0건이다.
    • 판정 근거는 추론이 아니라 실측이다. merge-base 9e5fbd1을 detached checkout해 test:visual을 그대로 실행했고(13 failed / 117 passed), 두 실행이 남긴 -actual.png를 SHA-256으로 대조했다. 13개는 merge-base와 HEAD의 실제 렌더 결과가 바이트 동일했다 — 즉 이 브랜치의 코드와 무관하다.
    • 물려받은 13개(갱신하지 않음): TECH_LOG_CASE 1440, known Public fixture /cases/collection-fetch-join-pagination 1440, TECH_LOG_STUDIO_DOCUMENT_PREVIEW 1440, TECH_LOG_STUDIO_PUBLICATION_PREVIEW 1440, Studio current-preview 1440, Studio publication-snapshot 1440, Studio immediate preview 1440(이상 7개는 높이가 줄었다), TECH_LOG_STUDIO_DOCUMENTS 360/1440, Studio document-list 1440, TECH_LOG_STUDIO_DOCUMENT_NEW 1440, Studio new-document 1440(이상 5개는 크기 변화 없는 픽셀 차이), TECH_LOG_STUDIO_DOCUMENT_NEW 360(360×1000 → 360×1130). 높이가 줄어든 7개의 원인은 main에 이미 있고 merge-base 9e5fbd1의 조상인 79e9aa8("fix: align TechLog article content widths")로, .evidence-figure의 CSS 폭을 min(61rem, calc(100% + 15rem))(≈912px)에서 min(var(--body-copy), 100%)(672px, 비율 73.6%)로 바꿨다. golden PNG는 79e9aa8보다 앞선 3a7c5de에서 마지막으로 기록됐다. 갱신은 이 브랜치가 아니라 main79e9aa8에 대해 기록해야 한다.
    • 이전 기록의 정정 2건: (1) "나머지 5개는 크기 변화 없는 픽셀 차이(추가 조사하지 않음)"로 남겨뒀던 항목은 조사 결과 전부 물려받은 것이다. (2) TECH_LOG_STUDIO_DOCUMENT_NEW 360(360×1000 → 360×1130)을 Asset Picker 때문이라고 원인 B로 분류했었는데, merge-base에서 동일한 픽셀 수(28,413)로 동일하게 실패한다 — 물려받은 것이다. 따라서 이 브랜치가 만든 실패는 6개가 아니라 5개다.
    • 이 브랜치가 만들어 갱신한 5개: TECH_LOG_STUDIO_DOCUMENT_EDIT 360(360×3313 → 360×3627), TECH_LOG_STUDIO_DOCUMENT_EDIT 1440·Studio case-editor 1440·Studio conflict-editor 1440·Studio dirty-leave dialog 1440(전부 1440×2706 → 1440×2999). 전부 Case 편집기 화면이고, 늘어난 293px 영역은 Task 10이 추가한 "EVIDENCE / 본문에 Asset 삽입" 패널(업로드 종류 select + Asset 업로드 버튼 + Picker 빈 상태)임을 갱신본에서 직접 확인했다. --update-snapshots는 이 5개 테스트에만 --grep으로 한정해 실행했다 — 일괄 갱신은 물려받은 13개까지 조용히 흡수해 이 구분을 없애기 때문이다.
  • 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는 프론트 격차로 "RecordKindPROJECT_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_REUSEDWARNING_ACKNOWLEDGEMENT_REQUIRED가 추가된다. 두 코드 모두 필요하다 — 전자는 §10이 요구하는 "version conflict와 idempotency replay 구분"의 실제 코드이고, 후자는 이미 존재하는 warning-acknowledgements.tsx가 처리해야 하는 거절 사유다. 23개 전부를 채택한다.
  • 원본 요구는 세션/CSRF를 다루지 않는다. canonical은 CSRF 토큰을 getStudioSession이 발급한다고 정한다. 아래 §"세션과 CSRF"에서 경계를 정한다.

플랫폼 제약

  • ContractContributionSourceTEMPLATE_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)는 이미 이 목적에 맞는 필드를 요구한다.

packageId            @tech-log/studio-contract
version              2.0.0                      (canonical info.version, exact SemVer)
digest               canonical studio-v1.yaml의 SHA-256
runtimeProtocolVersion 1
sourceRevision       설계 패키지 git revision

digest·sourceRevision의 실제 값은 이 문서가 아니라 contracts/studio/canonical-source.json에 기록한다(§상태 참고). 계약 기여가 그 파일을 직접 읽으므로, 문서에 값을 복제하면 갱신을 한쪽에서만 하다가 어긋난다 — 승인본이 실제로 그렇게 어긋났다.

assertPackageIdentity는 shape을 검증하므로 npm 레지스트리 없이 지금 사용할 수 있다. 실제 패키지 배포로 승격할 때 같은 필드를 그대로 채운다.

drift 방지는 저장소 관례(generate:* / check:*)를 따르는 스크립트 한 쌍으로 강제한다.

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에 스위치를 추가한다.

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 — 포트 분리와 업로드 전송 경계

StudioAssetGatewayStudioGateway와 별도 포트로 둔다. 파일 전송과 JSON orchestration의 실패 모델이 다르고, 향후 presigned/resumable 교체가 이 포트 뒤에서 끝나야 한다.

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으로 분리한다.

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/assetstech-log-route-contract.tslayoutGroup: "STUDIO"로 추가한다(현재 27개 → 28개). 라우트 registry 거버넌스 기준선을 함께 갱신한다.

Evidence Figure와 Asset 연결

현재 content format directive를 유지한다.

:::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<img src>로 렌더링한다.

정적 evidenceAssets는 기존 하드코딩 Case 화면의 parity 유지를 위해 fixture로 남기고, Asset 기반 경로와 공존시킨다.

alt / decorative 규칙 정정

현재 parser는 빈 alt를 syntax error로 차단한다.

parse-case-content.ts:505
  if (!attributes.alt) invalid(node, "evidence alt text is required");

Asset capability와 연결하면 이 위치에서는 판단할 수 없다 — 필요 여부가 Asset의 decorative에 달려 있다. 규칙을 옮긴다.

parser         빈 alt를 문법 오류로 차단하지 않는다 (구조만 검증)
publish 검증   Asset decorative=false + 사용 위치 alt 비어 있음  → ERROR
               Asset decorative=true                          → alt="" 허용

이는 content format의 의미 변경이므로 parser/serializer round-trip 픽스처와 기존 검증 테스트를 함께 갱신한다.

대상 아키텍처

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 한 곳에 둔다.

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 사이클로 수행한다.