feat: let an author delete a working copy

An author who opens a draft and thinks better of it had no way out — the
working-copy list could create, edit, validate and publish, and that was all.
The three delete operations existed in the contract with no implementation.

Deleting is not a cascade, which is the part worth being careful about. A
document's own rows follow it: the detail row, its tags, the relations it
points outward. But five tables reference `document` without ON DELETE CASCADE
— another document's relation target, a question's link, a project's
membership, a topic's featured list, a decision's source case — and two do the
same for `open_question`. Deleting through any of them is a foreign-key
violation, which reaches the author as a 500 that explains nothing. So the
delete checks first and refuses with DOCUMENT_IN_USE, the same refusal
TOPIC_IN_USE already makes. Quietly editing someone else's record to make room
is the worse option.

A published record is refused outright. Public pages, search and other records
link to it, and one that vanishes leaves all of them pointing at nothing —
unpublishing is the way out, and it already exists.

Case and Reference share one table split by `document_type`, so the type is
part of the lookup: without it, the Case route would happily delete a
Reference. Decisions have no delete at all, and that is the contract's
judgment rather than an omission — accept, reject and supersede record what
happened instead of erasing it.
This commit is contained in:
DongHyeonka
2026-08-21 13:30:37 +09:00
parent 386f360122
commit 1befdc37a4
11 changed files with 495 additions and 48 deletions
+65
View File
@@ -1403,3 +1403,68 @@ errors:
runbook_link: null
compatibility_impact: additive
required_test: ManagementErrorRegistryTest
# source: studio-management-v1.yaml ApiError.code — DOCUMENT_NOT_FOUND (ManagementError.DOCUMENT_NOT_FOUND)
- code: DOCUMENT_NOT_FOUND
category: NOT_FOUND
http_status: 404
retryable: false
retry_after_seconds: null
owner_branch: feature-techlog-management-v1
owner_layer: application
client_safe_message: "작업본을 찾을 수 없습니다"
log_level: INFO
runbook_link: null
compatibility_impact: additive
required_test: ManagementErrorRegistryTest
# source: studio-management-v1.yaml ApiError.code — DOCUMENT_PUBLISHED (ManagementError.DOCUMENT_PUBLISHED)
- code: DOCUMENT_PUBLISHED
category: CONFLICT
http_status: 409
retryable: false
retry_after_seconds: null
owner_branch: feature-techlog-management-v1
owner_layer: application
client_safe_message: "공개된 기록은 삭제할 수 없습니다. 먼저 공개를 취소해 주세요"
log_level: INFO
runbook_link: null
compatibility_impact: additive
required_test: ManagementErrorRegistryTest
# source: studio-management-v1.yaml ApiError.code — QUESTION_NOT_FOUND (ManagementError.QUESTION_NOT_FOUND)
- code: QUESTION_NOT_FOUND
category: NOT_FOUND
http_status: 404
retryable: false
retry_after_seconds: null
owner_branch: feature-techlog-management-v1
owner_layer: application
client_safe_message: "질문을 찾을 수 없습니다"
log_level: INFO
runbook_link: null
compatibility_impact: additive
required_test: ManagementErrorRegistryTest
# source: studio-management-v1.yaml ApiError.code — DOCUMENT_IN_USE (ManagementError.DOCUMENT_IN_USE)
- code: DOCUMENT_IN_USE
category: CONFLICT
http_status: 409
retryable: false
retry_after_seconds: null
owner_branch: feature-techlog-management-v1
owner_layer: application
client_safe_message: "이 기록을 참조하는 곳이 있어 삭제할 수 없습니다"
log_level: INFO
runbook_link: null
compatibility_impact: additive
required_test: ManagementErrorRegistryTest
# source: studio-management-v1.yaml ApiError.code — QUESTION_IN_USE (ManagementError.QUESTION_IN_USE)
- code: QUESTION_IN_USE
category: CONFLICT
http_status: 409
retryable: false
retry_after_seconds: null
owner_branch: feature-techlog-management-v1
owner_layer: application
client_safe_message: "이 질문을 참조하는 곳이 있어 삭제할 수 없습니다"
log_level: INFO
runbook_link: null
compatibility_impact: additive
required_test: ManagementErrorRegistryTest
@@ -26,6 +26,11 @@ public final class ManagementClientSafeMessages {
case RELEASE_NOT_FOUND -> "요청한 릴리즈를 찾을 수 없습니다";
case RELEASE_VERSION_TAKEN -> "같은 버전의 릴리즈가 이미 있습니다";
case RELEASE_NOT_PUBLISHABLE -> "지금 상태에서는 발행할 수 없습니다";
case DOCUMENT_NOT_FOUND -> "작업본을 찾을 수 없습니다";
case DOCUMENT_PUBLISHED -> "공개된 기록은 삭제할 수 없습니다. 먼저 공개를 취소해 주세요";
case DOCUMENT_IN_USE -> "이 기록을 참조하는 곳이 있어 삭제할 수 없습니다";
case QUESTION_NOT_FOUND -> "질문을 찾을 수 없습니다";
case QUESTION_IN_USE -> "이 질문을 참조하는 곳이 있어 삭제할 수 없습니다";
};
}
}
@@ -0,0 +1,68 @@
package dev.caskeleton.adapter.inbound.web.techlog.management.controller;
import dev.caskeleton.adapter.inbound.web.auth.AuthenticatedPrincipal;
import dev.caskeleton.adapter.inbound.web.techlog.management.ManagementPrincipals;
import dev.caskeleton.adapter.inbound.web.techlog.management.api.model.ExpectedVersionRequest;
import dev.caskeleton.application.techlog.management.command.DeleteDocumentCommand;
import dev.caskeleton.application.techlog.management.service.DeleteDocumentDraftUseCase;
import dev.caskeleton.application.techlog.management.service.DeleteQuestionUseCase;
import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
/**
* 작업본 삭제. 계약의 delete 3개.
*
* <p>경로가 종류별로 갈리는 것은 계약이 그렇게 선언했기 때문이고, 그럴 이유도 있다 — Case 와 Reference 는 한 테이블을 나눠 쓰지만 Question 은 다른
* 테이블이고, 종류를 경로에 두면 Case 주소로 Reference 를 지우는 요청이 애초에 성립하지 않는다.
*/
@RestController
public class ManagementDocumentController {
private final DeleteDocumentDraftUseCase deleteDocument;
private final DeleteQuestionUseCase deleteQuestion;
public ManagementDocumentController(
DeleteDocumentDraftUseCase deleteDocument, DeleteQuestionUseCase deleteQuestion) {
this.deleteDocument = deleteDocument;
this.deleteQuestion = deleteQuestion;
}
@DeleteMapping("/v1/studio/cases/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteCaseDraft(
@AuthenticationPrincipal AuthenticatedPrincipal principal,
@PathVariable("id") UUID id,
@RequestBody ExpectedVersionRequest body) {
deleteDocument.handle(command(id, body, principal), "CASE");
}
@DeleteMapping("/v1/studio/references/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteReferenceDraft(
@AuthenticationPrincipal AuthenticatedPrincipal principal,
@PathVariable("id") UUID id,
@RequestBody ExpectedVersionRequest body) {
deleteDocument.handle(command(id, body, principal), "REFERENCE");
}
@DeleteMapping("/v1/studio/questions/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteQuestion(
@AuthenticationPrincipal AuthenticatedPrincipal principal,
@PathVariable("id") UUID id,
@RequestBody ExpectedVersionRequest body) {
deleteQuestion.handle(command(id, body, principal));
}
private static DeleteDocumentCommand command(
UUID id, ExpectedVersionRequest body, AuthenticatedPrincipal principal) {
return new DeleteDocumentCommand(
id, body.getExpectedVersion(), ManagementPrincipals.require(principal));
}
}
@@ -0,0 +1,109 @@
package dev.caskeleton.adapter.outbound.persistence.techlog.management;
import dev.caskeleton.application.techlog.management.port.out.DocumentDeletionPort;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import java.util.UUID;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Repository;
/**
* 작업본 삭제.
*
* <p>작업본 자신의 것(상세, 태그, 나가는 관계)은 {@code ON DELETE CASCADE} 로 따라 지워진다. 하지만 <em>다른</em> 기록이 이쪽을 가리키는
* 참조는 그렇지 않다 — 다른 문서의 관계 대상, 질문의 링크, 프로젝트 소속, 주제의 추천 목록, 결정의 근거 Case 다섯 곳이 CASCADE 없이 걸려 있다. 그대로
* 지우면 외래키 위반이고, 작성자에게는 500 으로 도착한다. 그래서 먼저 확인하고 거절한다 — 남의 기록을 조용히 고쳐 주는 것보다 낫다.
*/
@Repository
public class JdbcDocumentDeletionAdapter implements DocumentDeletionPort {
private final JdbcClient jdbcClient;
public JdbcDocumentDeletionAdapter(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
private static DeletableDocument mapDocument(ResultSet rs, int rowNum) throws SQLException {
return new DeletableDocument(
rs.getObject("id", UUID.class),
rs.getString("document_type"),
rs.getString("workflow_status"),
rs.getLong("version"));
}
private static DeletableQuestion mapQuestion(ResultSet rs, int rowNum) throws SQLException {
return new DeletableQuestion(rs.getObject("id", UUID.class), rs.getLong("version"));
}
@Override
public Optional<DeletableDocument> findDocument(UUID id, String documentType) {
return jdbcClient
.sql(
"SELECT id, document_type, workflow_status, version FROM document"
+ " WHERE id = :id AND document_type = :type")
.param("id", id)
.param("type", documentType)
.query(JdbcDocumentDeletionAdapter::mapDocument)
.optional();
}
@Override
public int deleteDocument(UUID id, long expectedVersion) {
return jdbcClient
.sql("DELETE FROM document WHERE id = :id AND version = :expected")
.param("id", id)
.param("expected", expectedVersion)
.update();
}
@Override
public boolean documentReferenced(UUID id) {
return Boolean.TRUE.equals(
jdbcClient
.sql(
"SELECT EXISTS ("
+ " SELECT 1 FROM document_relation WHERE target_document_id = :id"
+ " UNION ALL SELECT 1 FROM question_document_link WHERE document_id = :id"
+ " UNION ALL SELECT 1 FROM project_document_link WHERE document_id = :id"
+ " UNION ALL SELECT 1 FROM topic_featured_document WHERE document_id = :id"
+ " UNION ALL SELECT 1 FROM project_decision WHERE source_case_id = :id"
+ " UNION ALL SELECT 1 FROM public_resource_projection WHERE document_id = :id"
+ ")")
.param("id", id)
.query(Boolean.class)
.single());
}
@Override
public boolean questionReferenced(UUID id) {
return Boolean.TRUE.equals(
jdbcClient
.sql(
"SELECT EXISTS ("
+ " SELECT 1 FROM project_question_link WHERE question_id = :id"
+ " UNION ALL SELECT 1 FROM home_focus_config WHERE open_question_id = :id"
+ ")")
.param("id", id)
.query(Boolean.class)
.single());
}
@Override
public Optional<DeletableQuestion> findQuestion(UUID id) {
return jdbcClient
.sql("SELECT id, version FROM open_question WHERE id = :id")
.param("id", id)
.query(JdbcDocumentDeletionAdapter::mapQuestion)
.optional();
}
@Override
public int deleteQuestion(UUID id, long expectedVersion) {
return jdbcClient
.sql("DELETE FROM open_question WHERE id = :id AND version = :expected")
.param("id", id)
.param("expected", expectedVersion)
.update();
}
}
@@ -1,12 +1,15 @@
package dev.caskeleton.bootstrap.techlog;
import dev.caskeleton.application.techlog.management.port.out.DocumentDeletionPort;
import dev.caskeleton.application.techlog.management.port.out.ProjectRepositoryPort;
import dev.caskeleton.application.techlog.management.port.out.ReleaseRepositoryPort;
import dev.caskeleton.application.techlog.management.port.out.TopicRepositoryPort;
import dev.caskeleton.application.techlog.management.service.ArchiveReleaseUseCase;
import dev.caskeleton.application.techlog.management.service.CreateProjectUseCase;
import dev.caskeleton.application.techlog.management.service.CreateReleaseUseCase;
import dev.caskeleton.application.techlog.management.service.DeleteDocumentDraftUseCase;
import dev.caskeleton.application.techlog.management.service.DeleteProjectUseCase;
import dev.caskeleton.application.techlog.management.service.DeleteQuestionUseCase;
import dev.caskeleton.application.techlog.management.service.DeleteReleaseUseCase;
import dev.caskeleton.application.techlog.management.service.DeleteTopicUseCase;
import dev.caskeleton.application.techlog.management.service.GetProjectForEditUseCase;
@@ -107,4 +110,15 @@ public class TechLogManagementConfig {
ArchiveReleaseUseCase archiveReleaseUseCase(ReleaseRepositoryPort releases, TransactionPort tx) {
return new ArchiveReleaseUseCase(releases, tx);
}
@Bean
DeleteDocumentDraftUseCase deleteDocumentDraftUseCase(
DocumentDeletionPort documents, TransactionPort tx) {
return new DeleteDocumentDraftUseCase(documents, tx);
}
@Bean
DeleteQuestionUseCase deleteQuestionUseCase(DocumentDeletionPort documents, TransactionPort tx) {
return new DeleteQuestionUseCase(documents, tx);
}
}
@@ -10,7 +10,7 @@ import dev.caskeleton.shared.error.Category;
* <p>{@link StudioError} 와 합치지 않는다. 두 계약이 각자의 code 집합을 열거하고 있고, 한쪽에만 있는 코드를 다른 쪽 응답으로 낼 수 있게 되면 그
* 순간 두 계약 모두 거짓이 된다.
*
* <p>계약의 {@code ApiError.code} 는 15종인데 여기는 14종이다. 나머지 하나 {@code INTERNAL_ERROR} 는 이 기능이 아니라 스켈레톤 공통
* <p>계약의 {@code ApiError.code} 는 20종인데 여기는 19종이다. 나머지 하나 {@code INTERNAL_ERROR} 는 이 기능이 아니라 스켈레톤 공통
* 처리기가 내는 코드({@code OperationalError.INTERNAL_ERROR}) 이고, 같은 code 를 두 enum 이 각자 status 와 retryable
* 을 달고 선언하면 레지스트리가 어느 쪽을 따라야 할지 알 수 없다 — 실제로 그쪽은 {@code retryable=true} 다. {@code PublicError} 가 같은
* 이유로 같은 선택을 했다.
@@ -29,7 +29,12 @@ public enum ManagementError implements ApiErrorCode {
PROJECT_IN_USE(Category.CONFLICT, 409, false),
RELEASE_NOT_FOUND(Category.NOT_FOUND, 404, false),
RELEASE_VERSION_TAKEN(Category.CONFLICT, 409, false),
RELEASE_NOT_PUBLISHABLE(Category.CONFLICT, 409, false);
RELEASE_NOT_PUBLISHABLE(Category.CONFLICT, 409, false),
DOCUMENT_NOT_FOUND(Category.NOT_FOUND, 404, false),
DOCUMENT_PUBLISHED(Category.CONFLICT, 409, false),
DOCUMENT_IN_USE(Category.CONFLICT, 409, false),
QUESTION_NOT_FOUND(Category.NOT_FOUND, 404, false),
QUESTION_IN_USE(Category.CONFLICT, 409, false);
private final Category category;
private final int httpStatus;
@@ -0,0 +1,9 @@
package dev.caskeleton.application.techlog.management.command;
import java.util.UUID;
/**
* 계약 {@code ExpectedVersionRequest}. Case / Reference / Question 삭제가 모두 같은 모양이라 하나로 둔다 — 대상 테이블만
* 다르고 요청은 "이 버전의 이 작업본을 지운다"로 동일하다.
*/
public record DeleteDocumentCommand(UUID id, long expectedVersion, String actor) {}
@@ -0,0 +1,35 @@
package dev.caskeleton.application.techlog.management.port.out;
import java.util.Optional;
import java.util.UUID;
/**
* 작업본 삭제.
*
* <p>편집·조회는 {@code studio} 쪽 포트가 이미 갖고 있다. 삭제만 따로 두는 이유는 이것이 관리 계약의 operation 이고, 지우기 전에 확인해야 하는
* 것(무슨 종류인지, 지금 공개돼 있는지)이 편집 화면이 읽는 것과 다르기 때문이다.
*/
public interface DocumentDeletionPort {
/** 삭제 가능 여부를 판단할 만큼만 읽는다. */
record DeletableDocument(UUID id, String documentType, String workflowStatus, long version) {}
record DeletableQuestion(UUID id, long version) {}
/** {@code documentType} 이 어긋나면 비어 있다 — Case 경로로 Reference 를 지울 수 없다. */
Optional<DeletableDocument> findDocument(UUID id, String documentType);
int deleteDocument(UUID id, long expectedVersion);
/**
* 자기 자식이 아닌 곳에서 이 작업본을 가리키고 있는지. 그런 참조는 {@code ON DELETE CASCADE} 가 아니므로 그대로 지우면 외래키 위반이 되고, 그건
* 작성자에게 500 으로 도착한다.
*/
boolean documentReferenced(UUID id);
Optional<DeletableQuestion> findQuestion(UUID id);
int deleteQuestion(UUID id, long expectedVersion);
boolean questionReferenced(UUID id);
}
@@ -0,0 +1,71 @@
package dev.caskeleton.application.techlog.management.service;
import dev.caskeleton.application.capability.Idempotency;
import dev.caskeleton.application.capability.RepositoryAccess;
import dev.caskeleton.application.capability.UseCaseCapability;
import dev.caskeleton.application.security.RequiresPermission;
import dev.caskeleton.application.techlog.error.ManagementError;
import dev.caskeleton.application.techlog.error.ManagementException;
import dev.caskeleton.application.techlog.management.command.DeleteDocumentCommand;
import dev.caskeleton.application.techlog.management.port.out.DocumentDeletionPort;
import dev.caskeleton.application.techlog.studio.service.StudioPermissions;
import dev.caskeleton.application.transaction.TransactionMode;
import dev.caskeleton.application.transaction.TransactionPort;
import java.util.Objects;
/**
* {@code deleteCaseDraft} 와 {@code deleteReferenceDraft}.
*
* <p>둘은 같은 일이고 대상 종류만 다르다 — {@code document} 한 테이블이 Case 와 Reference 를 {@code document_type} 으로 나눠
* 갖는다. 종류를 인자로 받아 조회 조건에 넣는 이유는, 그렇게 하지 않으면 Case 경로로 Reference 를 지울 수 있게 되기 때문이다.
*
* <p>공개된 기록은 지우지 않는다. 공개 화면과 검색과 다른 기록의 링크가 그것을 가리키고 있고, 조용히 사라지면 읽던 쪽에서 무슨 일이 있었는지 알 방법이 없다 — 공개
* 취소가 그 경로다.
*/
@RequiresPermission(StudioPermissions.WRITE)
@UseCaseCapability(
transactionMode = TransactionMode.WRITE,
idempotency = Idempotency.NOT_IDEMPOTENT,
repositoryAccess = RepositoryAccess.WRITE_REPOSITORY)
public class DeleteDocumentDraftUseCase {
private final DocumentDeletionPort documents;
private final TransactionPort transactions;
public DeleteDocumentDraftUseCase(DocumentDeletionPort documents, TransactionPort transactions) {
this.documents = Objects.requireNonNull(documents, "documents");
this.transactions = Objects.requireNonNull(transactions, "transactions");
}
public void handle(DeleteDocumentCommand command, String documentType) {
Objects.requireNonNull(command, "command");
Objects.requireNonNull(documentType, "documentType");
transactions.inWrite(
() -> {
DocumentDeletionPort.DeletableDocument current =
documents
.findDocument(command.id(), documentType)
.orElseThrow(
() ->
ManagementException.of(
ManagementError.DOCUMENT_NOT_FOUND, "no such working copy"));
if ("PUBLISHED".equals(current.workflowStatus())) {
throw ManagementException.of(
ManagementError.DOCUMENT_PUBLISHED,
"the record is published; unpublish it before deleting");
}
if (documents.documentReferenced(command.id())) {
throw ManagementException.of(
ManagementError.DOCUMENT_IN_USE,
"another record still links to this one; unlink it first");
}
if (documents.deleteDocument(command.id(), command.expectedVersion()) == 0) {
throw ManagementException.withDetails(
ManagementError.VERSION_CONFLICT,
"the working copy changed since it was loaded",
new SaveTopicUseCase.VersionConflict(current.version()));
}
return null;
});
}
}
@@ -0,0 +1,60 @@
package dev.caskeleton.application.techlog.management.service;
import dev.caskeleton.application.capability.Idempotency;
import dev.caskeleton.application.capability.RepositoryAccess;
import dev.caskeleton.application.capability.UseCaseCapability;
import dev.caskeleton.application.security.RequiresPermission;
import dev.caskeleton.application.techlog.error.ManagementError;
import dev.caskeleton.application.techlog.error.ManagementException;
import dev.caskeleton.application.techlog.management.command.DeleteDocumentCommand;
import dev.caskeleton.application.techlog.management.port.out.DocumentDeletionPort;
import dev.caskeleton.application.techlog.studio.service.StudioPermissions;
import dev.caskeleton.application.transaction.TransactionMode;
import dev.caskeleton.application.transaction.TransactionPort;
import java.util.Objects;
/**
* {@code deleteQuestion}. 질문은 {@code document} 가 아니라 {@code open_question} 이 갖고 있어 별도 use case 다 —
* 같은 화면의 같은 줄로 보이지만 다른 테이블이다.
*/
@RequiresPermission(StudioPermissions.WRITE)
@UseCaseCapability(
transactionMode = TransactionMode.WRITE,
idempotency = Idempotency.NOT_IDEMPOTENT,
repositoryAccess = RepositoryAccess.WRITE_REPOSITORY)
public class DeleteQuestionUseCase {
private final DocumentDeletionPort documents;
private final TransactionPort transactions;
public DeleteQuestionUseCase(DocumentDeletionPort documents, TransactionPort transactions) {
this.documents = Objects.requireNonNull(documents, "documents");
this.transactions = Objects.requireNonNull(transactions, "transactions");
}
public void handle(DeleteDocumentCommand command) {
Objects.requireNonNull(command, "command");
transactions.inWrite(
() -> {
DocumentDeletionPort.DeletableQuestion current =
documents
.findQuestion(command.id())
.orElseThrow(
() ->
ManagementException.of(
ManagementError.QUESTION_NOT_FOUND, "no such question"));
if (documents.questionReferenced(command.id())) {
throw ManagementException.of(
ManagementError.QUESTION_IN_USE,
"another record still links to this question; unlink it first");
}
if (documents.deleteQuestion(command.id(), command.expectedVersion()) == 0) {
throw ManagementException.withDetails(
ManagementError.VERSION_CONFLICT,
"the question changed since it was loaded",
new SaveTopicUseCase.VersionConflict(current.version()));
}
return null;
});
}
}
+52 -46
View File
@@ -5,11 +5,12 @@ info:
description: |-
⚠ 봉투 결정(ADR-006) 부분 반영 — 이 파일에는 두 모양이 공존한다.
topics 4개, projects 5개, releases 7개를 studio-v1.yaml과 같은 방식으로
변환했다 (`application/json` + ErrorEnvelope / <Payload>Envelope). 그 16개가
구현된 것이자 소비자가 있는 것이기 때문이다.
topics 4개, projects 5개, releases 7개, 그리고 작업본 삭제 3개
(deleteCaseDraft / deleteReferenceDraft / deleteQuestion)를 studio-v1.yaml과
같은 방식으로 변환했다 (`application/json` + ErrorEnvelope / <Payload>Envelope).
그 19개가 구현된 것이자 소비자가 있는 것이기 때문이다.
나머지 63개는 아직 bare payload + `application/problem+json` + ProblemDetails
나머지 60개는 아직 bare payload + `application/problem+json` + ProblemDetails
다. 구현에 착수할 때 같은 방식으로 따라온다 — 소비자가 없는 operation을 미리
변환해 두면 검증되지 않은 모양이 계약에 고정된다.
@@ -260,45 +261,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:
@@ -509,45 +510,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:
@@ -1795,45 +1796,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:
@@ -5270,6 +5271,11 @@ components:
- RELEASE_NOT_FOUND
- RELEASE_VERSION_TAKEN
- RELEASE_NOT_PUBLISHABLE
- DOCUMENT_NOT_FOUND
- DOCUMENT_PUBLISHED
- DOCUMENT_IN_USE
- QUESTION_NOT_FOUND
- QUESTION_IN_USE
- INTERNAL_ERROR
description: '`INTERNAL_ERROR` 는 이 기능이 아니라 스켈레톤의 공통 처리기가 내는 코드다. 계약이 그것까지 열거해야 500 응답이 계약을 벗어나지 않는다.'
category: