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>
395 lines
25 KiB
Markdown
395 lines
25 KiB
Markdown
# Tech Log 운영 출시 전 체크리스트 — 실측 검증 보고서
|
||
|
||
**검증일** 2026-08-19 · **방식** 두 저장소를 로컬에서 실제 기동해 엔드포인트·브라우저 단위로 실측
|
||
|
||
| 대상 | 위치 | 리비전 |
|
||
|---|---|---|
|
||
| Frontend | `tech-log-frontend` | `main` eb86708 → `fix/release-gate-frontend` fff5e6f |
|
||
| Backend | `tech-log-backend` | `develop` ab0447a |
|
||
| Keycloak | 로컬 컨테이너 `local-keycloak` | 26.7.0 (`:18080`) |
|
||
| PostgreSQL | 로컬 컨테이너 `techlog-pg` | 16.15 (`:5433`) |
|
||
|
||
---
|
||
|
||
## 요약 판정: **출시 보류 (P0 미충족)**
|
||
|
||
체크리스트 §31의 P0 항목 중 **인증 우회 불가 · 인가 우회 불가 · Studio 주요 기능 정상 · Publish 정상**이
|
||
현재 충족되지 않는다. 아래 근거는 전부 실행 결과다.
|
||
|
||
### 가장 중요한 구조적 사실
|
||
|
||
백엔드는 계약(`studio-v1.yaml`)이 선언한 **18개 오퍼레이션 중 2개**만 구현되어 있다.
|
||
|
||
| 상태 | 오퍼레이션 |
|
||
|---|---|
|
||
| 구현됨 (2) | `getStudioSession`, `listStudioCatalog` |
|
||
| 미구현 (16) | `getStudioDashboard`, `listStudioDocuments`, `createStudioDocument`, `getStudioDocument`, `saveStudioDocument`, `validateStudioDocument`, `getCurrentStudioPreview`, `createStudioPreview`, `publishStudioDocument`, `listStudioPublications`, `unpublishStudioPublication`, `getStudioPublicationSnapshot`, `listStudioAssets`, `uploadStudioAsset`, `getStudioAsset`, `updateStudioAsset`, `deleteStudioAsset` |
|
||
|
||
또한 **Public 읽기 엔드포인트는 계약에 아예 없다.** `studio-v1.yaml`은 Studio 전용이고,
|
||
프론트엔드의 Public 화면(`/`, `/explore`, `/projects`, `/releases`, 문서 상세)은
|
||
`src/features/tech-log/adapters/static/public-content.ts`의 **번들에 컴파일된 정적 콘텐츠**를 읽는다.
|
||
|
||
따라서 체크리스트의 다음 절은 검증 대상 자체가 존재하지 않는다:
|
||
§2(탐색·검색·프로젝트·변경기록의 백엔드 연동), §3.1~3.3(문서 작성·관계·Publish),
|
||
§12(Public/Private 데이터 경계), §17(파일/Object Storage), §29(E2E 시나리오).
|
||
|
||
---
|
||
|
||
## P0 — 출시 차단 결함
|
||
|
||
### P0-1. Studio 라우트가 인증을 검사하지 않았다 — **수정 완료**
|
||
|
||
`TECH_LOG_ROUTE_REGISTRY`가 모든 TechLog 라우트를 `access: "public"`으로 등록하고 있었다.
|
||
라우터에 `decideRouteAccessForDefinition` 가드가 존재하지만 Studio에 대해 무력화된 상태였다.
|
||
|
||
운영 프로파일 빌드(`AUTH_MODE=external`)로 실측한 수정 전:
|
||
|
||
```
|
||
/studio http=200 h1="작업 흐름" ← 비로그인 상태에서 Studio UI 렌더링
|
||
/studio/documents http=200 h1="작업본"
|
||
/studio/assets http=200 h1="Asset"
|
||
```
|
||
|
||
`spec.layoutGroup === "STUDIO"`에서 `access`를 유도하도록 수정한 뒤:
|
||
|
||
```
|
||
/studio http=200 h1="로그인 연동이 필요합니다."
|
||
/studio/documents http=200 h1="로그인 연동이 필요합니다."
|
||
/ , /explore 변화 없음
|
||
```
|
||
|
||
로그인 후 원래 요청 화면으로 복귀하는 것도 확인했다(`/studio/documents` → 로그인 → `작업본`).
|
||
|
||
커밋 `fff5e6f`.
|
||
|
||
### P0-2. 백엔드 Studio API에 인가 검사가 없다 — **미해결**
|
||
|
||
`SecurityConfig`는 `anyRequest().authenticated()`로 끝나고, Studio 컨트롤러에
|
||
`@RequiresPermission` 계열 애노테이션이 **하나도 없다**.
|
||
|
||
Keycloak에 Studio 권한이 없는 사용자(`plain`, realm role `plain-user`)를 만들어 확인:
|
||
|
||
```
|
||
GET /api/v1/studio/catalog?type=TOPIC
|
||
studio 사용자 (studio-author) → HTTP 200
|
||
plain 사용자 (권한 없음) → HTTP 200 ← 인가 우회
|
||
```
|
||
|
||
체크리스트 §11 "인증된 사용자라고 해서 무조건 Studio API를 호출할 수 있지 않다",
|
||
§31 P0 "인가 우회 불가" 미충족.
|
||
|
||
### P0-3. 모든 Studio 경로가 `/api/api/v1/...`에 매핑된다 (Double Prefix) — **미해결**
|
||
|
||
`PresentationWebConfig`가 `configurer.addPathPrefix("/api", c -> true)`로 전 컨트롤러에
|
||
`/api`를 붙이는데, Studio 컨트롤러는 `@GetMapping("/api/v1/studio/...")`로 이미 `/api`를 포함해 선언한다.
|
||
|
||
```
|
||
GET /api/v1/studio/catalog → 404 ROUTE_NOT_FOUND
|
||
GET /api/api/v1/studio/catalog → 200
|
||
GET /api/v1/studio/session → 404 ROUTE_NOT_FOUND
|
||
GET /api/api/v1/studio/session → 503
|
||
```
|
||
|
||
프론트엔드는 계약대로 `/api/v1/studio/...`를 호출하므로 **현재 상태로는 단 한 건도 연결되지 않는다.**
|
||
체크리스트 §25 "`/api` Prefix 처리에서 Double Prefix가 발생하지 않는다" 미충족.
|
||
|
||
### P0-4. `getStudioSession`이 항상 503을 반환한다 — **미해결**
|
||
|
||
`auth-mode: jwt`(저장소 기본값, `src/.env:115`)에서 `SecurityConfig`가 `csrf.disable()`로
|
||
`CsrfFilter`를 제거하므로 `CsrfToken` 파라미터가 항상 `null`이고, 컨트롤러는 이를
|
||
`STUDIO_UNAVAILABLE`(503)로 정직하게 보고한다.
|
||
|
||
```
|
||
GET /api/api/v1/studio/session (유효한 studio 토큰)
|
||
→ 503 {"code":"STUDIO_UNAVAILABLE","category":"TRANSIENT_DEPENDENCY","retryable":true}
|
||
로그: "CSRF token unavailable: CSRF protection is disabled for the active auth-mode"
|
||
```
|
||
|
||
프론트엔드 HTTP 모드는 `getStudioSession`으로 CSRF 토큰을 받아 부트스트랩하므로,
|
||
**이 한 건 때문에 Studio HTTP 경로 전체가 시작조차 못 한다.**
|
||
`auth-mode: redis-session`에 필요한 세션 빈이 저장소에 없다는 점은 백엔드 HANDOFF.md도 명시하고 있다.
|
||
|
||
### P0-5. `main` 브랜치의 dev 부팅이 깨져 있었다 — **수정 완료**
|
||
|
||
`eb86708`(계약 3.0.0 머지) 이후 `public/release-manifest.json`이 2.0.0으로 남아
|
||
부팅 시 contract-set 검증이 fail-closed → **빈 화면**. 이전에 한 번 겪은 것과 같은 실패 양식이다.
|
||
|
||
```
|
||
setDigest drift: manifest sha256:e0da7765…, build sha256:261ac630…
|
||
package drift: manifest 2.0.0 / ce2e748 vs build 3.0.0 / b20d7a2
|
||
```
|
||
|
||
`generate:dev-release-manifest`로 재생성하고, 같은 값을 하드코딩하던
|
||
`tests/runtime-schema/release-manifest.test.ts`도 함께 갱신했다. 커밋 `fff5e6f`.
|
||
|
||
### P0-6. 커밋된 `.env`로는 prod 프로파일이 부팅하지 않는다 — **미해결**
|
||
|
||
`src/.env:140`이 `APP_DATASOURCE_DDL_AUTO=update`인데, `application-prod.yml`이 문서화한
|
||
`JpaSchemaSafetyValidator`는 prod에서 `none|validate`만 허용하고 위반 시 exit 71로 종료한다.
|
||
|
||
### P0-7. `ddl-auto=validate`로는 PostgreSQL에서 부팅하지 않는다 — **미해결**
|
||
|
||
```
|
||
SchemaManagementException: Schema-validation: missing table [fs_cleanup_item]
|
||
```
|
||
|
||
`PostgreSqlPersistenceConfig`가 Flyway 위치를 `classpath:db/migration/postgresql`로 고정해
|
||
`db/migration/jpa/fileserver` 트리가 **한 번도 적용되지 않는데**, 해당 JPA 엔티티는 스캔된다.
|
||
`ddl-auto=update`가 이 사실을 가려 온 것이고, prod가 요구하는 `validate`로 바꾸는 순간 드러난다.
|
||
(본 검증은 `ddl-auto=none`으로 우회해 진행했다.)
|
||
|
||
---
|
||
|
||
## P1 — 출시 전 해결 권장
|
||
|
||
| # | 항목 | 실측 근거 |
|
||
|---|---|---|
|
||
| P1-1 | Keycloak realm 구성이 두 저장소 어디에도 없다 | compose에 keycloak 서비스 없음, realm export 파일 없음. 검증을 위해 `ca-skeleton` realm·클라이언트·audience 매퍼·테스트 사용자를 수기로 생성해야 했다. §27 "Keycloak Realm 설정을 복원할 수 있다" 미충족 |
|
||
| P1-2 | 프론트엔드에 로그인 구현이 없다 | OIDC/Keycloak 클라이언트 코드 0건. `AUTH_MODE=external`은 호스팅 페이지가 `window.__CA_FRONTEND_AUTH_OWNER__`를 주입하기를 기대하며, 없으면 `createUnavailableSessionAdapter`가 "로그인 연동이 필요합니다"를 띄운다. §1.4 인증 항목 전부 검증 불가 |
|
||
| P1-3 | production 런타임 설정이 플레이스홀더 | `API_BASE_URL: https://api.example.com/`, `TELEMETRY_ENDPOINT: https://telemetry.example.com/v1/events` |
|
||
| P1-4 | Rate Limit 비활성 | `APP_RATE_LIMIT_ENABLED=false`, `APP_RATE_LIMIT_PROVIDER=disabled`. 60회 연속 호출 전부 200 |
|
||
| P1-5 | 보안 헤더를 적용하는 주체가 없다 | `config/hosting/security-headers.json`에 CSP·HSTS·X-Frame-Options 등이 정의돼 있으나 `dist/server.mjs`는 **하나도 적용하지 않는다**. `verify:hosting-headers`는 기본적으로 fixture 모드로 동작해 실 서버를 검사하지 않는다 |
|
||
| P1-6 | 캐시 정책도 미적용 | `cache-policy.json`은 `/assets/*`에 `public, max-age=31536000, immutable`을 요구하나 실제 응답은 전부 `no-cache` |
|
||
| P1-7 | 프론트엔드 배포 아티팩트 부재 | Dockerfile·nginx conf·compose 없음. `dist/server.mjs`는 프리뷰용이지 운영 파일 서버가 아니다 |
|
||
| P1-8 | robots.txt / sitemap.xml 없음 | **Studio 경로가 검색 엔진에 차단되지 않는다.** §7 미충족 |
|
||
| P1-9 | Open Graph·canonical 메타데이터 없음 | `dist/index.html`에 `og:*`·canonical 없음. `<title>`은 라우트별로 정상 동작하나 **런타임에 설정**되므로 JS를 실행하지 않는 공유 미리보기 크롤러에는 "Tech Log" 고정값만 노출된다 |
|
||
| P1-10 | DB 타임아웃 30초 | `APP_DATASOURCE_CONNECTION_TIMEOUT=30000`. `application.yml`이 문서화한 D2 fail-fast 의도(기본 5s)와 어긋난다. 프론트엔드 `REQUEST_TIMEOUT_MS=10000`이므로 DB 장애 시 프론트가 항상 먼저 끊겨 `DB_UNAVAILABLE` 503을 보지 못한다 |
|
||
| P1-11 | Tech Log Asset의 Object Storage 배선 없음 | objectstorage 어댑터는 템플릿 자산으로 존재하나 techlog 참조 0건, MinIO/S3 환경변수 0건, `uploadStudioAsset` 엔드포인트 미구현 |
|
||
|
||
---
|
||
|
||
## 검증되어 통과한 항목
|
||
|
||
### Frontend
|
||
|
||
| 항목 | 결과 |
|
||
|---|---|
|
||
| Production Build | PASS (local·production 프로파일 모두) |
|
||
| TypeScript compile | PASS (`check:types` 6개 프로젝트) |
|
||
| ESLint | PASS (수정 후 0 error) |
|
||
| 전체 테스트 | 1,818 passed / 16 skipped / **1 기존 flake** (`provider-guardian-transaction` — 단독 실행 2회 모두 PASS, 부하 의존) |
|
||
| architecture / contract / dev-release-manifest / browser-security 게이트 | PASS |
|
||
| Production 번들에 dev·localhost URL 없음 | PASS (`localhost`·`127.0.0.1` 0건, `.local` 매치는 전부 `locale`/`localeCompare`) |
|
||
| Production 번들에 Mock API 미포함 | PASS (`createMockStudioGateway` 0건) |
|
||
| Source Map 비공개 | PASS (`.map` 0개) |
|
||
| Route 단위 Lazy Loading | PASS (30 청크, 총 954 KB / 최대 569 KB) |
|
||
| SPA 라우팅·새로고침 | PASS (열거형 allowlist 방식. 존재하지 않는 문서 경로는 의도적으로 404) |
|
||
| Route별 `<title>` | PASS (`탐색 · Tech Log`, `프로젝트 · Tech Log` …) |
|
||
| 반응형 | PASS — 360/414/768/1440 × 6개 Public 라우트 **24개 조합 전부 가로 스크롤 없음** |
|
||
| 접근성 | PASS — axe(wcag2a/2aa/21a/21aa) **serious+critical 0건** (Public 6 + Studio 4 라우트). h1 정확히 1개, heading 건너뜀 없음, alt 누락 0, 레이블 없는 icon button 0 |
|
||
| 로그인 흐름 | PASS (게이트 → 로그인 → 원래 화면 복귀) |
|
||
|
||
### Backend
|
||
|
||
| 항목 | 결과 |
|
||
|---|---|
|
||
| Production Profile Build | PASS — `:app-bootstrap:bootJar` 성공 |
|
||
| 전체 테스트 | PASS — **3,530 tests / 0 failures / 7 skipped** (BUILD SUCCESSFUL 8m 9s). app-bootstrap 797 · application-core 568 · cache-redis 423 · fileserver 398 · inbound-web 341 · httpclient 283 · shared-contract 224 · objectstorage 140 · persistence-jpa 122 · 그 외 |
|
||
| Docker Image Build | PASS — 623MB. `BUILD_VERSION`/`GIT_SHA`/`SOURCE_URL` build-arg를 강제하는 provenance 게이트가 있어 인자 없이는 의도적으로 실패한다 |
|
||
| Production Image 실제 실행 | PASS — 컨테이너에서 14.5초 기동, `healthcheck` 200 · `readiness` 200 · `catalog` 200(실데이터 2건) |
|
||
| Flyway 마이그레이션 (신규 DB, 처음부터) | PASS — 6개 적용, V7 techlog core 포함, 테이블 33개 생성 |
|
||
| 응답 봉투 일관성 | PASS — `{success,data,error,meta}` 전 경로 동일 |
|
||
| HTTP 상태 코드 | PASS — 401 / 404 / 405 / 422 / 500 / 503 모두 적절 |
|
||
| 인증 오류 코드 분리 | PASS — `AUTH_TOKEN_MISSING` / `AUTH_TOKEN_MALFORMED` / `AUTH_TOKEN_INVALID_SIGNATURE` / `AUTH_TOKEN_EXPIRED` |
|
||
| Validation | PASS — 잘못된 enum·필수 누락은 422 + `fieldErrors`, `limit` 상·하한 강제 |
|
||
| SQL Injection | PASS — `' OR 1=1--` 파라미터 바인딩되어 빈 결과 |
|
||
| Visibility 필터 | PASS — `ARCHIVED` 토픽이 catalog 결과에서 제외됨 |
|
||
| CORS | PASS — 허용 origin 200 + `Allow-Credentials: true`, 미허용 origin 403, 와일드카드 없음 |
|
||
| 보안 헤더 | PASS — `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Cache-Control: no-store` |
|
||
| 오류 정보 노출 | PASS — Stack trace·SQL·내부 클래스명 모두 미노출 (`details: null`) |
|
||
| 로그 위생 | PASS — 토큰·Authorization·쿠키·비밀번호 **0건**. `user=`는 가명화 해시 |
|
||
| 추적성 | PASS — 모든 요청 로그에 `req=`·`trace=`, `http_request method= uri_template= status= duration_ms=` |
|
||
| Metrics | PASS — Prometheus 127개 메트릭 패밀리 (`http_server_requests_seconds_bucket`, `jvm_gc_*`, `jvm_memory_*`, `hikaricp_connections_*`) |
|
||
| Liveness / Readiness 분리 | PASS — DB 중단 시 readiness 503 DOWN, liveness 200 UP 유지 |
|
||
| 의존성 장애 대응 | PASS(동작) — DB 중단 시 무한 대기 없이 **503 `DB_UNAVAILABLE` (retryable)** 반환, DB 복구 후 27ms/정상 데이터로 자동 회복. 단 응답까지 30초 소요(P1-10) |
|
||
|
||
---
|
||
|
||
## 다음 단계 (권장 순서)
|
||
|
||
1. **Double Prefix 해소** (P0-3) — 컨트롤러 매핑에서 `/api`를 제거하거나 `addPathPrefix` 대상에서 제외. 이걸 고치기 전에는 프론트-백엔드가 한 건도 연결되지 않으므로 최우선.
|
||
2. **세션 인프라** (P0-4) — `redis-session` 배선. 백엔드 HANDOFF.md도 Plan 02보다 앞선 선행 작업으로 지목하고 있다.
|
||
3. **Studio 인가** (P0-2) — Studio 컨트롤러에 권한 검사 추가 + 권한 없는 사용자 403 회귀 테스트.
|
||
4. **prod 부팅 설정** (P0-6, P0-7) — `.env`의 `ddl-auto`, fileserver 마이그레이션 위치.
|
||
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 연동 필요) |
|