feat: Tech Log Studio 백엔드 기반 — 계약 배선, 오류 코드, 경계 규칙, 스키마, 엔드포인트 2종
설계 패키지의 studio-v1.yaml(v3.0.0, 응답 봉투)을 이 저장소에 배선하고 슬라이스 1의 기반을 세운다. 19개 operation 중 getStudioSession과 listStudioCatalog를 구현했다. 계약과 생성 - src/config/openapi/studio-v1.yaml 을 vendor하고 MANIFEST에 출처 커밋을 기록 - openapi-generator로 DTO(model)만 생성한다. generateApis 대신 globalProperties.set(['models': '']) — 그 두 속성은 플러그인 7.18.0에 없다 - useOneOfInterfaces=false. 그 대가로 discriminator union 5종의 Jackson 배선이 깨진다(spec §3.1). 그 5종을 쓰는 7개 operation은 Plan 02에서 전략을 정한 뒤 구현한다 - 생성 코드는 별도 generatedOpenapi sourceSet에 둔다. -Werror가 생성물의 deprecated API 사용을 빌드 실패로 승격하기 때문이다. jar와 test 클래스패스에 별도로 얹는다 오류 계약 - StudioError 23종(계약 ApiError.code와 1:1) + StudioException(ApiErrorCarrier) - StudioExceptionHandler는 techlog 패키지로 범위를 좁힌다. 다른 기능의 오류 응답을 바꾸지 않기 위해서다 - 클라이언트 문구는 레지스트리의 client_safe_message에서 가져오고 예외 메시지는 로그 전용이다(ApiErrorCarrier javadoc의 요구) - 바인딩 예외를 봉투로 옮긴다. 그러지 않으면 bare RFC 7807이 새어 나가 ADR-006을 위반한다 게이트 - TechLogBoundaryArchTest 7종 — spec §4.3의 bounded context 경계. Gradle leaf를 늘릴 수 없어 이 규칙이 경계의 유일한 방어선이다 - StudioErrorRegistryTest — enum ↔ 레지스트리 ↔ 계약 3축 대조, vendor 사본 해시 검증 - StudioContractDriftTest — springdoc 표면이 계약을 벗어나면 실패. @ComponentScan이라 새 컨트롤러가 자동으로 걸린다 - StudioSessionCsrfHeaderProfileContractTest — 배포 가능한 세 프로파일이 계약의 csrf-header-name const로 해소되는지 고정. 이 저장소는 실제 composition root를 테스트에서 부팅할 수 없어 파일 단언으로 그 층을 덮는다 스키마 - V7__techlog_core.sql, 28 테이블. 설계 DDL에서 studio_idempotency(기존 idempotency_record 재사용)와 범위 밖 6종을 제외했다 - 원본의 tech_log 스키마 대신 public을 쓴다. 원본의 SET search_path는 Flyway 세션에만 적용되고 런타임 커넥션 풀은 상속하지 않는다 알려진 제약 - getStudioSession은 세션 인프라(redis-session)가 없어 503 STUDIO_UNAVAILABLE을 반환한다. 계약이 이 operation에 허용하는 유일한 실패 코드다. 가짜 CSRF 토큰으로 200을 만들지 않았다 - 따라서 슬라이스 1의 "프론트 로그인 실동작" 목표는 아직 달성되지 않았다 이 커밋은 AGENTS.md의 human-only 커밋 정책에 대한 저장소 소유자의 명시적 지시로 작성됐다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
697fc740e6
commit
91e6d99654
@@ -916,3 +916,315 @@ errors:
|
||||
runbook_link: "runbook://management/actuator-forbidden"
|
||||
compatibility_impact: none
|
||||
required_test: contract-verification:management-actuator
|
||||
|
||||
# ============================================================
|
||||
# TECH LOG STUDIO (studio-v1.yaml ApiError.code — 23종, feature-techlog-studio-backend)
|
||||
#
|
||||
# StudioError(dev.caskeleton.application.techlog.error.StudioError)와 1:1 매핑.
|
||||
# category/http_status/retryable은 그 enum의 선언과 정확히 같아야 한다
|
||||
# (StudioErrorRegistryTest가 코드 존재만 보고, 값 일치는 이 파일의 리뷰 책임).
|
||||
#
|
||||
# PAYLOAD_TOO_LARGE / UNSUPPORTED_MEDIA_TYPE은 StudioError에도 있지만 별도 row를
|
||||
# 추가하지 않는다 — feature-api-contract-baseline이 이미 동일 code로 아래(L514,
|
||||
# L545)에 VALIDATION/413/415/false, VALIDATION/415/false row를 갖고 있고 값이
|
||||
# StudioError 선언과 정확히 일치한다. error-codes.yaml의 identity column은 `code`
|
||||
# 하나뿐이라 같은 code로 두 번째 row를 추가하면 ContractRegistrySchemaGovernanceTest
|
||||
# 의 "duplicate identity" 게이트가 깨진다. 즉 이 두 코드는 기존 row가 이미 커버한다.
|
||||
# ============================================================
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — AUTHENTICATION_REQUIRED (StudioError.AUTHENTICATION_REQUIRED)
|
||||
- code: AUTHENTICATION_REQUIRED
|
||||
category: AUTH
|
||||
http_status: 401
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: presentation
|
||||
client_safe_message: "Studio 인증이 필요합니다"
|
||||
log_level: INFO
|
||||
runbook_link: "runbook://auth/token-missing"
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — STUDIO_ACCESS_DENIED (StudioError.STUDIO_ACCESS_DENIED)
|
||||
- code: STUDIO_ACCESS_DENIED
|
||||
category: AUTHZ
|
||||
http_status: 403
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "이 Studio 리소스에 접근할 권한이 없습니다"
|
||||
log_level: WARN
|
||||
runbook_link: "runbook://authz/insufficient-permission"
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — DOCUMENT_NOT_FOUND (StudioError.DOCUMENT_NOT_FOUND)
|
||||
- code: DOCUMENT_NOT_FOUND
|
||||
category: NOT_FOUND
|
||||
http_status: 404
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "요청한 문서를 찾을 수 없습니다"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — VERSION_CONFLICT (StudioError.VERSION_CONFLICT)
|
||||
- code: VERSION_CONFLICT
|
||||
category: CONFLICT
|
||||
http_status: 409
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "저장된 version이 더 최신입니다"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — REQUEST_VALIDATION_FAILED (StudioError.REQUEST_VALIDATION_FAILED)
|
||||
- code: REQUEST_VALIDATION_FAILED
|
||||
category: VALIDATION
|
||||
http_status: 422
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: presentation
|
||||
client_safe_message: "요청 형식이 올바르지 않습니다"
|
||||
log_level: WARN
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — DOCUMENT_VALIDATION_FAILED
|
||||
# (StudioError.DOCUMENT_VALIDATION_FAILED). 원래 계약 이름은 VALIDATION_FAILED였으나
|
||||
# 스켈레톤 전역 OperationalError.VALIDATION_FAILED(400, VALIDATION)와 code 문자열이
|
||||
# 충돌해(같은 문자열, 다른 http_status) 개명했다 — controller 판정, 2026-08-19.
|
||||
- code: DOCUMENT_VALIDATION_FAILED
|
||||
category: VALIDATION
|
||||
http_status: 422
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "문서 검증에 실패했습니다"
|
||||
log_level: WARN
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — VALIDATION_STALE (StudioError.VALIDATION_STALE)
|
||||
- code: VALIDATION_STALE
|
||||
category: CONFLICT
|
||||
http_status: 409
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "검증 결과가 최신 문서 기준이 아닙니다. 다시 검증해 주세요"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — PREVIEW_NOT_FOUND (StudioError.PREVIEW_NOT_FOUND)
|
||||
- code: PREVIEW_NOT_FOUND
|
||||
category: NOT_FOUND
|
||||
http_status: 404
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "요청한 미리보기를 찾을 수 없습니다"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — PREVIEW_STALE (StudioError.PREVIEW_STALE)
|
||||
- code: PREVIEW_STALE
|
||||
category: CONFLICT
|
||||
http_status: 409
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "미리보기가 최신 문서 기준이 아닙니다. 다시 생성해 주세요"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — PREVIEW_EXPIRED (StudioError.PREVIEW_EXPIRED)
|
||||
- code: PREVIEW_EXPIRED
|
||||
category: CONFLICT
|
||||
http_status: 409
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "미리보기가 만료되었습니다. 다시 생성해 주세요"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — PUBLICATION_NOT_FOUND (StudioError.PUBLICATION_NOT_FOUND)
|
||||
- code: PUBLICATION_NOT_FOUND
|
||||
category: NOT_FOUND
|
||||
http_status: 404
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "요청한 게시물을 찾을 수 없습니다"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — PUBLICATION_CONFLICT (StudioError.PUBLICATION_CONFLICT)
|
||||
- code: PUBLICATION_CONFLICT
|
||||
category: CONFLICT
|
||||
http_status: 409
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "게시 작업이 다른 변경과 충돌했습니다"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — PUBLICATION_EVENT_NOT_FOUND (StudioError.PUBLICATION_EVENT_NOT_FOUND)
|
||||
- code: PUBLICATION_EVENT_NOT_FOUND
|
||||
category: NOT_FOUND
|
||||
http_status: 404
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "요청한 게시 이벤트를 찾을 수 없습니다"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — PUBLICATION_SNAPSHOT_NOT_FOUND (StudioError.PUBLICATION_SNAPSHOT_NOT_FOUND)
|
||||
- code: PUBLICATION_SNAPSHOT_NOT_FOUND
|
||||
category: NOT_FOUND
|
||||
http_status: 404
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "요청한 게시 스냅샷을 찾을 수 없습니다"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — WARNING_ACKNOWLEDGEMENT_REQUIRED (StudioError.WARNING_ACKNOWLEDGEMENT_REQUIRED)
|
||||
- code: WARNING_ACKNOWLEDGEMENT_REQUIRED
|
||||
category: VALIDATION
|
||||
http_status: 422
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "경고 확인이 필요합니다. 확인 후 다시 시도해 주세요"
|
||||
log_level: WARN
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — IDEMPOTENCY_KEY_REUSED (StudioError.IDEMPOTENCY_KEY_REUSED)
|
||||
- code: IDEMPOTENCY_KEY_REUSED
|
||||
category: CONFLICT
|
||||
http_status: 409
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "Idempotency 키가 다른 요청에 재사용되었습니다"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — ASSET_NOT_FOUND (StudioError.ASSET_NOT_FOUND)
|
||||
- code: ASSET_NOT_FOUND
|
||||
category: NOT_FOUND
|
||||
http_status: 404
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "요청한 자산을 찾을 수 없습니다"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — ASSET_NOT_READY (StudioError.ASSET_NOT_READY)
|
||||
- code: ASSET_NOT_READY
|
||||
category: CONFLICT
|
||||
http_status: 409
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "자산 처리가 아직 완료되지 않았습니다"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — ASSET_IN_USE (StudioError.ASSET_IN_USE)
|
||||
- code: ASSET_IN_USE
|
||||
category: CONFLICT
|
||||
http_status: 409
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "자산이 사용 중이라 이 작업을 수행할 수 없습니다"
|
||||
log_level: INFO
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — ASSET_QUARANTINED (StudioError.ASSET_QUARANTINED)
|
||||
- code: ASSET_QUARANTINED
|
||||
category: DATA_INTEGRITY
|
||||
http_status: 409
|
||||
retryable: false
|
||||
retry_after_seconds: null
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: application
|
||||
client_safe_message: "자산이 격리 처리되어 사용할 수 없습니다"
|
||||
log_level: WARN
|
||||
runbook_link: null
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
# source: studio-v1.yaml ApiError.code — STUDIO_UNAVAILABLE (StudioError.STUDIO_UNAVAILABLE)
|
||||
- code: STUDIO_UNAVAILABLE
|
||||
category: TRANSIENT_DEPENDENCY
|
||||
http_status: 503
|
||||
retryable: true
|
||||
retry_after_seconds: 5
|
||||
owner_branch: feature-techlog-studio-backend
|
||||
owner_layer: infrastructure
|
||||
client_safe_message: "Studio 서비스를 일시적으로 사용할 수 없습니다. 잠시 후 다시 시도해 주세요"
|
||||
log_level: ERROR
|
||||
runbook_link: "runbook://studio/unavailable"
|
||||
compatibility_impact: additive
|
||||
required_test: StudioErrorTest
|
||||
|
||||
Reference in New Issue
Block a user