「손으로 나열한 목록」이 표로 바뀌었나 — 현재 코드
출처: tech-log-frontend @ 2b2f443 / tech-log-backend @ 8cd8ee3
조회: 2026-09-04

§3 의 주장: 종류를 나열하는 자리를 Record<Kind,_> 나 sealed switch 식으로 바꿔
            새 종류가 늘면 컴파일러가 빈 자리를 잡게 했다.
======================================================================

### 프론트 — Record<Kind, _> 로 바뀐 자리
  src/features/tech-log/presentation/public/components/explore-filter-form.tsx:12:  목록을 손으로 적지 않고 `Record<RecordKind, …>` 에서 뽑는다. 종류가 늘면 이 표가 비어 있는
  src/features/tech-log/presentation/public/components/explore-filter-form.tsx:16:const EXPLORE_KIND_ORDER: Record<RecordKind, number> = {
  src/features/tech-log/presentation/shared/document-kind-labels.ts:18: * 타입이 잡는다 — `Record<RecordKind, string>` 이므로 빠진 종류가 있으면 컴파일되지 않는다.
  src/features/tech-log/presentation/shared/document-kind-labels.ts:20:export const DOCUMENT_KIND_LABELS: Record<RecordKind, string> = {
  src/features/tech-log/presentation/shared/document-kind-labels.ts:35:export const DOCUMENT_KIND_STATES: Record<RecordKind, "CONFIRMED" | "SETTLED" | "OPEN"> = {
  src/features/tech-log/presentation/shared/document-kind-labels.ts:53:export const EXPLORE_KIND_PATHS: Record<RecordKind, string> = {
  src/features/tech-log/presentation/studio/components/document-list.tsx:15:  작업본 목록이 거를 수 있는 종류. 손으로 나열하지 않고 `Record<RecordKind, …>` 에서 뽑는다 —
  src/features/tech-log/presentation/studio/components/document-list.tsx:18:const STUDIO_KIND_ORDER: Record<RecordKind, number> = {
  src/features/tech-log/adapters/mock/validate-working-copy.ts:58:  const BRANCH_FIELDS: Record<RecordKind, string[]> = {
  src/features/tech-log/adapters/http/http-management-gateway.ts:153:        아래 표가 `Record<DeletableDocumentKind, …>` 인 것이 실제로 종류를 강제하는 지점이다.
  src/features/tech-log/adapters/http/http-management-gateway.ts:155:      const OPERATIONS: Record<DeletableDocumentKind, string> = {
  src/features/tech-log/adapters/http/http-public-content-gateway.ts:186:    const OPERATIONS: Record<RecordKind, string> = {

### 프론트 — 남아 있는 종류 삼항 사슬이 있나 (있으면 여기 나온다)
  src/features/tech-log/presentation/studio/components/publication-event-preview-screen.tsx:157:            renderModel.kind === "CASE" ? renderModel.bodyBlocks : [],
  src/features/tech-log/adapters/mock/validate-working-copy.ts:71:  const stringFields = ["title", "slug", "summary", ...(kind === "CASE" ? ["problem", "conclusion", "environment", "reproduction", "bodyMarkdown"] : []), ...(kind === "CONCEPT" ? ["bodyMarkdown", "basisVersion"] : []), ...(kind === "REFERENCE" ? ["purpose"] : []), ...(kind === "QUESTION" ? ["nextValidation"] : []), ...(kind === "PROJECT_DECISION" ? ["statement", "rationale"] : [])];

### 백엔드 — sealed switch 를 식으로 쓴 자리 (컴파일러가 빠진 가지를 요구한다)
  ../tech-log-backend/src/application-core/src/main/java/dev/caskeleton/application/techlog/studio/model/PublicPaths.java:25:    return switch (kind) {
  ../tech-log-backend/src/application-core/src/main/java/dev/caskeleton/application/techlog/studio/model/PublicPaths.java-26-      case CASE -> "/cases/" + slug;
  ../tech-log-backend/src/application-core/src/main/java/dev/caskeleton/application/techlog/studio/model/PublicPaths.java-27-      case REFERENCE -> "/references/" + slug;

### 백엔드 — PublicSql.pathOf 에 CONCEPT 이 들어갔나 (13번째 사례)
    static String pathOf(String resourceType, String slug, String projectSlug) {
      return switch (resourceType) {
        case "CASE" -> "/cases/" + slug;
        case "REFERENCE" -> "/references/" + slug;
        case "QUESTION" -> "/questions/" + slug;
        case "CONCEPT" -> "/concepts/" + slug;
        case "PROJECT" -> "/projects/" + slug;
        case "PROJECT_DECISION" ->
            projectSlug == null ? null : "/projects/" + projectSlug + "/decisions#" + slug;
        case "RELEASE" -> "/releases/" + slug;
        default -> null;
      };
    }

======================================================================
정직한 평가 — 이 증거가 드러내는 남은 구멍 둘

[1] PublicSql.pathOf 는 여전히 컴파일러가 강제하지 못한다

    switch 의 대상이 sealed enum(RecordKind)이 아니라 String(resourceType)이다.
    그래서 `default -> null` 이 남아 있고, 새 종류를 더할 때 이 자리를 빠뜨리면
    컴파일은 통과하고 경로가 null 로 나간다 — §3 이 경고하는 바로 그 모양이다.

    같은 파일의 PublicPaths.forKind 는 RecordKind 로 switch 하므로 강제된다.
    둘의 차이는 pathOf 가 공개 투영의 resource_type 을 다루기 때문이다. 그 칸은
    RecordKind 에 없는 값(PROJECT, RELEASE)도 담는다 — 그래서 String 이다.

    지금은 PublicPathsTest 가 RecordKind 전수를 돌며 막고 있지만, pathOf 만 쓰는
    경로(홈 focus 의 recentDecision)는 그 테스트가 닿지 않는다.

[2] validate-working-copy.ts 의 stringFields 는 아직 삼항 사슬이다

    다만 모양이 다르다 — 배타적 사슬이 아니라 「종류마다 칸을 더한다」는 가산형이다.
    종류를 빠뜨리면 "잘못된 분기로 떨어진다"가 아니라 "그 종류의 추가 칸을 검사하지
    않는다"가 된다. 덜 위험하지만 조용하기는 마찬가지다.

    같은 파일의 BRANCH_FIELDS 는 Record<RecordKind, string[]> 로 바뀌어 있다(58줄).
    그쪽이 실제로 종류를 강제하는 자리다.

즉 §3 의 「표로 바꿨다」는 대부분 사실이지만 전부는 아니다. 위 둘은 남아 있다.
