main
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
561d02ae3a |
feat: 프로젝트 게시와 홈 focus, 그리고 기록 사이 연결을 실제로 가능하게 한다
계약에 선언만 되어 있고 구현이 없던 네 오퍼레이션과, 의도된 스텁으로 남아 있던 catalog 두 종류가 공개 화면 다섯 곳을 조용히 비워 두고 있었다. 프로젝트 게시 — 프로젝트는 `RecordKind` 에 없어 문서 게시 파이프라인을 타지 못하는데, 공개 조회들(프로젝트 목록·상세·프로필의 "현재 프로젝트"·홈 focus)은 전부 `public_resource_projection` 의 PROJECT 행을 가시성 관문으로 쓴다. 그 행을 세우는 경로가 없었으므로 프로젝트는 만들 수는 있어도 공개될 수는 없었다. 릴리스가 자체 경로를 갖는 것과 같은 이유로, 문서 파이프라인에 끼워 넣지 않고 원본 상태·투영·canonical route 세 곳을 한 트랜잭션에서 함께 세운다. 홈 focus — `home_focus_config` 는 마이그레이션이 빈 행 하나만 넣어 두었고 그 값을 읽고 쓸 use case 가 없었다. 지목한 대상이 실제로 있는지는 여기서 확인한다. 테이블에 FK 가 없어(설정이 대상보다 오래 살아남는 것을 허용하는 설계다) 없는 id 도 저장되고, 그러면 공개 화면은 조용히 빈 focus 를 그린다 — 저장은 성공했는데 아무것도 안 나오는, 이유를 알 수 없는 실패가 된다. 대상이 공개인지는 확인하지 않는다: 미리 지목해 두고 게시와 동시에 뜨게 하는 것이 정상적인 순서다. catalog RELATION/EVIDENCE — 「슬라이스 2·5에서 채운다」는 주석과 함께 `List.of()` 로 남아 있었다. 그래서 어떤 기록도 연결 대상 목록을 채울 수 없었다. RELATION 은 작성 중에 고르는 것이라 작업본까지 포함하고(두 문서를 같이 쓰면서 서로 잇는 것이 정상적인 순서다), EVIDENCE 는 읽는 사람이 따라갈 수 있어야 하므로 공개된 것만 포함한다. 계약은 이 네 오퍼레이션을 ProblemDetails 모양으로 두고 있었다. 백엔드가 모든 JSON 응답을 envelope 으로 감싸므로 구현하는 순간 어긋난다 — 나머지와 같은 모양으로 옮겼다. `/home-focus` 는 케밥 세그먼트라 D19(AIP-122)를 위반해 `/home/focus` 로 나눴다. 함께 고친 것들 (모두 이 작업 전부터 빨간 상태였다): - `error-codes.yaml` 의 DOCUMENT_NOT_FOUND 가 표면마다 하나씩 두 행이었다. 이 레지스트리의 식별자는 code 하나뿐이라 로딩 자체가 깨졌고, 그 여파로 거버넌스 테스트 네 개와 outbox 계약 테스트가 함께 넘어졌다. 같은 코드는 같은 말을 해야 한다. - 벤더된 계약 세 개의 MANIFEST.sha256 이 실제 파일과 어긋나 있었다. - ActuatorSecurityHttpTest 는 "DB·Redis 없는 슬라이스"라고 적어 두고 Redis 자동설정을 막지 않아, localhost:6379 연결 실패가 /actuator/health 를 503 으로 만들었다. 보안 태세와 무관한 이유로 빨개지던 테스트다. - 테스트 JVM 힙이 Gradle 기본 512m 이라 Spring context 캐시 + ArchUnit + Testcontainers 조합에서 OOM 이 났다. 증상이 테스트 실패가 아니라 "Executor 를 완료할 수 없음"이어서 원인을 가리켰다. - APP_SESSION_TIMEOUT 이 env-keys 레지스트리에 없었다. 새 SQL 은 실제 PostgreSQL 위에서 돌린다 — 컴파일도 단위 테스트도 컬럼 이름을 검증하지 못한다는 것이 이 파일이 존재하는 이유다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu |
||
|
|
96521a94d4 |
feat: serve uploaded media, delete a decision, and stop orphaning publications
Three gaps that all showed up as something broken on screen. An uploaded image could never be fetched. Every asset address the backend builds pointed at a path nothing served, so a figure was missing in the Studio preview and in the published page alike. There is now a public media endpoint under the prefix that is already anonymous and already routed, so no edge configuration has to change for an image to appear. Only READY assets are served; narrowing it to assets a published record uses would break previewing, which is the one thing an author does before publishing. Deleting a working copy left wreckage. Publications, validations and previews address a document by (kind, id) with no foreign key, so deleting one left rows pointing at nothing — and the dashboard and publication history read a title off that nothing and crashed. Both screens went down in production this way. A record with publication history is now refused outright, because that history says what was once public; validations and previews go with the record, because they are its scaffolding. A Decision could not be deleted at all while every other kind could. It can now, under the same two rules, and refuses when another decision supersedes it or the home page features it — neither cascades, so deleting would have been a foreign-key violation reaching the author as a 500. The persistence tests cover every one of these queries against real PostgreSQL. That suite exists because a column name I assumed rather than checked reached production once already; three more assumptions were caught here before it could happen again. |
||
|
|
1befdc37a4 |
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. |
||
|
|
386f360122 |
feat: implement release authoring, so the changelog can be written
The public site has a Releases page and a footer link to the latest release, and both were empty — the read side has existed since the public surface landed, but nothing could ever create a row. The seven release operations were in the contract with no implementation, so the changelog was a page that could only ever be blank. The release model is not a blob of prose. It splits into six markdown sections because a release note answers fixed questions — why, what, what changes for a reader, what it leaves in the code, how it was verified, what is still missing — and a single text column cannot say which of those went unanswered. Publishing is the only thing that makes a release public: the public query filters on `workflow_status = 'PUBLISHED'` and nothing else. So publish is where the contract's required fields are actually enforced. Saving stays permissive — a draft you cannot save until it is complete is a draft you cannot write — and the two demands are deliberately different. `version_label` is NOT NULL UNIQUE but a draft has no version yet, so creation writes a placeholder derived from the row id and publication refuses to ship one. Relaxing the column instead would open a window where a published release is publicly visible with no version at all. A published release cannot be deleted, only archived: a public changelog entry that vanishes leaves everyone who linked it with no way to learn what happened. Also registers `adapter-outbound-objectstorage` as an app-bootstrap runtime member. It was added as a dependency when asset upload was fixed but never registered, and `verifyRuntimeModuleMembership` had not been run since. |
||
|
|
bb6d2330bb |
feat: implement topic and project management, so documents can be authored
Publishing was impossible on an empty database. Validation requires a topic,
the studio catalog answered zero topics, and nothing in the two implemented
contracts could create one — `studio-management-v1.yaml` owned that surface
and none of its 79 operations existed. Every path to a published record ran
through a door with no handle.
This implements the nine that unblock authoring: topics (list/create/update/
delete) and projects (list/get/create/update/delete). The remaining seventy
stay unimplemented; each has its own consumer and its own moment.
The contract was converted to the response envelope first (ADR-006), which is
what its own header said to do at implementation time. Doing it after would
have meant changing the wire shape of endpoints the frontend had already been
written against.
ManagementError is a separate enum rather than an extension of StudioError.
Each contract enumerates its own ApiError.code set, so a code reachable from
the wrong surface makes that contract false. It deliberately omits
INTERNAL_ERROR: the skeleton's OperationalError owns that code with
retryable=true, and declaring it twice with different values leaves the
registry with no answer. PublicError made the same call for the same reason.
Two contract defects surfaced while implementing. TopicEdit had neither id nor
version, so a listed topic could not be addressed by the `/topics/{id}` path
and a client had no source for the expectedVersion the write operations
require; both are fixed in the design package. The AWS SDK BOM had to be
imported in app-bootstrap as well — module-scoped dependency management does
not propagate to consumers, and this is the first runtime consumer of that
pattern.
Topic and project deletion refuse while records still reference them rather
than cascading. A topic disappearing should not silently reclassify the
documents that used it; moving them first is the caller's decision to make.
ActuatorSecurityHttpTest.healthEndpointIsPermitAll fails on this branch before
this change as well; it is untouched here.
|
||
|
|
365560efb6 |
feat: Tech Log 공개 조회 백엔드 — public-v1 18개 operation 구현
public-v1.yaml의 18개 operation 전부를 구현한다. 사이트·홈·프로필, 탐색 2종, 주제
2종, 문서 상세 3종, 프로젝트 5종, 릴리스 2종, 검색. studio-v1(19/19)에 이어
public-v1도 18/18이다.
생성기가 계약 필드를 조용히 빠뜨리고 있었다 — 근본 원인은 파생 단계의 YAML alias
swagger-parser가 이 문서의 스키마 15개를 "is not of type `object`"로 거절했다.
거절당한 스키마들은 전부 type: object를 명시하고 있어서 계약 결함처럼 보이지 않았고,
validateSpec을 끄면 생성은 성공했다. 그런데 그렇게 만든 모델에서 LatestEntry.publishedAt,
ProjectListItem.updatedAt, SearchResultItem.matchedFields, ReleaseListItem.changeTypes가
사라져 있었다. 컴파일은 통과한다 — 아직 아무도 그 필드를 안 쓰니까.
원인은 prepare 단계였다. 변환들이 같은 Map 인스턴스를 여러 property에 재사용했고
snakeyaml이 그 지점을 anchor/alias(&id001 / *id001)로 덤프했다. swagger-parser는
alias 노드를 해석하지 못해 그 스키마 전체를 거절하고, generator는 검증을 끄면 문서를
받아들이되 alias였던 property를 말없이 버린다. 파생 스펙에 alias가 34곳 있었다.
- 덤프 직전 deep copy로 노드 identity를 끊어 alias를 원천 차단하고, 남으면 빌드가
실패하도록 fail-closed 게이트를 뒀다. validateSpec은 다시 켰다
- verifyPublicGeneratedModels를 schema 이름 대조에서 property 대조로 강화했다.
이번 누락을 이 게이트가 통과시켰기 때문이다. 지금은 schema 62개 · property 250개를 센다
계약이 선언했는데 서버가 무시하던 필터를 채웠다
지정해도 오류가 아니라 "결과 0건"으로 보여서 소비자가 자기 요청이 틀렸다는 걸 알 수 없었다.
- exploreQuestions: tag 필터 없음, sort 3값이 SQL에 반영되지 않음
- listPublicProjectDecisions: status 필터 없음
- listPublicProjectRecords: type/relation 필터 없음, QUESTION이 대상에서 빠져 있었음
- 필터는 목록과 총계 두 쿼리에 같이 걸린다. 갈라지면 마지막 페이지가 비어 보인다
- enum 파라미터는 요청 경계에서 검사해 PUBLIC_REQUEST_INVALID로 거절한다
응답 봉투와 오류 경계
- 컨트롤러는 봉투를 반환하지 않는다. EnvelopeBodyAdvice가 감싼다(ADR-006)
- PublicExceptionHandler를 publicapi 스코프로 두고, StudioExceptionHandler의 스코프를
...web.techlog → ...web.techlog.studio로 좁혔다. 좁히지 않으면 공개 조회의 파라미터
오류가 Studio 계약 코드(REQUEST_VALIDATION_FAILED, 422)로 나가는데, 그 코드는
public-v1의 ApiError.code enum에 없어 프론트엔드의 응답 파싱 자체가 깨진다
- FieldError 모양이 studio({path,message})와 public({field,code,message})이 다르다
실행이 잡아낸 결함
컴파일과 단위 테스트로는 드러나지 않았고 실제 PostgreSQL과 실제 기동이 잡았다.
- profile()의 selectedEvidence가 List.of() 하드코딩이었다. 계약 필드가 항상 비어 있었다
- latestEntries/latestRecords가 projection의 모든 resource_type을 흘렸다. 계약의
LatestEntry.entryType은 4값뿐이라 QUESTION이 섞이면 매퍼가 500을 낸다
- home_focus_config.default_focus_type은 마이그레이션 직후 NULL인데 계약은 이 필드를
required + enum 3값으로 선언한다. 배포 직후 첫 요청부터 /home이 깨졌다.
HomeFocusView.resolve가 반드시 유효한 값 하나를 정하도록 고쳤다
V9__techlog_public_surface.sql
설계 패키지 database/V1__init.sql이 정의한 공개 표면 6종(release, site_config,
profile_page, home_focus_config, project_topic, topic_featured_document)과 단일 행
시딩. 릴리스는 Publication 파이프라인을 거치지 않고 자체 workflow_status로 공개된다.
게이트
- PublicContractDriftTest: springdoc이 게시하는 표면과 계약을 양방향 대조한다.
계약의 servers(/api/v1/public)를 경로에 더해 비교하며, operation 수 18을 함께 고정해
"비교 대상이 0건이라 통과"를 실패로 만든다. 봉투 래핑도 확인한다
- PublicErrorRegistryTest: PublicError ↔ error-codes.yaml ↔ 계약 enum 3자 대조.
INTERNAL_ERROR는 스켈레톤 소유라 재선언하지 않으므로 "계약 = public 소유 ∪ 그 하나"로
고정한다. vendored 계약의 MANIFEST 해시도 확인한다
- postgresqlTechLogPublicPersistenceIntegrationTest: 어댑터 7종과 V9를 실제
PostgreSQL에서 돌린다. 표준 check는 Testcontainers를 돌리지 않으므로 이 태스크가
없으면 이 SQL은 한 번도 실행되지 않은 채 빌드가 통과한다. 게시 취소·비공개 자료를
함께 심어 어느 경로로도 새지 않는지 확인한다
검증
./gradlew check BUILD SUCCESSFUL (248 task). 공개 조회 통합 테스트 24/24.
실제 PostgreSQL로 앱을 띄워 18개 operation 전부 실호출 — 5xx 0건, 파라미터 검증 5종
전부 계약 코드. 한때 사라졌던 publishedAt/matchedFields/changeTypes가 실응답에 있다.
알려진 선재 실패: ActuatorSecurityHttpTest가 /actuator/health 503으로 실패한다.
기저 커밋 743fee3에서도 동일하게 재현되며, 원인은 redis가 호스트 포트에 노출되지 않아
헬스가 DOWN인 환경 문제다. 이 커밋과 무관하다.
AGENTS.md의 commit 정책은 human-only다. 이 커밋은 사용자가 "지금 변경했던 내용을
전부 반영하고 develop과 main에 반영하도록" 지시해 예외로 수행한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
48ff648112 |
feat: Tech Log Studio 백엔드 — 남은 17개 operation 구현 (슬라이스 2~5)
studio-v1.yaml 19개 operation 중 Plan 01이 남긴 17개를 구현한다. 문서 CRUD,
검증·미리보기, 게시, Asset. 이로써 studio-v1은 19/19다.
Plan 01이 Plan 02로 미룬 생성기 union 차단 요인
- 계약 원본은 그대로 두고 prepareStudioCodegenSpec이 생성 직전에 사본을 파생시킨다.
oneOf+discriminator를 가진 스키마의 하위 타입에 x-implements를 주입하고, union 자체는
생성을 억제한 뒤 같은 package에 Jackson 다형성 인터페이스를 계약에서 파생해 써 넣는다
- 파생 규칙을 계약의 oneOf/discriminator.mapping에서 읽으므로 union 목록을 손으로
관리하지 않는다. 계약에 union이 늘면 따라온다
- openApiNullable=false. JsonNullable을 읽는 모듈은 Jackson 2용인데 이 앱의 HTTP
변환기는 Jackson 3(tools.jackson)다 — 등록될 수 없어 직렬화가 POJO로 새고
역직렬화가 깨진다. 해당 필드는 계약상 required라 "없음"과 "null"을 구분할 필요도 없다
- 모든 분기가 type:string인 이름 없는 oneOf는 접는다. 안 접으면 필드 0개 껍데기
클래스가 나와 slug가 {}로 직렬화된다
- oneOf:[X,null]도 접는다. 그대로 두면 같은 모양의 래퍼 타입이 7벌 더 생긴다
- StudioContractUnionJacksonTest가 이 배선을 지킨다. 파생이 깨지면 컴파일이 깨진다
설계 스키마의 구멍 — V8__techlog_studio_working_copy.sql
V7(설계 패키지 database/V1__init.sql)은 유형마다 다른 물리 모델인데 계약은 네 유형을
공통 base + 유형별 확장이라는 하나의 편집 흐름으로 다룬다. 계약이 요구하는데 없던 것:
- document.summary / case_detail.environment,reproduction / reference_detail.rules,examples
- open_question.options,resolution_evidence_target_id,resolution_link_label
- project_decision.title,slug,summary,primary_topic_id
- problem/conclusion/scope_summary/statement가 varchar라 계약의 100000자를 담을 수 없어 text로 넓힘
- project_decision.project_id NOT NULL은 계약이 명시적으로 허용한 초안 저장을
구조적으로 막고 있었다(게시 필수 여부는 검증이 판단한다) — 풀었다
- studio_relation: 계약의 relations[]는 네 유형 공통이고 항목마다 자체 id와 reason이
있다. document_relation은 복합 PK라 둘 다 없고 문서끼리만 성립한다.
(source_kind, source_id) 다형 참조는 studio_validation/studio_preview가 이미 쓰는 방식이다
영속은 JdbcClient
spec §8.3은 쓰기에 JPA @Version을 적었지만 이 네 aggregate는 Studio 저장 경로에서만
쓰이고 UPDATE ... WHERE version = :expectedVersion의 갱신 행 수가 정확히 같은 의미를
준다. 여덟 개 넘는 테이블에 엔티티를 세우는 비용에 상응하는 이득이 없다. 포트 계약이
같으므로 나중에 JPA가 필요하면 어댑터만 바뀐다.
nextAction/dependencyRevision 계산은 SQL 한 벌(StudioDocumentSql)
목록과 상세가 각자 계산하면 "목록에선 게시하라더니 열어보니 검증하라"가 된다.
계약의 nextAction 필터도 SQL이라야 페이지네이션을 깨지 않고 걸 수 있다.
렌더러 (ADR-005)
- commonmark + GFM 확장. 설계 05장 §16대로 라이브러리는 render 패키지 밖으로 안 나간다.
프론트가 remark 계열로 같은 CommonMark+GFM 기준을 쓰므로 동등성이 유지된다
- ::: directive는 줄 단위 스캔이다. v1 문법에서 중첩이 없고 줄 맨 앞에서만 열린다.
코드 펜스 안의 :::는 directive로 보지 않는다
- 컨테이너/leaf 판정은 닫는 줄이 실제로 있는지로 한다. 이름 목록으로 정하면 directive를
더할 때마다 목록을 고쳐야 하고, "닫는 줄 없으면 문서 끝까지"면 닫기를 빠뜨린
directive 하나가 뒤 내용을 통째로 삼킨다
- 계약이 표현 못 하는 것은 조용히 바꾸지 않고 경고로 남긴다 — 수평선, 머리글 없는 표,
알 수 없는 directive, 미해결 asset key(경로를 지어내지 않고 버린다)
- RenderModelPort 구현이 inbound web에 있다. 렌더 모델은 계약 DTO이고 그 타입을 소유한
모듈이 거기다. application에 같은 모양을 한 벌 더 두면 두 정의가 갈라진다
검증 체인
판정 기준은 하나다 — 이 편집본으로 계약이 요구하는 PublicRenderModel을 만들 수 있는가.
각 규칙은 렌더 모델의 required/minLength/minItems에서 나온다. 다른 기준을 쓰면 검증을
통과한 문서가 렌더 단계에서 계약을 위반한다. 첫 오류에서 멈추지 않고 끝까지 모은다.
게시 (spec §7.5 20단계)
- Snapshot의 렌더 모델은 게시 시점에 다시 렌더링하지 않고 사용자가 확인한 미리보기의
것을 그대로 쓴다. 다시 렌더링하면 승인한 화면과 공개된 화면이 달라질 수 있다
- 단계별 실패가 서로 다른 계약 코드로 나간다. DOCUMENT_VALIDATION_FAILED(지금 검증하면
실패)와 VALIDATION_STALE(통과했으나 전제가 바뀜)은 다른 사건이고 할 일도 다르다
- 게시 취소는 route도 Snapshot도 지우지 않는다. 지우면 공개된 링크가 끊긴다
Asset
- 확장자와 클라이언트 Content-Type을 신뢰하지 않고 파일 시작 바이트로 판정한다.
모르는 형식은 저장하지 않고 415로 거절한다
- 바이너리는 기존 object storage 어댑터에 위임한다(spec §9). ObjectStoragePort는
deprecated지만 이 저장소에서 실제 구현이 붙어 있는 유일한 포트다 — 선택을 브리지
한 클래스에 가뒀다. 저장 백엔드가 없는 배포는 업로드·삭제만 503이고 나머지는 동작한다
- 공개 이력이 있거나 사용 중인 Asset은 hard delete하지 않는다
검증 — "통과하는데 동작 안 함"을 세 겹으로 막았다
- StudioContractDriftTest에 반대 방향(계약 → published)을 추가했다. 기존 한 방향은
사라진 operation을 못 잡는다. 양방향 모두 실제로 RED가 되는 것을 확인했다
- postgresqlTechLogStudioPersistenceIntegrationTest 신규 11개. 이 저장소의 check는
Testcontainers를 돌리지 않아 이 테스트가 없으면 SQL이 한 번도 실행되지 않는다.
첫 실행에서 실제 결함을 잡았다: fk_publication_latest_event의 지연 검사는 트랜잭션
끝에 일어나므로 autocommit이면 첫 INSERT에서 위반된다 → 어댑터가 진입 시 활성
트랜잭션을 확인하고 아니면 원인을 그대로 말하며 실패한다
- 실제 앱 부팅으로 두 건을 더 잡았다. check에 전체 앱 부팅 테스트가 없어 생긴 구멍이다
1. 생성자 모호성 — 프로덕션/테스트 두 생성자에 표시가 없어 기본 생성자를 찾다 실패
2. final 클래스 + AOP — @RequiresPermission은 CGLIB 프록시를 쓰는데 final은 subclass
불가. 템플릿의 NotificationDispatchUseCase가 final이면서 무사한 것은 그 능력이 꺼진
배포에서 빈으로 등록되지 않아서다. Studio use case는 항상 등록된다
가드레일이 잡은 것
- MUTATING_USE_CASES_DECLARE_REQUIRED_PERMISSION → studio:write 부여, role 매핑은
프로파일에. application.yml의 role-permissions:{} 기준선은 SampleRemovalSmokeContractTest가 지킨다
- NO_CONTEXT_DEPENDS_ON_STUDIO_FACADE 223건 → 어댑터 패키지를 persistence.techlog.studio.*로
옮겼다. 규칙을 고치지 않았고, 그 이름이 우회가 아니라 더 정확하다
- verifyEnvKeys → 새 APP_ 키 4건 등록
범위
studio-v1의 19개 전부. public-v1(18) / studio-management-v1(79)은 spec §2.2가 선언한
out of scope다 — 전자는 소비자가 아직 없고 후자는 secondary capability 보존 계약이다.
검증: ./gradlew check BUILD SUCCESSFUL (245 task), 전체 3,721 테스트 실패 0,
techlog PostgreSQL 통합 테스트 3종 통과, 실제 앱 부팅 확인.
AGENTS.md의 commit 정책은 human-only다. 이 커밋은 사용자가 "전부 커밋하고 머지
진행하세요"로 명시적으로 지시해 예외로 수행한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
91e6d99654 |
feat: Tech Log Studio 백엔드 기반 — 계약 배선, 오류 코드, 경계 규칙, 스키마, 엔드포인트 2종
설계 패키지의 studio-v1.yaml(v3.0.0, 응답 봉투)을 이 저장소에 배선하고 슬라이스 1의 기반을 세운다. 19개 operation 중 getStudioSession과 listStudioCatalog를 구현했다. 계약과 생성 - src/config/openapi/studio-v1.yaml 을 vendor하고 MANIFEST에 출처 커밋을 기록 - openapi-generator로 DTO(model)만 생성한다. generateApis 대신 globalProperties.set(['models': '']) — 그 두 속성은 플러그인 7.18.0에 없다 - useOneOfInterfaces=false. 그 대가로 discriminator union 5종의 Jackson 배선이 깨진다(spec §3.1). 그 5종을 쓰는 7개 operation은 Plan 02에서 전략을 정한 뒤 구현한다 - 생성 코드는 별도 generatedOpenapi sourceSet에 둔다. -Werror가 생성물의 deprecated API 사용을 빌드 실패로 승격하기 때문이다. jar와 test 클래스패스에 별도로 얹는다 오류 계약 - StudioError 23종(계약 ApiError.code와 1:1) + StudioException(ApiErrorCarrier) - StudioExceptionHandler는 techlog 패키지로 범위를 좁힌다. 다른 기능의 오류 응답을 바꾸지 않기 위해서다 - 클라이언트 문구는 레지스트리의 client_safe_message에서 가져오고 예외 메시지는 로그 전용이다(ApiErrorCarrier javadoc의 요구) - 바인딩 예외를 봉투로 옮긴다. 그러지 않으면 bare RFC 7807이 새어 나가 ADR-006을 위반한다 게이트 - TechLogBoundaryArchTest 7종 — spec §4.3의 bounded context 경계. Gradle leaf를 늘릴 수 없어 이 규칙이 경계의 유일한 방어선이다 - StudioErrorRegistryTest — enum ↔ 레지스트리 ↔ 계약 3축 대조, vendor 사본 해시 검증 - StudioContractDriftTest — springdoc 표면이 계약을 벗어나면 실패. @ComponentScan이라 새 컨트롤러가 자동으로 걸린다 - StudioSessionCsrfHeaderProfileContractTest — 배포 가능한 세 프로파일이 계약의 csrf-header-name const로 해소되는지 고정. 이 저장소는 실제 composition root를 테스트에서 부팅할 수 없어 파일 단언으로 그 층을 덮는다 스키마 - V7__techlog_core.sql, 28 테이블. 설계 DDL에서 studio_idempotency(기존 idempotency_record 재사용)와 범위 밖 6종을 제외했다 - 원본의 tech_log 스키마 대신 public을 쓴다. 원본의 SET search_path는 Flyway 세션에만 적용되고 런타임 커넥션 풀은 상속하지 않는다 알려진 제약 - getStudioSession은 세션 인프라(redis-session)가 없어 503 STUDIO_UNAVAILABLE을 반환한다. 계약이 이 operation에 허용하는 유일한 실패 코드다. 가짜 CSRF 토큰으로 200을 만들지 않았다 - 따라서 슬라이스 1의 "프론트 로그인 실동작" 목표는 아직 달성되지 않았다 이 커밋은 AGENTS.md의 human-only 커밋 정책에 대한 저장소 소유자의 명시적 지시로 작성됐다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
697fc740e6 | chore: remove sample portfolio module | ||
|
|
e552e317d6 | chore: initialize tech log backend | ||
|
|
e64e701fe5 |
chore: initialize from backend template 0a6dd0e
|