`resolution` leaves the input's required list, so the generated type makes it optional. The editor already sent null for an unresolved question; nothing in the frontend changes behaviour.
1842 lines
90 KiB
YAML
1842 lines
90 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: Tech Log Studio API
|
|
version: 3.0.0
|
|
description: |
|
|
Tech Log Studio orchestration 계약이다.
|
|
|
|
이 파일이 Studio HTTP 계약의 canonical source다. Frontend의
|
|
`contracts/studio-api.openapi.yaml`은 이 계약에서 생성되어야 하며
|
|
독립적인 두 번째 canonical source가 되어서는 안 된다. (ADR-004)
|
|
|
|
## 이 계약의 성격
|
|
|
|
현재 Studio UI의 단일 문서 제작 흐름을 그대로 계약으로 승격한 것이다.
|
|
|
|
```text
|
|
작업본 → 편집 → 저장 → 검증 → Public Preview → 게시/재게시 → 게시 기록/Snapshot
|
|
```
|
|
|
|
유형별 CMS 메뉴(Case 관리 / Reference 관리 / Question 관리)는 이 계약의
|
|
primary mental model이 아니다. 유형별 specialized management capability는
|
|
`studio-management-v1.yaml`에 secondary API로 보존한다.
|
|
|
|
## WorkingCopy는 API projection이다
|
|
|
|
`WorkingCopy`는 Domain Aggregate가 아니다. Studio API가 여러 도메인을
|
|
동일한 편집 경험으로 보여주기 위한 API/Application projection이며,
|
|
Backend는 이를 기존 유형별 use case로 dispatch한다.
|
|
|
|
```text
|
|
createStudioDocument(kind=CASE) → CreateCaseDraft
|
|
saveStudioDocument(kind=CASE) → UpdateCaseDraft
|
|
validateStudioDocument(kind=CASE) → CasePublicationValidator
|
|
publishStudioDocument(kind=CASE) → CasePublicationHandler
|
|
|
|
kind=REFERENCE → Reference capability
|
|
kind=QUESTION → Inquiry capability
|
|
kind=PROJECT_DECISION → ProjectDecision capability
|
|
```
|
|
|
|
`documentId`는 source aggregate id를 그대로 사용한다. 별도 surrogate
|
|
Studio id를 만들지 않으며 `working_copy` 범용 테이블도 만들지 않는다.
|
|
|
|
## API 용어 ↔ Domain 용어 mapper 경계
|
|
|
|
API enum은 UI 용어이고 Domain enum은 그대로 보존한다. 변환은 mapper가 한다.
|
|
|
|
| API (이 계약) | Domain | 비고 |
|
|
|---|---|---|
|
|
| `questionStatus=OPEN` | `OPEN`, `INVESTIGATING`, `PAUSED` | 축약 view. 저장이 Domain 상태를 덮어쓰지 않는다 |
|
|
| `questionStatus=RESOLVED` | `RESOLVED` | Resolve command로 해석하며 기존 resolve invariant를 통과해야 한다 |
|
|
| `decisionStatus=ADOPTED` | `ACCEPTED`/`ADOPTED` | Domain enum을 UI 용어 때문에 변경하지 않는다 |
|
|
| `documentId` | `source_kind` + `source_id` | Publication 계열 테이블은 두 컬럼으로 저장한다 |
|
|
| `nextAction` | (없음) | Domain state가 아니라 Studio projection이다. `workflow_status`에 저장하지 않는다 |
|
|
|
|
`saveStudioDocument`는 편집 가능한 content field만 저장한다. lifecycle
|
|
전이는 명시적인 Domain Action이 담당한다. Frontend가 `OPEN`을 보냈다고 해서
|
|
`INVESTIGATING → OPEN`으로 자동 전이하면 안 된다.
|
|
|
|
## Frontend 계약과의 현재 차이 (2026-08-17 실측)
|
|
|
|
`tech-log-frontend`의 `src/features/tech-log/contracts/studio/studio-api.openapi.yaml`과
|
|
대조한 결과 다음이 이미 일치한다.
|
|
|
|
```text
|
|
operation 13개 전부 operationId · method · path 일치
|
|
RecordKind 양쪽 4종 동일 (CASE / REFERENCE / QUESTION / PROJECT_DECISION)
|
|
오류 코드 Frontend 15종이 이 계약의 23종에 모두 포함
|
|
```
|
|
|
|
Backend가 추가로 제공하는 것은 두 가지이며, Frontend가 이 계약에서 재생성할
|
|
때 흡수된다.
|
|
|
|
1. Asset operation 5개와 `getStudioSession`.
|
|
Frontend는 아직 Asset capability를 구현하지 않았다.
|
|
2. mutating operation의 `X-CSRF-TOKEN`.
|
|
Frontend 계약은 `security: []`이며 이 header를 선언하지 않는다.
|
|
|
|
## 게시 경로는 하나다
|
|
|
|
`studio-management-v1.yaml`의 publish/unpublish 계열 operation도 이 계약과
|
|
동일한 Publication Event/Snapshot 경로를 거쳐야 한다. 서로 다른 두 개의
|
|
게시 경로를 허용하지 않는다.
|
|
servers:
|
|
- url: /
|
|
security:
|
|
- sessionCookie: []
|
|
tags:
|
|
- name: Session
|
|
description: Studio 접근 제어
|
|
- name: Dashboard
|
|
description: Workspace 대시보드
|
|
- name: Documents
|
|
description: 통합 Working Copy 편집
|
|
- name: Validation
|
|
description: Validation Artifact
|
|
- name: Preview
|
|
description: 인증된 Public Preview Artifact
|
|
- name: Publication
|
|
description: Publication Aggregate / Event / Snapshot
|
|
- name: Catalog
|
|
description: Editor picker 통합 조회
|
|
- name: Assets
|
|
description: Asset Library / Picker / Upload
|
|
paths:
|
|
/api/v1/studio/session:
|
|
get:
|
|
operationId: getStudioSession
|
|
tags: [Session]
|
|
summary: 현재 Studio 세션과 CSRF 토큰을 조회한다
|
|
responses:
|
|
"200": { description: 인증된 Studio 세션, content: { application/json: { schema: { $ref: "#/components/schemas/StudioSessionEnvelope" } } } }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/dashboard:
|
|
get:
|
|
operationId: getStudioDashboard
|
|
tags: [Dashboard]
|
|
summary: Get the Studio dashboard
|
|
description: |
|
|
`nextAction`을 포함한 모든 workflow 상태는 서버가 계산한다.
|
|
Frontend는 여러 endpoint를 조합해 workflow 상태를 재추론하지 않는다.
|
|
responses:
|
|
"200": { description: Dashboard lists and totals, content: { application/json: { schema: { $ref: "#/components/schemas/StudioDashboardEnvelope" } } } }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/documents:
|
|
get:
|
|
operationId: listStudioDocuments
|
|
tags: [Documents]
|
|
summary: List working copies
|
|
description: |
|
|
Case/Reference/OpenQuestion/ProjectDecision은 서로 다른 table/aggregate에
|
|
존재한다. 공통 CRUD repository를 만들지 않고 query side에서 union projection을
|
|
구성한다(`StudioDocumentQueryService`).
|
|
|
|
`cursor`는 normalized filter/sort와 결합된 opaque 값이다. 필터가 달라진
|
|
cursor 재사용은 거절한다.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/Query" }
|
|
- { $ref: "#/components/parameters/DocumentKind" }
|
|
- { $ref: "#/components/parameters/PublicationStatus" }
|
|
- { $ref: "#/components/parameters/NextAction" }
|
|
- { $ref: "#/components/parameters/ProjectId" }
|
|
- { $ref: "#/components/parameters/DocumentSort" }
|
|
- { $ref: "#/components/parameters/Cursor" }
|
|
- { $ref: "#/components/parameters/Limit" }
|
|
responses:
|
|
"200": { description: Working-copy cursor page, content: { application/json: { schema: { $ref: "#/components/schemas/DocumentPageEnvelope" } } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"422": { $ref: "#/components/responses/RequestValidationFailed" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
post:
|
|
operationId: createStudioDocument
|
|
tags: [Documents]
|
|
summary: Create a working copy
|
|
description: 불완전한 초안도 생성할 수 있다. 생성은 Public Projection을 변경하지 않는다.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/IdempotencyKey" }
|
|
- { $ref: "#/components/parameters/CsrfToken" }
|
|
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreateDocumentInput" } } } }
|
|
responses:
|
|
"201":
|
|
description: Created working copy
|
|
headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } }
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/WorkingCopyEnvelope" } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"409": { $ref: "#/components/responses/CommandConflict" }
|
|
"422": { $ref: "#/components/responses/RequestValidationFailed" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/documents/{documentId}:
|
|
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
|
|
get:
|
|
operationId: getStudioDocument
|
|
tags: [Documents]
|
|
summary: Get a working copy and its current state
|
|
responses:
|
|
"200": { description: Working-copy detail, content: { application/json: { schema: { $ref: "#/components/schemas/WorkingCopyDetailEnvelope" } } } }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"404": { $ref: "#/components/responses/DocumentNotFound" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
put:
|
|
operationId: saveStudioDocument
|
|
tags: [Documents]
|
|
summary: Save a full working copy
|
|
description: |
|
|
편집 가능한 content field만 저장한다. lifecycle 전이는 명시적인 Domain
|
|
Action이 담당한다. 저장은 Public Projection을 변경하지 않는다.
|
|
|
|
`expectedVersion` 불일치는 `VERSION_CONFLICT`이며 응답 `error.details`
|
|
(`VersionConflictDetails`)의 `latestDocument`로 현재 상태를 함께 제공한다.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/IdempotencyKey" }
|
|
- { $ref: "#/components/parameters/CsrfToken" }
|
|
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/SaveDocumentCommand" } } } }
|
|
responses:
|
|
"200":
|
|
description: Saved working-copy detail
|
|
headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } }
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/WorkingCopyDetailEnvelope" } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"404": { $ref: "#/components/responses/DocumentNotFound" }
|
|
"409": { $ref: "#/components/responses/CommandConflict" }
|
|
"422": { $ref: "#/components/responses/RequestValidationFailed" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/documents/{documentId}/validate:
|
|
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
|
|
post:
|
|
operationId: validateStudioDocument
|
|
tags: [Validation]
|
|
summary: Validate a saved working copy
|
|
description: |
|
|
단순 request validation이 아니다. 다음 체인을 모두 수행한다.
|
|
|
|
```text
|
|
Schema/Input Validation
|
|
→ 유형별 Domain Validation
|
|
→ 관계/Project/Topic 존재 검증
|
|
→ Asset READY 검증
|
|
→ Slug/Route 충돌 검증
|
|
→ Publication Validation
|
|
```
|
|
|
|
결과는 일급 artifact인 `ValidationReport`로 영속되며 `validationId`로
|
|
`createStudioPreview`와 `publishStudioDocument`가 이를 참조한다.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/IdempotencyKey" }
|
|
- { $ref: "#/components/parameters/CsrfToken" }
|
|
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ValidateDocumentCommand" } } } }
|
|
responses:
|
|
"200":
|
|
description: Validation report
|
|
headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } }
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/ValidationReportEnvelope" } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"404": { $ref: "#/components/responses/DocumentNotFound" }
|
|
"409": { $ref: "#/components/responses/CommandConflict" }
|
|
"422": { $ref: "#/components/responses/RequestValidationFailed" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/documents/{documentId}/preview:
|
|
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
|
|
get:
|
|
operationId: getCurrentStudioPreview
|
|
tags: [Preview]
|
|
summary: Get the latest preview and its computed state
|
|
description: |
|
|
Preview 상태(`CURRENT`/`STALE`/`EXPIRED`)는 서버가 계산한다.
|
|
anonymous `/preview/{token}` 계약은 폐기되었다. Preview는 인증된
|
|
Studio API로만 조회한다.
|
|
responses:
|
|
"200": { description: Preview detail, content: { application/json: { schema: { $ref: "#/components/schemas/PreviewDetailEnvelope" } } } }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"404": { $ref: "#/components/responses/PreviewNotFound" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
post:
|
|
operationId: createStudioPreview
|
|
tags: [Preview]
|
|
summary: Create a public-layout preview
|
|
description: |
|
|
저장된 working version + validationId + dependency revision을 묶어
|
|
`PublicRenderModel` snapshot을 만든다. Preview는 Public과 동일한
|
|
semantic renderer와 Asset resolver를 사용한다.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/IdempotencyKey" }
|
|
- { $ref: "#/components/parameters/CsrfToken" }
|
|
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreatePreviewCommand" } } } }
|
|
responses:
|
|
"201":
|
|
description: Created preview
|
|
headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } }
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/PublicPreviewEnvelope" } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"404": { $ref: "#/components/responses/DocumentNotFound" }
|
|
"409": { $ref: "#/components/responses/CommandConflict" }
|
|
"422": { $ref: "#/components/responses/PreviewRejected" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/documents/{documentId}/publish:
|
|
parameters: [{ $ref: "#/components/parameters/DocumentId" }]
|
|
post:
|
|
operationId: publishStudioDocument
|
|
tags: [Publication]
|
|
summary: Publish or republish a validated preview
|
|
description: |
|
|
하나의 transaction으로 다음을 수행한다.
|
|
|
|
```text
|
|
1. source version lock/check
|
|
2. validationId / current dependency revision 검증
|
|
3. previewId / current dependency revision 검증
|
|
4. warning acknowledgement 검증
|
|
5. 유형별 publication validation
|
|
6. Publication Event 생성 (PUBLISHED | REPUBLISHED)
|
|
7. Publication Snapshot 생성 (immutable)
|
|
8. public_resource_projection 교체
|
|
9. public_route 교체 / alias 처리
|
|
10. public relation/tag/project projection 교체
|
|
11. published Asset reference 교체
|
|
12. Publication Aggregate 갱신
|
|
13. commit
|
|
```
|
|
|
|
재시도가 중복 Publication Event를 만들면 안 된다. `Idempotency-Key`로
|
|
최초 결과를 재생한다.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/IdempotencyKey" }
|
|
- { $ref: "#/components/parameters/CsrfToken" }
|
|
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PublishDocumentCommand" } } } }
|
|
responses:
|
|
"200":
|
|
description: Publication aggregate and immutable event
|
|
headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } }
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/PublishResultEnvelope" } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"404": { $ref: "#/components/responses/DocumentNotFound" }
|
|
"409": { $ref: "#/components/responses/CommandConflict" }
|
|
"422": { $ref: "#/components/responses/PublishRejected" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/publications:
|
|
get:
|
|
operationId: listStudioPublications
|
|
tags: [Publication]
|
|
summary: List immutable publication events
|
|
description: Events are ordered by occurredAt DESC, then publicationEventId.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/Query" }
|
|
- { $ref: "#/components/parameters/PublicationEventType" }
|
|
- { $ref: "#/components/parameters/Cursor" }
|
|
- { $ref: "#/components/parameters/Limit" }
|
|
responses:
|
|
"200": { description: Publication cursor page, content: { application/json: { schema: { $ref: "#/components/schemas/PublicationPageEnvelope" } } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"422": { $ref: "#/components/responses/RequestValidationFailed" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/publications/{publicationId}/unpublish:
|
|
parameters: [{ $ref: "#/components/parameters/PublicationId" }]
|
|
post:
|
|
operationId: unpublishStudioPublication
|
|
tags: [Publication]
|
|
summary: Unpublish the current publication
|
|
description: |
|
|
`UNPUBLISHED` Event를 생성하고 current projection을 `WITHDRAWN`으로 바꾼다.
|
|
canonical route ownership/history는 보존한다.
|
|
과거 Snapshot은 삭제하거나 재계산하지 않는다.
|
|
|
|
Backend 내부에서 working copy가 `DRAFT`로 되돌아가는 lifecycle 전이는
|
|
유지하되 이 API로 직접 노출하지 않는다. 응답은
|
|
`publicationStatus=UNPUBLISHED`와 다음 `nextAction`으로 표현한다.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/IdempotencyKey" }
|
|
- { $ref: "#/components/parameters/CsrfToken" }
|
|
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/UnpublishCommand" } } } }
|
|
responses:
|
|
"200":
|
|
description: Updated publication aggregate and event
|
|
headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } }
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/PublishResultEnvelope" } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"404": { $ref: "#/components/responses/PublicationNotFound" }
|
|
"409": { $ref: "#/components/responses/CommandConflict" }
|
|
"422": { $ref: "#/components/responses/RequestValidationFailed" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/publications/{publicationEventId}/preview:
|
|
parameters: [{ $ref: "#/components/parameters/PublicationEventId" }]
|
|
get:
|
|
operationId: getStudioPublicationSnapshot
|
|
tags: [Publication]
|
|
summary: Get an immutable publication snapshot
|
|
description: |
|
|
`PUBLISHED`/`REPUBLISHED` Event 시점에 저장된 불변 `PublicRenderModel`을
|
|
반환한다. 현재 Working Copy나 현재 Projection에서 재계산하지 않는다.
|
|
|
|
`UNPUBLISHED` Event는 자체 snapshot을 갖지 않는다. 이 경우
|
|
`sourcePublishedEventId`가 가리키는 마지막 공개 Snapshot을 사용한다.
|
|
responses:
|
|
"200": { description: Publication snapshot, content: { application/json: { schema: { $ref: "#/components/schemas/PublicationSnapshotEnvelope" } } } }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"404": { $ref: "#/components/responses/PublicationSnapshotNotFound" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/catalog:
|
|
get:
|
|
operationId: listStudioCatalog
|
|
tags: [Catalog]
|
|
summary: List catalog entries for editor pickers
|
|
description: |
|
|
Editor picker가 사용하는 통합 read API다. Backend source는 다음과 같다.
|
|
|
|
```text
|
|
TOPIC → topic capability
|
|
PROJECT → project capability
|
|
RELATION → Case/Reference/Question/ProjectDecision 중 연결 가능한 대상
|
|
EVIDENCE → resolution/decision 근거로 사용할 수 있는 공개 기록
|
|
```
|
|
|
|
Asset 검색/업로드/metadata는 Catalog가 아니라 `/api/v1/studio/assets`가
|
|
소유한다.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/CatalogType" }
|
|
- { $ref: "#/components/parameters/Query" }
|
|
- { $ref: "#/components/parameters/Cursor" }
|
|
- { $ref: "#/components/parameters/Limit" }
|
|
responses:
|
|
"200": { description: Catalog cursor page, content: { application/json: { schema: { $ref: "#/components/schemas/CatalogPageEnvelope" } } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"422": { $ref: "#/components/responses/RequestValidationFailed" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/assets:
|
|
get:
|
|
operationId: listStudioAssets
|
|
tags: [Assets]
|
|
summary: List assets for the library and the editor picker
|
|
parameters:
|
|
- { $ref: "#/components/parameters/Query" }
|
|
- { $ref: "#/components/parameters/AssetKindFilter" }
|
|
- { $ref: "#/components/parameters/AssetStatusFilter" }
|
|
- { $ref: "#/components/parameters/Cursor" }
|
|
- { $ref: "#/components/parameters/Limit" }
|
|
responses:
|
|
"200": { description: Asset cursor page, content: { application/json: { schema: { $ref: "#/components/schemas/AssetPageEnvelope" } } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"422": { $ref: "#/components/responses/RequestValidationFailed" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
post:
|
|
operationId: uploadStudioAsset
|
|
tags: [Assets]
|
|
summary: Upload an image, diagram, or attachment
|
|
description: |
|
|
MVP 업로드는 `multipart/form-data`를 사용한다. 향후 presigned/resumable로
|
|
교체하더라도 같은 Asset port 뒤에서 처리한다.
|
|
|
|
Backend는 확장자를 신뢰하지 않는다. MIME/type 검증과 크기 제한을 적용하고
|
|
검사에 실패하면 `REJECTED` 또는 `QUARANTINED`로 저장한다. `READY` 전환은
|
|
검증 완료 후에만 일어난다.
|
|
|
|
`image/svg+xml`을 지원하지만 업로드 원문을 HTML에 inline하지 않는다.
|
|
Public/Preview는 검증된 delivery URL을 `<img src>`로 렌더링한다.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/IdempotencyKey" }
|
|
- { $ref: "#/components/parameters/CsrfToken" }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema: { $ref: "#/components/schemas/AssetUploadForm" }
|
|
encoding:
|
|
file: { contentType: "image/png, image/jpeg, image/webp, image/gif, image/svg+xml, application/pdf" }
|
|
responses:
|
|
"201":
|
|
description: Stored asset
|
|
headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } }
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/AssetEnvelope" } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"409": { $ref: "#/components/responses/CommandConflict" }
|
|
"413": { $ref: "#/components/responses/PayloadTooLarge" }
|
|
"415": { $ref: "#/components/responses/UnsupportedMediaType" }
|
|
"422": { $ref: "#/components/responses/RequestValidationFailed" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
/api/v1/studio/assets/{assetId}:
|
|
parameters: [{ $ref: "#/components/parameters/AssetId" }]
|
|
get:
|
|
operationId: getStudioAsset
|
|
tags: [Assets]
|
|
summary: Get an asset with its usage
|
|
responses:
|
|
"200": { description: Asset detail, content: { application/json: { schema: { $ref: "#/components/schemas/AssetDetailEnvelope" } } } }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"404": { $ref: "#/components/responses/AssetNotFound" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
put:
|
|
operationId: updateStudioAsset
|
|
tags: [Assets]
|
|
summary: Update asset metadata
|
|
description: |
|
|
`altText`, `decorative`, `kind`와 `READY ↔ ARCHIVED` 전환만 허용한다.
|
|
`REJECTED`/`QUARANTINED`는 서버 검증 결과이며 클라이언트가 지정할 수 없다.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/IdempotencyKey" }
|
|
- { $ref: "#/components/parameters/CsrfToken" }
|
|
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/UpdateAssetCommand" } } } }
|
|
responses:
|
|
"200":
|
|
description: Updated asset
|
|
headers: { Idempotency-Replayed: { $ref: "#/components/headers/IdempotencyReplayed" } }
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/AssetEnvelope" } } }
|
|
"400": { $ref: "#/components/responses/MalformedRequest" }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"404": { $ref: "#/components/responses/AssetNotFound" }
|
|
"409": { $ref: "#/components/responses/CommandConflict" }
|
|
"422": { $ref: "#/components/responses/AssetRejected" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
delete:
|
|
operationId: deleteStudioAsset
|
|
tags: [Assets]
|
|
summary: Delete an unused asset
|
|
description: |
|
|
공개 이력이 있는 Asset과 사용 중인 Asset은 hard delete하지 않는다.
|
|
두 경우 모두 `ASSET_IN_USE`로 거절하고 `ARCHIVED` 전환을 사용한다.
|
|
parameters:
|
|
- { $ref: "#/components/parameters/IdempotencyKey" }
|
|
- { $ref: "#/components/parameters/CsrfToken" }
|
|
responses:
|
|
"204": { description: Deleted }
|
|
"401": { $ref: "#/components/responses/AuthenticationRequired" }
|
|
"403": { $ref: "#/components/responses/AccessDenied" }
|
|
"404": { $ref: "#/components/responses/AssetNotFound" }
|
|
"409": { $ref: "#/components/responses/CommandConflict" }
|
|
"503": { $ref: "#/components/responses/StudioUnavailable" }
|
|
|
|
components:
|
|
securitySchemes:
|
|
sessionCookie:
|
|
type: apiKey
|
|
in: cookie
|
|
name: TECHLOG_SESSION
|
|
description: |
|
|
Backend Session Cookie. `TECH_LOG_ADMIN` 권한이 필요하다.
|
|
mutating operation은 추가로 `X-CSRF-TOKEN` header를 요구한다.
|
|
|
|
parameters:
|
|
DocumentId: { name: documentId, in: path, required: true, schema: { type: string, format: uuid } }
|
|
PublicationId: { name: publicationId, in: path, required: true, schema: { type: string, format: uuid } }
|
|
PublicationEventId: { name: publicationEventId, in: path, required: true, schema: { type: string, format: uuid } }
|
|
AssetId: { name: assetId, in: path, required: true, schema: { type: string, format: uuid } }
|
|
IdempotencyKey:
|
|
name: Idempotency-Key
|
|
in: header
|
|
required: true
|
|
description: |
|
|
동일 key + 동일 normalized request는 최초 결과를 재생한다.
|
|
동일 key + 다른 request는 `IDEMPOTENCY_KEY_REUSED` conflict다.
|
|
key 원문을 로그에 남길 때 요청 본문의 민감정보가 함께 기록되지 않게 한다.
|
|
schema: { type: string, minLength: 1, maxLength: 200 }
|
|
CsrfToken:
|
|
name: X-CSRF-TOKEN
|
|
in: header
|
|
required: true
|
|
description: "`getStudioSession`이 발급한 CSRF 토큰."
|
|
schema: { type: string, minLength: 1, maxLength: 200 }
|
|
Query: { name: q, in: query, description: Free-text query normalized as part of the opaque cursor, schema: { type: string, maxLength: 100 } }
|
|
Cursor: { name: cursor, in: query, description: Opaque cursor bound to normalized filters and sort, schema: { type: string, minLength: 1, maxLength: 2000 } }
|
|
Limit: { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
|
|
DocumentKind: { name: kind, in: query, schema: { $ref: "#/components/schemas/RecordKind" } }
|
|
PublicationStatus: { name: publicationStatus, in: query, schema: { $ref: "#/components/schemas/PublicationStatus" } }
|
|
NextAction: { name: nextAction, in: query, schema: { $ref: "#/components/schemas/NextAction" } }
|
|
ProjectId: { name: projectId, in: query, schema: { type: string, format: uuid } }
|
|
DocumentSort: { name: sort, in: query, schema: { type: string, enum: [UPDATED_DESC, UPDATED_ASC, TITLE_ASC], default: UPDATED_DESC } }
|
|
PublicationEventType: { name: type, in: query, schema: { $ref: "#/components/schemas/PublicationEventType" } }
|
|
CatalogType: { name: type, in: query, required: true, schema: { $ref: "#/components/schemas/CatalogEntryType" } }
|
|
AssetKindFilter: { name: kind, in: query, schema: { $ref: "#/components/schemas/AssetKind" } }
|
|
AssetStatusFilter: { name: managementStatus, in: query, schema: { $ref: "#/components/schemas/AssetManagementStatus" } }
|
|
|
|
headers:
|
|
IdempotencyReplayed: { description: True when the original result was replayed, schema: { type: boolean } }
|
|
|
|
responses:
|
|
MalformedRequest: { description: Malformed request, x-error-codes: [REQUEST_VALIDATION_FAILED], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
AuthenticationRequired: { description: Authentication required, x-error-codes: [AUTHENTICATION_REQUIRED], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
AccessDenied: { description: Studio access denied, x-error-codes: [STUDIO_ACCESS_DENIED], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
DocumentNotFound: { description: Document not found, x-error-codes: [DOCUMENT_NOT_FOUND], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
PreviewNotFound: { description: Document or preview not found, x-error-codes: [DOCUMENT_NOT_FOUND, PREVIEW_NOT_FOUND], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
PublicationNotFound: { description: Publication not found, x-error-codes: [PUBLICATION_NOT_FOUND], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
PublicationSnapshotNotFound: { description: Publication event or snapshot not found, x-error-codes: [PUBLICATION_EVENT_NOT_FOUND, PUBLICATION_SNAPSHOT_NOT_FOUND], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
AssetNotFound: { description: Asset not found, x-error-codes: [ASSET_NOT_FOUND], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
CommandConflict:
|
|
description: |
|
|
Command conflicts with current state, freshness, or idempotency.
|
|
|
|
`ASSET_IN_USE`는 사용 중이거나 공개 이력이 있는 Asset의 hard delete 시도다.
|
|
x-error-codes: [VERSION_CONFLICT, PUBLICATION_CONFLICT, VALIDATION_STALE, PREVIEW_STALE, PREVIEW_EXPIRED, IDEMPOTENCY_KEY_REUSED, ASSET_IN_USE]
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } }
|
|
RequestValidationFailed: { description: Request validation failed, x-error-codes: [REQUEST_VALIDATION_FAILED], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
PreviewRejected:
|
|
description: Preview 생성이 도메인 규칙으로 거절되었다
|
|
x-error-codes: [REQUEST_VALIDATION_FAILED, DOCUMENT_VALIDATION_FAILED, ASSET_NOT_READY, ASSET_QUARANTINED]
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } }
|
|
PublishRejected:
|
|
description: |
|
|
Publication validation이 실패했다.
|
|
|
|
`WARNING_ACKNOWLEDGEMENT_REQUIRED`는 `acknowledgedWarningCodes`가
|
|
현재 Validation의 WARNING 집합을 덮지 못한 경우다.
|
|
x-error-codes: [REQUEST_VALIDATION_FAILED, DOCUMENT_VALIDATION_FAILED, WARNING_ACKNOWLEDGEMENT_REQUIRED, ASSET_NOT_READY, ASSET_QUARANTINED]
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } }
|
|
AssetRejected:
|
|
description: Asset metadata 변경이 거절되었다
|
|
x-error-codes: [REQUEST_VALIDATION_FAILED, ASSET_NOT_READY, ASSET_QUARANTINED]
|
|
content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } }
|
|
PayloadTooLarge: { description: Upload exceeds the configured size limit, x-error-codes: [PAYLOAD_TOO_LARGE], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
UnsupportedMediaType: { description: Unsupported media type, x-error-codes: [UNSUPPORTED_MEDIA_TYPE], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
StudioUnavailable: { description: Studio unavailable, x-error-codes: [STUDIO_UNAVAILABLE], content: { application/json: { schema: { $ref: "#/components/schemas/ErrorEnvelope" } } } }
|
|
|
|
schemas:
|
|
# ------------------------------------------------------------- envelope
|
|
# wire format은 봉투다 (ADR-006). payload 스키마는 그대로 두고
|
|
# 응답만 <Payload>Envelope으로 감싼다.
|
|
ResponseMeta:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [requestId, traceId]
|
|
properties:
|
|
requestId: { type: string, minLength: 1, maxLength: 200 }
|
|
traceId: { type: string, minLength: 1, maxLength: 200 }
|
|
correlationId: { type: [string, "null"], maxLength: 200 }
|
|
page: { type: ["object", "null"], additionalProperties: true, description: "Studio는 body 안 cursor 페이지네이션을 쓰므로 항상 null이다. 백엔드 템플릿의 ResponseMeta record가 이 필드를 직렬화한다." }
|
|
ApiError:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [code, category, message, retryable]
|
|
properties:
|
|
code:
|
|
type: string
|
|
enum: [AUTHENTICATION_REQUIRED, STUDIO_ACCESS_DENIED, DOCUMENT_NOT_FOUND, VERSION_CONFLICT,
|
|
REQUEST_VALIDATION_FAILED, DOCUMENT_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]
|
|
category:
|
|
type: string
|
|
enum: [VALIDATION, AUTH, AUTHZ, NOT_FOUND, CONFLICT, RATE_LIMIT,
|
|
TRANSIENT_DEPENDENCY, PERMANENT_DEPENDENCY, DATA_INTEGRITY, INTERNAL]
|
|
message: { type: string, minLength: 1, maxLength: 5000 }
|
|
retryable: { type: boolean }
|
|
details:
|
|
oneOf:
|
|
- $ref: "#/components/schemas/ValidationErrorDetails"
|
|
- $ref: "#/components/schemas/VersionConflictDetails"
|
|
- $ref: "#/components/schemas/PublicationConflictDetails"
|
|
- type: "null"
|
|
ErrorEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, error, meta]
|
|
properties:
|
|
success: { type: boolean, const: false }
|
|
error: { $ref: "#/components/schemas/ApiError" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
ValidationErrorDetails:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [fieldErrors]
|
|
properties:
|
|
fieldErrors: { type: array, maxItems: 200, items: { $ref: "#/components/schemas/FieldError" } }
|
|
VersionConflictDetails:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [latestDocument]
|
|
properties:
|
|
latestDocument: { $ref: "#/components/schemas/WorkingCopyDetail" }
|
|
conflictingFields:
|
|
type: array
|
|
uniqueItems: true
|
|
maxItems: 200
|
|
items: { type: string, pattern: "^(?:/(?:[^~/]|~0|~1)*)*$" }
|
|
PublicationConflictDetails:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [latestPublication]
|
|
properties:
|
|
latestPublication: { $ref: "#/components/schemas/PublicationAggregate" }
|
|
StudioSessionEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/StudioSession" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
StudioDashboardEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/StudioDashboard" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
DocumentPageEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/DocumentPage" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
WorkingCopyDetailEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/WorkingCopyDetail" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
WorkingCopyEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/WorkingCopy" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
ValidationReportEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/ValidationReport" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
PreviewDetailEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/PreviewDetail" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
PublicPreviewEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/PublicPreview" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
PublishResultEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/PublishResult" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
PublicationPageEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/PublicationPage" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
PublicationSnapshotEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/PublicationSnapshot" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
CatalogPageEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/CatalogPage" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
AssetPageEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/AssetPage" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
AssetDetailEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/AssetDetail" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
AssetEnvelope:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [success, data, meta]
|
|
properties:
|
|
success: { type: boolean, const: true }
|
|
data: { $ref: "#/components/schemas/Asset" }
|
|
meta: { $ref: "#/components/schemas/ResponseMeta" }
|
|
# ---------------------------------------------------------------- session
|
|
StudioSession:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [authenticated, displayName, roles, csrfToken, csrfHeaderName]
|
|
properties:
|
|
authenticated: { type: boolean }
|
|
displayName: { type: string, minLength: 1, maxLength: 120 }
|
|
roles: { type: array, uniqueItems: true, maxItems: 20, items: { type: string, minLength: 1, maxLength: 60 } }
|
|
csrfToken: { type: string, minLength: 1, maxLength: 200 }
|
|
csrfHeaderName: { type: string, const: X-CSRF-TOKEN }
|
|
|
|
# ------------------------------------------------------------ enumerations
|
|
RecordKind:
|
|
type: string
|
|
enum: [CASE, REFERENCE, QUESTION, PROJECT_DECISION]
|
|
description: |
|
|
Studio 편집 대상 유형. API projection discriminator이며 Domain Aggregate가
|
|
아니다. `PROJECT_DECISION`은 `ProjectDecision` capability로 dispatch된다.
|
|
PublicationStatus: { type: string, enum: [NEVER_PUBLISHED, PUBLISHED, UNPUBLISHED] }
|
|
NextAction:
|
|
type: string
|
|
enum: [CONTINUE_EDITING, VALIDATE, FIX_VALIDATION, CREATE_PREVIEW, PUBLISH, NONE]
|
|
description: |
|
|
서버가 계산하는 Studio projection이다. Domain state machine이 아니며
|
|
`workflow_status` 같은 domain 컬럼에 저장하지 않는다.
|
|
|
|
# --------------------------------------------------------- shared fragments
|
|
RelationInput:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, targetId, reason, order]
|
|
properties:
|
|
id: { type: [string, "null"], format: uuid }
|
|
targetId: { type: [string, "null"], format: uuid }
|
|
reason: { type: string, maxLength: 100000 }
|
|
order: { type: integer, minimum: 0 }
|
|
Relation:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, targetId, reason, order]
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
targetId: { type: string, format: uuid }
|
|
reason: { type: string, maxLength: 100000 }
|
|
order: { type: integer, minimum: 0 }
|
|
OrderedText:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, text, order]
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
text: { type: string, minLength: 1, maxLength: 100000 }
|
|
order: { type: integer, minimum: 0 }
|
|
ReferenceRule:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, title, body, order]
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
title: { type: string, minLength: 1, maxLength: 120 }
|
|
body: { type: string, minLength: 1, maxLength: 100000 }
|
|
order: { type: integer, minimum: 0 }
|
|
QuestionOption:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, title, description, order]
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
title: { type: string, minLength: 1, maxLength: 120 }
|
|
description: { type: string, maxLength: 100000 }
|
|
order: { type: integer, minimum: 0 }
|
|
QuestionResolution:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [summary, evidenceTargetId, linkLabel]
|
|
properties:
|
|
summary: { type: string, maxLength: 100000 }
|
|
evidenceTargetId: { type: [string, "null"], format: uuid }
|
|
linkLabel: { type: string, maxLength: 120 }
|
|
|
|
# -------------------------------------------------------- working copy input
|
|
WorkingCopyInputBase:
|
|
type: object
|
|
description: |
|
|
불완전한 초안도 저장할 수 있어야 하므로 필드는 required이되 빈 값과 null을
|
|
허용한다. 게시 가능 여부는 `validateStudioDocument`가 판단한다.
|
|
required: [kind, title, slug, summary, topicId, projectId, relations]
|
|
properties:
|
|
kind: { $ref: "#/components/schemas/RecordKind" }
|
|
title: { type: string, maxLength: 120 }
|
|
slug:
|
|
oneOf:
|
|
- { type: string, const: "" }
|
|
- { type: string, minLength: 3, maxLength: 100, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" }
|
|
summary: { type: string, maxLength: 300 }
|
|
topicId: { type: [string, "null"], format: uuid }
|
|
projectId:
|
|
type: [string, "null"]
|
|
format: uuid
|
|
description: "`kind=PROJECT_DECISION`은 게시 시점에 non-null이어야 한다. 저장 시점에는 강제하지 않는다."
|
|
relations: { type: array, maxItems: 20, items: { $ref: "#/components/schemas/RelationInput" } }
|
|
CaseInput:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/WorkingCopyInputBase" }
|
|
- type: object
|
|
required: [kind, problem, conclusion, environment, reproduction, lastVerifiedOn, bodyMarkdown]
|
|
properties:
|
|
kind: { type: string, enum: [CASE] }
|
|
problem: { type: string, maxLength: 100000 }
|
|
conclusion: { type: string, maxLength: 100000 }
|
|
environment: { type: string, maxLength: 100000 }
|
|
reproduction: { type: string, maxLength: 100000 }
|
|
lastVerifiedOn: { type: [string, "null"], format: date }
|
|
bodyMarkdown:
|
|
type: string
|
|
maxLength: 100000
|
|
description: |
|
|
Markdown 원문. Asset은 `:::evidence key="<assetKey>"` directive로
|
|
참조한다. object storage URL을 원문에 직접 저장하지 않는다.
|
|
ReferenceInput:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/WorkingCopyInputBase" }
|
|
- type: object
|
|
required: [kind, purpose, rules, applyWhen, exceptions, examples, verifiedOn]
|
|
properties:
|
|
kind: { type: string, enum: [REFERENCE] }
|
|
purpose: { type: string, maxLength: 100000 }
|
|
rules: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/ReferenceRule" } }
|
|
applyWhen: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
exceptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
examples: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
verifiedOn: { type: [string, "null"], format: date }
|
|
QuestionInput:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/WorkingCopyInputBase" }
|
|
- type: object
|
|
# `resolution` 은 여기 없다. 미해결 질문에는 해결 내용이 없고, 그것을 required 로 두면
|
|
# Java 생성기가 nullable 여부와 무관하게 @NotNull 을 찍는다 — oneOf 로 적은 null 을 그
|
|
# 생성기는 읽지 못한다. 실제로 그래서 Question 작업본을 만들 수 없었다: 프론트가 계약대로
|
|
# resolution: null 을 보냈고 백엔드가 422 로 거절했다. 같은 목록의 `questionStatus` 가
|
|
# 통과하는 것은 그쪽이 nullability 를 `type: [string, "null"]` 로 적었기 때문이다.
|
|
required: [kind, questionStatus, facts, assumptions, unknowns, constraints, options, nextValidation]
|
|
properties:
|
|
kind: { type: string, enum: [QUESTION] }
|
|
questionStatus:
|
|
type: [string, "null"]
|
|
enum: [OPEN, RESOLVED, null]
|
|
description: |
|
|
Backend Inquiry lifecycle의 축약 view다.
|
|
`OPEN`은 Domain의 `OPEN`/`INVESTIGATING`/`PAUSED`를 모두 대표하므로
|
|
저장 시 Domain 상태를 `OPEN`으로 덮어쓰지 않는다.
|
|
`RESOLVED`로의 변경만 Resolve command로 해석하며 기존 resolve
|
|
invariant를 통과해야 한다.
|
|
facts: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
assumptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
unknowns: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
constraints: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
options: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/QuestionOption" } }
|
|
nextValidation: { type: string, maxLength: 100000 }
|
|
resolution:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/QuestionResolution" }
|
|
- { type: "null" }
|
|
ProjectDecisionInput:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/WorkingCopyInputBase" }
|
|
- type: object
|
|
required: [kind, decisionStatus, decidedOn, statement, rationale, consequences]
|
|
properties:
|
|
kind: { type: string, enum: [PROJECT_DECISION] }
|
|
decisionStatus:
|
|
type: [string, "null"]
|
|
enum: [PROPOSED, ADOPTED, null]
|
|
description: |
|
|
UI 용어다. Backend Domain의 `ACCEPTED`/`ADOPTED` 명칭이 다르면
|
|
mapper에서 변환하고 Domain enum을 UI 용어 때문에 변경하지 않는다.
|
|
`supersede`/`reject`는 secondary management 계약이 소유한다.
|
|
decidedOn: { type: [string, "null"], format: date }
|
|
statement: { type: string, maxLength: 100000 }
|
|
rationale: { type: string, maxLength: 100000 }
|
|
consequences: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
WorkingCopyInput:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/CaseInput" }
|
|
- { $ref: "#/components/schemas/ReferenceInput" }
|
|
- { $ref: "#/components/schemas/QuestionInput" }
|
|
- { $ref: "#/components/schemas/ProjectDecisionInput" }
|
|
discriminator:
|
|
propertyName: kind
|
|
mapping:
|
|
CASE: "#/components/schemas/CaseInput"
|
|
REFERENCE: "#/components/schemas/ReferenceInput"
|
|
QUESTION: "#/components/schemas/QuestionInput"
|
|
PROJECT_DECISION: "#/components/schemas/ProjectDecisionInput"
|
|
|
|
# ------------------------------------------------------- working copy output
|
|
WorkingCopyBase:
|
|
allOf:
|
|
- { $ref: "#/components/schemas/WorkingCopyInputBase" }
|
|
- type: object
|
|
required: [id, version, relations, updatedAt]
|
|
properties:
|
|
id:
|
|
type: string
|
|
format: uuid
|
|
description: source aggregate id를 그대로 사용한다. 별도 Studio surrogate id를 만들지 않는다.
|
|
version: { type: integer, minimum: 1 }
|
|
relations: { type: array, maxItems: 20, items: { $ref: "#/components/schemas/Relation" } }
|
|
updatedAt: { type: string, format: date-time }
|
|
CaseWorkingCopy:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/WorkingCopyBase" }
|
|
- type: object
|
|
required: [kind, problem, conclusion, environment, reproduction, lastVerifiedOn, bodyMarkdown]
|
|
properties:
|
|
kind: { type: string, enum: [CASE] }
|
|
problem: { type: string, maxLength: 100000 }
|
|
conclusion: { type: string, maxLength: 100000 }
|
|
environment: { type: string, maxLength: 100000 }
|
|
reproduction: { type: string, maxLength: 100000 }
|
|
lastVerifiedOn: { type: [string, "null"], format: date }
|
|
bodyMarkdown: { type: string, maxLength: 100000 }
|
|
ReferenceWorkingCopy:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/WorkingCopyBase" }
|
|
- type: object
|
|
required: [kind, purpose, rules, applyWhen, exceptions, examples, verifiedOn]
|
|
properties:
|
|
kind: { type: string, enum: [REFERENCE] }
|
|
purpose: { type: string, maxLength: 100000 }
|
|
rules: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/ReferenceRule" } }
|
|
applyWhen: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
exceptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
examples: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
verifiedOn: { type: [string, "null"], format: date }
|
|
QuestionWorkingCopy:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/WorkingCopyBase" }
|
|
- type: object
|
|
required: [kind, questionStatus, facts, assumptions, unknowns, constraints, options, nextValidation, resolution]
|
|
properties:
|
|
kind: { type: string, enum: [QUESTION] }
|
|
questionStatus: { type: [string, "null"], enum: [OPEN, RESOLVED, null] }
|
|
facts: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
assumptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
unknowns: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
constraints: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
options: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/QuestionOption" } }
|
|
nextValidation: { type: string, maxLength: 100000 }
|
|
resolution:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/QuestionResolution" }
|
|
- { type: "null" }
|
|
ProjectDecisionWorkingCopy:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/WorkingCopyBase" }
|
|
- type: object
|
|
required: [kind, decisionStatus, decidedOn, statement, rationale, consequences]
|
|
properties:
|
|
kind: { type: string, enum: [PROJECT_DECISION] }
|
|
decisionStatus: { type: [string, "null"], enum: [PROPOSED, ADOPTED, null] }
|
|
decidedOn: { type: [string, "null"], format: date }
|
|
statement: { type: string, maxLength: 100000 }
|
|
rationale: { type: string, maxLength: 100000 }
|
|
consequences: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
WorkingCopy:
|
|
description: |
|
|
API union이다. DB에 `working_copy` 범용 테이블을 만들지 않는다.
|
|
|
|
```text
|
|
WorkingCopy
|
|
= CaseWorkingCopy
|
|
| ReferenceWorkingCopy
|
|
| QuestionWorkingCopy
|
|
| ProjectDecisionWorkingCopy
|
|
```
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/CaseWorkingCopy" }
|
|
- { $ref: "#/components/schemas/ReferenceWorkingCopy" }
|
|
- { $ref: "#/components/schemas/QuestionWorkingCopy" }
|
|
- { $ref: "#/components/schemas/ProjectDecisionWorkingCopy" }
|
|
discriminator:
|
|
propertyName: kind
|
|
mapping:
|
|
CASE: "#/components/schemas/CaseWorkingCopy"
|
|
REFERENCE: "#/components/schemas/ReferenceWorkingCopy"
|
|
QUESTION: "#/components/schemas/QuestionWorkingCopy"
|
|
PROJECT_DECISION: "#/components/schemas/ProjectDecisionWorkingCopy"
|
|
|
|
# -------------------------------------------------------------- commands
|
|
CreateDocumentInput: { $ref: "#/components/schemas/WorkingCopyInput" }
|
|
SaveDocumentCommand:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [expectedVersion, document]
|
|
properties:
|
|
expectedVersion: { type: integer, minimum: 1 }
|
|
document: { $ref: "#/components/schemas/WorkingCopyInput" }
|
|
ValidateDocumentCommand:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [expectedVersion]
|
|
properties: { expectedVersion: { type: integer, minimum: 1 } }
|
|
CreatePreviewCommand:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [expectedVersion, validationId]
|
|
properties:
|
|
expectedVersion: { type: integer, minimum: 1 }
|
|
validationId: { type: string, format: uuid }
|
|
PublishDocumentCommand:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [expectedVersion, validationId, previewId, acknowledgedWarningCodes]
|
|
properties:
|
|
expectedVersion: { type: integer, minimum: 1 }
|
|
validationId: { type: string, format: uuid }
|
|
previewId: { type: string, format: uuid }
|
|
acknowledgedWarningCodes:
|
|
type: array
|
|
uniqueItems: true
|
|
maxItems: 200
|
|
items: { type: string, minLength: 1, maxLength: 100 }
|
|
description: 현재 Validation의 WARNING code 집합을 모두 덮지 못하면 `WARNING_ACKNOWLEDGEMENT_REQUIRED`로 거절한다.
|
|
UnpublishCommand:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [expectedPublicationRevision]
|
|
properties: { expectedPublicationRevision: { type: integer, minimum: 1 } }
|
|
|
|
# ------------------------------------------------------------- validation
|
|
ValidationIssue:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [code, severity, path, message]
|
|
properties:
|
|
code: { type: string, minLength: 1, maxLength: 100 }
|
|
severity: { type: string, enum: [ERROR, WARNING] }
|
|
path:
|
|
type: string
|
|
pattern: "^(?:/(?:[^~/]|~0|~1)*)*$"
|
|
description: JSON Pointer to the affected field
|
|
message: { type: string, minLength: 1, maxLength: 1000 }
|
|
ValidationReport:
|
|
type: object
|
|
additionalProperties: false
|
|
description: |
|
|
일급 application artifact다. 실행 결과를 그때그때 반환하고 버리지 않고
|
|
`studio_validation`에 영속한다.
|
|
required: [validationId, documentId, validatedVersion, status, issues, validatedAt, validUntil, dependencyRevision]
|
|
properties:
|
|
validationId: { type: string, format: uuid }
|
|
documentId: { type: string, format: uuid }
|
|
validatedVersion: { type: integer, minimum: 1 }
|
|
status: { type: string, enum: [INVALID, WARNINGS, VALID] }
|
|
issues: { type: array, maxItems: 200, items: { $ref: "#/components/schemas/ValidationIssue" } }
|
|
validatedAt: { type: string, format: date-time }
|
|
validUntil: { type: string, format: date-time }
|
|
dependencyRevision: { $ref: "#/components/schemas/DependencyRevision" }
|
|
DependencyRevision:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
description: |
|
|
검증 결과에 영향을 주는 외부 의존 상태를 대표하는 값이다.
|
|
|
|
```text
|
|
Topic/Project 존재와 publishability
|
|
relation target 상태
|
|
Asset READY/QUARANTINED 상태
|
|
slug/route ownership
|
|
catalog revision
|
|
필요 시 renderer/content-format version
|
|
```
|
|
|
|
모든 테이블의 global counter일 필요는 없다. 검증에 사용한 dependency
|
|
identity/version을 정규화해 hash로 만들 수 있다. Publish 시 동일
|
|
dependency set을 다시 계산해 값이 다르면 `VALIDATION_STALE`로 거절한다.
|
|
|
|
# ------------------------------------------------------- public render model
|
|
DisplayTarget:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, label, publicPath]
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
label: { type: string, minLength: 1, maxLength: 200 }
|
|
publicPath: { type: [string, "null"], maxLength: 500 }
|
|
ResolvedRelation:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, targetId, targetKind, title, publicPath, reason, order]
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
targetId: { type: string, format: uuid }
|
|
targetKind: { type: string, enum: [CASE, REFERENCE, QUESTION, PROJECT, PROJECT_DECISION] }
|
|
title: { type: string, minLength: 1, maxLength: 200 }
|
|
publicPath: { type: [string, "null"], maxLength: 500 }
|
|
reason: { type: string, maxLength: 100000 }
|
|
order: { type: integer, minimum: 0 }
|
|
RenderContext:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [generatedAt, dependencyRevision]
|
|
properties:
|
|
generatedAt: { type: string, format: date-time }
|
|
dependencyRevision: { $ref: "#/components/schemas/DependencyRevision" }
|
|
PublicRenderModelBase:
|
|
type: object
|
|
required: [kind, slug, title, summary, publicPath, topic, project, relations, renderContext]
|
|
properties:
|
|
kind: { $ref: "#/components/schemas/RecordKind" }
|
|
slug: { type: string, minLength: 3, maxLength: 100, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" }
|
|
title: { type: string, minLength: 1, maxLength: 120 }
|
|
summary: { type: string, minLength: 1, maxLength: 300 }
|
|
publicPath: { type: string, minLength: 1, maxLength: 500 }
|
|
topic: { $ref: "#/components/schemas/DisplayTarget" }
|
|
project:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/DisplayTarget" }
|
|
- { type: "null" }
|
|
relations: { type: array, maxItems: 20, items: { $ref: "#/components/schemas/ResolvedRelation" } }
|
|
renderContext: { $ref: "#/components/schemas/RenderContext" }
|
|
InlineText:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [type, text]
|
|
properties:
|
|
type: { type: string, enum: [TEXT] }
|
|
text: { type: string, minLength: 1, maxLength: 100000 }
|
|
InlineContainer:
|
|
type: object
|
|
required: [type, children]
|
|
properties:
|
|
type: { type: string, enum: [EMPHASIS, STRONG] }
|
|
children: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } }
|
|
InlineCode:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [type, code]
|
|
properties:
|
|
type: { type: string, enum: [INLINE_CODE] }
|
|
code: { type: string, minLength: 1, maxLength: 100000 }
|
|
InlineLink:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [type, label, href]
|
|
properties:
|
|
type: { type: string, enum: [LINK] }
|
|
label: { type: string, minLength: 1, maxLength: 100000 }
|
|
href: { type: string, format: uri, maxLength: 2000 }
|
|
InlineStatus:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [type, label, tone]
|
|
properties:
|
|
type: { type: string, enum: [STATUS] }
|
|
label: { type: string, minLength: 1, maxLength: 120 }
|
|
tone: { type: string, enum: [warning, evidence, neutral] }
|
|
Inline:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/InlineText" }
|
|
- { $ref: "#/components/schemas/InlineEmphasis" }
|
|
- { $ref: "#/components/schemas/InlineStrong" }
|
|
- { $ref: "#/components/schemas/InlineCode" }
|
|
- { $ref: "#/components/schemas/InlineLink" }
|
|
- { $ref: "#/components/schemas/InlineStatus" }
|
|
discriminator:
|
|
propertyName: type
|
|
mapping:
|
|
TEXT: "#/components/schemas/InlineText"
|
|
EMPHASIS: "#/components/schemas/InlineEmphasis"
|
|
STRONG: "#/components/schemas/InlineStrong"
|
|
INLINE_CODE: "#/components/schemas/InlineCode"
|
|
LINK: "#/components/schemas/InlineLink"
|
|
STATUS: "#/components/schemas/InlineStatus"
|
|
InlineEmphasis:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/InlineContainer" }
|
|
- { type: object, properties: { type: { type: string, enum: [EMPHASIS] } } }
|
|
InlineStrong:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/InlineContainer" }
|
|
- { type: object, properties: { type: { type: string, enum: [STRONG] } } }
|
|
HeadingBlock:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [type, id, level, content]
|
|
properties:
|
|
type: { type: string, enum: [HEADING] }
|
|
id: { type: string, minLength: 1, maxLength: 200 }
|
|
level: { type: integer, minimum: 2, maximum: 4 }
|
|
content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } }
|
|
ParagraphBlock:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [type, content]
|
|
properties:
|
|
type: { type: string, enum: [PARAGRAPH] }
|
|
content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } }
|
|
BlockquoteBlock:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [type, content]
|
|
properties:
|
|
type: { type: string, enum: [BLOCKQUOTE] }
|
|
content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } }
|
|
ListItem:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, content]
|
|
properties:
|
|
id: { type: string, minLength: 1, maxLength: 200 }
|
|
content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } }
|
|
ListBlockBase:
|
|
type: object
|
|
required: [type, items]
|
|
properties:
|
|
type: { type: string, enum: [UNORDERED_LIST, ORDERED_LIST] }
|
|
items: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/ListItem" } }
|
|
UnorderedListBlock:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/ListBlockBase" }
|
|
- { type: object, properties: { type: { type: string, enum: [UNORDERED_LIST] } } }
|
|
OrderedListBlock:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/ListBlockBase" }
|
|
- { type: object, properties: { type: { type: string, enum: [ORDERED_LIST] } } }
|
|
CodeBlock:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [type, code, language, label]
|
|
properties:
|
|
type: { type: string, enum: [CODE_BLOCK] }
|
|
code: { type: string, maxLength: 100000 }
|
|
language: { type: [string, "null"], maxLength: 100 }
|
|
label: { type: [string, "null"], maxLength: 200 }
|
|
DataTableColumn:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, label, alignment]
|
|
properties:
|
|
id: { type: string, minLength: 1, maxLength: 200 }
|
|
label: { type: string, minLength: 1, maxLength: 500 }
|
|
alignment: { type: string, enum: [LEFT, CENTER, RIGHT] }
|
|
DataTableCell:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [columnId, content]
|
|
properties:
|
|
columnId: { type: string, minLength: 1, maxLength: 200 }
|
|
content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } }
|
|
DataTableRow:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, cells]
|
|
properties:
|
|
id: { type: string, minLength: 1, maxLength: 200 }
|
|
cells: { type: array, maxItems: 100, items: { $ref: "#/components/schemas/DataTableCell" } }
|
|
DataTableBlock:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [type, id, caption, rowHeaderColumn, columns, rows]
|
|
properties:
|
|
type: { type: string, enum: [DATA_TABLE] }
|
|
id: { type: string, minLength: 1, maxLength: 200 }
|
|
caption: { type: string, maxLength: 1000 }
|
|
rowHeaderColumn: { type: [integer, "null"], minimum: 1 }
|
|
columns: { type: array, minItems: 1, maxItems: 100, items: { $ref: "#/components/schemas/DataTableColumn" } }
|
|
rows: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/DataTableRow" } }
|
|
CalloutBlock:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [type, tone, label, content]
|
|
properties:
|
|
type: { type: string, enum: [CALLOUT] }
|
|
tone: { type: string, enum: [warning, info] }
|
|
label: { type: string, maxLength: 200 }
|
|
content: { type: array, maxItems: 1000, items: { $ref: "#/components/schemas/Inline" } }
|
|
EvidenceFigureBlock:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [type, key, alt, caption, zoom, asset]
|
|
description: |
|
|
`key`는 managed `asset_key`다. object storage key나 raw URL이 아니다.
|
|
|
|
```text
|
|
asset_key → Asset lookup → current approved delivery path
|
|
```
|
|
|
|
`alt`는 빈 문자열을 허용한다. 빈 alt 자체를 syntax error로 차단하지 않고
|
|
Publication Validation이 Asset metadata와 함께 의미 검증한다.
|
|
|
|
```text
|
|
Asset decorative=false + 사용 위치 alt 비어 있음 → PublishValidationFailed
|
|
Asset decorative=true → alt="" 허용
|
|
```
|
|
properties:
|
|
type: { type: string, enum: [EVIDENCE_FIGURE] }
|
|
key: { type: string, minLength: 1, maxLength: 200 }
|
|
alt: { type: string, maxLength: 1000 }
|
|
caption: { type: string, maxLength: 1000 }
|
|
zoom: { type: boolean }
|
|
asset: { $ref: "#/components/schemas/ResolvedAsset" }
|
|
ResolvedAsset:
|
|
type: object
|
|
additionalProperties: false
|
|
description: |
|
|
renderer가 사용하는 Asset descriptor다. Preview/Public/Snapshot이 동일한
|
|
resolver를 통해 동일 semantic output을 만들어야 한다.
|
|
SVG도 URL 기반 `<img>`로 렌더링하며 원문을 inline하지 않는다.
|
|
required: [assetId, assetKey, mediaType, publicPath, width, height, decorative]
|
|
properties:
|
|
assetId: { type: string, format: uuid }
|
|
assetKey: { type: string, minLength: 1, maxLength: 200 }
|
|
mediaType: { type: string, minLength: 1, maxLength: 200 }
|
|
publicPath: { type: string, minLength: 1, maxLength: 500 }
|
|
width: { type: [integer, "null"], minimum: 1 }
|
|
height: { type: [integer, "null"], minimum: 1 }
|
|
decorative: { type: boolean }
|
|
CaseRenderBlock:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/HeadingBlock" }
|
|
- { $ref: "#/components/schemas/ParagraphBlock" }
|
|
- { $ref: "#/components/schemas/BlockquoteBlock" }
|
|
- { $ref: "#/components/schemas/UnorderedListBlock" }
|
|
- { $ref: "#/components/schemas/OrderedListBlock" }
|
|
- { $ref: "#/components/schemas/CodeBlock" }
|
|
- { $ref: "#/components/schemas/DataTableBlock" }
|
|
- { $ref: "#/components/schemas/CalloutBlock" }
|
|
- { $ref: "#/components/schemas/EvidenceFigureBlock" }
|
|
discriminator:
|
|
propertyName: type
|
|
mapping:
|
|
HEADING: "#/components/schemas/HeadingBlock"
|
|
PARAGRAPH: "#/components/schemas/ParagraphBlock"
|
|
BLOCKQUOTE: "#/components/schemas/BlockquoteBlock"
|
|
UNORDERED_LIST: "#/components/schemas/UnorderedListBlock"
|
|
ORDERED_LIST: "#/components/schemas/OrderedListBlock"
|
|
CODE_BLOCK: "#/components/schemas/CodeBlock"
|
|
DATA_TABLE: "#/components/schemas/DataTableBlock"
|
|
CALLOUT: "#/components/schemas/CalloutBlock"
|
|
EVIDENCE_FIGURE: "#/components/schemas/EvidenceFigureBlock"
|
|
CasePublicRenderModel:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/PublicRenderModelBase" }
|
|
- type: object
|
|
required: [kind, problem, conclusion, environment, reproduction, lastVerifiedOn, bodyBlocks]
|
|
properties:
|
|
kind: { type: string, enum: [CASE] }
|
|
problem: { type: string, minLength: 1, maxLength: 100000 }
|
|
conclusion: { type: string, minLength: 1, maxLength: 100000 }
|
|
environment: { type: string, maxLength: 100000 }
|
|
reproduction: { type: string, maxLength: 100000 }
|
|
lastVerifiedOn: { type: string, format: date }
|
|
bodyBlocks: { type: array, maxItems: 10000, items: { $ref: "#/components/schemas/CaseRenderBlock" } }
|
|
ReferencePublicRenderModel:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/PublicRenderModelBase" }
|
|
- type: object
|
|
required: [kind, purpose, rules, applyWhen, exceptions, examples, verifiedOn]
|
|
properties:
|
|
kind: { type: string, enum: [REFERENCE] }
|
|
purpose: { type: string, minLength: 1, maxLength: 100000 }
|
|
rules: { type: array, minItems: 1, maxItems: 50, items: { $ref: "#/components/schemas/ReferenceRule" } }
|
|
applyWhen: { type: array, minItems: 1, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
exceptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
examples: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
verifiedOn: { type: string, format: date }
|
|
ResolvedQuestionResolution:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [summary, evidenceTarget, linkLabel]
|
|
properties:
|
|
summary: { type: string, minLength: 1, maxLength: 100000 }
|
|
evidenceTarget: { $ref: "#/components/schemas/DisplayTarget" }
|
|
linkLabel: { type: string, minLength: 1, maxLength: 120 }
|
|
QuestionPublicRenderModel:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/PublicRenderModelBase" }
|
|
- type: object
|
|
required: [kind, status, facts, assumptions, unknowns, constraints, options, nextValidation, resolution]
|
|
properties:
|
|
kind: { type: string, enum: [QUESTION] }
|
|
status:
|
|
type: string
|
|
enum: [OPEN, RESOLVED]
|
|
description: 공개 표현용 축약 상태다. Domain의 `INVESTIGATING`/`PAUSED`는 `OPEN`으로 표현된다.
|
|
facts: { type: array, minItems: 1, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
assumptions: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
unknowns: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
constraints: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
options: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/QuestionOption" } }
|
|
nextValidation: { type: string, minLength: 1, maxLength: 100000 }
|
|
resolution:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/ResolvedQuestionResolution" }
|
|
- { type: "null" }
|
|
ProjectDecisionPublicRenderModel:
|
|
unevaluatedProperties: false
|
|
allOf:
|
|
- { $ref: "#/components/schemas/PublicRenderModelBase" }
|
|
- type: object
|
|
required: [kind, status, decidedOn, statement, rationale, consequences]
|
|
properties:
|
|
kind: { type: string, enum: [PROJECT_DECISION] }
|
|
status: { type: string, enum: [PROPOSED, ADOPTED] }
|
|
decidedOn: { type: string, format: date }
|
|
statement: { type: string, minLength: 1, maxLength: 100000 }
|
|
rationale: { type: string, minLength: 1, maxLength: 100000 }
|
|
consequences: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } }
|
|
PublicRenderModel:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/CasePublicRenderModel" }
|
|
- { $ref: "#/components/schemas/ReferencePublicRenderModel" }
|
|
- { $ref: "#/components/schemas/QuestionPublicRenderModel" }
|
|
- { $ref: "#/components/schemas/ProjectDecisionPublicRenderModel" }
|
|
discriminator:
|
|
propertyName: kind
|
|
mapping:
|
|
CASE: "#/components/schemas/CasePublicRenderModel"
|
|
REFERENCE: "#/components/schemas/ReferencePublicRenderModel"
|
|
QUESTION: "#/components/schemas/QuestionPublicRenderModel"
|
|
PROJECT_DECISION: "#/components/schemas/ProjectDecisionPublicRenderModel"
|
|
|
|
# ---------------------------------------------------------------- preview
|
|
PublicPreview:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [previewId, documentId, previewVersion, validationId, dependencyRevision, createdAt, expiresAt, renderModel]
|
|
properties:
|
|
previewId: { type: string, format: uuid }
|
|
documentId: { type: string, format: uuid }
|
|
previewVersion: { type: integer, minimum: 1 }
|
|
validationId: { type: string, format: uuid }
|
|
dependencyRevision: { $ref: "#/components/schemas/DependencyRevision" }
|
|
createdAt: { type: string, format: date-time }
|
|
expiresAt: { type: string, format: date-time }
|
|
renderModel: { $ref: "#/components/schemas/PublicRenderModel" }
|
|
PreviewDetail:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [preview, state, currentDocumentVersion, currentValidationId]
|
|
properties:
|
|
preview: { $ref: "#/components/schemas/PublicPreview" }
|
|
state:
|
|
type: string
|
|
enum: [CURRENT, STALE, EXPIRED]
|
|
description: |
|
|
서버가 계산한다.
|
|
`STALE`은 working version 또는 dependency revision이 달라진 경우,
|
|
`EXPIRED`는 `expiresAt`이 지난 경우다.
|
|
currentDocumentVersion: { type: integer, minimum: 1 }
|
|
currentValidationId: { type: [string, "null"], format: uuid }
|
|
|
|
# ------------------------------------------------------------ publication
|
|
PublicationAggregate:
|
|
type: object
|
|
additionalProperties: false
|
|
description: 현재 게시 상태다. 게시 이력(`PublicationEvent`)과 구분한다.
|
|
required: [publicationId, documentId, status, publishedVersion, publicationRevision, latestEventId, publicPath, updatedAt]
|
|
properties:
|
|
publicationId: { type: string, format: uuid }
|
|
documentId: { type: string, format: uuid }
|
|
status: { type: string, enum: [PUBLISHED, UNPUBLISHED] }
|
|
publishedVersion: { type: integer, minimum: 1 }
|
|
publicationRevision: { type: integer, minimum: 1 }
|
|
latestEventId: { type: string, format: uuid }
|
|
publicPath: { type: string, minLength: 1, maxLength: 500 }
|
|
updatedAt: { type: string, format: date-time }
|
|
PublicationEventType: { type: string, enum: [PUBLISHED, REPUBLISHED, UNPUBLISHED] }
|
|
PublicationEvent:
|
|
type: object
|
|
additionalProperties: false
|
|
description: 불변 이력이다. 생성 후 수정하지 않는다.
|
|
required: [publicationEventId, publicationId, documentId, type, occurredAt, publishedVersion, sourcePublishedEventId, snapshotAvailable]
|
|
properties:
|
|
publicationEventId: { type: string, format: uuid }
|
|
publicationId: { type: string, format: uuid }
|
|
documentId: { type: string, format: uuid }
|
|
type: { $ref: "#/components/schemas/PublicationEventType" }
|
|
occurredAt: { type: string, format: date-time }
|
|
publishedVersion: { type: integer, minimum: 1 }
|
|
sourcePublishedEventId:
|
|
type: [string, "null"]
|
|
format: uuid
|
|
description: "`UNPUBLISHED` Event가 참조하는 마지막 공개 Snapshot의 Event id다."
|
|
snapshotAvailable: { type: boolean }
|
|
PublicationSnapshot:
|
|
type: object
|
|
additionalProperties: false
|
|
description: |
|
|
`PUBLISHED`/`REPUBLISHED` 시점의 불변 `PublicRenderModel`이다.
|
|
현재 source에서 재생성하지 않는다.
|
|
required: [event, renderModel, contentFormatVersion, rendererContractVersion]
|
|
properties:
|
|
event: { $ref: "#/components/schemas/PublicationEvent" }
|
|
renderModel: { $ref: "#/components/schemas/PublicRenderModel" }
|
|
contentFormatVersion: { type: string, minLength: 1, maxLength: 50 }
|
|
rendererContractVersion: { type: string, minLength: 1, maxLength: 50 }
|
|
PublishResult:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [publication, event]
|
|
properties:
|
|
publication: { $ref: "#/components/schemas/PublicationAggregate" }
|
|
event: { $ref: "#/components/schemas/PublicationEvent" }
|
|
|
|
# ---------------------------------------------------------------- listing
|
|
DocumentSummary:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, title, kind, project, updatedAt, publicationStatus, publishedVersion, hasUnpublishedChanges, nextAction]
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
title: { type: string, maxLength: 120 }
|
|
kind: { $ref: "#/components/schemas/RecordKind" }
|
|
project:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/DisplayTarget" }
|
|
- { type: "null" }
|
|
updatedAt: { type: string, format: date-time }
|
|
publicationStatus: { $ref: "#/components/schemas/PublicationStatus" }
|
|
publishedVersion: { type: [integer, "null"], minimum: 1 }
|
|
hasUnpublishedChanges:
|
|
type: boolean
|
|
description: "`currentWorkingVersion != currentPublication.publishedVersion`. 게시 취소 상태에서도 과거 publishedVersion과 비교한다."
|
|
nextAction: { $ref: "#/components/schemas/NextAction" }
|
|
PublicationAction: { type: string, enum: [VIEW_SNAPSHOT, VIEW_SOURCE_SNAPSHOT, UNPUBLISH] }
|
|
PublicationListItem:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [event, publication, document, availableActions]
|
|
properties:
|
|
event: { $ref: "#/components/schemas/PublicationEvent" }
|
|
publication: { $ref: "#/components/schemas/PublicationAggregate" }
|
|
document: { $ref: "#/components/schemas/DocumentSummary" }
|
|
availableActions: { type: array, uniqueItems: true, maxItems: 3, items: { $ref: "#/components/schemas/PublicationAction" } }
|
|
WorkingCopyDetail:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [document, currentValidation, latestPreview, currentPublication, dependencyRevision, nextAction]
|
|
properties:
|
|
document: { $ref: "#/components/schemas/WorkingCopy" }
|
|
currentValidation:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/ValidationReport" }
|
|
- { type: "null" }
|
|
latestPreview:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/PublicPreview" }
|
|
- { type: "null" }
|
|
currentPublication:
|
|
oneOf:
|
|
- { $ref: "#/components/schemas/PublicationAggregate" }
|
|
- { type: "null" }
|
|
dependencyRevision: { $ref: "#/components/schemas/DependencyRevision" }
|
|
nextAction: { $ref: "#/components/schemas/NextAction" }
|
|
StudioDashboard:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [continueWriting, readyToPublish, recentPublications, totals]
|
|
properties:
|
|
continueWriting: { type: array, maxItems: 5, items: { $ref: "#/components/schemas/DocumentSummary" } }
|
|
readyToPublish: { type: array, maxItems: 5, items: { $ref: "#/components/schemas/DocumentSummary" } }
|
|
recentPublications: { type: array, maxItems: 5, items: { $ref: "#/components/schemas/PublicationListItem" } }
|
|
totals: { $ref: "#/components/schemas/DashboardTotals" }
|
|
DashboardTotals:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [documents, needsValidation, readyToPublish, publications]
|
|
properties:
|
|
documents: { type: integer, minimum: 0 }
|
|
needsValidation: { type: integer, minimum: 0 }
|
|
readyToPublish: { type: integer, minimum: 0 }
|
|
publications: { type: integer, minimum: 0 }
|
|
DocumentPage:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [items, nextCursor]
|
|
properties:
|
|
items: { type: array, maxItems: 100, items: { $ref: "#/components/schemas/DocumentSummary" } }
|
|
nextCursor: { type: [string, "null"], maxLength: 2000 }
|
|
PublicationPage:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [items, nextCursor]
|
|
properties:
|
|
items: { type: array, maxItems: 100, items: { $ref: "#/components/schemas/PublicationListItem" } }
|
|
nextCursor: { type: [string, "null"], maxLength: 2000 }
|
|
|
|
# ---------------------------------------------------------------- catalog
|
|
CatalogEntryType: { type: string, enum: [TOPIC, PROJECT, RELATION, EVIDENCE] }
|
|
CatalogEntry:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, type, label, dependencyRevision]
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
type: { $ref: "#/components/schemas/CatalogEntryType" }
|
|
label: { type: string, minLength: 1, maxLength: 200 }
|
|
kind: { type: string, enum: [CASE, REFERENCE, QUESTION, PROJECT, PROJECT_DECISION] }
|
|
publicPath: { type: string, minLength: 1, maxLength: 500 }
|
|
dependencyRevision: { $ref: "#/components/schemas/DependencyRevision" }
|
|
CatalogPage:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [items, nextCursor]
|
|
properties:
|
|
items: { type: array, maxItems: 100, items: { $ref: "#/components/schemas/CatalogEntry" } }
|
|
nextCursor: { type: [string, "null"], maxLength: 2000 }
|
|
|
|
# ----------------------------------------------------------------- assets
|
|
AssetKind: { type: string, enum: [IMAGE, DIAGRAM, ATTACHMENT] }
|
|
AssetManagementStatus:
|
|
type: string
|
|
enum: [READY, ARCHIVED, REJECTED, QUARANTINED]
|
|
description: |
|
|
`READY`만 Public Preview/Publish에 사용할 수 있다.
|
|
`REJECTED`/`QUARANTINED`는 서버 검증 결과이며 클라이언트가 지정할 수 없다.
|
|
Asset:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, assetKey, kind, mediaType, originalFilename, byteSize, width, height, altText, decorative, managementStatus, publicPath, usageCount, version, createdAt, updatedAt]
|
|
properties:
|
|
id: { type: string, format: uuid }
|
|
assetKey:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
description: |
|
|
Public content가 사용하는 안정적인 key다. object storage key나 raw URL이
|
|
아니다. immutable이며 공개 이력 이후 재사용을 금지한다.
|
|
kind: { $ref: "#/components/schemas/AssetKind" }
|
|
mediaType: { type: string, minLength: 1, maxLength: 200 }
|
|
originalFilename: { type: string, minLength: 1, maxLength: 500 }
|
|
byteSize: { type: integer, minimum: 0 }
|
|
width: { type: [integer, "null"], minimum: 1 }
|
|
height: { type: [integer, "null"], minimum: 1 }
|
|
altText: { type: [string, "null"], maxLength: 1000 }
|
|
decorative: { type: boolean }
|
|
managementStatus: { $ref: "#/components/schemas/AssetManagementStatus" }
|
|
publicPath: { type: [string, "null"], maxLength: 500 }
|
|
usageCount: { type: integer, minimum: 0 }
|
|
version: { type: integer, minimum: 1 }
|
|
createdAt: { type: string, format: date-time }
|
|
updatedAt: { type: string, format: date-time }
|
|
AssetUsage:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [documentId, documentKind, title, published]
|
|
properties:
|
|
documentId: { type: string, format: uuid }
|
|
documentKind: { $ref: "#/components/schemas/RecordKind" }
|
|
title: { type: string, minLength: 1, maxLength: 200 }
|
|
published: { type: boolean }
|
|
AssetDetail:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [asset, usages, hasPublicationHistory]
|
|
properties:
|
|
asset: { $ref: "#/components/schemas/Asset" }
|
|
usages: { type: array, maxItems: 200, items: { $ref: "#/components/schemas/AssetUsage" } }
|
|
hasPublicationHistory:
|
|
type: boolean
|
|
description: true면 hard delete를 금지하고 `ARCHIVED` 전환만 허용한다.
|
|
AssetUploadForm:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [file, kind]
|
|
properties:
|
|
file: { type: string, format: binary }
|
|
kind: { $ref: "#/components/schemas/AssetKind" }
|
|
altText: { type: string, maxLength: 1000 }
|
|
decorative: { type: boolean, default: false }
|
|
UpdateAssetCommand:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [expectedVersion]
|
|
properties:
|
|
expectedVersion: { type: integer, minimum: 1 }
|
|
kind: { $ref: "#/components/schemas/AssetKind" }
|
|
altText: { type: [string, "null"], maxLength: 1000 }
|
|
decorative: { type: boolean }
|
|
managementStatus: { type: string, enum: [READY, ARCHIVED] }
|
|
AssetPage:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [items, nextCursor]
|
|
properties:
|
|
items: { type: array, maxItems: 100, items: { $ref: "#/components/schemas/Asset" } }
|
|
nextCursor: { type: [string, "null"], maxLength: 2000 }
|
|
|
|
# ------------------------------------------------------------------ errors
|
|
FieldError:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [path, message]
|
|
properties:
|
|
path:
|
|
type: string
|
|
pattern: "^(?:/(?:[^~/]|~0|~1)*)*$"
|
|
description: JSON Pointer to the invalid field
|
|
message: { type: string, minLength: 1, maxLength: 1000 }
|