Files
tech-log-backend/docs/superpowers/specs/2026-08-18-techlog-studio-backend-design.md
T
DongHyeonkaandClaude Opus 5 91e6d99654 feat: Tech Log Studio 백엔드 기반 — 계약 배선, 오류 코드, 경계 규칙, 스키마, 엔드포인트 2종
설계 패키지의 studio-v1.yaml(v3.0.0, 응답 봉투)을 이 저장소에 배선하고
슬라이스 1의 기반을 세운다. 19개 operation 중 getStudioSession과
listStudioCatalog를 구현했다.

계약과 생성
- src/config/openapi/studio-v1.yaml 을 vendor하고 MANIFEST에 출처 커밋을 기록
- openapi-generator로 DTO(model)만 생성한다. generateApis 대신
  globalProperties.set(['models': '']) — 그 두 속성은 플러그인 7.18.0에 없다
- useOneOfInterfaces=false. 그 대가로 discriminator union 5종의 Jackson 배선이
  깨진다(spec §3.1). 그 5종을 쓰는 7개 operation은 Plan 02에서 전략을 정한 뒤 구현한다
- 생성 코드는 별도 generatedOpenapi sourceSet에 둔다. -Werror가 생성물의 deprecated
  API 사용을 빌드 실패로 승격하기 때문이다. jar와 test 클래스패스에 별도로 얹는다

오류 계약
- StudioError 23종(계약 ApiError.code와 1:1) + StudioException(ApiErrorCarrier)
- StudioExceptionHandler는 techlog 패키지로 범위를 좁힌다. 다른 기능의 오류 응답을
  바꾸지 않기 위해서다
- 클라이언트 문구는 레지스트리의 client_safe_message에서 가져오고 예외 메시지는
  로그 전용이다(ApiErrorCarrier javadoc의 요구)
- 바인딩 예외를 봉투로 옮긴다. 그러지 않으면 bare RFC 7807이 새어 나가 ADR-006을 위반한다

게이트
- TechLogBoundaryArchTest 7종 — spec §4.3의 bounded context 경계. Gradle leaf를
  늘릴 수 없어 이 규칙이 경계의 유일한 방어선이다
- StudioErrorRegistryTest — enum ↔ 레지스트리 ↔ 계약 3축 대조, vendor 사본 해시 검증
- StudioContractDriftTest — springdoc 표면이 계약을 벗어나면 실패. @ComponentScan이라
  새 컨트롤러가 자동으로 걸린다
- StudioSessionCsrfHeaderProfileContractTest — 배포 가능한 세 프로파일이 계약의
  csrf-header-name const로 해소되는지 고정. 이 저장소는 실제 composition root를
  테스트에서 부팅할 수 없어 파일 단언으로 그 층을 덮는다

스키마
- V7__techlog_core.sql, 28 테이블. 설계 DDL에서 studio_idempotency(기존
  idempotency_record 재사용)와 범위 밖 6종을 제외했다
- 원본의 tech_log 스키마 대신 public을 쓴다. 원본의 SET search_path는 Flyway
  세션에만 적용되고 런타임 커넥션 풀은 상속하지 않는다

알려진 제약
- getStudioSession은 세션 인프라(redis-session)가 없어 503 STUDIO_UNAVAILABLE을
  반환한다. 계약이 이 operation에 허용하는 유일한 실패 코드다. 가짜 CSRF 토큰으로
  200을 만들지 않았다
- 따라서 슬라이스 1의 "프론트 로그인 실동작" 목표는 아직 달성되지 않았다

이 커밋은 AGENTS.md의 human-only 커밋 정책에 대한 저장소 소유자의 명시적 지시로
작성됐다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 15:14:52 +09:00

46 KiB
Raw Blame History

Tech Log Studio Backend — 설계

  • 작성일: 2026-08-18
  • 대상 저장소: tech-log-backend (clean-architecture-backend-template 스냅샷)
  • 브랜치: feature/techlog-studio-backend
  • 설계 원본: /home/donghyeon/workspace/tech-log-design-package
  • 소비자: /home/donghyeon/workspace/desktop-server-git/tech-log-frontend (Studio SPA, 구현 완료)

1. 배경

설계 패키지가 Tech Log의 제품·정보구조·계약·DB·백엔드 모듈을 확정했다. 프론트는 Studio SPA가 이미 구현되어 있고 백엔드만 없다. 이 문서는 설계 패키지의 결정을 이 저장소의 구조·게이트에 맞춰 어떻게 구현할지를 정의한다.

설계 패키지의 docs/plans/02-backend-core-plan.md는 본문에 STALE 배너가 붙어 있고 파일맵도 services/api/...라 이 저장소에 적용되지 않는다. 이 문서가 그 자리를 대신한다.

1.1 계약 정합 (검증됨)

tech-log-design-package/scripts/check-contract-parity.py 실행 결과:

Frontend operation 19개 / Backend primary operation 19개
[OK] Frontend operation 13개가 모두 Backend primary 계약에 존재한다.
[OK] 공통 operation의 method/path가 모두 일치한다.
RecordKind  FE=BE=['CASE','PROJECT_DECISION','QUESTION','REFERENCE']
오류 코드  FE=23개 / BE=23개  [OK]

contracts/openapi/studio-v1.yaml이 프론트가 실제로 호출하는 계약 그대로다.


2. 범위

2.1 In scope

studio-v1.yaml의 19개 operation 전부.

operation method · path
getStudioSession GET /api/v1/studio/session
getStudioDashboard GET /api/v1/studio/dashboard
listStudioDocuments GET /api/v1/studio/documents
createStudioDocument POST /api/v1/studio/documents
getStudioDocument GET /api/v1/studio/documents/{documentId}
saveStudioDocument PUT /api/v1/studio/documents/{documentId}
validateStudioDocument POST /api/v1/studio/documents/{documentId}/validate
createStudioPreview POST /api/v1/studio/documents/{documentId}/preview
getCurrentStudioPreview GET /api/v1/studio/documents/{documentId}/preview
publishStudioDocument POST /api/v1/studio/documents/{documentId}/publish
listStudioPublications GET /api/v1/studio/publications
unpublishStudioPublication POST /api/v1/studio/publications/{publicationId}/unpublish
getStudioPublicationSnapshot GET /api/v1/studio/publications/{publicationEventId}/preview
listStudioCatalog GET /api/v1/studio/catalog
listStudioAssets GET /api/v1/studio/assets
uploadStudioAsset POST /api/v1/studio/assets
getStudioAsset GET /api/v1/studio/assets/{assetId}
updateStudioAsset PUT /api/v1/studio/assets/{assetId}
deleteStudioAsset DELETE /api/v1/studio/assets/{assetId}

RecordKind 4종(CASE, REFERENCE, QUESTION, PROJECT_DECISION)을 하나의 문서 편집 흐름으로 다룬다.

2.2 Out of scope (이번 브랜치)

  • contracts/openapi/public-v1.yaml (Public 조회 API) — 소비자(Astro Public 사이트)가 아직 없다.
  • contracts/openapi/studio-management-v1.yaml (secondary capability 보존 계약).
  • release, topic/tag 관리 UI, identity(SiteConfig/Profile/HomeFocus) 편집 API.
  • 이번 범위에 필요한 만큼의 topic/project 조회는 catalog에서 다루되, 그 편집 API는 만들지 않는다.

3. 상위 결정

ID 결정 근거
D1 새 Gradle leaf를 만들지 않고 기존 18 leaf 안의 하위 패키지로 배치한다 src/settings.gradleexpectedModuleCount = 18로 fail-closed 검증. leaf 추가는 registry·settings·dependency gate·ArchUnit을 동시에 바꾸는 template-level 변경이고 template.lock.json 기반 향후 sync와 충돌한다
D2 패키지는 dev.caskeleton.{domain,application,adapter...} 루트 아래techlog 하위로 넣는다 일부 ArchUnit 규칙이 dev.caskeleton.application..처럼 루트를 고정한다(CleanArchitectureTest.java:221). 다른 루트에 두면 가드레일이 조용히 미적용된다
D3 계약 우선(openapi-generator)으로 DTO(model)만 생성하고 controller는 얇게 손으로 쓴다 (globalProperties.set(['models': '']), useOneOfInterfaces=false) — 실제 설정은 §5.2, 실측과의 차이는 §3.1 ADR-004를 유지하되 D4와 충돌하지 않는 형태. 생성 API interface는 봉투 wrapper 타입을 반환하게 되고, 그 타입은 dev.caskeleton.shared.response.Envelope가 아니라서 EnvelopeBodyAdvice한 번 더 감싼다(이중 래핑). 드리프트 위험의 실체는 93개 schema·enum·required 필드이고 그건 model 생성으로 덮인다. controller 19개 signature 드리프트는 §5.5 회귀 테스트가 잡는다
D4 /api/v1/studio/**도 템플릿 응답 봉투를 그대로 쓴다. 계약을 봉투 형태로 재정의한다 봉투는 shared-contract/README.md:230의 문서화된 결정(boundary D5/D6)이고 정보 손실이 없다. 적응 코드를 백엔드 템플릿 파일이 아니라 프론트 제품 코드에 두는 편이 template sync 충돌이 없다(§5.3)
D5 studio_idempotency 테이블을 만들지 않고 기존 idempotency_record를 쓴다 기존 스키마가 설계 요구의 상위집합이고 IdempotencyExecutorV2까지 있다(§8.2)
D6 Studio 통합 목록은 물리 인덱스 테이블 없이 union query로 시작한다 설계 08장 §4. 성능이 입증되면 read model 추가
D7 커밋은 하지 않는다 AGENTS.md:64 — commit 정책 human-only

3.1 D3 정정 — 실제 구현과의 차이 (2026-08-19, final whole-branch review B2)

이 절 작성 시점(§12 슬라이스 0 계획 단계)에는 openapi-generator 7.x가 이 저장소의 Spring Boot 버전과 함께 검증된 바 없었고, §5.2가 예정한 generateApis=false / generateModels=true는 실제 스파이크(슬라이스 0)에서 두 가지가 틀린 것으로 드러났다. 아래는 spec이 스스로 요구한 "폴백을 쓰면 D3을 갱신한다"(구 §5.2)는 약속이 이행되지 않고 있던 것을 바로잡는 정정이다 — 구현이 폴백으로 넘어간 것은 아니고(생성기는 여전히 model을 생성한다), 생성기 설정 자체가 설계 시점에 존재하지 않는 API를 가정했던 것이 실측으로 확인됐다.

  • generateApis/generateModels는 openapi-generator-gradle-plugin 7.18.0의 openApiGenerate 확장에 존재하지 않는다 (디컴파일로 확인, task-4-report.md). 대신 CLI --global-property와 같은 뜻인 globalProperties.set(['models': ''])로 "models만" 생성하도록 제한한다. 실제 설정은 §5.2를 그대로 참조.
  • 생성 소스는 sourceSets.main.java.srcDir에 있지 않다. 별도 generatedOpenapi sourceSet에 있고, main/test의 compile·runtime classpath와 jar 산출물에 각각 명시적으로 이어 붙인다 (src/adapter/inbound/web/build.gradle:22-38). 이유: 생성 코드가 deprecated org.springframework.lang.Nullable을 참조하는데, 저장소 루트 build.gradle-Werror/-Xlint:deprecation이 이걸 컴파일 실패로 승격한다. main에 직접 넣으면 그 플래그가 생성 코드에도 적용돼 빌드가 깨진다 — 별도 sourceSet으로 분리하고 그 sourceSet의 compileGeneratedOpenapiJava 태스크에서만 -Werror/-Xlint:deprecation을 뺀다.
  • spec에 없던 useOneOfInterfaces=false가 결정적 옵션으로 추가됐다. discriminator(oneOf) union을 부모 Java interface로 생성하면(useOneOfInterfaces=true, openapi-generator 기본값) discriminator getter가 항상 String을 반환하도록 SpringCodegen이 고정하는데, 하위 타입의 실제 getter 타입(공유 enum이든 아니든)과 충돌해 컴파일이 깨진다. 판별 필드를 하위 타입에서 narrowing하지 않도록 계약을 고쳐도 동일하게 깨지는 것까지 스크래치에서 직접 검증했다(3개 설정 조합, task-4-report.md) — 계약 쪽에서 우회할 수 없는 SpringCodegen 자체의 제약이다.

useOneOfInterfaces=false의 대가 — 생성 union 5종이 파손됐다. 이 경고는 지금 src/adapter/inbound/web/build.gradle:139-155의 주석에만 있고 spec 본문에는 없었다 — Plan 02 작성자가 읽는 문서는 이 spec이므로 여기 옮긴다.

useOneOfInterfaces=false는 컴파일은 통과시키지만, discriminator union 5종 (WorkingCopyInput, WorkingCopy, Inline, CaseRenderBlock, PublicRenderModel)의 하위 타입이 Java implements 관계를 전혀 갖지 않는 독립 클래스로 생성된다. 실측 결과 Jackson 양방향 배선도 계약을 어긴다:

  • 역직렬화InvalidTypeIdException으로 실패한다 (예: Class CaseInput not subtype of WorkingCopyInput).
  • 직렬화는 생성된 @JsonIgnoreProperties(value="kind", allowSetters=true) 때문에 실제 discriminator 값 대신 클래스 simple name이 나간다 (예: 응답에 "kind":"WorkingCopyInput").

이 union들을 요청·응답에 직접 또는 (409 VersionConflictDetails처럼) 간접적으로 포함하는 operation은 최소 createStudioDocument/getStudioDocument/saveStudioDocument (WorkingCopyInput/WorkingCopy)와 createStudioPreview/getCurrentStudioPreview/ getStudioPublicationSnapshot(PublicRenderModel)이며, validateStudioDocument를 포함해 409 conflict 응답 경로로 더 넓게 새어 들어갈 수 있다 — Task 8/9(getStudioSession, listStudioCatalog)는 이 union들을 쓰지 않아 막히지 않았을 뿐, 영향받는 operation의 정확한 목록과 대응 전략(수동 Jackson @JsonTypeInfo/@JsonSubTypes 재작성, 계약 재구조화, 또는 별도 수기 DTO)은 Plan 02가 착수 전에 확정해야 한다.


4. 코드 배치

4.1 패키지

:domain-core
  dev.caskeleton.domain.techlog.content        Document / CaseDetail / ReferenceDetail
  dev.caskeleton.domain.techlog.inquiry        OpenQuestion
  dev.caskeleton.domain.techlog.project        ProjectDecision
  dev.caskeleton.domain.techlog.asset          Asset
  dev.caskeleton.domain.techlog.publication    Publication / PublicationEvent
  dev.caskeleton.domain.techlog.<ctx>.vo       Value Object

:application-core
  dev.caskeleton.application.techlog.<ctx>.port.in     Command · Query · *UseCase 인터페이스
  dev.caskeleton.application.techlog.<ctx>.port.out    Repository · Renderer · Clock 포트
  dev.caskeleton.application.techlog.<ctx>.service     *UseCase 구현
  dev.caskeleton.application.techlog.studio.facade     WorkingCopy / Validation / Preview / Publication
  dev.caskeleton.application.techlog.studio.query      Dashboard / Document / Publication / Catalog
  dev.caskeleton.application.techlog.studio.mapper     API 용어 ↔ Domain 용어
  dev.caskeleton.application.techlog.studio.nextaction NextAction 계산
  dev.caskeleton.application.techlog.studio.port.out   Studio 자신의 read 포트(예: CatalogQueryPort) —
                                                        §4.3 경계 규칙 2가 금지하는 것은 studio가
                                                        *타* context의 port.out을 참조하는 것이지,
                                                        studio 자신의 port.out이 아니다

:adapter:outbound:persistence-jpa
  dev.caskeleton.adapter.outbound.persistence.techlog.<ctx>.entity
  dev.caskeleton.adapter.outbound.persistence.techlog.<ctx>.repository
  dev.caskeleton.adapter.outbound.persistence.techlog.<ctx>.mapper
  dev.caskeleton.adapter.outbound.persistence.techlog.query    JdbcClient union query

:adapter:inbound:web
  dev.caskeleton.adapter.inbound.web.techlog.studio.controller
  dev.caskeleton.adapter.inbound.web.techlog.studio.mapper
  dev.caskeleton.adapter.inbound.web.techlog.studio.problem

:app-bootstrap
  dev.caskeleton.bootstrap.techlog             조립·설정만

modules.json, settings.gradle, verifyCleanArchitectureDependencies는 변경하지 않는다.

4.2 상속되는 기존 가드레일

신규 코드에 자동으로 적용된다.

  • ..domain.. 순수성 (프레임워크·전송·DB 의존 금지)
  • ..domain.vo.. / @ValueObject: public 무인자 생성자 금지
  • @AggregateRoot: set* 메서드 public 금지
  • @DomainEvent: record 필수
  • ..application..: @Transactional 금지 → TransactionPort 사용
  • ..application..: ApplicationContext 의존 금지
  • CommandUseCase/QueryUseCase 구현: 이름이 UseCase로 끝나야 하고 @UseCaseCapability 필수
  • verifyApplicationCoreDependencyPurity: application-core 생산 의존은 project-only, 클래스패스에 Spring/slf4j/logback/micrometer 금지

4.3 추가할 경계 규칙 — TechLogBoundaryArchTest

:app-bootstrap 테스트에 추가한다.

  1. techlog.content / techlog.inquiry / techlog.project / techlog.asset은 서로 의존하지 않는다.
  2. application.techlog.studio는 타 context의 port.in만 참조한다. 타 context의 domain, port.out, service 직접 참조는 위반이다. (studio 자신의 application.techlog.studio.port.out은 이 규칙의 대상이 아니다 — §4.1 참조.)
  3. 타 context는 application.techlog.studio를 참조하지 않는다 (역방향 금지).
  4. domain.techlog.publication을 제외한 어떤 domain 패키지도 Publication을 직접 변경하지 않는다.

설계 08장의 "studio는 도메인 모듈이 아니다"를 빌드로 강제하는 장치다.


5. 계약 → 코드

5.1 계약 원본

studio-v1.yamlsrc/config/openapi/studio-v1.yaml로 복사한다 (src/config/architecture, src/config/messaging과 같은 authority 위치). src/config/openapi/MANIFEST.sha256에 해시를 기록해 설계 패키지와의 드리프트를 가시화한다.

5.2 생성

:adapter:inbound:weborg.openapi.generator 플러그인을 추가한다. 아래는 실제 구현 설정 (src/adapter/inbound/web/build.gradle)이다 — 이 절이 원래 예정했던 generateApis=false/ generateModels=true는 openapi-generator-gradle-plugin 7.18.0에 존재하지 않는 프로퍼티였다. 무엇이 왜 달라졌는지는 §3.1 참조.

generatorName      spring
globalProperties   ['models': '']     ← "models만" 생성 (CLI --global-property와 동치)
modelPackage       dev.caskeleton.adapter.inbound.web.techlog.studio.api.model
useSpringBoot3     true
useJakartaEe       true
openApiNullable    true                (jackson-databind-nullable 이미 선언됨)
useOneOfInterfaces false               ← §3.1 — discriminator union 5종의 Jackson 배선이 깨지는 대가
output             build/generated/openapi
  • 생성 소스는 sourceSets.main.java.srcDir가 아니라 별도 generatedOpenapi sourceSet에 있다. 생성 코드가 deprecated org.springframework.lang.Nullable을 쓰는데 저장소 루트의 -Werror가 이를 빌드 실패로 승격하기 때문이다 — 자세한 배선은 §3.1과 build.gradle:1-38의 주석 참조. jar/test 클래스패스에는 별도로 명시적으로 얹는다.
  • spotless / checkstyle / spotbugs / errorprone 대상에서 제외한다.
  • adapter/inbound/web/gradle.lockfile--write-locks로 재생성한다.

왜 API interface를 생성하지 않는가

D4로 응답 봉투를 유지하면 계약의 성공 응답 스키마가 봉투 wrapper가 된다. 그러면 생성 interface의 signature가 ResponseEntity<StudioSessionEnvelope>가 되는데, StudioSessionEnvelope는 생성된 별개 클래스라 dev.caskeleton.shared.response.Envelope가 아니다. EnvelopeBodyAdvice.beforeBodyWriteEnvelope/BulkEnvelope만 통과시키므로 이 본문을 한 번 더 감싼다.

controller → StudioSessionEnvelope
EnvelopeBodyAdvice → Envelope<StudioSessionEnvelope>
wire → {"success":true,"data":{"success":true,"data":{...},"meta":{...}},"meta":{...}}

이걸 피하려면 EnvelopeBodyAdvice(템플릿 파일)를 고쳐야 하는데, 그건 D4가 피하려던 바로 그 template sync 충돌이다.

따라서 model만 생성하고 controller는 손으로 쓴다.

@RestController
final class StudioSessionController {
  @GetMapping("/api/v1/studio/session")
  StudioSession getStudioSession() {          // 생성된 payload DTO를 그대로 반환
    return mapper.toApi(facade.currentSession());
  }
}
// EnvelopeBodyAdvice가 여기서 정확히 한 번 감싼다.

controller는 facade 호출과 매핑만 하고 비즈니스 로직을 두지 않는다.

봉투 wrapper 스키마도 함께 생성되지만 백엔드는 쓰지 않는다. 계약의 wrapper는 소비자(프론트·외부 도구)를 위한 기술이고, 백엔드에서는 advice가 그 역할을 한다.

선행 검증 결과: openapi-generator 7.x × 이 저장소의 Spring Boot 조합은 슬라이스 0의 폐기용 스파이크로 검증했다. 폴백(생성기 1회 실행 후 수기 유지)으로 넘어가지는 않았다 — 생성기는 지금도 빌드마다 model을 생성한다. 대신 §5.2 상단에 적은 세 가지(globalProperties, 별도 sourceSet, useOneOfInterfaces=false)가 스파이크에서 드러난 실제 조건이었다. §3.1이 그 정정 기록이다.

5.3 wire format — 템플릿 봉투를 그대로 쓴다

성공과 실패가 한 모양을 공유한다.

// 성공 (200 / 201)
{ "success": true,  "data": { /* studio-v1 payload */ }, "meta": { "requestId": "...", "traceId": "...", "correlationId": "..." } }

// 실패 (4xx / 5xx)  — HTTP status는 그대로 의미를 갖는다
{ "success": false, "error": { "code": "VERSION_CONFLICT", "category": "CONFLICT",
                               "message": "...", "retryable": false,
                               "details": { /* code별 polymorphic */ } },
  "meta": { "requestId": "...", "traceId": "...", "correlationId": "..." } }

// 204 (deleteStudioAsset) — 본문 없음. EnvelopeBodyAdvice는 null body를 감싸지 않는다.

미디어 타입은 성공·실패 모두 application/json이다. application/problem+json은 쓰지 않는다.

왜 이 방향인가

  • 봉투는 이 템플릿의 문서화된 결정이다. shared-contract/README.md:230 — "RFC 7807 ProblemDetail 을 대체한다(boundary D5/D6)", "D5 가 RFC 7807 ProblemDetail 을 거부하고, D10 이 category를 1급 필드로 추가했다".
  • 정보 손실이 없다. §5.4의 매핑표 참조. error.category는 봉투 쪽이 추가로 준다.
  • 적응 코드의 위치가 결정적이다. 봉투를 벗기려면 EnvelopeBodyAdvice.supports()를 고쳐야 하는데 이는 템플릿 파일이고, 이 저장소는 template.lock.json"materialization": "tracked-snapshot"이라 이후 모든 template sync의 충돌 지점이 된다. 반대로 봉투를 유지하면 적응은 프론트 제품 코드 (src/features/tech-log/contracts/tech-log-studio-contract-contribution.ts) 안에서 끝나고 프론트 플랫폼(src/adapters/http/http-execution-v3.ts)도 무변경이다.

백엔드가 해야 할 일

봉투 관련 신규 작업은 없다. EnvelopeBodyAdvice, ErrorResponseFactory, GlobalExceptionHandler, EnvelopeAuthenticationEntryPoint, EnvelopeAccessDeniedHandler, RateLimitInterceptor를 그대로 쓴다. 필요한 것은 §5.4의 오류 코드 등록과 Studio 전용 예외 → ApiError 매핑뿐이다.

계약과 프론트가 해야 할 일

이 결정은 세 저장소에 걸친다. 상세는 §5.6.

tech-log-design-package   studio-v1.yaml을 봉투 형태로 재정의 (+ 06장 · ADR · MASTER_SPEC · 검증 스크립트)
tech-log-frontend         계약 재생성 + validator 2개를 봉투 언랩으로 교체
tech-log-backend          오류 코드 등록 + 예외 매핑 (봉투 자체는 무변경)

5.4 오류 코드

계약의 오류 코드 23개를 ApiError.code에 그대로 싣는다.

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
WARNING_ACKNOWLEDGEMENT_REQUIRED   IDEMPOTENCY_KEY_REUSED
ASSET_NOT_FOUND           ASSET_NOT_READY               ASSET_IN_USE
ASSET_QUARANTINED         PAYLOAD_TOO_LARGE             UNSUPPORTED_MEDIA_TYPE
STUDIO_UNAVAILABLE

필드 매핑 — 정보 손실 없음

기존 계약 ProblemDetails 봉투
code error.code
retryable error.retryable (1급)
detail error.message (client-safe. stack trace·내부 ID 금지)
status HTTP status (봉투도 실제 status를 유지한다)
traceId meta.traceId (D7 — 응답에서 절대 null이 아니다)
fieldErrors error.details = ValidationErrorDetails
latestDocument error.details = VersionConflictDetails
latestPublication error.details = PublicationConflictDetails
conflictingFields 위 두 details 안
type / title 버린다. 프론트가 이미 code에서 합성한다(studio-error-mapping.tssynthetic())
error.category — 봉투가 추가로 준다

details는 code별 polymorphic이므로 계약에서 oneOf 타입 변형으로 선언한다. Object 자유형으로 두지 않는다.

레지스트리 등록

docs/registries/error-codes.yaml에 23개 row를 additive로 추가한다. row 스키마가 요구하는 필드를 모두 채운다 — category(10-value Category enum), http_status, retryable, owner_layer, client_safe_message, log_level, runbook_link, compatibility_impact: additive, required_test.

runbook 정책: retryable=false이고 category가 AUTH/AUTHZ/RATE_LIMIT/INTERNAL/ TRANSIENT_DEPENDENCY/PERMANENT_DEPENDENCYrunbook_link가 필수다. VALIDATION/NOT_FOUND/CONFLICT/DATA_INTEGRITY는 client-error로 면제 가능하다. 따라서 AUTHENTICATION_REQUIRED(AUTH), STUDIO_ACCESS_DENIED(AUTHZ), STUDIO_UNAVAILABLE(TRANSIENT_DEPENDENCY)는 runbook을 함께 작성한다.

category 배정:

AUTH                   AUTHENTICATION_REQUIRED
AUTHZ                  STUDIO_ACCESS_DENIED
NOT_FOUND              DOCUMENT_NOT_FOUND  PREVIEW_NOT_FOUND  PUBLICATION_NOT_FOUND
                       PUBLICATION_EVENT_NOT_FOUND  PUBLICATION_SNAPSHOT_NOT_FOUND
                       ASSET_NOT_FOUND
CONFLICT               VERSION_CONFLICT  PUBLICATION_CONFLICT  IDEMPOTENCY_KEY_REUSED
                       VALIDATION_STALE  PREVIEW_STALE  PREVIEW_EXPIRED
                       ASSET_IN_USE  ASSET_NOT_READY
VALIDATION             REQUEST_VALIDATION_FAILED  VALIDATION_FAILED
                       WARNING_ACKNOWLEDGEMENT_REQUIRED
                       PAYLOAD_TOO_LARGE  UNSUPPORTED_MEDIA_TYPE
DATA_INTEGRITY         ASSET_QUARANTINED
TRANSIENT_DEPENDENCY   STUDIO_UNAVAILABLE

5.5 계약 회귀 테스트

실제 구현 범위 (Task 10, StudioContractDriftTest, :app-bootstrap의 functional test 소스셋): springdoc이 노출하는 /v3/api-docssrc/config/openapi/studio-v1.yaml과 대조해 operation id · method · path가 어긋나면 실패시킨다 (published ⊆ contract 방향 — 아직 구현하지 않은 operation이 있어도 green을 유지한다). dev.caskeleton.adapter.inbound.web.techlog 아래를 @ComponentScan하므로 그 패키지 트리 밖에 컨트롤러를 두면 이 게이트가 못 본다 (스캔 범위를 벗어나는 즉시 유효성을 잃는다는 뜻이므로 §4.1의 패키지 배치를 반드시 따른다). 별도로 getStudioSession/listStudioCatalog 각 1개 케이스에 대해 성공 응답이 {success:true, data, meta} 모양이고 이중 래핑이 없는지도 고정한다 — 모든 operation의 봉투 래핑을 확인하는 것은 아니다.

아직 구현하지 않은 것 (Plan 02 몫):

  • 응답 media type 대조 (application/json 고정 여부)
  • 스키마 필수 필드 대조 (93개 schema·enum·required 필드가 model 생성으로 덮인다는 D3의 전제를 실제로 검증하는 회귀 테스트는 없다 — 지금은 생성이 컴파일에 성공하는 것으로 암묵 검증한다)
  • 나머지 17개 operation에 대한 봉투 래핑 확인
  • deleteStudioAsset 204 본문 없음 확인 (해당 operation 미구현)

401/403/429 코드 주장 — 삭제. 이 절은 원래 401/403/429가 각각 AUTHENTICATION_REQUIRED/STUDIO_ACCESS_DENIED/rate-limit 코드로 난다고 적었으나 이는 코드상 사실이 아니다. 기존(템플릿 소유, 무변경 재사용) 경로가 실제로 내는 코드는:

  • 401 — EnvelopeAuthenticationEntryPointSecurityErrorClassifier.classifyAuthentication (src/adapter/inbound/web/src/main/java/.../auth/SecurityErrorClassifier.java:21-35)이 AUTH_TOKEN_MISSING(InsufficientAuthenticationException) 또는 AUTH_TOKEN_MALFORMED(그 외 분류 불가 인증 실패)를 낸다.
  • 403 — EnvelopeAccessDeniedHandlerSecurityErrorClassifier.classifyAccessDenied (같은 파일:39)가 AUTHZ_INSUFFICIENT_PERMISSION을 낸다.
  • 429 — RateLimitInterceptor:75RATE_LIMIT_EXCEEDED를 낸다.

이 세 코드 모두 계약(studio-v1.yaml) 23종 ApiError.code enum에 없다. 429는 계약에 rate-limit 코드 자체가 존재하지 않는다. §5.3 "백엔드가 해야 할 일"이 "봉투 관련 신규 작업은 없다"고 못 박았고 이 경로들은 템플릿 파일(EnvelopeAuthenticationEntryPoint, EnvelopeAccessDeniedHandler, RateLimitInterceptor, SecurityErrorClassifier)만으로 동작해 Studio가 손댈 수 없다 — 계약을 이 세 코드로 확장할지, 별도 Studio 매핑 계층을 둘지는 이 spec이 결정하지 않은 채 남아 있다. Plan 02가 착수 전에 결정해야 한다.

5.6 계약 재정의 명세

studio-v1.yaml을 다음과 같이 바꾼다. 이것이 세 저장소의 공유 SSOT가 된다.

추가 스키마

ResponseMeta               requestId, traceId, correlationId, page(nullable)
ApiError                   code(enum 23), category(enum 10), message, retryable, details(oneOf|null)
ErrorEnvelope              success(const false), error(ApiError), meta(ResponseMeta)
ValidationErrorDetails     fieldErrors[]
VersionConflictDetails     latestDocument, conflictingFields[]
PublicationConflictDetails latestPublication

성공 응답 래핑 — JSON 본문을 갖는 18개 operation(200 15개 / 201 3개)의 응답 스키마를 {success: const true, data: <기존 payload>, meta: ResponseMeta} 래퍼로 교체한다. OpenAPI 3.1에 제네릭이 없으므로 payload별 래퍼 스키마를 만든다. 기존 payload 스키마(StudioSession, WorkingCopyDetail, …)는 그대로 남긴다 — 프론트가 components["schemas"]["StudioSession"]로 도메인 타입을 계속 뽑아 쓴다.

오류 응답 교체components.responses의 16개 항목 content를 application/problem+json + ProblemDetails에서 application/json + ErrorEnvelope로 바꾼다. ProblemDetails 스키마는 제거한다.

deleteStudioAsset의 204는 변경 없다.

public-v1.yaml / studio-management-v1.yaml — 두 계약도 ProblemDetails를 쓰지만 이번 구현 범위 밖이고 소비자가 없다. 지금 변환하지 않고 각 파일 상단에 "봉투 결정(ADR-006) 반영 대기" 배너를 붙인다. 설계 패키지가 docs/plans/02에 쓴 STALE 배너와 같은 방식이다. 구현에 착수할 때 변환한다.

따라오는 파일

tech-log-design-package
  contracts/openapi/studio-v1.yaml            위 변경
  docs/specs/06-api-contract-design.md        7장 오류 계약 재작성
  decisions/ADR-006-response-envelope.md      신규 — 봉투 채택 근거와 RFC 7807 미채택 기록
  TECH_LOG_MASTER_SPEC.md                     scripts/build-master-spec.sh 재생성
  MANIFEST.sha256                             scripts/update-manifest.sh 재생성
  scripts/check-contract-parity.py            ProblemDetails.code → ApiError.code 참조 변경
  scripts/check-consistency.py                동일

tech-log-frontend
  src/features/tech-log/contracts/studio/studio-api.openapi.yaml   동기화
  src/features/tech-log/contracts/studio/generated.ts              재생성
  src/features/tech-log/contracts/studio/canonical-source.json     재생성
  src/features/tech-log/contracts/studio/contract.ts               타입 export 확인
  src/features/tech-log/contracts/tech-log-studio-contract-contribution.ts
        outputValidator  passthrough      → 봉투 data 언랩
        problemValidator PROBLEM(bare)    → 봉투 error 언랩 + code enum 검증 유지
  src/features/tech-log/adapters/http/studio-error-mapping.ts      ApiError → StudioGatewayError
  관련 테스트 · 픽스처의 HTTP 본문

mock-studio-gateway.tsStudioGateway 포트를 직접 구현하는 mock은 전송 경계 아래가 아니라 위에 있으므로 변경 대상이 아니다. 앱·도메인·프레젠테이션 계층도 언랩이 경계에서 끝나므로 무변경이다.


6. 도메인 모델

6.1 Aggregate

Aggregate 소유 테이블 비고
Document document, case_detail, reference_detail, document_tag, document_relation RecordKind.CASE / REFERENCE
OpenQuestion open_question, question_point, question_update, question_tag, question_document_link RecordKind.QUESTION
ProjectDecision project_decision RecordKind.PROJECT_DECISION
Asset asset, asset_reference
Publication publication, publication_event, publication_snapshot Event·Snapshot은 불변

WorkingCopy는 Aggregate가 아니라 API projection이다. working_copy 범용 테이블을 만들지 않는다.

6.2 상태 기계 (설계 09장)

Document      DRAFT → IN_REVIEW → PUBLISHED → ARCHIVED  (+ unpublish: PUBLISHED → DRAFT)
OpenQuestion  OPEN → INVESTIGATING → PAUSED → RESOLVED → ARCHIVED
Publication   PUBLISHED → REPUBLISHED → UNPUBLISHED → REPUBLISHED

API 용어와 Domain 용어를 분리한다.

API questionStatus=OPEN      ← Domain OPEN | INVESTIGATING | PAUSED
API questionStatus=RESOLVED  ← Domain RESOLVED

saveStudioDocument는 편집 가능한 content field만 저장하며 lifecycle 전이를 유발하지 않는다. OPEN 계열 안에서의 값 변화는 무시한다. 프론트가 축약 상태를 보냈다는 이유로 INVESTIGATINGOPEN으로 덮어쓰면 조사 이력이 소실된다.

6.3 저장하지 않는 값

nextAction, publicationStatus, hasUnpublishedChanges, Preview state는 조회 시점 계산이다. workflow_status 같은 domain 컬럼에 저장하지 않는다.


7. 애플리케이션 계층

7.1 use case 규약

context별 use case는 CommandUseCase<C, R> / QueryUseCase<Q, R>를 구현하고 이름이 UseCase로 끝나며 @UseCaseCapability를 선언한다.

@UseCaseCapability(
    transactionMode = TransactionMode.WRITE,
    idempotency = Idempotency.KEYED,
    repositoryAccess = RepositoryAccess.WRITE_REPOSITORY)
final class PublishCaseUseCase implements CommandUseCase<PublishCaseCommand, PublishResult> { }

트랜잭션 경계는 @Transactional이 아니라 TransactionPort로 연다. TransactionModeWRITE / READ_ONLY / REQUIRES_NEW 계열이고, IdempotencyIDEMPOTENT / KEYED / NOT_IDEMPOTENT, RepositoryAccessNONE / READ_REPOSITORY / WRITE_REPOSITORY다. Idempotency-Key를 쓰는 mutation은 KEYED로 선언한다.

7.2 studio facade

StudioWorkingCopyFacade    create / get / save / list  → 유형별 port.in dispatch
StudioValidationFacade     validate → validation artifact 영속
StudioPreviewFacade        create / get preview artifact + state 계산
StudioPublicationFacade    publish / unpublish / 이력 / snapshot 조회
StudioDashboardQuery       union query 기반 대시보드
StudioDocumentQuery        union query 기반 문서 목록
StudioCatalogQuery         TOPIC | PROJECT | RELATION | EVIDENCE
NextActionCalculator       §7.4
StudioDocumentLocator      documentId(UUID) → (sourceKind, sourceId)

facade는 domain object를 직접 수정하지 않는다. 타 context의 port.in만 호출하고 결과를 통합 DTO로 조립한다.

documentId는 계약상 source aggregate id를 그대로 쓴다(별도 surrogate 없음). StudioDocumentLocatordocument / open_question / project_decision union query로 kind를 해소한다.

7.3 Validation / Preview artifact

studio_validation   validationId, sourceKind, sourceId, validatedVersion,
                    status(INVALID|WARNINGS|VALID), issues(jsonb),
                    dependencyRevision, validatedAt, validUntil, createdBy

studio_preview      previewId, sourceKind, sourceId, sourceVersion,
                    validationId(FK), dependencyRevision,
                    renderModel(jsonb), createdAt, expiresAt, createdBy

dependencyRevision은 검증에 쓴 외부 의존 상태(topic/project publishability, relation target 상태, asset READY/QUARANTINED, slug/route ownership, catalog revision, renderer/content-format version)의 identity+version을 정규화해 만든 해시다. 전역 카운터가 아니다.

상태는 저장하지 않고 조회 시 계산한다.

Validation 유효   validatedVersion == 현재 working version
              AND dependencyRevision == 현재 계산값
              AND now() < validUntil

Preview CURRENT   sourceVersion == 현재 working version
              AND dependencyRevision == 현재 계산값
              AND now() < expiresAt
        STALE     version 또는 dependencyRevision 불일치
        EXPIRED   now() >= expiresAt

VALIDATION_FAILED(지금 검증하면 실패)와 VALIDATION_STALE(통과했으나 전제가 바뀜)은 다른 사건이며 코드도 다르다.

7.4 nextAction 계산

저장 가능한 형태조차 미달                       → CONTINUE_EDITING
현재 version에 대한 Validation 없음              → VALIDATE
현재 Validation = INVALID                        → FIX_VALIDATION
유효 Validation + 현재 version Preview 없음      → CREATE_PREVIEW
Preview가 STALE 또는 EXPIRED                     → CREATE_PREVIEW
현재 Validation + 현재 Preview
  + Publication version 불일치                   → PUBLISH
Publication version == working version           → NONE

7.5 Publish 트랜잭션 (설계 07장 §14, 20단계)

0.  idempotency 선검사 — 동일 key + 동일 fingerprint면 최초 응답 재생, 이하 미실행
1.  Document SELECT FOR UPDATE
2.  expectedVersion 검증                      불일치 → VERSION_CONFLICT
3.  validationId 조회 + dependencyRevision 재계산 비교   불일치 → VALIDATION_STALE
4.  previewId 조회 + sourceVersion / dependencyRevision 비교
                                              불일치 → PREVIEW_STALE, 만료 → PREVIEW_EXPIRED
5.  acknowledgedWarningCodes가 WARNING 집합을 덮는지
                                              미달 → WARNING_ACKNOWLEDGEMENT_REQUIRED
6.  유형별 publication validation (게시 필수 필드 / slug 충돌)
7.  Markdown 분석
8.  Asset READY 및 사용 위치 alt/decorative 검증
9.  공개 payload 생성
10. Publication Event 생성 (PUBLISHED | REPUBLISHED)
11. Publication Snapshot 생성 (immutable)
12. public_resource_projection upsert
13. public_route canonical/alias 변경
14. public_resource_tag / project link 교체
15. PUBLISHED asset_reference 교체
16. asset.first_published_at 갱신
17. Publication aggregate 갱신 (latest_event_id, publication_revision)
18. Document publish metadata 갱신
19. 선택적 ProjectActivity 생성
20. Commit

전 단계가 하나의 트랜잭션이다.

Snapshot의 render model은 게시 시점에 다시 렌더링하지 않고 사용자가 확인한 Preview의 render model을 그대로 쓴다. 재렌더링하면 승인한 화면과 공개된 화면이 달라질 수 있다.

Unpublish는 expectedPublicationRevision 검증 → UNPUBLISHED Event(반드시 sourcePublishedEventId 보유) → Publication 상태 전환 및 revision 증가 → Projection ACTIVE → WITHDRAWN → Working workflow_status → DRAFT → route 유지 → commit. Snapshot은 삭제하지 않는다.


8. 영속화

8.1 마이그레이션

PostgreSqlPersistenceConfig.java:57이 Flyway location을 classpath:db/migration/postgresql로 고정한다. 기존 최대 버전이 V6이고 out-of-order: false이므로 V7__techlog_core.sql부터 추가한다.

이번 범위에 필요한 테이블:

topic  tag  document  case_detail  reference_detail  document_tag  document_relation
open_question  question_point  question_update  question_tag  question_document_link
project  project_decision  project_document_link  project_question_link  project_activity
asset  asset_reference
studio_validation  studio_preview
publication  publication_event  publication_snapshot
public_resource_projection  public_route  public_resource_tag  public_resource_project_link

publication.latest_event_idpublication_event.publication_id 순환 FK는 DEFERRABLE INITIALLY DEFERRED로 선언한다. 즉시 검사로 두면 첫 게시가 불가능하다.

publication_event / publication_snapshot은 어떤 cleanup job도 삭제하지 않는다.

8.2 멱등 — 기존 자산 재사용 (D5)

설계의 studio_idempotency 테이블을 만들지 않는다.

설계 studio_idempotency 기존 idempotency_record
idempotency_key idempotency_key
operation_id use_case_name
request_fingerprint request_hash (char(64) sha256)
response_status / response_body response_payload
created_at / expires_at created_at / expires_at
created_by principal (+ tenant)

기존 것이 상위집합이고 IdempotencyExecutorV2, IdempotencyStorePortV2, IdempotencyKeySupport(header 추출 · scope · sha256 fingerprint · JSON codec)까지 있다. 설계 대비 결손은 두 가지뿐이며 web 계층에서 채운다.

  1. 재생 시 Idempotency-Replayed: true 응답 헤더
  2. fingerprint 불일치 시 IDEMPOTENCY_KEY_REUSED ProblemDetails (409)

적용 대상: create / save / validate / create preview / publish / unpublish / asset upload · update · delete.

8.3 읽기·쓰기 분리

  • 쓰기: JPA aggregate + @Version 낙관적 잠금
  • Studio 목록 / 대시보드 / 카탈로그: JdbcClient union query (도메인 repository 우회)
  • 공통 CRUD repository를 만들지 않는다

9. 재사용 매핑 (신규 구현 금지)

설계 요구 재사용 대상
Idempotency-Key application/idempotency/v2, web/idempotency/IdempotencyKeySupport, idempotency_record
expectedVersion / 409 JPA @Version, web/conditional/ETags, PreconditionFailedException
Studio cursor 페이지네이션 web/cursor/CursorCodec, web/pagination/PageParams
오류 응답 골격 shared/response/{Envelope,ApiError,ResponseMeta}, web/error/ErrorResponseFactory, GlobalExceptionHandler무변경 재사용
401 / 403 / 429 EnvelopeAuthenticationEntryPoint, EnvelopeAccessDeniedHandler, RateLimitInterceptor무변경 재사용
Keycloak 세션 · CSRF web/auth/SecurityConfig, JwtDecoderConfig, PrimitiveSessionSecurityContextRepository, RedisSessionWebConfig
권한 application/security/RequiresPermission, web/authz/RolePermissionPolicy, RequiresPermissionAuthorizationManager
Asset 업로드 · 저장 adapter:outbound:fileserver, adapter:outbound:objectstorage
ID 생성 adapter:outbound:identifier
캐시 application/cache, adapter:outbound:cache-redis (필요 입증 시에만)
관측 web/observability, docs/registries/{metrics,mdc-keys,headers}.yaml
트랜잭션 application/transaction/TransactionPort

Asset은 새 저장 계층을 만들지 않는다. asset 테이블은 metadata·asset_key· lifecycle만 소유하고 바이너리 저장·전송은 fileserver/objectstorage 어댑터에 위임한다. asset_key(안정 참조)와 object_key(저장 위치)를 분리한다 — 본문에 object storage URL을 저장하지 않는다.


10. 렌더러

PublicRenderModel은 Markdown을 의미 블록으로 변환한 결과다. 계약이 정의하는 블록·인라인 타입:

블록    HeadingBlock  ParagraphBlock  CodeBlock  BlockquoteBlock  CalloutBlock
        OrderedListBlock  UnorderedListBlock  DataTableBlock  EvidenceFigureBlock
인라인  InlineText  InlineStrong  InlineEmphasis  InlineCode  InlineLink  InlineStatus

제약:

  • Public과 Studio Preview가 같은 의미의 렌더 결과를 써야 한다 (ADR-005).
  • Snapshot은 contentFormatVersion, rendererContractVersion, asset manifest를 함께 보존한다. 이후 Asset이 교체되어도 과거 Snapshot의 표현이 변하지 않는다.
  • Asset은 assetKey로 참조하고 렌더 시 ResolvedAsset으로 해소한다.

이 항목이 이번 구현의 최대 리스크다. 슬라이스 3 착수 전에 프론트의 기존 렌더 모델 구현(src/features/tech-log/adapters/mock/project-public-render-model.ts 및 static content 경로)을 기준선으로 대조해 블록 의미가 일치하는지 확인한다.


11. 테스트 전략

leaf별 템플릿 정책을 그대로 따른다.

대상 방식
domain-core 순수 JUnit. 상태 전이·불변 조건
application-core 손수 만든 fake 포트. 웹·영속 컨텍스트 금지
adapter:inbound:web 전송 slice 테스트 + §5.5 봉투 래핑·오류 코드 회귀 테스트
adapter:outbound:persistence-jpa 매핑·포트 계약 테스트. 벤더 의미가 필요한 것(deferrable FK, SELECT FOR UPDATE)은 postgresqlIntegrationTest 소스셋
app-bootstrap 배선·TechLogBoundaryArchTest·계약 회귀 테스트

필수 시나리오:

  • publish 재시도가 중복 publication_event를 만들지 않는다
  • VALIDATION_STALEVALIDATION_FAILED가 구분된다
  • saveStudioDocumentINVESTIGATINGOPEN으로 되돌리지 않는다
  • unpublish 후 과거 Snapshot이 그대로 조회된다
  • 성공 응답이 {success:true, data, meta}이고 meta.traceId가 non-null이다
  • 오류 응답이 {success:false, error, meta}이고 error.code가 계약의 23개 중 하나다
  • 409가 error.detailslatestDocument / latestPublication을 싣는다

검증 명령 (src/에서):

./gradlew :domain-core:test --console=plain
./gradlew :application-core:test --console=plain
./gradlew :adapter:inbound:web:test --console=plain
./gradlew :adapter:outbound:persistence-jpa:test --console=plain
./gradlew verifyCleanArchitectureDependencies --console=plain
./gradlew :app-bootstrap:test --tests '*CleanArchitectureTest' --console=plain
./gradlew :app-bootstrap:test --tests '*TechLogBoundaryArchTest' --console=plain

12. 슬라이스

# 내용 완료 판정
0 계약 봉투 재정의(§5.6) → 프론트 재생성·validator 교체, 생성기 스파이크 → 배선, 계약 복사, TechLogBoundaryArchTest, 오류코드 레지스트리 23개, V7__techlog_core.sql 빌드·아키텍처 검증 통과 + 3 저장소 계약 parity 통과
1 getStudioSession, listStudioCatalog 프론트 로그인·카탈로그 실동작
2 listStudioDocuments, createStudioDocument, getStudioDocument, saveStudioDocument (+ 낙관적 잠금, 멱등) 프론트 편집 실동작
3 validateStudioDocument, createStudioPreview, getCurrentStudioPreview, dependencyRevision, nextAction, 렌더러 프론트 검증·미리보기 실동작
4 publishStudioDocument, unpublishStudioPublication, listStudioPublications, getStudioPublicationSnapshot, getStudioDashboard 프론트 게시 실동작
5 asset 5종 프론트 자산 실동작

getStudioDashboard는 publication·validation 데이터에 의존하므로 슬라이스 4에서 완성한다.


13. 리스크

리스크 완화
openapi-generator × Spring Boot 4.0.0 미검증 슬라이스 0 첫 스텝을 폐기용 스파이크로. 실패 시 §5.2 폴백
3 저장소(계약·프론트·백엔드) 동기화 실패 슬라이스 0에서 계약을 먼저 확정하고 check-contract-parity.py를 게이트로 삼는다. 백엔드는 §5.5 계약 회귀 테스트로 고정
프론트 언랩 누락 시 조용한 실패 (outputValidator가 passthrough라 전송 계층이 잡지 못한다) 언랩 validator에 success/data 존재 검증을 넣어 하드 실패로 바꾼다
렌더 모델 의미 불일치 슬라이스 3 착수 전 프론트 구현 대조
publish 20단계 트랜잭션 복잡도 단계별 실패 코드를 먼저 테스트로 고정한 뒤 구현
설계 DDL과 템플릿 스키마 충돌 studio_idempotency 제거(D5) 외에는 이름 충돌 없음을 V7 작성 시 재확인

14. 운영 제약

  • 커밋 금지AGENTS.md:64 commit 정책 human-only. 브랜치 생성과 파일 작성까지 수행하고 stage/commit/push는 사용자가 한다. 이 제약은 tech-log-backend의 규약이며 tech-log-design-package / tech-log-frontend에는 적용되지 않는다.
  • 이 설계는 세 저장소를 건드린다(§5.6). 계약이 SSOT이므로 순서는 tech-log-design-packagetech-log-frontendtech-log-backend다.
  • git flow init은 워킹트리에 미커밋 삭제분(*-superpowers-package/, scripts/verify-httpclient-docs.py)이 있어 중단되었다. gitflow 설정은 기록했고 develop / feature/techlog-studio-backend 브랜치는 수동 생성했다. 미커밋 삭제분은 손대지 않았다.

15. 참조

  • 설계: tech-log-design-package/docs/specs/{06,07,08,09}, decisions/ADR-00{1..5}
  • 계약: tech-log-design-package/contracts/openapi/studio-v1.yaml
  • 프론트 계약: tech-log-frontend/src/features/tech-log/contracts/studio/studio-api.openapi.yaml
  • 프론트 실행 경로: tech-log-frontend/src/adapters/http/http-execution-v3.ts
  • 저장소 권위: src/config/architecture/modules.json, src/settings.gradle, AGENTS.md