feat: add the Studio release editor and point the footer at the changelog

The public site shipped a Releases page and a footer link to a release, and
neither could ever have content: the read path existed, the write path did not.
This adds the seven release operations to the contract contribution and the
gateway, and a Studio screen that can actually write one.

The editor is six markdown fields rather than one, because that is what the
contract models and what a release note is — why, what, what a reader notices,
what it leaves in the code, how it was verified, what is still open. Publishing
is separate from saving: a draft saves in any state, but the public query keys
on PUBLISHED alone, so publish is where completeness is demanded.

No new CSS. The screen reuses the document editor's field classes and the
working-copy list's row classes, so it inherits Studio's spacing and type
instead of introducing a second look.

The footer previously linked `/releases/0.1.0` — a version that did not exist,
so the link 404'd, and one that would have gone stale at 0.2.0 anyway. It now
points at the changelog index, which is the only place that knows what the
latest release is and which reads correctly when there are none.

Route inventory, navigation order, message catalog, manual accessibility
evidence, artifact baseline, and the pinned gate-shape digest all move with the
new route. The digest was recomputed by first reproducing the previous constant
from the previous gates.json, so the computation is known to be the one it was
pinned under.
This commit is contained in:
DongHyeonka
2026-08-21 03:08:41 +09:00
parent 760071156d
commit 3754269118
25 changed files with 831 additions and 172 deletions
@@ -1,8 +1,8 @@
{
"packageId": "@tech-log/management-contract",
"version": "1.0.0",
"digest": "sha256:ec5e432215fb041abee980787366a6db29ff1ecdd78416aa9c61e09b9b91022f",
"sourceRevision": "6ef5c1c",
"digest": "sha256:e16a9695e9034a273c769792ea8901fd706c61514137228b2c20e1b117ea0c01",
"sourceRevision": "0c10a4a",
"operationIds": [
"createCaseDraft",
"getCaseForEdit",
@@ -10,3 +10,8 @@ export type ProjectUpdateRequest = Schemas["ProjectUpdateRequest"];
export type CreateDraftRequest = Schemas["CreateDraftRequest"];
export type CreateDraftResponse = Schemas["CreateDraftResponse"];
export type ExpectedVersionRequest = Schemas["ExpectedVersionRequest"];
export type ReleaseEditResponse = Schemas["ReleaseEditResponse"];
export type ReleaseIndexItem = Schemas["ReleaseIndexItem"];
export type ReleaseIndexPage = Schemas["ReleaseIndexPage"];
export type ReleaseUpdateRequest = Schemas["ReleaseUpdateRequest"];
export type PublishResponse = Schemas["PublishResponse"];
@@ -902,7 +902,7 @@ export interface components {
* @description `INTERNAL_ERROR` . 500 .
* @enum {string}
*/
code: "AUTHENTICATION_REQUIRED" | "STUDIO_ACCESS_DENIED" | "REQUEST_VALIDATION_FAILED" | "VERSION_CONFLICT" | "TOPIC_NOT_FOUND" | "TOPIC_NAME_TAKEN" | "TOPIC_SLUG_TAKEN" | "TOPIC_IN_USE" | "PROJECT_NOT_FOUND" | "PROJECT_SLUG_TAKEN" | "PROJECT_IN_USE" | "INTERNAL_ERROR";
code: "AUTHENTICATION_REQUIRED" | "STUDIO_ACCESS_DENIED" | "REQUEST_VALIDATION_FAILED" | "VERSION_CONFLICT" | "TOPIC_NOT_FOUND" | "TOPIC_NAME_TAKEN" | "TOPIC_SLUG_TAKEN" | "TOPIC_IN_USE" | "PROJECT_NOT_FOUND" | "PROJECT_SLUG_TAKEN" | "PROJECT_IN_USE" | "RELEASE_NOT_FOUND" | "RELEASE_VERSION_TAKEN" | "RELEASE_NOT_PUBLISHABLE" | "INTERNAL_ERROR";
/** @enum {string} */
category: "VALIDATION" | "AUTH" | "AUTHZ" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMIT" | "TRANSIENT_DEPENDENCY" | "PERMANENT_DEPENDENCY" | "DATA_INTEGRITY" | "INTERNAL";
message: string;
@@ -946,6 +946,24 @@ export interface components {
data: components["schemas"]["ProjectIndexPage"];
meta: components["schemas"]["ResponseMeta"];
};
ReleaseIndexPageEnvelope: {
/** @constant */
success: true;
data: components["schemas"]["ReleaseIndexPage"];
meta: components["schemas"]["ResponseMeta"];
};
ReleaseEditResponseEnvelope: {
/** @constant */
success: true;
data: components["schemas"]["ReleaseEditResponse"];
meta: components["schemas"]["ResponseMeta"];
};
PublishResponseEnvelope: {
/** @constant */
success: true;
data: components["schemas"]["PublishResponse"];
meta: components["schemas"]["ResponseMeta"];
};
ProjectEditResponseEnvelope: {
/** @constant */
success: true;
@@ -6267,7 +6285,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReleaseIndexPage"];
"application/json": components["schemas"]["ReleaseIndexPageEnvelope"];
};
};
/** @description Bad Request */
@@ -6276,7 +6294,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unauthorized */
@@ -6285,7 +6303,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Forbidden */
@@ -6294,7 +6312,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Not Found */
@@ -6303,7 +6321,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Internal Server Error */
@@ -6312,7 +6330,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
};
@@ -6338,7 +6356,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CreateDraftResponse"];
"application/json": components["schemas"]["CreateDraftResponseEnvelope"];
};
};
/** @description Bad Request */
@@ -6347,7 +6365,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unauthorized */
@@ -6356,7 +6374,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Forbidden */
@@ -6365,7 +6383,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Not Found */
@@ -6374,7 +6392,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Conflict */
@@ -6383,7 +6401,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unprocessable Content */
@@ -6392,7 +6410,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Internal Server Error */
@@ -6401,7 +6419,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
};
@@ -6423,7 +6441,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReleaseEditResponse"];
"application/json": components["schemas"]["ReleaseEditResponseEnvelope"];
};
};
/** @description Bad Request */
@@ -6432,7 +6450,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unauthorized */
@@ -6441,7 +6459,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Forbidden */
@@ -6450,7 +6468,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Not Found */
@@ -6459,7 +6477,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Internal Server Error */
@@ -6468,7 +6486,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
};
@@ -6496,7 +6514,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReleaseEditResponse"];
"application/json": components["schemas"]["ReleaseEditResponseEnvelope"];
};
};
/** @description Bad Request */
@@ -6505,7 +6523,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unauthorized */
@@ -6514,7 +6532,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Forbidden */
@@ -6523,7 +6541,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Not Found */
@@ -6532,7 +6550,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Conflict */
@@ -6541,7 +6559,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unprocessable Content */
@@ -6550,7 +6568,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Internal Server Error */
@@ -6559,7 +6577,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
};
@@ -6594,7 +6612,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unauthorized */
@@ -6603,7 +6621,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Forbidden */
@@ -6612,7 +6630,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Not Found */
@@ -6621,7 +6639,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Conflict */
@@ -6630,7 +6648,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unprocessable Content */
@@ -6639,7 +6657,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Internal Server Error */
@@ -6648,7 +6666,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
};
@@ -6676,7 +6694,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["PublishResponse"];
"application/json": components["schemas"]["PublishResponseEnvelope"];
};
};
/** @description Bad Request */
@@ -6685,7 +6703,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unauthorized */
@@ -6694,7 +6712,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Forbidden */
@@ -6703,7 +6721,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Not Found */
@@ -6712,7 +6730,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Conflict */
@@ -6721,7 +6739,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unprocessable Content */
@@ -6730,7 +6748,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Internal Server Error */
@@ -6739,7 +6757,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
};
@@ -6767,7 +6785,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ReleaseEditResponse"];
"application/json": components["schemas"]["ReleaseEditResponseEnvelope"];
};
};
/** @description Bad Request */
@@ -6776,7 +6794,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unauthorized */
@@ -6785,7 +6803,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Forbidden */
@@ -6794,7 +6812,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Not Found */
@@ -6803,7 +6821,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Conflict */
@@ -6812,7 +6830,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Unprocessable Content */
@@ -6821,7 +6839,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
/** @description Internal Server Error */
@@ -6830,7 +6848,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDetails"];
"application/json": components["schemas"]["ErrorEnvelope"];
};
};
};
@@ -5,11 +5,11 @@ info:
description: |-
⚠ 봉투 결정(ADR-006) 부분 반영 — 이 파일에는 두 모양이 공존한다.
topics 4개 projects 5개 studio-v1.yaml과 같은 방식으로 변환했다
(`application/json` + ErrorEnvelope / <Payload>Envelope). 그 9개가 첫 구현
대상이자 소비자이기 때문이다.
topics 4개, projects 5개, releases 7개를 studio-v1.yaml과 같은 방식으로
변환했다 (`application/json` + ErrorEnvelope / <Payload>Envelope). 그 16개가
구현된 것이자 소비자가 있는 것이기 때문이다.
나머지 70개는 아직 bare payload + `application/problem+json` + ProblemDetails
나머지 63개는 아직 bare payload + `application/problem+json` + ProblemDetails
다. 구현에 착수할 때 같은 방식으로 따라온다 — 소비자가 없는 operation을 미리
변환해 두면 검증되지 않은 모양이 계약에 고정된다.
@@ -3630,49 +3630,49 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateDraftResponse'
$ref: '#/components/schemas/CreateDraftResponseEnvelope'
'400':
description: Bad Request
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Unauthorized
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Forbidden
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: Not Found
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'409':
description: Conflict
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'422':
description: Unprocessable Content
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'500':
description: Internal Server Error
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
requestBody:
required: true
content:
@@ -3705,37 +3705,37 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ReleaseIndexPage'
$ref: '#/components/schemas/ReleaseIndexPageEnvelope'
'400':
description: Bad Request
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Unauthorized
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Forbidden
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: Not Found
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'500':
description: Internal Server Error
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
security:
- sessionCookie: []
/api/v1/studio/releases/{id}:
@@ -3756,37 +3756,37 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ReleaseEditResponse'
$ref: '#/components/schemas/ReleaseEditResponseEnvelope'
'400':
description: Bad Request
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Unauthorized
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Forbidden
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: Not Found
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'500':
description: Internal Server Error
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
security:
- sessionCookie: []
put:
@@ -3807,49 +3807,49 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ReleaseEditResponse'
$ref: '#/components/schemas/ReleaseEditResponseEnvelope'
'400':
description: Bad Request
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Unauthorized
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Forbidden
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: Not Found
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'409':
description: Conflict
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'422':
description: Unprocessable Content
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'500':
description: Internal Server Error
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
requestBody:
required: true
content:
@@ -3876,45 +3876,45 @@ paths:
'400':
description: Bad Request
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Unauthorized
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Forbidden
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: Not Found
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'409':
description: Conflict
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'422':
description: Unprocessable Content
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'500':
description: Internal Server Error
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
requestBody:
required: true
content:
@@ -3942,49 +3942,49 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/PublishResponse'
$ref: '#/components/schemas/PublishResponseEnvelope'
'400':
description: Bad Request
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Unauthorized
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Forbidden
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: Not Found
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'409':
description: Conflict
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'422':
description: Unprocessable Content
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'500':
description: Internal Server Error
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
requestBody:
required: true
content:
@@ -4012,49 +4012,49 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ReleaseEditResponse'
$ref: '#/components/schemas/ReleaseEditResponseEnvelope'
'400':
description: Bad Request
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Unauthorized
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Forbidden
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'404':
description: Not Found
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'409':
description: Conflict
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'422':
description: Unprocessable Content
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
'500':
description: Internal Server Error
content:
application/problem+json:
application/json:
schema:
$ref: '#/components/schemas/ProblemDetails'
$ref: '#/components/schemas/ErrorEnvelope'
requestBody:
required: true
content:
@@ -5267,6 +5267,9 @@ components:
- PROJECT_NOT_FOUND
- PROJECT_SLUG_TAKEN
- PROJECT_IN_USE
- RELEASE_NOT_FOUND
- RELEASE_VERSION_TAKEN
- RELEASE_NOT_PUBLISHABLE
- INTERNAL_ERROR
description: '`INTERNAL_ERROR` 는 이 기능이 아니라 스켈레톤의 공통 처리기가 내는 코드다. 계약이 그것까지 열거해야 500 응답이 계약을 벗어나지 않는다.'
category:
@@ -5392,6 +5395,51 @@ components:
$ref: '#/components/schemas/ProjectIndexPage'
meta:
$ref: '#/components/schemas/ResponseMeta'
ReleaseIndexPageEnvelope:
type: object
additionalProperties: false
required:
- success
- data
- meta
properties:
success:
type: boolean
const: true
data:
$ref: '#/components/schemas/ReleaseIndexPage'
meta:
$ref: '#/components/schemas/ResponseMeta'
ReleaseEditResponseEnvelope:
type: object
additionalProperties: false
required:
- success
- data
- meta
properties:
success:
type: boolean
const: true
data:
$ref: '#/components/schemas/ReleaseEditResponse'
meta:
$ref: '#/components/schemas/ResponseMeta'
PublishResponseEnvelope:
type: object
additionalProperties: false
required:
- success
- data
- meta
properties:
success:
type: boolean
const: true
data:
$ref: '#/components/schemas/PublishResponse'
meta:
$ref: '#/components/schemas/ResponseMeta'
ProjectEditResponseEnvelope:
type: object
additionalProperties: false
@@ -4,5 +4,8 @@ export const publicSiteConfig = Object.freeze({
operator: "동현",
contactLabel: "프로필",
contactPath: "/profile",
latestRelease: "/releases/0.1.0",
// 변경 기록 목록. 이전에는 특정 버전(`/releases/0.1.0`)을 박아 두었는데, 그 릴리즈가 아직
// 없어서 푸터 링크가 404 였고, 있었더라도 다음 버전이 나오면 다시 낡는 자리였다. 목록은 어떤
// 버전이 최신인지 아는 유일한 곳이고 릴리즈가 하나도 없어도 성립한다.
releasesPath: "/releases",
});
@@ -2,7 +2,7 @@
"packageId": "@tech-log/public-contract",
"version": "2.0.0",
"digest": "sha256:8ac71425b38658f34641102b4c2e6e21288c811efebdb92c0a46fb9d4790e23e",
"sourceRevision": "6ef5c1c",
"sourceRevision": "0c10a4a",
"operationIds": [
"getPublicSite",
"getPublicHome",
@@ -2,7 +2,7 @@
"packageId": "@tech-log/studio-contract",
"version": "3.0.0",
"digest": "sha256:6cae9924403d0761f401643a022980b8e04183eea0d890c143c9fbbbbc7431e4",
"sourceRevision": "6ef5c1c",
"sourceRevision": "0c10a4a",
"operationIds": [
"getStudioSession",
"getStudioDashboard",
@@ -31,6 +31,9 @@ const MANAGEMENT_ERROR_CODES = Object.freeze([
"PROJECT_NOT_FOUND",
"PROJECT_SLUG_TAKEN",
"PROJECT_IN_USE",
"RELEASE_NOT_FOUND",
"RELEASE_VERSION_TAKEN",
"RELEASE_NOT_PUBLISHABLE",
"INTERNAL_ERROR",
]);
@@ -155,6 +158,7 @@ const byId = (input: never) => {
const T = "/api/v1/studio/topics";
const P = "/api/v1/studio/projects";
const R = "/api/v1/studio/releases";
const HTTP_CONTRACTS = Object.freeze([
readOperation("listStudioTopics", T, 131_072),
@@ -248,6 +252,84 @@ const HTTP_CONTRACTS = Object.freeze([
});
},
),
readOperation("listStudioReleases", R, 262_144, (input: never) => {
const value = input as unknown as Readonly<{ page?: number; size?: number }> | undefined;
const entries: (readonly [string, string])[] = [];
if (value?.page !== undefined) entries.push(["page", String(value.page)]);
if (value?.size !== undefined) entries.push(["size", String(value.size)]);
return Object.freeze({ pathValues: NO_PATH, queryEntries: Object.freeze(entries) });
}),
readOperation("getReleaseForEdit", `${R}/{id}`, 262_144, byId),
writeOperation(
"createRelease",
"POST",
R,
{ acceptedStatuses: [201], requestByteLimit: 4_096, responseByteLimit: 8_192 },
(input: never) =>
Object.freeze({ pathValues: NO_PATH, queryEntries: NO_QUERY, body: input }),
),
writeOperation(
"updateRelease",
"PUT",
`${R}/{id}`,
// 릴리즈 본문은 마크다운 여섯 구획이라 문서 다음으로 큰 요청이다.
{ acceptedStatuses: [200], requestByteLimit: 262_144, responseByteLimit: 262_144 },
(input: never) => {
const value = input as unknown as Readonly<{ id: string; body: unknown }>;
return Object.freeze({
pathValues: Object.freeze({ id: value.id }),
queryEntries: NO_QUERY,
body: value.body,
});
},
),
writeOperation(
"deleteRelease",
"DELETE",
`${R}/{id}`,
{
acceptedStatuses: [204],
emptyBodyStatuses: [204],
requestByteLimit: 1_024,
responseByteLimit: 1_024,
},
(input: never) => {
const value = input as unknown as Readonly<{ id: string; expectedVersion: number }>;
return Object.freeze({
pathValues: Object.freeze({ id: value.id }),
queryEntries: NO_QUERY,
body: { expectedVersion: value.expectedVersion },
});
},
),
writeOperation(
"publishRelease",
"POST",
`${R}/{id}/publish`,
{ acceptedStatuses: [200], requestByteLimit: 1_024, responseByteLimit: 8_192 },
(input: never) => {
const value = input as unknown as Readonly<{ id: string; expectedVersion: number }>;
return Object.freeze({
pathValues: Object.freeze({ id: value.id }),
queryEntries: NO_QUERY,
body: { expectedVersion: value.expectedVersion },
});
},
),
writeOperation(
"archiveRelease",
"POST",
`${R}/{id}/archive`,
{ acceptedStatuses: [200], requestByteLimit: 1_024, responseByteLimit: 262_144 },
(input: never) => {
const value = input as unknown as Readonly<{ id: string; expectedVersion: number }>;
return Object.freeze({
pathValues: Object.freeze({ id: value.id }),
queryEntries: NO_QUERY,
body: { expectedVersion: value.expectedVersion },
});
},
),
]);
export const TECH_LOG_MANAGEMENT_OPERATION_IDS = Object.freeze(
@@ -49,6 +49,7 @@ const TECH_LOG_ROUTE_SPECS = [
defineSpec({ routeId: "TECH_LOG_STUDIO_PUBLICATION_PREVIEW", path: "/studio/publications/:publicationEventId/preview", layoutGroup: "STUDIO", paramsSchema: "TechLogPublicationEventIdParams", searchSchema: null, title: "게시 Snapshot", navigationLabel: null, navigationOrder: null }),
defineSpec({ routeId: "TECH_LOG_STUDIO_ASSETS", path: "/studio/assets", layoutGroup: "STUDIO", paramsSchema: null, searchSchema: null, title: "Asset", navigationLabel: null, navigationOrder: null }),
defineSpec({ routeId: "TECH_LOG_STUDIO_TAXONOMY", path: "/studio/taxonomy", layoutGroup: "STUDIO", paramsSchema: null, searchSchema: null, title: "주제와 프로젝트", navigationLabel: "주제·프로젝트", navigationOrder: 40 }),
defineSpec({ routeId: "TECH_LOG_STUDIO_RELEASES", path: "/studio/releases", layoutGroup: "STUDIO", paramsSchema: null, searchSchema: null, title: "릴리즈", navigationLabel: "릴리즈", navigationOrder: 50 }),
defineSpec({ routeId: "TECH_LOG_STUDIO_NOT_FOUND", path: "/studio/*", layoutGroup: "STUDIO", paramsSchema: "TechLogStudioSplat", searchSchema: null, title: "Studio 화면을 찾을 수 없습니다", navigationLabel: null, navigationOrder: null }),
defineSpec({ routeId: "NOT_FOUND", path: "*", layoutGroup: "PUBLIC", paramsSchema: "NotFoundSplat", searchSchema: null, title: "페이지를 찾을 수 없습니다.", navigationLabel: null, navigationOrder: null }),
] as const;