# 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 없음. ``은 라우트별로 정상 동작하나 **런타임에 설정**되므로 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 연동 필요) |