docs: complete the locally-verifiable half of the release checklist
Second pass. The first pass called several sections impossible; most were
not. The Studio mock implements all 18 contract operations with real
semantics (optimistic locking, validation staleness, preview expiry,
warning acknowledgement, idempotency), so the whole authoring flow is
exercisable without a backend, and the Public surface's UI behaviour is
testable against its static content.
Corrections to the first pass:
- prod refusing to boot on the committed .env is the design working,
not a defect: five startup validators reject development values, two
of which were observed firing in order. The real gap is that no
production value set exists anywhere yet.
- ddl-auto=validate failing is a constraint, not a blocker -- prod
accepts none as well, which is how this run booted.
- two first-pass findings were false positives: the "Studio exposure"
hits were release-note body text (zero /studio links on any public
page), and the missing code block was a test artifact (no static
document contains one; injecting one renders correctly).
New defects found:
- no way to log out: the session button lives in the template's
AppShell, which TechLog never renders -- it supplies its own shells.
- duplicate relations are not prevented, at the contract level, so a
backend implementation would inherit the same hole.
- CLS 0.192, from a single footer shift at t=538ms.
- no index on navigation_path: 236ms seq scan over 20k rows for the
slug lookup the checklist names as a query pattern.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
44caa477e3
commit
5fe355483e
@@ -214,3 +214,181 @@ SchemaManagementException: Schema-validation: missing table [fs_cleanup_item]
|
||||
5. **나머지 16개 오퍼레이션** — 백엔드 HANDOFF.md가 지적한 생성 union 5종의 Jackson 파손 전략 결정이 선행.
|
||||
6. 배포 레이어 (P1-5·6·7) — nginx/CDN에 보안 헤더·캐시 정책 적용, 프론트엔드 이미지.
|
||||
7. robots.txt로 Studio 차단 (P1-8).
|
||||
|
||||
---
|
||||
|
||||
# 2차 검증 — 로컬에서 가능한 항목 완주 (2026-08-19)
|
||||
|
||||
1차에서 "물리적으로 불가능"이라 분류했던 항목 중 상당수가 실제로는 검증 가능했다.
|
||||
Studio는 mock 게이트웨이가 18개 오퍼레이션을 **전부** 구현하고 있고(낙관적 락·검증
|
||||
staleness·미리보기 만료·경고 승인·멱등성 포함), Public은 정적 콘텐츠지만 UI 동작
|
||||
항목은 그대로 검증된다. 아래는 그 재검증 결과다.
|
||||
|
||||
## 1차 판정 정정
|
||||
|
||||
### 정정 1 — P0-6 은 결함이 아니다
|
||||
|
||||
`prod` 프로파일이 커밋된 `src/.env`로 부팅하지 않는 것은 **의도된 설계**다.
|
||||
`application-prod.yml`이 문서화한 5개 startup validator가 개발용 값을 거부한다:
|
||||
|
||||
```
|
||||
JpaSchemaSafetyValidator ddl-auto must be none|validate (exit 71)
|
||||
FlywayProdSafetyValidator baseline-on-migrate / out-of-order / clean 비활성
|
||||
StartupSafetyValidator error-detail 노출 · body-capture 로깅 off
|
||||
PostgreSqlTransportSecurityValidator pgJDBC sslmode=verify-full
|
||||
PersistenceVendorProdSafetyValidator vendor·URL 모두 H2 금지
|
||||
```
|
||||
|
||||
실측으로 2개가 순서대로 발화하는 것을 확인했다:
|
||||
|
||||
```
|
||||
exit=71 error.code=PROFILE_MISMATCH
|
||||
"prod profile requires APP_DATASOURCE_DDL_AUTO ... to be none or validate,
|
||||
but was update; Flyway is the production schema writer"
|
||||
|
||||
ddl-auto=none 으로 넘긴 뒤:
|
||||
"prod PostgreSQL transport requires pgJDBC sslmode=verify-full"
|
||||
```
|
||||
|
||||
즉 **§9 "운영 환경에서 개발용 설정이 활성화되지 않는다"는 PASS**다.
|
||||
남는 진짜 갭은 별개다 — **운영 값 세트가 저장소에도 배포 시스템에도 아직 없다**(P1로 이동).
|
||||
|
||||
### 정정 2 — P0-7 의 심각도 하향
|
||||
|
||||
`ddl-auto=validate`가 `fs_cleanup_item` 누락으로 실패하는 것은 사실이나,
|
||||
prod는 `none|validate` **둘 다** 허용하므로 `none`으로 부팅할 수 있다(실제로 그렇게 기동해 검증했다).
|
||||
따라서 출시 차단은 아니고, **스키마 검증을 포기해야 한다는 제약**으로 남는다 → P1.
|
||||
|
||||
### 정정 3 — 1차의 오탐 2건
|
||||
|
||||
- **"Public UI에 Studio 노출"** — 오탐. 매칭된 "Studio"는 전부 게시된 릴리스 노트의 본문
|
||||
텍스트였다("TechLog Public·Studio 경계를 확정했습니다"). 실제 `a[href^="/studio"]`는
|
||||
모든 Public 화면에서 **0건**. §1.3 PASS.
|
||||
- **"코드 블록 미표시"** — 오탐. `code-block.tsx`가 `<figure class="code-block">` +
|
||||
`<pre role="region" tabindex=0>`을 렌더하고 CSS가 `overflow-x:auto`·`max-width:100%`를
|
||||
준다. 정적 공개 문서에 CODE_BLOCK이 0건이라 발견하지 못한 것이며, Studio 편집기에
|
||||
직접 넣어 확인하니 정상 렌더되고 페이지 가로 오버플로도 없었다. §4 PASS.
|
||||
|
||||
## 새로 발견한 결함
|
||||
|
||||
| # | 항목 | 근거 |
|
||||
|---|---|---|
|
||||
| **N-1** | **로그아웃할 방법이 없다** | `signOut` 포트와 `app-shell.tsx`의 세션 버튼(`action.signOut`="로그아웃")은 존재하지만, **TechLog는 자체 셸(`public-shell.tsx` + Studio 셸)을 쓰고 `AppShell`을 렌더하지 않는다.** 로그인 후 Public·Studio 어느 화면에서도 로그아웃 버튼이 없다. §1.4 "로그아웃", "로그아웃 후 보호된 데이터가 UI 상태에 남지 않는다" 미충족 |
|
||||
| **N-2** | **중복 관계 생성이 방지되지 않는다** | 같은 대상을 두 번 연결해 저장해도 경고가 없다. 계약에 `uniqueItems` 제약이 없고(`relations: maxItems 20`뿐), `validate-working-copy.ts`도 slug 중복만 검사한다(`SLUG_DUPLICATE`). **백엔드를 구현해도 계약이 허용하므로 같은 결과가 난다.** §3.2 미충족 |
|
||||
| **N-3** | **CLS 0.192 (기준 0.1)** | 원인 단일: `FOOTER.site-footer`가 t=538ms에 0.1922 이동. 나머지 shift는 0.0001. 세 라우트 모두 동일 값 → 앱 셸 마운트 시점의 footer 점프. §4 "주요 화면의 Layout Shift가 없다" 미충족 |
|
||||
| **N-4** | **`navigation_path`(slug 조회)에 인덱스가 없다** | 20,000행 기준 `Seq Scan`, `Rows Removed by Filter: 19999`, **236ms**. `enable_seqscan=off`로도 인덱스를 못 쓴다 → 존재하지 않는다. 체크리스트가 명시한 "Slug 조회" 쿼리 패턴 |
|
||||
| **N-5** | 검색 trgm 인덱스가 플래너에 선택되지 않음 | GIN trgm 인덱스는 존재하고 강제하면 3.96ms로 동작하나, 20k 규모에서 플래너가 Seq Scan(10.3ms)을 고른다. 운영 규모에서 재확인 필요 |
|
||||
| **N-6** | `/api/v3/api-docs`가 500 | `/swagger-ui`·`/v3/api-docs`는 404로 미배포(정상)인데, path prefix가 붙은 `/api/v3/api-docs`만 500 INTERNAL_ERROR |
|
||||
|
||||
## 검증 결과 — 절별
|
||||
|
||||
### §1.2 Routing · §1.3 경계 · §2 기능 — 27/27 PASS
|
||||
|
||||
```
|
||||
§1.2 존재하지 않는 Case / 잘못된 explore kind / 없는 프로젝트 / 없는 릴리스
|
||||
→ 전부 "페이지를 찾을 수 없습니다."
|
||||
§1.2 Not Found 화면, Back/Forward (/explore→/projects→back→forward) 정상
|
||||
§1.3 Public 6개 화면에 studio 링크 0건, Draft 표식 0건
|
||||
§2.1 탐색 목록 6건 · 중복 0 · 필터 적용 6→2건
|
||||
§2.2 검색창 열림 / Focus 이동 / Overlay 겹침 없음 / 입력 중 과요청 0
|
||||
결과 없음 UI / ESC 닫기 / 빈 검색어 정책 / 결과 클릭 → 상세 이동
|
||||
§2.3 프로젝트 목록 2건 · 상세("Backend Skeleton") · 포함 문서 6건
|
||||
§2.4 변경 기록 목록·상세, 연결 문서 7건, 시간순 정렬 일관
|
||||
```
|
||||
|
||||
### §3 Studio — 20/22 PASS (mock 기준)
|
||||
|
||||
```
|
||||
§3.1 새 문서(유형 4종) → 편집 진입 → 저장 → 상태 전달 PASS
|
||||
§3.1 저장 버튼 3연타 → 문서 수 8→9 (증가 1) PASS ← 멱등성 실동작
|
||||
§3.1 미저장 변경 이동 경고 [머무르기/변경 버리기/저장 후 이동] PASS
|
||||
§3.1 머무르기 후 입력값 보존 PASS
|
||||
§3.1 검증 화면("저장본 검증") / 게시 화면("게시 준비") PASS
|
||||
§3.2 관계 추가·순서 이동·삭제, 대상 카탈로그 4건 PASS
|
||||
§3.2 중복 관계 방지 FAIL (N-2)
|
||||
§3.3 즉시 미리보기 렌더 / Public Preview 화면 PASS
|
||||
§3.3 게시 기록 8건 · 게시 취소 버튼 3개 PASS
|
||||
§20 저장 충돌(409) 사용자 안내 PASS
|
||||
콘솔 오류 0건
|
||||
```
|
||||
|
||||
문서 **삭제**는 계약에 오퍼레이션 자체가 없다(`deleteStudioAsset`만 존재). §3.1의 "삭제"는 설계 범위 밖.
|
||||
|
||||
### §4 UX/UI · §5 접근성 · §6 성능 — 15/18 PASS
|
||||
|
||||
```
|
||||
§4 Layout Shift FAIL CLS=0.1924 (N-3)
|
||||
§4 Header가 콘텐츠를 가리지 않음 PASS
|
||||
§4 긴 제목(150자)/긴 본문/긴 URL PASS scrollWidth==clientWidth 1440
|
||||
§4 코드 블록 (pre overflow-x:auto) PASS
|
||||
§5 Modal Focus 이동 / role=dialog / Focus Trap / 닫은 뒤 복귀 PASS
|
||||
§5 키보드 순회 19개 요소 · Focus 표시 전부 존재 PASS
|
||||
§6 긴 문서 렌더링 296ms PASS
|
||||
§6 이미지 lazy loading · width/height 명시 PASS
|
||||
§6 동일 요청 중복 0 · 2초간 DOM 변경 0건(render loop 없음) PASS
|
||||
§6 검색 21자 입력+반영 847ms PASS
|
||||
```
|
||||
|
||||
### §14 데이터베이스
|
||||
|
||||
```
|
||||
Constraint PK 33 · FK 36 · UNIQUE 18 · CHECK 89 · NOT NULL 267 · PK 없는 테이블 0 PASS
|
||||
Index 실행계획 (20,000행 기준)
|
||||
Public 목록(최신순) Index Scan idx_public_latest 0.113ms PASS
|
||||
유형별 조회 Index Scan idx_public_type 0.129ms PASS
|
||||
Topic별 조회 Bitmap Index Scan idx_public_topic 0.229ms PASS
|
||||
검색(trgm) Seq Scan (인덱스 미선택) 10.3ms 주의 (N-5)
|
||||
slug 조회 Seq Scan (인덱스 부재) 236ms FAIL (N-4)
|
||||
```
|
||||
|
||||
`public_resource_projection`의 인덱스들이 `WHERE publication_state='ACTIVE' AND
|
||||
visibility='PUBLIC'` 부분 인덱스로 정의되어 있다 — Public/Private 경계를 인덱스 수준에서
|
||||
강제하는 좋은 설계다(§12를 구현할 때 그대로 활용 가능).
|
||||
|
||||
### §19 악용 방지 · §28 Swagger
|
||||
|
||||
```
|
||||
pagination 최대 크기 (limit=1000) 422 REQUEST_VALIDATION_FAILED PASS
|
||||
q 길이 제한 (500자) 422 REQUEST_VALIDATION_FAILED PASS
|
||||
Rate Limit APP_RATE_LIMIT_ENABLED=false 미적용
|
||||
대용량 Body 쓰기 엔드포인트 부재로 검증 불가
|
||||
/swagger-ui, /v3/api-docs 404 (미배포) PASS
|
||||
/api/v3/api-docs 500 주의 (N-6)
|
||||
```
|
||||
|
||||
### §26 의존성 장애
|
||||
|
||||
```
|
||||
PostgreSQL Down catalog 503 DB_UNAVAILABLE(retryable) 30s · readiness 503 DOWN
|
||||
liveness 200 UP 유지 · 복구 후 27ms 정상 PASS
|
||||
Keycloak Down JWKS 캐시로 기존 토큰 32ms/200 · 잘못된 서명 21ms/401
|
||||
readiness 200 UP 유지(외부 IdP를 readiness에 걸지 않음)
|
||||
복구 후 정상 PASS
|
||||
Backend 단절 Public 화면 정상 유지(정적 소스) PASS
|
||||
MinIO / Redis 해당 없음(미배선)
|
||||
```
|
||||
|
||||
### §0 · §9 설정
|
||||
|
||||
```
|
||||
src/.env 가 git에 커밋되어 있다 — 값은 local 프로파일용이지만 .gitignore에 .env가 없어
|
||||
구조적으로 막혀 있지 않다. Redis HMAC은 secret://environment/... 간접 참조를 쓴다(좋은 패턴).
|
||||
prod 5개 validator 실동작 확인 (정정 1)
|
||||
show-sql=false · 로그에 토큰/쿠키/비밀번호 0건 · user= 는 가명화 해시
|
||||
```
|
||||
|
||||
## 남은 것 — 로컬에서 불가능
|
||||
|
||||
| 절 | 이유 |
|
||||
|---|---|
|
||||
| §12 Public/Private 경계 | Public 엔드포인트·문서 엔드포인트 부재 |
|
||||
| §15 N+1 / JPA Query | Tech Log에 JPA 리포지토리 0건 (catalog는 raw JDBC 단일 쿼리) |
|
||||
| §16 Transaction | 쓰기 유스케이스 부재 |
|
||||
| §17 파일/Object Storage | 업로드 엔드포인트·스토리지 배선 부재 |
|
||||
| §18 HTTPS/HSTS/Redirect | TLS 종단 필요 |
|
||||
| §22 Grafana·Loki 대시보드 | 관측 스택 필요 (수집 측 127개 메트릭은 확인 완료) |
|
||||
| §24 Kubernetes | 매니페스트·오케스트레이터 부재 |
|
||||
| §25 Ingress 라우팅 · X-Forwarded-* | 리버스 프록시 필요 |
|
||||
| §27 Backup / Restore | 실제 볼륨·운영 DB 필요 |
|
||||
| §30 Production Smoke Test | 운영 환경 부재 |
|
||||
| §1.4 세션 만료 · 토큰 만료 후 프론트 동작 | demo 어댑터에 만료 개념이 없음 (외부 IdP 연동 필요) |
|
||||
|
||||
Reference in New Issue
Block a user