diff --git a/docs/release/2026-08-19-release-gate-verification.md b/docs/release/2026-08-19-release-gate-verification.md new file mode 100644 index 0000000..df217cd --- /dev/null +++ b/docs/release/2026-08-19-release-gate-verification.md @@ -0,0 +1,216 @@ +# 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 없음. `