1603 lines
102 KiB
Markdown
1603 lines
102 KiB
Markdown
# 계약이 먼저인 시스템에서 값이 사라지는 자리들 — TechLog를 만들며 만난 결함의 전수 기록
|
|
|
|
제가 만들려던 것은 기술 기록을 쓰고 게시하는 사이트였습니다. 저장소는 셋입니다. 설계 패키지가
|
|
OpenAPI 계약을 소유하고, 백엔드가 그것을 반입해 구현하고, 프론트엔드가 같은 계약에서 타입을
|
|
생성합니다. 계약이 한 곳에 있으니 세 저장소가 어긋날 일이 없을 것이라고 생각했습니다.
|
|
|
|
실제로는 계속 어긋났습니다. 다만 어긋나는 방식이 제가 예상한 것과 달랐습니다. 계약이 틀려서
|
|
깨진 적은 거의 없었고, **계약은 맞는데 그 값이 어딘가의 경계에서 조용히 사라지는** 경우가
|
|
대부분이었습니다. 화면은 오류를 내지 않고 빈칸을 그렸고, 저는 그것을 "아직 안 쓴 글"로
|
|
읽었습니다. 이 문서는 그 자리들을 하나씩 짚고, 각각을 어떻게 막았는지 적은 글입니다.
|
|
|
|
> **이 문서의 출처와 한계** — 여기 적은 결함은 2026년 8월 20일부터 9월 2일까지 세 저장소에
|
|
> 쌓인 **커밋 198개**(frontend 108 · backend 48 · design-package 42)의 메시지와, 그 기간의
|
|
> 작업 대화 기록에서 복원했습니다. 전체 목록은 부록 A 에 있습니다. 각 항목에는 커밋 해시를 붙였으므로 원문을 확인할 수
|
|
> 있습니다. 다만 커밋으로 남지 않은 것 — 중간에 버렸다가 되돌린 시도, 배포 로그, 화면을
|
|
> 눈으로 확인만 하고 지나간 것 — 은 이 기록에 없습니다. 그리고 여기 적힌 "이렇게 고쳤다"는
|
|
> 그 시점의 판단이고, 뒤에 다시 뒤집힌 것이 몇 건 있습니다(§5.3, §7.2). 뒤집힌 것은 뒤집혔다고
|
|
> 적었습니다.
|
|
|
|
> **근거 자료** — 본문의 주장 중 지금 재현할 수 있는 것은 `evidence/` 아래에 원본을 두었고,
|
|
> 각 항목에서 링크합니다. 이미 고쳐진 과거 결함은 실패 상태를 다시 만들 수 없으므로, 그 경우
|
|
> **「가드가 실제로 잡는다」와 「현재 상태가 고쳐져 있다」** 를 증거로 남겼습니다.
|
|
>
|
|
> | 파일 | 무엇 |
|
|
> |---|---|
|
|
> | [`evidence/raw/db/topic-variant-rows.txt`](./evidence/raw/db/topic-variant-rows.txt) | 주제·축의 실제 행 |
|
|
> | [`evidence/raw/db/record-variant-links.txt`](./evidence/raw/db/record-variant-links.txt) | 축에 걸린 기록과 공통 기록 |
|
|
> | [`evidence/raw/db/decision-path-after-v15.txt`](./evidence/raw/db/decision-path-after-v15.txt) | 결정 주소가 앵커로 고쳐진 상태 · V15 적용 확인 |
|
|
> | [`evidence/raw/db/delete-blocked-by-project-link.txt`](./evidence/raw/db/delete-blocked-by-project-link.txt) | 삭제를 막던 참조와 그 해소 |
|
|
> | [`evidence/raw/api/decision-anchor-fixed.txt`](./evidence/raw/api/decision-anchor-fixed.txt) | 그 링크가 실제로 200 인가 |
|
|
> | [`evidence/raw/audit/dead-link-sweep.txt`](./evidence/raw/audit/dead-link-sweep.txt) | 서버가 내보내는 주소 35개 전수 감사 |
|
|
> | [`evidence/raw/audit/link-audit.py`](./evidence/raw/audit/link-audit.py) | 그 감사를 다시 돌리는 스크립트 |
|
|
> | [`evidence/raw/guards/guards-actually-fail.txt`](./evidence/raw/guards/guards-actually-fail.txt) | 가드 셋을 되돌려 실제로 빨개지는 것을 확인 |
|
|
> | [`evidence/raw/guards/kind-tables-now.txt`](./evidence/raw/guards/kind-tables-now.txt) | 손 목록이 표로 바뀌었는지 · **남은 구멍 둘** |
|
|
> | [`evidence/browser/`](./evidence/browser/) | 홈 주제 탭 세 단계의 화면과 실측값 |
|
|
>
|
|
> 도식 셋은 `assets/diagrams/` 아래에 SVG·`.drawio` 편집 원본·`.alt.md`·`.manifest.json` 로
|
|
> 있고, `.techviz/<id>/spec.json` 이 각 도식이 답하는 질문과 근거 목록을 적어 둡니다.
|
|
|
|
---
|
|
|
|
## 1. 시스템의 모양
|
|
|
|
### 1.1 세 저장소와 계약의 흐름
|
|
|
|
```
|
|
tech-log-design-package OpenAPI 3.1 계약 3종을 소유한다
|
|
contracts/openapi/
|
|
public-v1.yaml 공개 조회 20 operation
|
|
studio-v1.yaml 작성/게시 19 operation
|
|
studio-management-v1.yaml 주제·프로젝트·릴리즈 관리 86 operation
|
|
│
|
|
├─ 반입(vendoring) ─→ tech-log-backend/src/config/openapi/
|
|
│ MANIFEST.sha256 으로 원본 리비전을 고정
|
|
│ 생성기가 Java 모델을 만든다
|
|
│
|
|
└─ 반입 ─────────────→ tech-log-frontend/src/features/tech-log/contracts/
|
|
npm run generate:tech-log-contract
|
|
openapi-typescript 가 타입을 만든다
|
|
```
|
|
|
|
계약은 설계 패키지에만 있고, 나머지 둘은 **복사본을 들고 그 해시를 기록합니다.** 이 구조가
|
|
의도한 것은 "계약이 바뀌면 양쪽이 반드시 다시 반입해야 한다"는 강제입니다. 실제로 그 강제는
|
|
작동했습니다. 문제는 그 다음이었습니다 — **반입된 계약이 맞아도 그 값이 화면까지 오지 못하는
|
|
경로가 계속 나왔습니다.**
|
|
|
|
### 1.2 값이 지나는 경계
|
|
|
|
공개 화면 한 줄이 그려지기까지 값이 지나는 경계는 이만큼입니다.
|
|
|
|
```
|
|
PostgreSQL 테이블
|
|
└─ public_resource_projection (게시 시점에 굳어진 투영)
|
|
└─ JDBC 어댑터의 SQL (컬럼 이름을 컴파일러가 검사하지 않는다)
|
|
└─ *View 레코드 (application-core)
|
|
└─ *ResponseMapper (adapter/inbound/web)
|
|
└─ 생성된 DTO (계약이 만든 모양)
|
|
└─ HTTP envelope
|
|
└─ openapi-typescript 타입
|
|
└─ http-public-content-gateway 의 매퍼
|
|
└─ 포트 타입 (application/ports)
|
|
└─ 화면 컴포넌트
|
|
```
|
|
|
|
<!-- techviz:begin id=value-boundaries context-sha256=93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f -->
|
|
<!-- techviz:generate id=value-boundaries -->
|
|

|
|
|
|
<details>
|
|
<summary>Diagram description</summary>
|
|
|
|
왼쪽에서 오른쪽으로 읽는다. tech-log-backend 구역에 저장 묶음과 백엔드 조립 묶음이 있고 각각 경계 둘과 넷을 담는다. 전선 구역에는 HTTP envelope 하나가 있다. tech-log-frontend 구역에는 프론트엔드 조립 묶음과 화면 컴포넌트가 있고 각각 경계 셋과 하나다. 다 더하면 열한 개이고, 각 경계의 이름은 그림 위 목록에 있다.
|
|
|
|
</details>
|
|
|
|
[Editable source](assets/diagrams/value-boundaries/value-boundaries.drawio) · [Grounded VizSpec](.techviz/value-boundaries/spec.json)
|
|
<!-- techviz:end id=value-boundaries -->
|
|
|
|
**열한 개입니다.** 그리고 이 문서에 적힌 결함의 절반 이상은 "이 중 한 경계가 값을 버렸다"는
|
|
같은 모양이었습니다. 버려도 아무도 오류를 내지 않습니다. `undefined` 는 빈 문자열로 그려지고,
|
|
빈 배열은 "항목이 없습니다"로 그려집니다.
|
|
|
|
### 1.3 배포
|
|
|
|
```
|
|
로컬 docker build → docker save | gzip → scp dh-server:/tmp/deploy.tar.gz
|
|
→ kube-system 의 containerd import Job → kubectl set image
|
|
```
|
|
|
|
레지스트리가 없습니다. 공개 Hub 는 소스가 들어간 이미지라 쓸 수 없고, k3s 의 containerd 소켓은
|
|
root 전용이라 사용자 셸에서 닿지 않습니다. 그래서 클러스터 안에 일회성 Job 을 띄워 tar 를
|
|
import 합니다. 배포 단위는 `hyeonworks.com`(prod) 하나이고 서브도메인은 쓰지 않습니다 —
|
|
공개는 `/`, API 는 `/api` 입니다.
|
|
|
|
---
|
|
|
|
## 2. 결함을 어떻게 갈랐나
|
|
|
|
198개 커밋을 읽고 나서, 결함이 **원인의 종류**로 갈린다는 것이 보였습니다. 화면 증상으로 나누면
|
|
"어디가 비었다"가 대부분이라 아무것도 배울 수 없습니다. 그래서 아래 열한 갈래로 나눴습니다.
|
|
|
|
| § | 갈래 | 건수 | 공통된 모양 |
|
|
|---|---|---|---|
|
|
| 3 | 손으로 나열한 목록이 새 종류를 삼킨다 | 13 | 삼항 사슬 / 배열 리터럴의 마지막 `else` |
|
|
| 4 | 계약에 선언만 있고 구현이 없다 | 10 | 화면이 조용히 빈다 |
|
|
| 5 | 계약에 자리가 없어 값이 경계에서 사라진다 | 12 | DB 에는 있는데 화면에 없다 |
|
|
| 6 | 타입 검사가 통과시키는 자리 | 7 | `as` / bivariance / `never` |
|
|
| 7 | 테스트가 지나지 않는 이음매 | 6 | "통과했는데 운영에서 깨진다" |
|
|
| 8 | 라우트를 더하면 함께 울리는 손 목록 | 8 | 배포 직전에야 드러난다 |
|
|
| 9 | 서버가 갈 곳 없는 주소를 만든다 | 4 | 404 |
|
|
| 10 | 실패를 없음으로 그린다 | 6 | 화면이 거짓말을 한다 |
|
|
| 11 | CSS 규칙이 구역을 넘어 샌다 | 3 | "디자인이 안 된 것처럼" 보인다 |
|
|
| 12 | 운영에서만 드러난 것 | 9 | CrashLoopBackOff / 배포 인자 |
|
|
| 13 | 글과 말 | 6 | 같은 것이 화면마다 다른 이름 |
|
|
| | **합계** | **84** | |
|
|
|
|
각 절은 **증상 → 원인 → 고친 방법 → 재발 방지**로 씁니다. 재발 방지가 없는 항목은 없다고
|
|
적었습니다.
|
|
|
|
> **건수를 세는 기준** — 커밋 하나가 결함 여럿을 고친 경우가 많아 **커밋 수(198)와 결함
|
|
> 수(84)는 다릅니다.** 여기서 한 건은 "증상 하나 · 원인 하나"이고, 같은 원인이 여러 화면에
|
|
> 나타난 것은 한 건으로 셉니다. 반대로 한 커밋이 서로 다른 원인 셋을 고쳤으면 세 건입니다.
|
|
|
|
---
|
|
|
|
## 3. 손으로 나열한 목록이 새 종류를 삼킨다
|
|
|
|
이것이 이 저장소에서 가장 많이 반복된 실패입니다. **열세 번** 나왔습니다. 매번 같은 모양이라
|
|
따로 이름을 붙였습니다.
|
|
|
|
### 3.1 모양
|
|
|
|
문서 종류는 다섯입니다 — `CASE`, `REFERENCE`, `QUESTION`, `CONCEPT`, `PROJECT_DECISION`.
|
|
이 다섯을 어딘가에서 **손으로 나열하는 코드**가 계속 생겼습니다. 삼항 사슬이거나 배열
|
|
리터럴이었습니다.
|
|
|
|
```ts
|
|
// 삼항 사슬 — 마지막 else 가 모르는 것을 다 받아 간다
|
|
const path = kind === "CASE" ? "/cases/"
|
|
: kind === "REFERENCE" ? "/references/"
|
|
: kind === "QUESTION" ? "/questions/"
|
|
: "/projects/"; // ← CONCEPT 이 여기로 떨어진다
|
|
```
|
|
|
|
새 종류(`CONCEPT`)를 더할 때 이 자리를 빠뜨리면, **오류가 나지 않고 잘못된 값이 나갑니다.**
|
|
마지막 `else` 가 모르는 것을 조용히 받아 가기 때문입니다.
|
|
|
|
### 3.2 실제로 일어난 열세 건
|
|
|
|
| # | 어디 | 증상 | 커밋 |
|
|
|---|---|---|---|
|
|
| 1 | 게이트웨이의 문서 삭제 분기 | 개념을 지우면 "질문을 찾을 수 없습니다" | `dec86bd` |
|
|
| 2 | 게이트웨이의 문서 조회 분기 | `/concepts/idp-brokering` 이 404 (질문 조회를 불렀다) | `8996430` |
|
|
| 3 | 응답→기록 변환 분기 | 불렸어도 질문 매핑으로 떨어졌을 것 | `8996430` |
|
|
| 4 | 공개 주소→종류 역추적 삼항 | 개념 관계가 전부 `PROJECT` 로 분류 | `618a228` |
|
|
| 5 | 탐색 목록 매퍼 | `type=CONCEPT` 결과 0건 (서버는 보냈다) | `4da6d77` |
|
|
| 6 | 지식 목록 매퍼 | 개념이 통째로 버려짐 | `dc2fda7` |
|
|
| 7 | 작업본 목록의 종류 필터 | 개념 작업본을 걸러 볼 수 없음 | `b89a54f` |
|
|
| 8 | 모의 검증기의 유형별 칸 목록 | 개념 편집 시 모든 칸이 "허용되지 않은 속성" | `77ef304` |
|
|
| 9 | 백엔드 컨트롤러의 허용 enum 상수 | `?type=CONCEPT` 이 `PUBLIC_REQUEST_INVALID` | `3a226fb` |
|
|
| 10 | `CatalogEntry.kind` (계약) | 개념 작업본 생성 즉시 `/studio/catalog` 400 | `32d1785` |
|
|
| 11 | `ResolvedRelation.targetKind` (계약) | 개념을 관계로 걸면 미리보기 깨짐 | `2c25ccc` |
|
|
| 12 | `RelatedEntry.type` (관리 계약) | Case 가 개념을 가리킬 수 없음 | `2c25ccc` |
|
|
| 13 | `PublicSql.pathOf` (백엔드) | CONCEPT 케이스 없음 → `null` 경로 | `8cd8ee3` |
|
|
|
|
10·11·12 는 **계약 자체**에 있던 것입니다. 계약이 종류를 열거하는 자리가 여러 곳이라, 계약을
|
|
고치면서도 같은 실수를 했습니다.
|
|
|
|
### 3.3 고친 방법 — 표로 바꾸고 컴파일러에게 맡긴다
|
|
|
|
삼항 사슬을 `Record<Kind, Value>` 로 바꿨습니다.
|
|
|
|
```ts
|
|
// 종류가 늘면 이 자리가 비어 있다고 컴파일러가 잡는다
|
|
const PATH_PREFIX_KINDS: Record<PublicRecord["kind"], string> = {
|
|
CASE: "/cases/",
|
|
REFERENCE: "/references/",
|
|
QUESTION: "/questions/",
|
|
CONCEPT: "/concepts/",
|
|
};
|
|
```
|
|
|
|
백엔드에서는 **sealed switch 를 식(expression)으로** 쓴 자리가 이 일을 이미 하고 있었습니다.
|
|
`fa5158d`(개념 종류 추가) 커밋 메시지에 그 효과가 적혀 있습니다:
|
|
|
|
> sealed switch 가 이 변경을 안내했다 — 종류를 더하자 컴파일러가 게시 상태 코드·활동 유형·
|
|
> 소유자 유형·slug 중복 검사·렌더 모델까지 빠짐없이 짚었다. 문이 아니라 식으로 써 둔 덕이다.
|
|
|
|
**같은 언어 안에서도 문(statement)으로 쓴 switch 는 아무것도 잡아 주지 않습니다.** 식으로
|
|
써야 컴파일러가 빠진 가지를 요구합니다.
|
|
|
|
### 3.4 재발 방지 — 계약을 읽어 대조하는 가드
|
|
|
|
표로 바꿔도 **계약과 코드가 어긋나는 것**은 컴파일러가 모릅니다. 그래서 계약 문서를 직접
|
|
파싱해 대조하는 가드를 넣었습니다.
|
|
|
|
- `knowledge-list-kinds.test.ts` — 계약의 종류 enum 을 읽어, 목록 매퍼의 표에 전부 있는지 본다
|
|
- `contract-operation-coverage.test.ts` — 계약이 선언한 연산이 기여 목록에 등록됐는지 본다
|
|
- `StudioContractUnionJacksonTest`(백엔드) — 모든 `RecordKind` 가 `CatalogEntry.KindEnum` 으로
|
|
변환되는지 순회한다. 계약에서 CONCEPT 을 빼면 실제로 빨개지는 것을 확인했다 (`dd7c70e`)
|
|
- 설계 패키지에서는 **세 계약을 파싱해 "CASE 와 REFERENCE 를 함께 열거하면서 CONCEPT 이 없는
|
|
enum"을 전부 뽑아** 확인했습니다 (`2c25ccc`). 눈으로 찾을 일이 아니었습니다.
|
|
|
|
> **근거** — 지금 코드에서 표로 바뀐 자리와 **아직 남은 구멍 둘**:
|
|
> [`evidence/raw/guards/kind-tables-now.txt`](./evidence/raw/guards/kind-tables-now.txt).
|
|
> `PublicSql.pathOf` 는 sealed enum 이 아니라 String 으로 switch 하므로 여전히 `default -> null`
|
|
> 이 남아 있고, `validate-working-copy.ts` 의 `stringFields` 도 아직 삼항 사슬입니다.
|
|
|
|
### 3.5 이 갈래에서 배운 것
|
|
|
|
같은 실수를 열세 번 하고 나서야 규칙으로 굳혔습니다.
|
|
|
|
1. **종류를 나열하는 자리는 반드시 `Record<Kind, _>` 나 sealed switch 식으로 쓴다.** 삼항
|
|
사슬과 배열 리터럴은 새 종류를 조용히 삼킨다.
|
|
2. **컴파일러가 잡을 수 없는 자리(계약↔코드)는 계약을 읽어 대조하는 테스트를 둔다.**
|
|
3. **가드를 넣었으면 그 가드가 실제로 잡는지 되돌려 확인한다.** 위 가드들은 전부 결함을
|
|
되돌려 빨개지는 것을 확인한 뒤에 커밋했습니다.
|
|
|
|
---
|
|
|
|
## 4. 계약에 선언만 있고 구현이 없다
|
|
|
|
계약은 "이 연산이 있다"고 말하는데 서버에는 그 컨트롤러가 없는 상태입니다. 프론트는 계약을
|
|
믿고 부르고, 서버는 404 를 돌려주고, **화면은 그것을 "데이터가 없음"으로 그립니다.**
|
|
|
|
### 4.1 화면 다섯 곳이 조용히 비어 있었다 (`561d02a`, `b3aa304`)
|
|
|
|
계약에 선언만 되어 있고 구현이 없던 네 연산과, 의도된 스텁으로 남아 있던 catalog 두 종류가
|
|
공개 화면 다섯 곳을 비워 두고 있었습니다.
|
|
|
|
| 무엇이 비었나 | 왜 |
|
|
|---|---|
|
|
| 홈 「지금 집중하는 것」 | `home_focus_config` 는 마이그레이션이 빈 행 하나만 넣었고, `getHomeFocus`/`updateHomeFocus` 는 구현이 없었다. 세 슬롯이 모두 비면 홈은 그 영역을 아예 그리지 않으므로 **운영에서 한 번도 나타난 적이 없다** |
|
|
| 프로젝트 공개 여부 | 프로젝트는 `RecordKind` 에 없어 문서 게시 파이프라인을 타지 못하는데, 공개 화면들은 전부 `public_resource_projection` 의 PROJECT 행을 가시성 관문으로 쓴다. 그 행을 세우는 경로가 없었으므로 **프로젝트는 영원히 비공개였다** |
|
|
| 문서 사이 관계 연결 | `JdbcCatalogQueryAdapter` 의 RELATION/EVIDENCE 가 「슬라이스 2·5에서 채운다」는 주석과 함께 `List.of()` 스텁이었다. 어떤 기록도 연결 대상 목록을 채울 수 없었다 |
|
|
| 프로젝트 활동 | 계약에 목록·생성·수정이 선언돼 있었지만 구현이 없었고 `project_activity` 는 0행이었다 (`4c14f1e`) |
|
|
| 릴리즈(변경 기록) | 읽는 쪽은 있는데 쓰는 쪽이 없어, 페이지는 영원히 빈 채였다 (`386f360`) |
|
|
|
|
가장 무서운 것은 **홈 focus** 였습니다. 세 슬롯이 다 비면 화면이 그 영역을 통째로 그리지
|
|
않으므로, 그런 영역이 있다는 사실조차 화면에서 알 수 없었습니다.
|
|
|
|
### 4.2 편집기가 부르는 두 목록이 없었다 (`911e8ba`, `46e4e81`)
|
|
|
|
`GET /v1/studio/questions` 와 `GET /v1/studio/projects/{id}/decisions` 가 계약에 있고 모델도
|
|
생성됐는데 **컨트롤러가 없었습니다.** 프론트는 계약을 믿고 불렀고 서버는 404 를 돌려줬으며,
|
|
화면은 그것을 「이 프로젝트에 열린 질문이 없습니다」로 그렸습니다 — 실제로는 넷이 있었고 공개
|
|
사이트에도 나오고 있었습니다.
|
|
|
|
**생성 모델 검사는 schema 와 property 만 보므로 이 구멍을 잡지 못합니다.** 모델은 멀쩡히
|
|
생성되기 때문입니다.
|
|
|
|
### 4.3 재발 방지 — 계약↔컨트롤러 전수 대조
|
|
|
|
`ContractRouteCoverageTest`(백엔드)를 세웠습니다. `@RestController` 들을 리플렉션으로 훑어
|
|
매핑을 모으고, 계약이 선언한 경로와 대조합니다.
|
|
|
|
- 작업본 API 로 대체된 **옛 연산 51개**는 `SUPERSEDED_BY_WORKING_COPY_API` 로 명시해 둡니다 —
|
|
"구현하지 않기로 한 것"과 "빠뜨린 것"은 다릅니다
|
|
- 봉투 없이 바이트를 주는 `/media` 하나만 `ELSEWHERE` 로 면제합니다
|
|
- 매핑을 떼어 보고 **그 연산 하나를 정확히 짚는 것**을 확인했습니다
|
|
|
|
프론트에도 같은 가드를 뒀습니다(`contract-operation-coverage.test.ts`) — **양쪽에서 봐야
|
|
한쪽만 지웠을 때 잡힙니다.**
|
|
|
|
### 4.4 등록되지 않은 연산은 타입에는 보이는데 부를 수가 없다
|
|
|
|
이건 프론트 쪽의 같은 병입니다. 계약에서 타입은 생성되므로 **에디터에서는 멀쩡히 보이는데**,
|
|
기여 목록(`tech-log-management-contract-contribution.ts`)에 등록하지 않으면 실행 시 부를 수가
|
|
없습니다. 이 누락을 **네 번** 만났습니다:
|
|
|
|
- `getPublicConcept` — 개념 화면이 질문 조회를 불렀다 (`8996430`)
|
|
- `deleteConceptDraft` — 개념 삭제가 질문 삭제를 불렀다 (`dec86bd`)
|
|
- `listStudioQuestions` / `listStudioProjectDecisions` — 홈 편집기가 빈 목록을 그렸다 (`2b04282`)
|
|
- 축(variant) CRUD 네 연산 (`15e6ea8`)
|
|
|
|
`15e6ea8` 커밋에서 가드를 둘 넣었습니다. 공개 계약은 **전수 대조**하고, 관리 계약은 **한 종류만
|
|
빠진 자리**를 봅니다 — 깨진 것이 늘 그 모양이었기 때문입니다.
|
|
|
|
---
|
|
|
|
## 5. 계약에 자리가 없어 값이 경계에서 사라진다
|
|
|
|
DB 에는 작성자가 쓴 값이 그대로 있는데, 계약에 그 칸이 없어서 화면까지 오지 못하는 경우입니다.
|
|
**열한 건**이 있었습니다. 이 갈래가 가장 오래 눈에 띄지 않았습니다 — 오류가 전혀 없기 때문입니다.
|
|
|
|
### 5.1 공개 Reference 가 통째로 비어 있었다 (`ff0c12a`, `a5f93b9`, `7211dd1`)
|
|
|
|
Reference 를 공개했는데 **Studio 에서는 다 보이고 공개 화면만 비어 있었습니다.**
|
|
|
|
원인이 둘 겹쳤습니다.
|
|
|
|
1. **게이트웨이가 읽던 이름이 계약에 없는 것들이었습니다** — `purposeSummary`,
|
|
`applyWhenMarkdown`, `exceptionsMarkdown`, `examplesMarkdown`. 계약이 주는 이름은
|
|
`scopeSummary`, `appliesTo`, `excludedScope` 입니다. 전부 `undefined` 로 떨어졌고,
|
|
**`as string` 단언 때문에 타입 검사는 아무 말도 하지 않았습니다.**
|
|
2. Reference 의 본문은 `body_markdown` 이 아니라 `reference_detail.rules`/`examples` 에
|
|
있습니다. Studio 편집기가 규칙(제목+본문)과 예시를 따로 받고 마크다운 본문은 비워 두기
|
|
때문입니다. 공개 조회는 `body_markdown` 만 봐서 `content: ""` 를 내보냈습니다.
|
|
|
|
고친 뒤에 **값이 아니라 이름을 지키는 테스트**를 뒀습니다. 계약에서 그 칸이 사라지면
|
|
`satisfies` 가 먼저 깨집니다 — 이번 결함은 값을 검사해서는 잡히지 않았습니다.
|
|
|
|
### 5.2 관계의 요약이 경계 세 곳을 지나며 사라졌다 (`642afa8`, `a3ed23e`, `fa67a64`)
|
|
|
|
라벨은 고쳤는데 요약이 여전히 비어 있었습니다. 값이 **경계 세 곳**을 지나며 사라지고
|
|
있었습니다.
|
|
|
|
```
|
|
계약(요약 있음)
|
|
└─ flattenRelations 가 담지 않음 ← 1차로 고침
|
|
└─ 렌더 모델로 바꿀 때 버림 ← 담을 자리 자체가 없었다
|
|
└─ 화면 목록으로 넘길 때 또 버림
|
|
```
|
|
|
|
렌더 모델 계약(`ResolvedRelation`)에 담을 자리가 없었고 `additionalProperties: false` 라
|
|
실을 수도 없었습니다. 계약에 `summary` 를 더하고(required 아님 — 이미 나가 있는 응답을 깨지
|
|
않는다) 세 경계를 모두 이었습니다.
|
|
|
|
**교훈:** 한 경계를 고치고 "고쳤다"고 판단하면 안 됩니다. 값의 **여정 끝에서** 확인해야 합니다.
|
|
|
|
### 5.3 관계 한 줄에 세 가지가 뭉쳐 있었다 (`618a228`, `ca1bbfe`)
|
|
|
|
관계 한 줄이 답해야 하는 것이 셋인데 `reason` 한 칸을 지나고 있었습니다.
|
|
|
|
| 무엇 | 뜻 | 경로별로 어떻게 나왔나 |
|
|
|---|---|---|
|
|
| 대상의 종류 | 「근거」「관련 기준」 같은 분류 | 렌더 모델 경로: 작성자의 문장이 이 자리에 눌려 나옴 |
|
|
| 작성자가 쓴 이유 | 「다음에 무엇을 읽을지」의 답 | 공개 조회 경로: **아예 버려짐** |
|
|
| 대상의 요약 | 대상이 무엇인지 | — |
|
|
|
|
셋을 `label` / `note` / `summary` 로 갈랐습니다. 설명 자리에는 문장이 있으면 문장을, 없으면
|
|
요약을 보입니다 — **요약은 대상을 설명하고 문장은 왜 지금 이것을 읽어야 하는지를 설명합니다.**
|
|
|
|
### 5.4 결정 화면이 네 가지를 못 그렸다 (`987c1b8`, `026460f`, `31afb4d`)
|
|
|
|
공개 결정 화면에 네 가지가 어긋나 있었습니다 — 제목 자리에 결정문 전문이 나오고, 요약이 아예
|
|
없고, 줄바꿈이 전부 접히고, 영향과 근거 기록이 늘 비어 있었습니다.
|
|
|
|
원인이 하나로 모입니다. **결정에는 상세 endpoint 가 없습니다** — 공개 주소가 목록 위의
|
|
앵커입니다. 그래서 화면이 그리는 칸은 전부 목록 항목에 있어야 하는데
|
|
`title`·`summary`·`consequences`·`evidence` 가 빠져 있었습니다. 그래서 프론트는 `statement`
|
|
를 제목 자리에도 썼고 영향은 빈 배열로 고정해 뒀습니다. **DB 에는 작성자가 쓴 제목, 여러 줄
|
|
요약, 영향 4건이 그대로 있었습니다.**
|
|
|
|
### 5.5 나머지 여섯 건
|
|
|
|
| 무엇이 비었나 | 원인 | 커밋 |
|
|
|---|---|---|
|
|
| 문서 요약(제목 아래 한 줄) | 공개 응답에 `summary` 자리가 없어 유형별 요약을 대신 씀 → 머리말이 바로 아래와 같은 글을 두 번 말함 | `0ffbc28`, `c6d9d2d` |
|
|
| 프로젝트 「주요 주제」 | `project_topic` 테이블도 조인도 가능했는데 **응답에 실을 자리가 없었다** | `06ae075`, `6aa1400` |
|
|
| 프로젝트 기록 목록의 요약·주제·게시일 | `RelatedEntry` 를 그대로 실어 칸이 없었다 → 모든 줄이 "제목만 있고 · 만 남은" 모양 | `76a7ccb`, `f0407d9` |
|
|
| 질문 목록의 주제 | 지식 목록은 처음부터 `primaryTopic` 을 실었는데 질문 목록만 빠짐 → 질문 줄만 맥락이 「· 프로젝트」로 시작 | `a58ad30`, `e185b87` |
|
|
| 프로젝트·주제의 논지(thesis) | 담을 칸이 없어 `purpose`(시작할 때 쓰는 글)를 대신 보여 줌 | `2d9672d`, `78ec5f9` |
|
|
| 주제 목록의 논지·축 | 이름과 개수만 실어, 독자가 들어갈지 말지 정할 근거가 없었다 | `559d04f`, `22a65dc` |
|
|
| 프로젝트 목록 행의 slug | 다른 목록이 프로젝트를 가리킬 때 쓰는 것은 id 가 아니라 slug 인데 행이 싣지 않았다 | `ffa088b`, `711b2c3` |
|
|
| 결정 목록 항목의 slug | 공개 주소가 `#{slug}` 앵커인데 항목에 slug 가 없어 화면이 앵커를 달 수 없었다 | `1aae8dc` |
|
|
|
|
### 5.6 이 갈래에서 배운 것
|
|
|
|
- **"Studio 에서는 보이는데 공개 쪽만 비어 있다"는 신호는 거의 항상 계약의 빈칸입니다.** 두
|
|
화면이 같은 DB 를 보는데 한쪽만 비면, 그 사이에 계약이 있습니다.
|
|
- 계약에 칸을 더할 때는 **required 에 넣을지**를 따로 판단해야 합니다. 이미 나가 있는 응답을
|
|
깨지 않으려면 required 가 아니어야 합니다(`fa67a64`).
|
|
- 화면이 그리는 칸이 전부 응답에 있는지는 **화면 쪽에서 역으로** 확인해야 합니다. 결정 목록이
|
|
그 예입니다 — 상세 endpoint 가 없으면 목록이 문서 전체를 실어야 합니다.
|
|
|
|
---
|
|
|
|
## 6. 타입 검사가 통과시키는 자리
|
|
|
|
"타입 검사가 통과했으니 반영됐다"는 판단이 여러 번 틀렸습니다. TypeScript 와 Java 각각에
|
|
**검사를 무력화하는 자리**가 있었고, 그 자리를 몰라서 잘못 판단했습니다.
|
|
|
|
### 6.1 메서드 매개변수는 bivariant 다 (`6429aee`)
|
|
|
|
개념 삭제가 계속 질문 삭제 경로로 나갔습니다. 앞선 커밋이 게이트웨이를 고치지 못했는데,
|
|
**타입 검사가 통과해서 반영된 줄 알았습니다.**
|
|
|
|
```ts
|
|
// 포트 시그니처
|
|
deleteDocument(kind: "CASE" | "REFERENCE" | "QUESTION" | "CONCEPT", id: string): Promise<void>;
|
|
|
|
// 구현이 이렇게 좁게 적혀 있어도 위 시그니처를 "만족"한다
|
|
deleteDocument(kind: "CASE" | "REFERENCE" | "QUESTION", id: string) { … }
|
|
```
|
|
|
|
**TypeScript 에서 메서드 매개변수는 bivariant 입니다.** 구현이 종류를 좁게 적어도 넓은 포트
|
|
시그니처를 만족한 것으로 통과합니다. 그래서 "타입 통과"를 보고 반영됐다고 판단한 것이
|
|
틀렸습니다.
|
|
|
|
배포된 번들에 옛 삼항이 그대로 남아 서버 로그에 `DELETE /api/v1/studio/questions/{id} 404`
|
|
가 계속 찍혔습니다.
|
|
|
|
**같은 병이 `RecordFilters` 에서도 났습니다**(`67a5491`). 포트와 정적 어댑터에 타입이 따로
|
|
있어, 포트에 필터가 늘어도 어댑터는 모르는 상태가 됐습니다. `satisfies` 가 잡지 못했습니다 —
|
|
같은 이유입니다. 타입을 하나로 합쳤습니다.
|
|
|
|
### 6.2 `as` 단언이 어긋남을 가린다 (`7211dd1`, `ab4d822`)
|
|
|
|
```ts
|
|
const summary = body.purposeSummary as string; // 계약에 그런 칸이 없다
|
|
```
|
|
|
|
전부 `undefined` 로 떨어졌는데 **타입 검사는 아무 말도 하지 않았습니다.** 계약의 타입을 그대로
|
|
쓰도록 바꿔서, 모양이 바뀌면 컴파일이 먼저 막게 했습니다.
|
|
|
|
`ab4d822` 는 더 나빴습니다. `points` 를 `{group, items}` 배열로 읽고 `.filter` 를 불렀는데
|
|
계약의 `QuestionPointGroup` 은 `facts`/`assumptions`/`unknowns`/`constraints` 를 키로 갖는
|
|
**객체**입니다. 객체에는 `.filter` 가 없으니 매핑이 통째로 터졌고, `as` 캐스트가 그 어긋남을
|
|
타입 검사에서 가렸습니다.
|
|
|
|
### 6.3 `(input: never)` 로 받아 캐스팅하는 조립기 (`22090a4`)
|
|
|
|
목록의 페이지 번호를 눌러도 쪽이 넘어가지 않았습니다. 요청을 만드는 조립기가 질의 인자를
|
|
손으로 나열하는데 거기 `page` 가 없었습니다.
|
|
|
|
**이것이 타입 검사를 통과한 이유:** 조립기가 입력을 `(input: never)` 로 받아 캐스팅합니다.
|
|
계약에 인자를 더해도 여기 적지 않으면 **컴파일러는 아무 말도 하지 않고 요청만 조용히 그 값을
|
|
뺍니다.**
|
|
|
|
### 6.4 루트 tsconfig 가 한 파일도 검사하지 않았다 (`e9b8661`)
|
|
|
|
운영에서 릴리즈 목록이 `ReferenceError` 로 비었습니다. `GuardedStudioLink` import 가 빠졌고
|
|
`navigate` 는 아예 정의된 적이 없었습니다.
|
|
|
|
**`npx tsc --noEmit` 이 통과했기 때문에 이것을 못 봤습니다.** 루트 tsconfig 는 `"files": []` 에
|
|
project references 만 나열하므로 그 명령은 **한 파일도 검사하지 않고 성공합니다.** 실제 검사는
|
|
`npm run check:types` 가 여섯 개 프로젝트를 돌며 합니다.
|
|
|
|
그 명령으로 돌리자 저장소에 남아 있던 다른 오류도 함께 드러났습니다 — `CatalogEntry` 가
|
|
export 되지 않는 것, 라우트 파라미터가 `unknown` 인 것, 메시지 키가 파라미터를 받도록
|
|
등록되지 않은 것, `ReleaseIndexItem` 에 `summary` 가 없는 것.
|
|
|
|
> 이 건은 메모리에 남겨 뒀습니다 — `tech-log-frontend-typecheck-command.md`
|
|
|
|
### 6.5 Java 쪽: 클래스패스에 남은 Jackson 2 (`0da7c7e`)
|
|
|
|
`JdbcProjectRepositoryAdapter` 가 `com.fasterxml.jackson.databind.ObjectMapper`(Jackson 2)를
|
|
요구했습니다. 이 빌드는 Jackson 3(`tools.jackson.databind`)이라 그런 빈이 없고, 컨텍스트가
|
|
refresh 에 실패해 **파드가 CrashLoopBackOff** 로 들어갔습니다.
|
|
|
|
**컴파일이 잡지 못한 이유:** Jackson 2 타입이 어떤 전이 의존성을 통해 클래스패스에 아직
|
|
남아 있어서, 잘못된 import 가 정상적으로 해석됩니다. 컨테이너만이 알려 줍니다.
|
|
|
|
### 6.6 이 갈래에서 배운 것
|
|
|
|
- **"타입 검사 통과"는 반영의 증거가 아닙니다.** bivariance·`as`·`never` 캐스트·검사하지 않는
|
|
tsconfig — 네 가지가 각각 통과시켰습니다.
|
|
- 반영의 증거는 **그 값의 여정 끝**입니다. 배포본에서 실제 요청을 보거나, 실제로 게이트웨이를
|
|
불러 어떤 연산이 실행되는지 확인해야 합니다. `6429aee` 에서 그 가드를 넣었습니다 — CONCEPT
|
|
을 `deleteQuestion` 으로 되돌리면 깨지는 것을 확인했습니다.
|
|
|
|
---
|
|
|
|
## 7. 테스트가 지나지 않는 이음매
|
|
|
|
"모든 검사가 통과했는데 운영에서 깨졌다"가 일곱 번 있었습니다. 매번 **테스트가 그 이음매를
|
|
지나지 않았기** 때문입니다.
|
|
|
|
### 7.1 컨텍스트를 띄우지 않는 테스트 (`ca63d7d`)
|
|
|
|
새 활동 어댑터가 생성자를 둘 갖고 있었습니다 — 하나는 운영용, 하나는 테스트가 id 생성기를
|
|
넣기 위한 것. 둘 중 어느 것에도 `@Autowired` 가 없어 컴포넌트 스캔이 고르지 못했습니다.
|
|
|
|
> 컴파일도, 단위 테스트도, **실제 PostgreSQL 위에서 도는 통합 테스트 26개도 전부 통과했다.
|
|
> 그 어느 것도 애플리케이션 컨텍스트를 띄우지 않기 때문이다.** 운영에서 파드가
|
|
> CrashLoopBackOff 로 들어갔고, 그때서야 드러났다.
|
|
|
|
**재발 방지:** D20 규칙을 세웠습니다 — 스캔되는 컴포넌트는 생성자가 하나이거나, 여럿이면
|
|
그중 하나에 `@Autowired` 가 붙어야 한다. 규칙이 실제로 잡는지 결함을 되돌려 확인했습니다.
|
|
|
|
### 7.2 SQL 이 한 번도 실행되지 않았다 (`37f474a`)
|
|
|
|
작업본 삭제가 500 을 돌려줬습니다. 참조 검사가
|
|
`public_resource_projection.document_id` 를 조회했는데 **그 컬럼이 없습니다** — 이 테이블은
|
|
한 테이블이 case·question·project·release 를 모두 담기 때문에 `(resource_type, resource_id)`
|
|
로 기록을 가리킵니다.
|
|
|
|
> 그 쿼리의 여섯 컬럼 중 다섯은 마이그레이션과 대조했다. 이 하나만 가정했고, 그것이 틀렸다.
|
|
|
|
**진짜 실패는 이 SQL 이 한 번도 실행된 적이 없다는 것이었습니다.** 표준 `check` 는
|
|
Testcontainers 를 띄우지 않으므로 **persistence SQL 은 한 번도 실행되지 않은 채 빌드가
|
|
통과합니다.** 컴파일도 단위 테스트도 컬럼 이름을 검증하지 못합니다.
|
|
|
|
**재발 방지:** 삭제 경로 전용 통합 테스트 태스크를 만들고, 실패했던 그 쿼리를 포함해 여덟
|
|
시나리오를 실제 PostgreSQL 에서 돌립니다.
|
|
|
|
### 7.3 HTTP 게이트웨이의 매핑을 지나는 테스트가 없었다 (`ab4d822`)
|
|
|
|
게시한 질문의 공개 상세가 「요청을 처리하지 못했습니다」만 띄웠습니다.
|
|
|
|
> 이 사고가 지나간 이유는 HTTP 게이트웨이의 질문 상세 매핑을 지나는 테스트가 없었기
|
|
> 때문이다. **화면 테스트는 정적 픽스처 어댑터를 쓰므로 계약 모양을 한 번도 통과시키지
|
|
> 않는다.**
|
|
|
|
**재발 방지:** 계약 모양 그대로의 응답을 진짜 게이트웨이에 넣고 네 칸이 채워져 나오는지 묻는
|
|
테스트를 넣었습니다 — 되돌려 보면 운영에서 난 것과 같은 `points.filter is not a function`
|
|
으로 실패합니다.
|
|
|
|
### 7.4 합성 루트(composition root)에 테스트가 없었다 (`03986da`, `7600711`)
|
|
|
|
**공개 사이트 전체가 오류 화면이었습니다.** 로그아웃 상태 방문자 — 공개 사이트의 전체
|
|
독자 — 가 브라우저에서 요청을 한 건도 내보내지 못했습니다.
|
|
|
|
세 결함이 겹쳐 있었고 각각이 다음 것을 가렸습니다.
|
|
|
|
1. `attachCredentials` 가 Studio 헬퍼에 먼저 묻는데, 그 헬퍼는 자기 것이 아닌 프로파일에
|
|
`null` 을 돌려줍니다. 그 아래 폴백이 세션을 읽고 인증되지 않은 것을 거절합니다. 공개
|
|
읽기는 ANONYMOUS 프로파일을 선언하므로 그 폴백에 떨어졌습니다.
|
|
2. 요청이 흐르자 두 번째가 드러났습니다 — `envelopeError()` 가 `ApiError.code` 를 **Studio
|
|
enum 에 고정**해 세 표면이 공유했습니다. 공개/관리는 각자 자기 계약에 enum 을 선언하므로
|
|
그들이 돌려준 모든 오류가 검증에 실패해 `CONTRACT_VIOLATION` 으로 도착했습니다.
|
|
**엄격한 enum 을 잘못된 표면의 계약에 대고 검사해도 여전히 엄격해 보입니다** — 그래서
|
|
어떤 게이트도 잡지 못했습니다.
|
|
3. not-found 경로가 봉투에 없는 `status` 를 읽고 있었습니다.
|
|
|
|
> 이 결함은 공개 소스가 HTTP 가 된 뒤에야 나타날 수 있었다. 이번 주까지 그 경로는 브라우저에서
|
|
> 한 번도 돌지 않았다. **스위트가 잡지 못한 이유는 게이트웨이와 화면을 검사할 뿐 합성 루트의
|
|
> credential 결정은 검사하지 않기 때문이다 — 그 이음매에는 테스트가 없고, 이것이 그 대가다.**
|
|
|
|
**재발 방지:** 회귀 테스트가 **실제 런타임 어댑터를 배포된 백엔드의 실제 404 본문에 대고**
|
|
조립합니다. 게이트웨이 테스트(실행기를 스텁)도 화면 테스트(게이트웨이를 스텁)도 이 이음매를
|
|
덮지 않고, 장애 전체가 거기 살고 있었습니다.
|
|
|
|
### 7.5 화면 테스트를 아예 돌리지 않았다 (`fd73bc8`)
|
|
|
|
> 화면 테스트는 `test:unit` 이 아니라 `test:tech-log` 가 돌린다. 그것을 돌리지 않아 위 두
|
|
> 결함과, 의도한 변경에 고정돼 있던 단언들이 **23건 빨간 채로 여러 커밋을 지나갔다.**
|
|
|
|
> 이 건도 메모리에 남겼습니다 — 배포 전 검증은 `check:types` + `lint` + `test:unit` +
|
|
> `test:component` + `test:tech-log` **다섯 개**를 다 돌려야 합니다.
|
|
|
|
### 7.6 생성기가 계약 필드를 조용히 빠뜨렸다 (`365560e`)
|
|
|
|
이 건은 결이 다릅니다. **테스트가 아니라 생성기가** 값을 버렸습니다.
|
|
|
|
파생 단계의 YAML alias 때문에 swagger-parser 가 스키마 15개를 "is not of type `object`" 로
|
|
거절했습니다. 거절당한 스키마들은 전부 `type: object` 를 명시하고 있어서 **계약 결함처럼
|
|
보이지 않았고**, `validateSpec` 을 끄면 생성은 성공했습니다. 그런데 그렇게 만든 모델에서
|
|
`LatestEntry.publishedAt`, `ProjectListItem.updatedAt`, `SearchResultItem.matchedFields`,
|
|
`ReleaseListItem.changeTypes` 가 사라져 있었습니다. **컴파일은 통과합니다 — 아직 아무도 그
|
|
필드를 안 쓰니까.**
|
|
|
|
원인은 prepare 단계였습니다. 변환들이 같은 `Map` 인스턴스를 여러 property 에 재사용했고
|
|
snakeyaml 이 그 지점을 anchor/alias(`&id001` / `*id001`)로 덤프했습니다. 파생 스펙에 alias 가
|
|
**34곳** 있었습니다.
|
|
|
|
**재발 방지:**
|
|
- 덤프 직전 deep copy 로 노드 identity 를 끊어 alias 를 원천 차단하고, 남으면 빌드가
|
|
실패하도록 fail-closed 게이트를 뒀습니다. `validateSpec` 은 다시 켰습니다
|
|
- `verifyPublicGeneratedModels` 를 **schema 이름 대조에서 property 대조로 강화**했습니다.
|
|
이번 누락을 그 게이트가 통과시켰기 때문입니다. 지금은 schema 62개 · property 250개를 셉니다
|
|
|
|
### 7.7 이 갈래에서 배운 것
|
|
|
|
| 이음매 | 무엇이 지나지 않았나 | 어떻게 덮었나 |
|
|
|---|---|---|
|
|
| 스프링 컨텍스트 | 어떤 테스트도 컨텍스트를 띄우지 않았다 | ArchUnit D20 규칙 |
|
|
| persistence SQL | `check` 가 Testcontainers 를 안 띄운다 | 전용 통합 테스트 태스크 |
|
|
| HTTP 매퍼 | 화면 테스트는 픽스처를 쓴다 | 계약 모양 응답을 진짜 게이트웨이에 넣는 테스트 |
|
|
| 합성 루트 | 게이트웨이/화면 테스트 둘 다 스텁을 쓴다 | 실제 어댑터 + 실제 404 본문 |
|
|
| 생성기 | 모델이 만들어지면 통과한다 | property 단위 대조 |
|
|
|
|
---
|
|
|
|
## 8. 라우트를 하나 더하면 함께 울리는 손 목록
|
|
|
|
이 저장소는 라우트를 여러 곳에서 셉니다. 라우트를 하나 더하면 그 자리가 전부 울립니다. 문제는
|
|
**어떤 것은 빌드 직전에야, 어떤 것은 배포 뒤에야** 운다는 것입니다.
|
|
|
|
### 8.1 라우트 하나가 건드리는 자리
|
|
|
|
`048c1b2`(개념 라우트 추가) 커밋이 그 목록을 남겼습니다.
|
|
|
|
```
|
|
라우트 계약 tech-log-route-contract.ts
|
|
런타임 등록 route-runtime-contract
|
|
메시지 카탈로그 화면 제목·설명
|
|
nginx 서빙 패턴 tech-log-serving-contract.json → 생성된 nginx conf
|
|
코드 분할 청크 vite.config.ts 의 chunk 이름 표
|
|
CI 게이트 FE-GATE-009 라우트마다 수동 접근성 증거 1개
|
|
CI 게이트 아티팩트 기준선 정확한 개수를 고정
|
|
CI 게이트 형상 digest 게이트 집합의 sha256
|
|
```
|
|
|
|
### 8.2 nginx 가 모르는 라우트는 404 다 (`ab8c6c1`, `6784eb1`)
|
|
|
|
`/studio/releases` 가 nginx 에서 **평문 404** 를 돌려줬습니다. 라우트는 있고 청크도 빌드됐고
|
|
SPA 내부 이동으로는 화면에 닿을 수 있었지만, **하드 로드나 새로고침은 거기까지 가지 못합니다** —
|
|
웹 서버가 그 경로의 존재를 들은 적이 없기 때문입니다.
|
|
|
|
> 서빙 계약의 공개 절반은 라우트 레지스트리에서 패턴을 유도한다. **Studio 절반은 손으로
|
|
> 유지하는 배열이었고, 손으로 유지하는 배열이 실패하는 방식 그대로 실패했다** — `^/studio/assets$`
|
|
> 위의 주석이 바로 그 버그를 한 번 고친 기록이고, 라우트를 더하니 즉시 반복됐다.
|
|
|
|
`6784eb1` 은 더 근본적이었습니다. 서빙 계약이 **번들된 픽스처에 우연히 들어 있던 공개 경로를
|
|
전부 열거**하고, 생성된 nginx 가 정확히 그것들을 `location =` 블록으로 게시했습니다. **빌드
|
|
이후에 게시된 기록** — 백엔드를 두는 이유 그 자체 — 은 SPA 에 묻기도 전에 엣지에서 404 였습니다.
|
|
경로 27개가 얼어 있었고, 28번째는 무엇이든 닿을 수 없었습니다.
|
|
|
|
이제 라우트 계약에서 **등록된 Public 라우트마다 정규식 하나**를 만듭니다. 파라미터는 한
|
|
세그먼트만 잡고 슬래시는 잡지 않으므로 `/cases/a/b` 는 404 로 남습니다. catch-all 라우트는
|
|
번역하지 않고 버립니다 — 모든 미매치 URL 에 index.html 을 주면 엣지 404 가 soft 200 이 되어
|
|
깨진 링크를 크롤러와 우리에게서 숨깁니다.
|
|
|
|
### 8.3 vite chunk 이름 표 (`197db74`)
|
|
|
|
주제 편집 화면을 더하고 이 표를 빠뜨렸더니 **번들은 만들어지는데 빌드 매니페스트 단계에서**
|
|
`Missing built route chunk: TECH_LOG_STUDIO_TOPIC_EDIT` 로 멈췄습니다 — 다섯 개의 검사를 다
|
|
통과한 뒤 **배포 직전에야** 드러난다는 뜻입니다.
|
|
|
|
이 표도 손으로 나열한 목록 중 하나이므로 다섯 검사 안에서 대조하게 했습니다
|
|
(`route-chunk-names.test.ts`).
|
|
|
|
### 8.4 CI 게이트 기준값이 함께 움직인다
|
|
|
|
FE-GATE-009 는 **설치된 라우트마다 수동 접근성 증거를 하나씩** 요구하고 그 집합이 정확히
|
|
일치하지 않으면 거절합니다. 그래서 라우트를 더할 때마다 이 셋이 함께 움직입니다.
|
|
|
|
| 커밋 | 라우트 | 아티팩트 기준선 | 증거 개수 | digest |
|
|
|---|---|---|---|---|
|
|
| `16e5b9f` | `/studio/projects/:id` | 132 → 133 | 111 → 112 | 187dbd96… 재계산 |
|
|
| `84d72c4` | `/studio/releases/:id` | 133 → 134 | 112 → 113 | f9e7e521… 재계산 |
|
|
| `048c1b2` | `/concepts/:slug` | +1 | +1 | fb138e7c… 재계산 |
|
|
| `fe6b56a` | `/topics`, `/topics/:s/:v`, `/studio/topics/:id` | 135 → 138 | 114 → 117 | 87a22f68… 재계산 |
|
|
|
|
**digest 재계산의 규칙:** 매번 **이전 gates.json 에서 옛 상수를 먼저 재현**해 계산 방법이
|
|
맞는지 확인한 뒤 새 파일을 해싱했습니다. 그렇게 하지 않으면 "계산이 달라졌는데 새 값이
|
|
나왔다"와 "파일이 바뀌어서 새 값이 나왔다"를 구분할 수 없습니다.
|
|
|
|
### 8.5 남은 문제
|
|
|
|
주제 화면 셋(`/topics`, `/topics/:slug/:variant`, `/studio/topics/:id`)을 더할 때 저는 이
|
|
목록을 **또 빠뜨렸습니다.** 게이트가 빨간 채로 여러 커밋을 지나갔고, 결정 404 를 고치던
|
|
`fe6b56a` 에서야 함께 맞췄습니다.
|
|
|
|
즉 **가드는 작동했지만 제가 그 가드를 돌리지 않았습니다.** §7.5 와 같은 병입니다.
|
|
|
|
---
|
|
|
|
## 9. 서버가 갈 곳 없는 주소를 만든다
|
|
|
|
화면 코드 어디에도 흔적이 없고 **방문자만 404 를 만나는** 부류입니다. 주소가 게시 시점에
|
|
굳어져 DB 에 저장되기 때문입니다.
|
|
|
|
### 9.1 축(variant) 링크가 자기 자신을 가리켰다 (`8828005`, `63eb177`, `71bab4c` → `67a5491`, `b93d62a`)
|
|
|
|
주제 화면의 네 줄(SPA·Mediator·BFF·Forward-Auth)은 링크인데 **눌러도 아무 일이 없었습니다.**
|
|
|
|
처음에 `/topics/{주제}/{축}` 이라 적어 두었는데 그런 화면이 없어서, 축의 주소를 **주제 화면
|
|
안의 앵커**로 바꿨습니다(`63eb177`, `71bab4c`). 그랬더니 정작 주제 화면에서는 그 링크가
|
|
**자기 자신을 가리켰습니다** — 주소만 바뀌고 화면은 그대로였습니다.
|
|
|
|
그래서 **축에 자기 화면을 줬습니다**(`67a5491`). 목록 조회에 `variant` 필터를 더해
|
|
`record_variant` 로 거릅니다. 축 slug 는 주제 안에서만 유일하므로 주제까지 함께 맞춥니다 —
|
|
주제를 빼면 다른 주제의 같은 이름 축이 함께 걸립니다.
|
|
|
|
> **이 건에서 제가 만든 2차 사고:** 축 화면을 만들고 **백엔드를 프론트보다 먼저 배포**했습니다.
|
|
> nginx 설정은 라우트 계약에서 생성되므로, 프론트가 배포되기 전까지 `/topics/x/y` 는 404 입니다.
|
|
> 서버는 이미 그 주소를 내보내고 있었고, 사용자는 네 링크가 전부 404 인 화면을 봤습니다.
|
|
> **순서가 있습니다 — 새 라우트는 프론트가 먼저입니다.**
|
|
|
|
### 9.2 결정 링크가 404 였다 (`1aae8dc`, `8cd8ee3`, `fe6b56a`)
|
|
|
|
`/references/external-idp-federation-application-boundary` 의 「다음에 읽을 것」 두 번째
|
|
항목이 404 였습니다.
|
|
|
|
<!-- techviz:begin id=decision-path-404 context-sha256=93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f -->
|
|
<!-- techviz:generate id=decision-path-404 -->
|
|

|
|
|
|
<details>
|
|
<summary>Diagram description</summary>
|
|
|
|
위에서 아래로 여섯 번의 이동이 있다. 계약 ProjectDecisionItem 은 공개 주소가 decisions#{slug} 앵커라고 규정한다. 게시 시점의 PublicPaths.forKind 는 그 대신 decisions/{slug} 를 만들어 public_resource_projection 에 저장한다. 조회 시점의 PublicSql.pathOf 가 저장된 주소를 읽고 방문자에게 링크로 내보낸다. 방문자가 그 주소를 요청하면 공개 라우트에는 projects/{slug}/decisions 하나뿐이라 맞는 라우트가 없고 404 가 돌아온다.
|
|
|
|
</details>
|
|
|
|
[Editable source](assets/diagrams/decision-path-404/decision-path-404.drawio) · [Grounded VizSpec](.techviz/decision-path-404/spec.json)
|
|
<!-- techviz:end id=decision-path-404 -->
|
|
|
|
**원인:** 결정에는 상세 화면이 없고 공개 라우트는 `/projects/{slug}/decisions` 하나뿐인데,
|
|
게시할 때 만든 주소는 `/projects/{slug}/decisions/{slug}` 였습니다. 계약은 **이미** 공개 주소가
|
|
`#{slug}` 앵커라고 적어 두었는데, 만드는 쪽(`PublicPaths.forKind`, `PublicSql.pathOf`)이
|
|
계약을 따르지 않았습니다.
|
|
|
|
**고친 것:**
|
|
- 두 곳이 앵커를 만들게 했다
|
|
- **주소는 게시 시점에 굳어져 저장되므로 이미 게시된 행도 V15 마이그레이션에서 함께 고쳤다** —
|
|
코드만 고치면 기존 링크는 깨진 채 남는다
|
|
- `public_route.slug` 는 앵커가 있으면 그 뒤를 조각으로 읽는다 — 마지막 `/` 뒤를 자르면
|
|
`decisions#slug` 가 slug 로 저장된다
|
|
- 목록 항목이 앵커를 달 수 있도록 계약에 `slug` 를 더했다
|
|
- 목록 화면이 `slug` 를 element id 로 달고, 앵커로 들어오면 데이터를 받아 그린 뒤 스크롤한다
|
|
|
|
**재발 방지 (두 겹):**
|
|
1. `PublicPathsTest`(백엔드) — 종류마다 만들어 낸 경로가 실제 공개 라우트 패턴에 맞는지 본다
|
|
2. `resolvesToPublicRoute`(프론트) — route contract 에서 읽은 라우트 표에 서버가 준 주소를
|
|
맞춰 보고, **맞는 라우트가 없으면 링크로 그리지 않는다.** 이 부류가 또 생겨도 방문자가
|
|
404 를 만나지는 않는다
|
|
|
|
배포 후 사이트 전체를 훑어 **서버가 내보내는 주소 26개 + 주제·축 9개 = 35개 전부 200** 임을
|
|
확인했습니다.
|
|
|
|
> **근거** —
|
|
> [`evidence/raw/db/decision-path-after-v15.txt`](./evidence/raw/db/decision-path-after-v15.txt) (저장된 주소가 앵커로 바뀌고 V15 가 적용된 것) ·
|
|
> [`evidence/raw/api/decision-anchor-fixed.txt`](./evidence/raw/api/decision-anchor-fixed.txt) (그 링크가 실제로 200) ·
|
|
> [`evidence/raw/audit/dead-link-sweep.txt`](./evidence/raw/audit/dead-link-sweep.txt) (35개 전수 200)
|
|
|
|
### 9.3 주제가 없는 기록이 죽은 링크를 달았다 (`23efcf0`)
|
|
|
|
주제 없이 게시된 기록이 있는데 화면이 그것을 모르고 `/topics/` 로 가는 **이름 없는 링크**를
|
|
만들고 있었습니다 — 문서 머리말의 breadcrumb 과 탐색의 「주제 없음」 묶음 둘 다. 프로젝트
|
|
조각은 처음부터 조건부였는데 주제 쪽만 아니었습니다.
|
|
|
|
### 9.4 주제 화면이 주제 셋만 열었다 (`2632850` → `15e6ea8`, `8828005`)
|
|
|
|
문서 머리말의 주제 링크가 `/topics/:slug` 로 가는데, 그 화면은 `jpa`/`authentication`/`redis`
|
|
**셋을 하드코딩**해 두고 있어 실제 주제는 무엇이든 404 였습니다. 게시한 모든 문서의 주제 링크가
|
|
거기로 갔습니다.
|
|
|
|
당시에는 주제 페이지를 채우는 대신 링크를 탐색 필터(`/explore?topic=`)로 **우회**했습니다
|
|
(`2632850`). 그 페이지만 줄 수 있는 것 — 설명, 범위, 선별한 대표 기록 — 이 전부 비어 있었고
|
|
Studio 에 주제 설명을 쓸 칸조차 없었기 때문입니다.
|
|
|
|
나중에 주제 화면을 계약에 잇고 하드코딩을 없앤 뒤(`15e6ea8`) 링크를 곧장 주제 화면으로
|
|
되돌렸습니다(`8828005`).
|
|
|
|
> **이건 뒤집힌 판단입니다.** 우회가 틀린 것은 아니었습니다 — 그때는 채울 내용이 없었습니다.
|
|
> 다만 우회를 남겨 두면 "왜 주제 링크가 탐색으로 가지?"라는 질문이 계속 남습니다. 우회할
|
|
> 때는 **되돌릴 조건**을 함께 적어야 합니다. `2632850` 커밋 메시지에 그 조건을 적어 뒀고,
|
|
> 실제로 그 조건이 충족됐을 때 되돌렸습니다.
|
|
|
|
---
|
|
|
|
## 10. 실패를 없음으로 그린다
|
|
|
|
화면이 **거짓말을 하는** 부류입니다. 못 읽은 것을 "없다"고 그리면 작성자는 자기가 아직 쓰지
|
|
않은 것으로 읽습니다.
|
|
|
|
### 10.1 「이 프로젝트에 열린 질문이 없습니다」 (`7acde27`)
|
|
|
|
홈 「지금 집중하는 것」 편집기는 열린 질문과 결정을 못 읽으면 **빈 배열로 삼키고** 「이
|
|
프로젝트에 열린 질문이 없습니다」라고 적었습니다. 실제로는 넷이 있었고 공개 사이트에도 나오고
|
|
있었습니다.
|
|
|
|
> 거짓말을 하느니 못 읽었다고 말한다.
|
|
|
|
### 10.2 한 칸의 실패가 옆 칸을 끌고 내려간다 (`6e784ed`, `fd73bc8`, `3bb724b`)
|
|
|
|
편집기가 질문과 결정을 `Promise.all` 로 묶어 읽어서, **결정만 터지는데 멀쩡히 오던 질문
|
|
목록까지** 「불러오지 못했습니다」가 됐습니다. 둘을 따로 읽도록 갈랐습니다.
|
|
|
|
`fd73bc8` 은 더 미묘한 변종입니다:
|
|
|
|
> `Promise.all([gateway.foo()])` 은 foo 가 **거절하는 것만** 잡는다. 호출이 **동기적으로
|
|
> 던지면** 배열을 만드는 중에 터져 rejection handler 를 지나지 못하고, 그러면 홈 focus 한 칸
|
|
> 때문에 대시보드 전체가 빈 화면이 된다.
|
|
|
|
이 판단은 나중에 주제 탭에도 적용했습니다(`3bb724b`) — 탭 하나를 못 받아도 탭 줄과 나머지는
|
|
그대로 남고, 그 자리에 못 받았다고 적습니다.
|
|
|
|
### 10.3 계약 밖 값이 500 을 만든다 (`365560e`, `edb0890`)
|
|
|
|
`latestEntries` 가 투영의 **모든** `resource_type` 을 흘렸습니다. 계약의 `LatestEntry.entryType`
|
|
은 네 값뿐이라 `QUESTION` 이 섞이면 매퍼가 500 을 냅니다 — **홈 화면 전체를 못 쓰게 만듭니다.**
|
|
|
|
그래서 질의가 먼저 걸러 냈고, 그 결과 **게시한 Open Question 이 홈 최근 기록에 나오지
|
|
않았습니다.** 백엔드가 담지 않은 것이 아니라 **담을 수 없었습니다.**
|
|
|
|
계약을 넓히고(`ef49d3a`) `LATEST_ENTRY_TYPES` 에 `QUESTION` 을 더했습니다. `pathOf` 는 이미
|
|
`/questions/{slug}` 를 만들고 있었고 projection 에도 질문 행이 `ACTIVE`/`PUBLIC` 으로
|
|
채워져 있었습니다 — **막고 있던 것은 이 `IN` 목록 하나였습니다.**
|
|
|
|
### 10.4 배포 직후 첫 요청부터 홈이 깨졌다 (`365560e`)
|
|
|
|
`home_focus_config.default_focus_type` 은 마이그레이션 직후 NULL 인데 계약은 이 필드를
|
|
**required + enum 3값**으로 선언합니다. **배포 직후 첫 요청부터 `/home` 이 깨졌습니다.**
|
|
|
|
`HomeFocusView.resolve` 가 반드시 유효한 값 하나를 정하도록 고쳤습니다.
|
|
|
|
### 10.5 스모크 스윕이 늑대를 외쳤다 (`7289ce9`)
|
|
|
|
미리보기가 아직 없는 문서는 현재 미리보기를 물으면 404 를 답하고, 화면은 그것을 "미리보기를
|
|
만드세요"로 바꿉니다. **스윕은 그것을 실패로 셌습니다.** 그래서 건강한 배포 아래에 매번 같은
|
|
빨간 줄이 남았습니다.
|
|
|
|
> 매번 늑대를 외치는 검사는 읽히지 않게 되고, 진짜 실패가 그 옆에 눈에 띄지 않은 채 앉아
|
|
> 있게 된다.
|
|
|
|
로그인 전 세션 탐침의 401 도 같은 부류라 같은 조건으로 제외하고, **나머지 4xx·5xx 는 전부
|
|
스윕을 실패시킵니다.**
|
|
|
|
### 10.6 기록이 조용히 사라졌다 (`77125d1`)
|
|
|
|
프로젝트 기록에서 Open Question 이 보이지 않았습니다. 이 목록은 탐색의 지식 목록과 **응답
|
|
모양이 다른데** 지식 목록의 매퍼를 그대로 쓰고 있었습니다. 그 매퍼는 CASE/REFERENCE 가 아니면
|
|
`null` 을 돌려주고 호출부가 `filter` 로 걸러 내므로, **질문과 개념은 오류도 빈 자리도 남기지
|
|
않고 조용히 없어졌습니다** — 목록이 한 줄 짧아질 뿐이라 눈으로는 알아채기 어렵습니다.
|
|
|
|
---
|
|
|
|
## 11. CSS 규칙이 구역을 넘어 샌다
|
|
|
|
"디자인이 안 된 것처럼 보인다"고 보고된 것 셋이 전부 **규칙이 샌 것**이었습니다.
|
|
|
|
### 11.1 구역 전체에 건 격자가 제목까지 잡았다 (`344dadb`)
|
|
|
|
```css
|
|
.home-comparison a {
|
|
display: grid;
|
|
grid-template-columns: 200px minmax(0, 1fr);
|
|
padding: 27px 2px 28px;
|
|
}
|
|
```
|
|
|
|
비교 **행**을 위한 규칙인데 선택자가 **구역 전체**라 제목 안의 링크까지 잡았습니다. 제목이
|
|
200px 칸에 갇혀 두 줄로 접히고 행용 padding 까지 물려 **h2 높이가 199px** 이 됐습니다. 같은
|
|
이유로 주제 화면의 안내 문단도 행의 크기·색으로 덮여 있었습니다.
|
|
|
|
사용자가 원인을 정확히 짚어 주었습니다 — 「디자인이 안 된 게 아니라 CSS 선택자가 새고
|
|
있습니다」.
|
|
|
|
**고친 것:** 배치를 거는 규칙은 그 배치를 쓰는 요소까지 좁혀 적습니다(`li > a`). 같은 모양이
|
|
**다른 구역 아홉 곳에도** 있어서 **51개 선택자**를 고쳤습니다.
|
|
|
|
**CSS 로만 막는 것은 임시방편이라 구조도 바꿨습니다** — 제목 안에 링크를 두지 않고, 주제로
|
|
가는 길은 아래 한 줄이 맡습니다.
|
|
|
|
**재발 방지:** `section-selector-scope.test.ts` — `.클래스 태그` 모양에 배치 속성
|
|
(`display: grid|flex`, `grid-template-columns`, `padding`)이 걸려 있으면 멈춥니다. 이미 좁혀
|
|
둔 자리는 `SETTLED` 로 명시합니다. **색이나 글꼴만 거는 규칙은 새어도 티가 나지 않으므로
|
|
대상이 아닙니다.**
|
|
|
|
### 11.2 규칙이 없었던 게 아니라 절반만 있었다 (`68538f2`)
|
|
|
|
「구조별로 알게 된 것」만 26px·굵기 400 으로 나왔습니다. 형제 구역은 30px·650 이라 같은
|
|
화면에서 이 구역만 급이 낮았습니다.
|
|
|
|
> 규칙이 없었던 게 아니라 **절반만 있었다.** 정본은 `.section-heading-row h2` 인데 그 안에
|
|
> 들어가지 않는 두 구역이 **크기만 각자 적어 두어 굵기를 아무도 정하지 않았고**, 그래서
|
|
> 기본값 400 으로 떨어졌다.
|
|
|
|
**재발 방지:** `section-heading-rank.test.ts` — 나란히 서는 구역 제목들이 정본과 같은
|
|
`font-size`/`font-weight` 를 쓰는지 CSS 를 파싱해 확인합니다. 굵기를 빼 보고 실제로 멈추는
|
|
것을 확인했습니다.
|
|
|
|
> **이때 제가 저지른 판단 오류:** 처음에 grid/columns 만 측정하고 "정상"이라고 답했습니다.
|
|
> 사용자가 다시 지적한 뒤 **전체 페이지 스크린샷**을 찍어서야 26px/400 을 봤습니다.
|
|
> **프록시 지표가 아니라 보이는 것을 측정해야 합니다.**
|
|
|
|
### 11.3 CSS module 은 전역 규칙이 닿지 않는다 (`8c5dbe1`)
|
|
|
|
버튼에서 상자를 걷어내는 변경이 `.studio-app` 규칙만 고쳤습니다. 게시 기록·게시 흐름·워크플로
|
|
게이트는 **CSS module 을 쓰므로 그 규칙이 닿지 않아**, 다른 화면에서 상자를 걷어낸 뒤에도
|
|
「Snapshot 보기」·「게시 취소」·「적용」만 테두리와 파란 채움으로 남아 있었습니다. **한 화면
|
|
안에서 두 언어가 섞여 더 눈에 띄었습니다.**
|
|
|
|
---
|
|
|
|
## 12. 운영에서만 드러난 것
|
|
|
|
### 12.1 파드가 CrashLoopBackOff 로 들어간 두 건
|
|
|
|
| 원인 | 왜 컴파일·테스트가 못 잡았나 | 커밋 |
|
|
|---|---|---|
|
|
| 스캔되는 컴포넌트에 생성자 둘, `@Autowired` 없음 | 어떤 테스트도 애플리케이션 컨텍스트를 띄우지 않는다 | `ca63d7d` |
|
|
| Jackson 2 `ObjectMapper` 를 요구(이 빌드는 Jackson 3) | Jackson 2 타입이 전이 의존성으로 클래스패스에 남아 있어 import 가 정상 해석된다 | `0da7c7e` |
|
|
|
|
### 12.2 배포 인자를 빠뜨려 배포본이 `api.example.com` 을 불렀다
|
|
|
|
프론트 이미지 빌드에 `RUNTIME_API_BASE_URL` 을 넘기지 않으면 **배포본이 존재하지 않는 주소를
|
|
부릅니다.** Dockerfile 이 문자 그대로 그 경고를 적어 두고 있는데도 빠뜨렸습니다.
|
|
`kubectl rollout undo` 로 되돌리고 다시 빌드했습니다.
|
|
|
|
> 메모리에 남겼습니다 — `techlog-deploy-runtime-api-base.md`
|
|
|
|
프론트 이미지가 요구하는 인자 전부:
|
|
|
|
```
|
|
APP_PROFILE=production
|
|
VITE_ROUTER_BASE_PATH=/
|
|
RUNTIME_API_BASE_URL=https://hyeonworks.com/ ← 빠뜨리면 api.example.com
|
|
VITE_BUILD_ID / VITE_COMMIT_SHA / RELEASE_ID
|
|
CI_RUNNER_IMAGE=node@sha256:… ← 반드시 @sha256 다이제스트
|
|
SOURCE_DATE_EPOCH
|
|
```
|
|
|
|
백엔드 이미지는 Dockerfile 이 `src/` 아래에 있고 `RELEASE_VERSION`/`BUILD_VERSION`/`GIT_SHA`/
|
|
`SOURCE_URL` 을 받습니다. 태그는 **짧은 SHA**(7자)입니다 — 배포된 것과 맞춰야 합니다.
|
|
|
|
### 12.3 stale JAR 검사
|
|
|
|
빌드 산출물 이름에 커밋 해시가 들어갑니다(`app-bootstrap-0.0.1+<sha>.jar`). 작업 트리가
|
|
더러우면 해시가 달라져 `verifyNoStaleTraceableJars` 가 멈춥니다. **커밋한 뒤
|
|
`cleanStaleTraceableJars build` 로 돌려야 합니다.** 이 순서를 몰라 두 번 헤맸습니다.
|
|
|
|
### 12.4 컨테이너가 읽을 수 없는 설정 파일 (`83409be`)
|
|
|
|
빌드가 `config.json` 을 0600 으로 씁니다. **nginx 가 읽지 못해** 컨테이너는 healthy 로
|
|
올라오고 **SPA 가 부팅에 필요한 그 파일 하나만 403** 을 돌려줬습니다. 이미지가 권한을
|
|
정규화하도록 고쳤습니다.
|
|
|
|
### 12.5 favicon 이 404 였다 (`83409be`)
|
|
|
|
`index.html` 이 `public/favicon.svg` 를 참조한 적이 없습니다. 파일은 이미지에 들어 있었고
|
|
nginx 도 서빙했지만 **브라우저는 `/favicon.ico` 를 물었고** 404 를 받아 기본 아이콘으로
|
|
떨어졌습니다.
|
|
|
|
### 12.6 robots.txt 가 404 였다 (`a936444`)
|
|
|
|
파일은 이미지에 있었지만 **nginx 설정이 서빙할 파일을 하나씩 명시하는 구조**라 등록되지 않은
|
|
것은 SPA 폴백으로 떨어집니다 — 크롤러가 index.html 을 규칙으로 읽을 수는 없으므로 **규칙이
|
|
없는 것과 같았습니다.**
|
|
|
|
### 12.7 테스트 JVM 이 OOM 났다 (`561d02a`)
|
|
|
|
테스트 JVM 힙이 Gradle 기본 512m 이라 **Spring context 캐시 + ArchUnit + Testcontainers**
|
|
조합에서 OOM 이 났습니다. **증상이 테스트 실패가 아니라 "Executor 를 완료할 수 없음"이어서
|
|
원인을 가렸습니다.**
|
|
|
|
### 12.8 npm 환경 변수 누출 (운영 아님, 검증 절차)
|
|
|
|
vitest 를 `npm`/`npx` 로 돌리면 `npm_config_*` 환경 변수가 설정되고
|
|
`ci-workflow-generation.test.ts` 가 실패합니다. 이 저장소에서 테스트를 돌릴 때는:
|
|
|
|
```bash
|
|
env $(env | grep -i "^npm_config" | cut -d= -f1 | sed 's/^/-u /' | tr '\n' ' ') \
|
|
./node_modules/.bin/vitest run …
|
|
```
|
|
|
|
---
|
|
|
|
## 13. 글과 말
|
|
|
|
기술 결함은 아니지만 같은 뿌리를 갖습니다 — **같은 것을 여러 곳에서 손으로 적으면 갈라집니다.**
|
|
|
|
### 13.1 한 화면에 종류 이름이 아홉 개 (`dc2fda7`, `ca1fc92`)
|
|
|
|
홈 한 화면에 종류 이름이 **아홉 개** 떠 있었습니다.
|
|
|
|
```
|
|
최근 기록 목록: CASE · CONCEPT · OPEN QUESTION · REFERENCE
|
|
바로 아래 「종류별로 읽기」: 검증 기록 · 동작 원리 · 적용 기준 · 열린 질문
|
|
```
|
|
|
|
독자는 둘이 같은 것이라는 단서를 어디서도 받지 못했습니다 — **이름을 바꾸기 전보다 나빠진
|
|
유일한 자리였습니다.**
|
|
|
|
`ca1fc92` 는 그 원인을 짚었습니다:
|
|
|
|
> 표가 화면마다 복사되어 **여섯 벌**이었고 그래서 갈라졌다: 같은 QUESTION 이 공개 화면에서
|
|
> "Open Question", 작업본 목록과 게시 기록에서 "Question", 편집기 상태 줄에서 "QUESTION"
|
|
> 이었다. **쓰는 사람은 같은 문서를 화면마다 다른 이름으로 만난다.**
|
|
|
|
`Record<RecordKind, string>` 하나로 모았습니다.
|
|
|
|
### 13.2 종류 이름을 두 번 바꿨다 (`a6413d0` → `af5a6bb`)
|
|
|
|
이 저장소의 종류 이름은 **글을 담아 둔 방식의 이름**이었습니다 — Case, Reference, Open
|
|
Question. 독자는 그 말을 배우고 나서야 목록을 읽을 수 있었고, 정작 뜻풀이는 홈 바닥(2,000px
|
|
아래)에 있었습니다.
|
|
|
|
1차로 이름이 하는 일을 말하게 했습니다:
|
|
|
|
```
|
|
Case → 직접 해보니 Reference → 다음에 쓸 기준
|
|
Question → 아직 모르는 것 Concept → 어떻게 동작하나
|
|
Decision → 이렇게 하기로
|
|
```
|
|
|
|
**그런데 이게 기술 기록의 톤에 비해 가벼웠습니다.** 역할은 그대로 말하되 문어체로 다시
|
|
세웠습니다(`af5a6bb`):
|
|
|
|
```
|
|
CASE → 검증 기록 CONCEPT → 동작 원리
|
|
REFERENCE → 적용 기준 DECISION → 설계 결정
|
|
QUESTION → 열린 질문
|
|
```
|
|
|
|
**계약의 kind 는 그대로 뒀습니다.** 바꾸는 것은 화면에 보이는 이름뿐입니다.
|
|
|
|
### 13.3 AI 스러운 문구 (`7acde27`, `6e784ed`, `eedc90b`)
|
|
|
|
사용자가 프로필의 「기록을 운영하는 원칙」이 AI 스럽다고 지적했습니다. 구체적으로:
|
|
|
|
- 「섞는다」 「함께 기록한다」 「흩어지지 않게」 — 무엇을 하는지 말하지 않는 동사로 끝남
|
|
- 「결론이 서는 조건」 「자리」 「프로젝트를 답니다」 「결정 순서로 읽는다」 「접근을 나눠
|
|
견주고」 — 번역투
|
|
|
|
**제가 고쳐 쓴 첫 번째 안도 거절당했습니다.** 결국 사용자가 직접 쓴 텍스트를 그대로
|
|
실었습니다.
|
|
|
|
> **여기서 배운 것:** 이 사이트의 글은 작성자의 목소리입니다. 제가 "더 나은 문장"을 제안하는
|
|
> 것과 **그 사람의 말투로 쓰는 것**은 다른 일이고, 후자는 제가 잘하지 못합니다. 톤 지적이
|
|
> 나오면 고쳐 쓰기보다 **어떤 말을 쓸지 물어야** 합니다.
|
|
|
|
같은 판단이 다른 자리에도 적용됐습니다:
|
|
- 「무엇을 견줬나」 → 의문형 꼬리 + 이 기록에서 쓰지 않는 낱말. 작성자가 Case 소제목에 쓰는
|
|
말은 「이 구조에서 감수한 것」처럼 **「~한 것」 명사형**이라 그쪽에 맞췄습니다 (`344dadb`)
|
|
- 「이 프로젝트가 밝힌 것」 → 「프로젝트를 통해 확인한 결과」 (`eedc90b`)
|
|
- 「운영 가능한 설계로 연결합니다」 → 「실제 운영에 적용할 수 있는 형태로 정리합니다」
|
|
|
|
### 13.4 오류 문구가 추측을 출력했다 (`1801414`)
|
|
|
|
작업본 삭제 실패가 **「게시됐거나, 참조하는 곳이 있거나, 누가 먼저 고쳤을 수 있습니다」** —
|
|
**세 가지 추측**을 출력했습니다. 서버는 정확히 하나를 답했는데도요:
|
|
「이 기록을 참조하는 곳이 있어 삭제할 수 없습니다」.
|
|
|
|
버전 충돌이 "사용 중"으로 읽혔고, 어떤 삭제는 되고 어떤 삭제는 안 되는 것을 지켜보는 작성자는
|
|
**둘을 구분할 방법이 없었습니다.**
|
|
|
|
게이트웨이가 서버의 클라이언트 안전 메시지를 실어 나르고 화면이 그것을 보이게 했습니다.
|
|
|
|
**이 문제는 아직 완전히 안 끝났습니다** — §14.2 를 보세요.
|
|
|
|
### 13.5 편집기 칸 이름을 공개 화면과 맞췄다 (`82e992d`)
|
|
|
|
```
|
|
목적 → 이 기준을 쓰는 이유 규칙 → 판단 기준
|
|
적용 조건 → 적용할 때 예외 → 예외와 주의
|
|
사실 → 확인한 사실 미지수 → 남은 미지수
|
|
선택지 → 검토한 선택지
|
|
```
|
|
|
|
쓰는 사람이 **지금 채우는 칸이 공개 화면 어디로 가는지 외우지 않아도 되게** 했습니다.
|
|
|
|
### 13.6 한글 slug (`5cffe30`, `7093d84`)
|
|
|
|
주제 만들기가 **간헐적으로** 실패했습니다 — "그 slug 를 가진 주제가 이미 있습니다". 다른
|
|
이름으로 다시 하면 됐습니다.
|
|
|
|
> 규칙은 간헐적이었던 적이 없다. **보이지 않았을 뿐이다** — slug 생성이 `[a-z0-9]` 만 남기고
|
|
> 나머지를 버려서, 한글 이름은 아무것도 기여하지 못했다.
|
|
|
|
두 가지로 어긋났고 사용자는 둘 다 만났습니다:
|
|
- `인증` → 빈 문자열 → 폼이 요청 전에 거절
|
|
- `Redis 캐시` 와 `Redis 클러스터` → **둘 다 `redis`** → 두 번째가 충돌
|
|
|
|
**한글을 버리지 않고 로마자로 옮깁니다.** 음절을 초성·중성·종성으로 산술 분해하므로 표가
|
|
필요 없고 결정적입니다: `백엔드 아키텍처` → `baekendeu-akitekcheo`. 국어의 로마자 표기법의
|
|
**자모 대응만** 적용하고 음운 변화 규칙은 일부러 뺐습니다 — slug 는 읽는 것이지 발음하는 것이
|
|
아니고, 그 규칙을 넣으면 같은 이름이 문맥에 따라 다른 slug 가 됩니다.
|
|
|
|
---
|
|
|
|
## 14. 정보 구조가 바뀐 과정 — 주제와 축
|
|
|
|
이 절은 결함이 아니라 **설계가 바뀐 과정**입니다. 다만 그 과정에서 나온 결함이 §9 의 절반을
|
|
차지하므로 함께 적습니다.
|
|
|
|
### 14.1 문제 — 하나의 질문에 네 개의 답
|
|
|
|
「브라우저와 서버 사이 credential 책임을 어디에 둘 것인가」 하나의 질문에 대해 네 구조
|
|
(SPA·Mediator·BFF·Forward-Auth)를 만들어 봤는데, **기록이 주제와 프로젝트로만 자리를 갖고
|
|
있어** 그 넷을 담을 데가 없었습니다. 화면은 그것을 **시간순 목록으로만** 보여 줄 수
|
|
있었습니다.
|
|
|
|
**주제를 넷으로 쪼개지 않았습니다.** 쪼개면 PKCE·CSRF·Authorization Code 처럼 네 구조가 함께
|
|
쓰는 기록을 어디에 둘지 애매해지고 비교도 어려워집니다. 대신 **주제 안에 축(variant)을 하나**
|
|
뒀습니다 (`2d9672d`, `d11cda8`).
|
|
|
|
```
|
|
topic (주제)
|
|
├─ variant_label 축의 이름 — 주제마다 다르다
|
|
│ 인증 경계 → 「구조」 / 조회 성능 → 「조회 전략」
|
|
└─ topic_variant 축의 값들 (SPA, Mediator, BFF, Forward-Auth)
|
|
└─ record_variant 어느 기록이 어느 축에 걸리는지 (kind, id) 쌍
|
|
```
|
|
|
|
<!-- techviz:begin id=topic-variant-model context-sha256=93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f -->
|
|
<!-- techviz:generate id=topic-variant-model -->
|
|

|
|
|
|
<details>
|
|
<summary>Diagram description</summary>
|
|
|
|
왼쪽에 topic 이 있고 variant_label 로 축의 이름을 스스로 정한다. 그 오른쪽에 topic_variant 가 있고 SPA, Mediator, BFF, Forward-Auth 같은 축의 값들을 담는다. 그 오른쪽에 record_variant 가 있고 어느 기록이 어느 축에 걸리는지를 종류와 아이디의 쌍으로 적는다. record_variant 는 오른쪽의 document, open_question, project_decision 세 테이블을 가리키는데, 기록이 종류마다 다른 테이블에 살기 때문에 외래키를 걸지 못하고 쌍으로만 가리킨다.
|
|
|
|
</details>
|
|
|
|
[Editable source](assets/diagrams/topic-variant-model/topic-variant-model.drawio) · [Grounded VizSpec](.techviz/topic-variant-model/spec.json)
|
|
<!-- techviz:end id=topic-variant-model -->
|
|
|
|
**설계 판단 셋:**
|
|
1. **축 이름은 주제가 정합니다.** 내부 이름은 `variant` 로 두고 화면에 보이는 이름은
|
|
`variantLabel` 로 둡니다
|
|
2. **기록은 여러 축에 걸릴 수 있습니다**(`variantIds` 배열). 아무 데도 걸리지 않은 기록은 그
|
|
주제의 **공통 기록**으로 읽습니다 — 「공통」 축을 따로 만들지 않습니다
|
|
3. **`record_variant` 는 외래키가 없습니다.** 기록이 종류마다 다른 테이블에 살기 때문입니다
|
|
(`document` / `open_question` / `project_decision`). `studio_validation`·`publication` 이
|
|
이미 쓰는 방식을 따랐습니다
|
|
|
|
**editorial 칸을 함께 세웠습니다.** `topic.thesis`, `project.thesis`,
|
|
`topic_variant.summary/conclusion` 은 **기록을 합쳐 자동으로 나오는 글이 아닙니다.** 특히
|
|
`conclusion` 은 비교표가 읽는 칸이라 기록의 요약 첫 줄을 잘라 쓰면 안 됩니다.
|
|
|
|
### 14.2 홈의 비교 구역이 세 번 바뀌었다
|
|
|
|
| 단계 | 무엇 | 왜 바꿨나 | 커밋 |
|
|
|---|---|---|---|
|
|
| 1 | 주제 하나만 펼치고 아래 「다른 주제 N개 보기」 한 줄 | 홈이 「무엇을 만들었나」로 시작했다. 30초 안에 알아야 할 것은 무엇을 견줬나다 | `604ded5`, `69eabc7` |
|
|
| 2 | 제목 자리를 **주제 이름 탭**이 대신 (30px/650) | 「다른 주제」 줄은 목록을 다 읽고 나서야 만나는 자리라 대개 지나쳤다 — JPA 주제는 홈에 있으면서도 없는 것과 같았다 | `de4cb8b` |
|
|
| 3 | 탭을 **칩 크기**로 낮추고 개수 상한 제거 | 주제가 열 개, 스무 개가 되면 이름만으로 화면이 덮인다. 상한은 주제마다 상세를 미리 받느라 둔 것인데, 그러면 상한 밖의 주제가 다시 밀려난다 | `3bb724b`, `2b2f443` |
|
|
|
|
**3단계에서 요청 구조를 바꿨습니다.** 탭은 목록 호출 하나가 주는 전부이고, 상세는 **고른
|
|
탭만 그때 받아 캐시**합니다. 그래서 주제가 몇 개가 되든 홈이 처음 보내는 요청은 **목록 1 +
|
|
주제 1** 로 고정됩니다.
|
|
|
|
**그리고 시각 언어를 두 번 고쳤습니다:**
|
|
- 고른 탭의 **파란 밑줄**을 없앴습니다 — 주제가 스무 개면 밑줄 설 자리 스무 개가 함께 늘어섭니다
|
|
- 칩으로 낮추니 **목록 위에 글자만 떠 있는 것처럼** 보였습니다. 고른 탭에 형태(알약)를 주고,
|
|
묶음의 윗선을 목록이 아니라 패널이 갖게 해서 탭 줄이 그 선에 바로 얹히게 했습니다
|
|
|
|
### 14.3 축이 무엇을 기준으로 묶이나 (실제 데이터)
|
|
|
|
> **근거** — [`evidence/raw/db/topic-variant-rows.txt`](./evidence/raw/db/topic-variant-rows.txt) ·
|
|
> [`evidence/raw/db/record-variant-links.txt`](./evidence/raw/db/record-variant-links.txt) ·
|
|
> 화면과 실측값은 [`evidence/browser/`](./evidence/browser/)
|
|
|
|
두 주제가 **같은 구조**를 씁니다. 다만 내용의 양이 달라 다르게 보입니다.
|
|
|
|
```
|
|
oauth-oidc-auth-boundary 축 이름 「구조」 축 4개
|
|
spa ← CASE 1 + REFERENCE 3 (기록 4)
|
|
mediator ← CASE 1 + QUESTION 2 + REFERENCE 2 (기록 5)
|
|
bff ← CASE 1 + QUESTION 3 + REFERENCE 1 (기록 5)
|
|
forward-auth ← CASE 1 + QUESTION 1 + REFERENCE 1 (기록 3)
|
|
공통 기록: CONCEPT 1 + 결정 2 + REFERENCE 2
|
|
|
|
jpa-feed-query-performance 축 이름 「조회 전략」 축 3개
|
|
derived-query ← CASE 1
|
|
fetch-join ← CASE 1
|
|
fetch-join-paging ← CASE 1
|
|
```
|
|
|
|
**JPA 가 「문서가 그대로 나온다」로 보이는 이유**는 축마다 붙은 기록이 1개씩이고 축 제목을
|
|
그 Case 제목과 비슷하게 적었기 때문입니다. **구조 차이가 아니라 내용 양의 차이입니다.**
|
|
|
|
기록을 20개 더 붙여도 **홈의 줄은 그대로 3줄**입니다 — 줄은 문서가 아니라 축입니다. 줄을
|
|
늘리려면 Studio 에서 축을 추가해야 합니다.
|
|
|
|
> **자동으로 안 따라오는 것:** 줄에 보이는 **결론 문장은 축에 손으로 쓴 글**입니다. 기록을
|
|
> 20개 붙여도 그 문장은 누가 고치기 전까지 그대로입니다. 의도된 설계이지만(요약은 「무엇인가」,
|
|
> 결론은 「무엇을 알게 됐나」) **사람이 갱신해야 하는 지점**입니다.
|
|
|
|
---
|
|
|
|
## 15. 재발 방지 장치 목록
|
|
|
|
이 기간에 세운 가드 전부입니다. **각각 결함을 되돌려 실제로 멈추는 것을 확인한 뒤** 커밋했습니다.
|
|
|
|
> **근거** — 가드 셋(`public-path-reachability` · `section-heading-rank` · `route-chunk-names`)을
|
|
> 각각 결함으로 되돌려 실제로 빨개지는 것을 확인한 기록:
|
|
> [`evidence/raw/guards/guards-actually-fail.txt`](./evidence/raw/guards/guards-actually-fail.txt)
|
|
|
|
### 15.1 프론트엔드
|
|
|
|
| 가드 | 무엇을 지키나 |
|
|
|---|---|
|
|
| `contract-operation-coverage.test.ts` | 계약이 선언한 연산이 기여 목록에 등록됐는가 |
|
|
| `knowledge-list-kinds.test.ts` | 목록 매퍼의 종류 표가 계약의 enum 을 전부 담는가 |
|
|
| `topic-variant-wiring.test.ts` | 축 필터가 주제와 함께 나가는가 |
|
|
| `route-chunk-names.test.ts` | vite chunk 이름 표에 모든 라우트가 있는가 |
|
|
| `section-selector-scope.test.ts` | 배치 규칙이 구역 전체가 아니라 대상까지 좁혀졌는가 |
|
|
| `section-heading-rank.test.ts` | 나란히 서는 구역 제목이 같은 급인가 |
|
|
| `home-focus-choices.test.ts` | 홈 편집기가 고른 프로젝트의 것만 거르는가 |
|
|
| `home-topic-tabs.test.tsx` | 탭이 데이터를 따르는가 · 상세를 고를 때만 받는가 · 실패해도 목록이 남는가 |
|
|
| `public-path-reachability.test.ts` | 서버가 만드는 주소가 실제 라우트에 맞는가 |
|
|
| `markdown-toolbar.test.ts` | 본문 도구가 유효한 문법을 넣는가 |
|
|
| `tech-log-serving-contract.test.ts` | nginx 로 나가는 경로 패턴이 라우트 계약과 같은가 |
|
|
| ESLint 규칙 (`f9d20e8`) | `setXxx(…)` 업데이터 안에서 `currentTarget` 을 읽지 않는가 |
|
|
|
|
### 15.2 백엔드
|
|
|
|
| 가드 | 무엇을 지키나 |
|
|
|---|---|
|
|
| `ContractRouteCoverageTest` | 계약이 선언한 연산에 컨트롤러 매핑이 있는가 (미구현 51개는 명시) |
|
|
| `StudioContractUnionJacksonTest` | 모든 `RecordKind` 가 계약 enum 으로 변환되는가 |
|
|
| `PublicPathsTest` | 종류마다 만든 공개 경로가 실제 라우트에 맞는가 |
|
|
| `DecisionConsequencesTest` | 운영 DB 의 실제 JSON 모양을 파싱하는가 |
|
|
| `PublicContractDriftTest` | springdoc 이 게시하는 표면과 계약을 양방향 대조 (operation 수 고정) |
|
|
| `PublicErrorRegistryTest` | `PublicError` ↔ `error-codes.yaml` ↔ 계약 enum 3자 대조 |
|
|
| `postgresqlTechLogPublicPersistenceIntegrationTest` | 어댑터 SQL 을 **실제 PostgreSQL** 에서 돌린다 |
|
|
| ArchUnit D20 | 스캔되는 컴포넌트의 생성자가 하나이거나 `@Autowired` 가 있는가 |
|
|
| `verifyNoStaleTraceableJars` | 빌드 산출물이 현재 커밋의 것인가 |
|
|
|
|
### 15.3 설계 패키지
|
|
|
|
| 가드 | 무엇을 지키나 |
|
|
|---|---|
|
|
| `check-openapi.py` | 계약 자체의 유효성 |
|
|
| `check-consistency.py` | 세 계약 사이의 정합 |
|
|
| `check-contract-parity.py` | FE/BE 가 아는 종류·오류 코드가 같은가 |
|
|
| alias fail-closed 게이트 | 파생 스펙에 YAML anchor/alias 가 남으면 빌드 실패 |
|
|
| `verifyPublicGeneratedModels` | 생성된 모델이 계약의 **property 단위**로 일치하는가 (schema 62 · property 250) |
|
|
|
|
### 15.4 배포 전 검증 (사람이 돌려야 하는 것)
|
|
|
|
```bash
|
|
# 프론트 — 다섯 개를 다 돌린다. npx tsc --noEmit 은 아무것도 검사하지 않는다
|
|
npm run check:types
|
|
npm run lint
|
|
env $(env | grep -i "^npm_config" | cut -d= -f1 | sed 's/^/-u /' | tr '\n' ' ') \
|
|
./node_modules/.bin/vitest run tests/unit tests/component tests/features/tech-log
|
|
|
|
# 백엔드 — 커밋한 뒤에 돌린다(산출물 이름에 커밋 해시가 들어간다)
|
|
cd src && ./gradlew cleanStaleTraceableJars build
|
|
|
|
# 설계 패키지
|
|
python3 scripts/check-openapi.py && python3 scripts/check-consistency.py \
|
|
&& python3 scripts/check-contract-parity.py
|
|
```
|
|
|
|
---
|
|
|
|
## 16. 아직 남은 것
|
|
|
|
정직하게 적습니다. **이 목록은 "고쳤다"가 아니라 "안 고쳤다"입니다.**
|
|
|
|
### 16.1 삭제를 막는 이유를 문구가 말하지 않는다
|
|
|
|
> **근거** — [`evidence/raw/db/delete-blocked-by-project-link.txt`](./evidence/raw/db/delete-blocked-by-project-link.txt)
|
|
> (진단 당시 캡처 + 사용자가 조치한 뒤의 사후 확인)
|
|
|
|
작업본 삭제 실패는 다섯 가지 이유가 **전부 같은 한 문장**으로 나옵니다:
|
|
|
|
```
|
|
another record still links to this one; unlink it first
|
|
```
|
|
|
|
실제로 막는 것은 이 중 하나입니다:
|
|
|
|
```sql
|
|
SELECT 1 FROM document_relation WHERE target_document_id = :id
|
|
UNION ALL SELECT 1 FROM question_document_link WHERE document_id = :id
|
|
UNION ALL SELECT 1 FROM project_document_link WHERE document_id = :id
|
|
UNION ALL SELECT 1 FROM topic_featured_document WHERE document_id = :id
|
|
UNION ALL SELECT 1 FROM project_decision WHERE source_case_id = :id
|
|
```
|
|
|
|
실제 사례: 「DTO 변환 과정에서 발생한 Highlight 컬렉션 N+1」(CASE, DRAFT/PRIVATE)을 지우려는데
|
|
관계를 다 지워도 삭제가 안 됐습니다. 남아 있던 것은 `project_document_link` 의 **PRIMARY 링크
|
|
1행**(프로젝트 「Liner N + 1문제」)이었습니다. **프로젝트 연결은 「관계」 편집기가 아니라 문서의
|
|
`Project` 필드**라서, 관계를 아무리 지워도 그 행은 남습니다.
|
|
|
|
문구가 「another record」라고 하니 관계를 찾아 지우게 되는데, **정작 막는 것은 record 가 아니라
|
|
프로젝트입니다.** 문구가 잘못된 것을 가리키고 있습니다.
|
|
|
|
- **해결 방법(사용자):** `Project` 필드를 「미지정」으로 바꾸고 저장 → 삭제
|
|
- **안 고친 것:** 오류가 무엇이 막는지 말하게 하기
|
|
|
|
### 16.2 홈 비교표에 기록 수가 없다
|
|
|
|
주제 화면에는 축마다 「기록 N」이 붙는데 **홈에는 없습니다.** 그래서 기록 1개짜리 축과 20개짜리
|
|
축이 똑같아 보이고, 기록을 20개 채워도 홈 화면은 오늘과 똑같습니다.
|
|
|
|
### 16.3 두 탭 줄의 표시 방식이 다르다
|
|
|
|
「지금 집중하는 것」 탭에는 파란 밑줄이 그대로 있고, 주제 탭은 알약입니다. 주제 탭 밑줄을 그쪽에서
|
|
베껴 왔다가 뺐기 때문입니다. **한 화면 안에서 두 언어가 섞여 있습니다** — §11.3 과 같은 모양입니다.
|
|
|
|
### 16.4 릴리즈 0.3.0 이 초안 상태
|
|
|
|
`/studio/releases/336bd1e7-68da-46bc-94a6-cfe17807960a` 에 초안으로 있고 **의도적으로 게시하지
|
|
않았습니다.** 사용자 검토 대상입니다. 이번 세션의 변경 일부만 담겨 있습니다.
|
|
|
|
### 16.5 수동 접근성 증거가 전부 미서명
|
|
|
|
`artifacts/tests/a11y-manual/*.md` 는 전부 `pending-manual-review` 입니다. 라우트마다 파일은
|
|
있지만 **사람이 서명한 것은 하나도 없습니다.** `review:a11y-manual` 스크립트는 그래서 실패하는
|
|
것이 정상입니다.
|
|
|
|
### 16.6 환경 의존으로 실패하는 테스트 3개
|
|
|
|
`ci-workflow-generation.test.ts` 의 세 케이스(`corepack pnpm` 하위 프로세스를 띄우는 것들)가
|
|
이 환경에서 실패합니다. **HEAD 에서도 동일하게 재현**되므로 코드 변경과 무관합니다.
|
|
|
|
### 16.7 종류 열거 두 곳이 아직 컴파일러의 보호를 못 받는다
|
|
|
|
이 문서를 쓰며 근거를 모으다가 새로 확인한 것입니다
|
|
([`evidence/raw/guards/kind-tables-now.txt`](./evidence/raw/guards/kind-tables-now.txt)).
|
|
|
|
- **`PublicSql.pathOf`** — `RecordKind` 가 아니라 `String`(공개 투영의 `resource_type`)으로
|
|
switch 합니다. 그 칸은 `RecordKind` 에 없는 값(`PROJECT`, `RELEASE`)도 담기 때문입니다.
|
|
그래서 `default -> null` 이 남아 있고, 새 종류를 더할 때 이 자리를 빠뜨리면 컴파일은 통과하고
|
|
경로가 `null` 로 나갑니다. 실제로 CONCEPT 이 여기서 빠져 있었습니다(§3.2 의 13번). 지금은
|
|
`PublicPathsTest` 가 막지만, `pathOf` 만 쓰는 경로(홈 focus 의 `recentDecision`)에는 그
|
|
테스트가 닿지 않습니다.
|
|
- **`validate-working-copy.ts` 의 `stringFields`** — 아직 삼항 사슬입니다. 다만 배타적 사슬이
|
|
아니라 가산형이라 종류를 빠뜨리면 "잘못된 분기로 떨어진다"가 아니라 "그 종류의 추가 칸을
|
|
검사하지 않는다"가 됩니다. 덜 위험하지만 조용하기는 마찬가지입니다.
|
|
|
|
즉 §3 의 「표로 바꿨다」는 대부분 사실이지만 전부는 아닙니다.
|
|
|
|
### 16.8 검토용 스크린샷 3장이 저장소에 커밋돼 있다
|
|
|
|
`3bb724b`·`2b2f443` 에서 `git add -A` 로 홈 탭 검토용 PNG 를 프론트 저장소 루트에 커밋했습니다
|
|
— `home-tabs-keycloak.png`, `home-topic-tabs.png`, `home-topic-tabs-2.png`. 소스에 들어갈
|
|
파일이 아닙니다. (이 문서의 `evidence/browser/` 에는 사본을 뒀습니다.)
|
|
|
|
### 16.9 주제 논지·축 결론의 출처
|
|
|
|
`topic.thesis`, `topic_variant.conclusion` 은 **2026-09-01 에 AI 가 써서 DB 에 직접 넣은
|
|
초안**입니다. 사용자 검토 대상이고, 아직 검토되지 않았습니다.
|
|
|
|
> 메모리에도 남겨 뒀습니다 — `techlog-topic-variant-content.md`
|
|
|
|
---
|
|
|
|
## 17. 이 기간 전체에서 배운 것
|
|
|
|
198개를 다 읽고 남는 것은 다섯 줄입니다.
|
|
|
|
### 17.1 값의 여정 끝에서 확인한다
|
|
|
|
한 경계를 고치고 "고쳤다"고 판단해서 세 번 틀렸습니다(§5.2, §6.1, §9.1). 값이 지나는 경계가
|
|
열한 개(§1.2)인데 그중 하나만 보고 판단했기 때문입니다.
|
|
|
|
**확인은 배포본에서, 그 값이 실제로 그려지는 자리에서 합니다.** 타입 검사·단위 테스트·"코드를
|
|
읽어 보니 맞다"는 전부 중간 지점입니다.
|
|
|
|
### 17.2 손으로 나열한 목록은 반드시 갈라진다
|
|
|
|
종류 목록(§3, 13건), 라우트에 딸린 목록(§8, 6건), 화면마다 복사된 이름 표(§13.1, 6벌).
|
|
**전부 같은 병입니다.**
|
|
|
|
고치는 방법도 하나입니다 — **컴파일러나 테스트가 대신 세게 만듭니다.**
|
|
`Record<Kind, _>`, sealed switch 식, 계약을 파싱해 대조하는 가드, 라우트 계약에서 유도하는
|
|
서빙 패턴.
|
|
|
|
### 17.3 화면은 못 읽은 것을 없다고 말하면 안 된다
|
|
|
|
빈 배열로 삼키면 작성자는 자기가 아직 쓰지 않은 것으로 읽습니다(§10.1). 실제로는 넷이 있었고
|
|
공개 사이트에도 나오고 있었습니다.
|
|
|
|
`Promise.all` 도 같은 병입니다 — 한 칸의 실패가 옆 칸을 끌고 내려갑니다(§10.2).
|
|
|
|
### 17.4 가드는 넣는 것보다 돌리는 것이 어렵다
|
|
|
|
가드를 넣었는데 **제가 그것을 돌리지 않아** 두 번 새어 나갔습니다:
|
|
|
|
- 화면 테스트를 `test:unit` 이 아니라 `test:tech-log` 가 돌리는데 그것을 안 돌려서 **23건이
|
|
빨간 채로 여러 커밋을 지나갔습니다** (§7.5)
|
|
- 주제 화면 셋을 더하면서 CI 게이트 기준값을 빠뜨려 **게이트가 빨간 채로 여러 커밋을
|
|
지나갔습니다** (§8.5)
|
|
|
|
**가드는 CI 에 묶여야 의미가 있습니다.** 사람이 기억해서 돌리는 가드는 절반만 존재합니다.
|
|
|
|
### 17.5 프록시 지표가 아니라 보이는 것을 측정한다
|
|
|
|
grid/columns 를 재고 "정상"이라 답했는데, 전체 페이지 스크린샷을 찍으니 26px/400 이었습니다
|
|
(§11.2). 목록 간격을 바운딩 박스로만 재서 「판단 기준」을 결함으로 잘못 지목한 적도 있습니다
|
|
(`9f14ca8`).
|
|
|
|
`f846f51` 에서 **촬영 스크립트**를 둔 이유가 이것입니다:
|
|
|
|
> 손으로 찍으면 두 가지를 반드시 놓친다. **뷰포트** — 브라우저 세션이 리셋되면 창은 약 877px
|
|
> 로 돌아가는데 이 사이트의 분기는 1179/1050/900/767 이라 **방문자 대부분이 보지 않는 배치**를
|
|
> 놓고 디자인을 논하게 된다. **축소** — 전체 페이지를 한 장으로 찍으면 1425x4466 이 638x2000
|
|
> 으로 들어와 **17px 글자가 7~8px** 이 된다.
|
|
|
|
폭 셋을 고정으로 돌고 화면 높이만큼 잘라 찍으며, **폭마다 측정도 함께 남깁니다.**
|
|
|
|
---
|
|
|
|
## 부록 A. 커밋 색인
|
|
|
|
이 문서가 인용한 커밋 전부입니다. 각 저장소에서 `git show <해시>` 로 원문을 볼 수 있습니다.
|
|
|
|
### A.1 tech-log-frontend
|
|
|
|
| 해시 | 날짜 | 제목 |
|
|
|---|---|---|
|
|
| `2b2f443` | 2026-09-02 | fix: 주제 탭을 아래 묶음에 붙인다 |
|
|
| `3bb724b` | 2026-09-02 | fix: 주제 탭을 칩 크기로 줄이고 개수 상한을 없앤다 |
|
|
| `de4cb8b` | 2026-09-02 | feat: 홈이 주제를 탭으로 나란히 세운다 |
|
|
| `fe6b56a` | 2026-09-02 | fix: 열리지 않는 주소를 링크로 그리지 않는다 |
|
|
| `eedc90b` | 2026-09-02 | fix: 프로필과 프로젝트의 문구를 사용자가 쓴 말로 바꾼다 |
|
|
| `094e755` | 2026-09-02 | fix: 편집기가 프로젝트 slug 를 목록 행에서 직접 읽는다 |
|
|
| `6e784ed` | 2026-09-02 | fix: 편집기의 결정 목록을 살리고, 프로필 원칙을 이 기록의 말로 고친다 |
|
|
| `aca4f18` | 2026-09-02 | feat: 주제 목록 화면을 만들어 주 내비에 넣는다 |
|
|
| `69eabc7` | 2026-09-02 | fix: 축 화면에서 결론을 빼고, 홈에서 다른 주제로 갈 길을 낸다 |
|
|
| `a71c588` | 2026-09-01 | fix: 축 화면의 머리말을 문서 화면과 같은 것으로 맞춘다 |
|
|
| `68538f2` | 2026-09-01 | fix: 구역 제목의 급이 갈리던 것을 정본에 맞춘다 |
|
|
| `67a5491` | 2026-09-01 | feat: 축에 자기 화면을 준다 |
|
|
| `344dadb` | 2026-09-01 | fix: 구역 규칙이 제목까지 잡던 것을 행에만 건다 |
|
|
| `46e4e81` | 2026-09-01 | test: 편집기가 부르는 두 목록이 사라지면 먼저 멈추게 한다 |
|
|
| `7acde27` | 2026-09-01 | fix: Studio 가 못 읽은 것을 없는 것으로 그리지 않게 한다 |
|
|
| `ffdeaa1` | 2026-09-01 | fix: 홈의 종류 배지도 목록과 같은 배지로 만든다 |
|
|
| `db83228` | 2026-09-01 | fix: 「먼저 읽을 것」을 종류마다 한 편으로 줄이고 나머지도 같은 순서로 둔다 |
|
|
| `dc2fda7` | 2026-09-01 | fix: 종류 이름을 화면마다 하나로 맞추고, 읽는 순서를 기록에서 만든다 |
|
|
| `88d841d` | 2026-09-01 | fix: 홈이 실제로 가장 많이 다룬 주제를 앞에 세운다 |
|
|
| `23efcf0` | 2026-09-01 | fix: 주제가 없는 기록이 죽은 링크를 달지 않게 한다 |
|
|
| `b89a54f` | 2026-09-01 | fix: 작업본 목록의 종류 필터에 개념을 넣고 이름을 맞춘다 |
|
|
| `3660404` | 2026-09-01 | fix: 관계 목록의 제목이 그 목록이 답하는 것을 말한다 |
|
|
| `197db74` | 2026-09-01 | fix: 새 화면의 chunk 이름을 표에 넣고, 빠뜨리면 먼저 멈추게 한다 |
|
|
| `77ef304` | 2026-09-01 | fix: 모의 검증기가 개념을 아는 종류로 다룬다 |
|
|
| `8828005` | 2026-09-01 | fix: 주제로 가는 링크가 실제로 주제 화면에 닿게 한다 |
|
|
| `3510ec0` | 2026-09-01 | feat: 프로젝트 화면이 어디서부터 읽을지를 말한다 |
|
|
| `604ded5` | 2026-09-01 | feat: 홈이 무엇을 견줬는지 먼저 말한다 |
|
|
| `4da6d77` | 2026-09-01 | feat: 기록을 축에 걸고, 탐색을 주제로 묶는다 |
|
|
| `3616502` | 2026-09-01 | feat: 주제의 논지와 축을 Studio 에서 쓸 수 있게 한다 |
|
|
| `15e6ea8` | 2026-09-01 | feat: 주제 화면을 계약에 잇고, 등록을 잊는 사고를 가드로 막는다 |
|
|
| `618a228` | 2026-09-01 | fix: 관계의 묶음 이름과 작성자가 쓴 문장을 갈라 놓는다 |
|
|
| `af5a6bb` | 2026-09-01 | fix: 종류 이름을 기록의 톤에 맞는 말로 바꾼다 |
|
|
| `a6413d0` | 2026-09-01 | feat: 문서 종류를 독자의 말로 바꾸고 미해결을 눈에 띄게 한다 |
|
|
| `2b04282` | 2026-09-01 | fix: 홈 초점의 질문·결정을 고른 프로젝트 것으로 좁힌다 |
|
|
| `d835276` | 2026-09-01 | fix: 개념 관계 라벨을 계약이 주는 이름에 맞추고 표를 계약에 묶는다 |
|
|
| `8996430` | 2026-09-01 | fix: 공개 개념 문서를 열 수 있게 한다 |
|
|
| `a3ed23e` | 2026-08-31 | fix: 관계 요약이 화면까지 닿게 한다 |
|
|
| `642afa8` | 2026-08-31 | fix: 관계 목록이 사람이 읽는 이름과 요약을 보여 준다 |
|
|
| `6429aee` | 2026-08-31 | fix: 개념 삭제가 실제로 개념 경로로 나가게 한다 |
|
|
| `dec86bd` | 2026-08-31 | fix: 개념 삭제가 질문 삭제로 떨어지던 것을 고친다 |
|
|
| `8c5dbe1` | 2026-08-31 | fix: 게시 기록과 워크플로 버튼도 글자만 남긴다 |
|
|
| `805d400` | 2026-08-31 | fix: 버튼을 글자만 남기고, 결정 절의 문단이 격자에 갇히던 것을 고친다 |
|
|
| `795a4bf` | 2026-08-31 | fix: 목록·검색 카드에서 백틱을 지운다 |
|
|
| `833943a` | 2026-08-31 | feat: 검색 결과에 찾은 말을 표시하고 릴리스 탭 제목을 구분한다 |
|
|
| `a936444` | 2026-08-31 | fix: 릴리스 노트를 목록으로 제대로 읽고 robots.txt 를 서빙한다 |
|
|
| `82e992d` | 2026-08-31 | fix: Studio 리뷰 지적을 반영하고 죽은 머리말 컴포넌트를 걷어낸다 |
|
|
| `ca1fc92` | 2026-08-31 | fix: 편집기에서 글을 쓸 수 있게 하고 종류 이름을 한 곳에 모은다 |
|
|
| `9b7ad82` | 2026-08-31 | fix: 산문 칸의 문단과 인라인 코드를 읽어서 그린다 |
|
|
| `f846f51` | 2026-08-30 | feat: 화면 검토용 촬영 스크립트를 둔다 |
|
|
| `22090a4` | 2026-08-30 | fix: 페이지 번호를 눌러도 쪽이 넘어가지 않던 것을 고치고 UI 를 정리한다 |
|
|
| `9f14ca8` | 2026-08-30 | fix: 공개 문서 본문을 읽을 수 있는 자수와 목록 구조로 되돌린다 |
|
|
| `4e486aa` | 2026-08-29 | fix: 요약을 2000자까지 쓰고, 목록과 머리말이 그 길이를 견디게 한다 |
|
|
| `8fce8ea` | 2026-08-29 | fix: 요약이 문장 중간에서 멈추지 않게 하고 남은 자리를 보여 준다 |
|
|
| `df371d3` | 2026-08-29 | feat: 작업본·게시 기록 목록을 번호로 오간다 |
|
|
| `77125d1` | 2026-08-29 | fix: 프로젝트 기록 목록이 모든 종류를 싣고 요약·주제·날짜를 보여 준다 |
|
|
| `31afb4d` | 2026-08-29 | fix: 결정 문서가 작성자가 채운 칸을 그대로 보여 준다 |
|
|
| `3b6ba64` | 2026-08-28 | fix: 개념을 관계로 가리킬 수 있게 계약을 다시 생성한다 |
|
|
| `071405a` | 2026-08-28 | feat: 개념의 공개 라우트와 탐색 유형을 설치한다 |
|
|
| `048c1b2` | 2026-08-28 | feat: 개념(CONCEPT) 문서 종류 — 편집기·공개 화면·탐색 |
|
|
| `9ecc017` | 2026-08-28 | docs: 개념(CONCEPT) 문서 종류 구현 계획 |
|
|
| `05df030` | 2026-08-28 | docs: 개념(CONCEPT) 문서 종류 설계 |
|
|
| `2420fce` | 2026-08-26 | fix: 공개 문서가 실제로 그리는 주제 링크도 탐색 필터로 돌린다 |
|
|
| `2632850` | 2026-08-26 | fix: 최근 기록에 Open Question 을 싣고, 주제 링크를 탐색 필터로 돌린다 |
|
|
| `ad8f322` | 2026-08-26 | fix: 질문 머리말이 관계에 담긴 프로젝트를 읽는다 |
|
|
| `ab4d822` | 2026-08-26 | fix: 게시된 Open Question 의 공개 상세가 열리게 한다 |
|
|
| `344a163` | 2026-08-26 | fix: 탐색 주제 필터가 slug 를 보내고, 편집기를 넓혀 두 칸이 함께 스크롤한다 |
|
|
| `60c8c82` | 2026-08-26 | fix: 편집과 미리보기를 한 화면에서 보고, 저장·게시를 아래에 고정한다 |
|
|
| `b311995` | 2026-08-25 | fix: 제목 아래에 문서의 요약을 그린다 |
|
|
| `7211dd1` | 2026-08-25 | fix: 공개 Reference 가 계약이 주는 이름을 읽게 한다 |
|
|
| `3036b8d` | 2026-08-25 | feat: Ctrl+S 로 저장한다 |
|
|
| `fd73bc8` | 2026-08-24 | fix: Decision 미리보기의 결정일 요구를 풀고, 화면 테스트가 실제 동작을 다시 말하게 한다 |
|
|
| `e5770df` | 2026-08-24 | fix: Decision 미리보기 오류가 할 일을 말하게 한다 |
|
|
| `a7fe069` | 2026-08-24 | feat: 프로젝트 활동을 게시가 남기는 로그로 바꾼다 |
|
|
| `ab67eb9` | 2026-08-24 | fix: 릴리즈 편집 하단 버튼이 두 벌 나오던 것을 하나로 되돌린다 |
|
|
| `e9b8661` | 2026-08-24 | fix: 릴리즈 목록에서 편집 흔적을 걷어내고, 타입 검사가 실제로 돌게 한다 |
|
|
| `84d72c4` | 2026-08-24 | feat: 릴리즈 편집을 자기 주소로 옮긴다 |
|
|
| `6b9dc3f` | 2026-08-24 | fix: 릴리즈 하단 버튼을 한 줄에 세우고, 로그인을 화면으로 만든다 |
|
|
| `f9d20e8` | 2026-08-23 | fix: setState 업데이터 안에서 event.currentTarget 을 읽지 않는다 |
|
|
| `014f21b` | 2026-08-23 | feat: 프로젝트 주제와 활동 연결을 열고, Case 목차를 되살리고, Studio 화면을 기존 디테일에 맞춘다 |
|
|
| `16e5b9f` | 2026-08-23 | feat: 프로젝트를 편집하고 활동을 남길 수 있게 한다 |
|
|
| `0eb3c86` | 2026-08-23 | fix: 새 목록 항목의 id 가 계약을 건너갈 수 있게 한다 |
|
|
| `c87e0a2` | 2026-08-23 | fix: home focus 경로를 계약과 맞춘다 |
|
|
| `b3aa304` | 2026-08-23 | feat: 프로젝트를 공개할 수 있게 하고, 홈이 무엇을 앞에 둘지 고를 수 있게 한다 |
|
|
| `c03b0c7` | 2026-08-22 | fix: 오류 수정 |
|
|
| `1801414` | 2026-08-21 | fix: show the reason the server gave for a failed action |
|
|
| `7345500` | 2026-08-21 | feat: show the way to publish, and what is blocking it, in the editor |
|
|
| `fb478f9` | 2026-08-21 | fix: say which version a stale validation judged, before showing its errors |
|
|
| `7289ce9` | 2026-08-21 | fix: stop the smoke sweep reporting an expected 404 as a failure |
|
|
| `3484206` | 2026-08-21 | fix: stop claiming a 1x1 size for an image whose dimensions are unknown |
|
|
| `89a73c1` | 2026-08-21 | feat: delete a decision, manage assets while writing, and sweep before deploying |
|
|
| `21f8425` | 2026-08-21 | fix: keep line breaks in the fields that are not Markdown |
|
|
| `7093d84` | 2026-08-21 | fix: give a document a slug, and say so when saving fails |
|
|
| `d2c289c` | 2026-08-21 | fix: keep the line breaks an author typed |
|
|
| `5cffe30` | 2026-08-21 | fix: derive a topic slug that survives a Korean name |
|
|
| `197b2c7` | 2026-08-21 | chore: pick up the regenerated Question input contract |
|
|
| `c5e8735` | 2026-08-21 | feat: read the profile's topics from Studio, and add working-copy deletion |
|
|
| `ab8c6c1` | 2026-08-21 | fix: derive the Studio serving patterns from the route contract |
|
|
| `3754269` | 2026-08-21 | feat: add the Studio release editor and point the footer at the changelog |
|
|
| `7600711` | 2026-08-21 | fix: give each API surface its own error-code enum |
|
|
| `03986da` | 2026-08-21 | fix: let a signed-out visitor read the public site |
|
|
| `31dca00` | 2026-08-21 | fix: keep the public screens usable on an empty site, and centre the dialogs |
|
|
| `11c2713` | 2026-08-20 | feat: let Studio create the topics and projects publishing requires |
|
|
| `4b62bf3` | 2026-08-20 | chore: re-vendor both contracts from the merged design package |
|
|
| `6784eb1` | 2026-08-20 | fix: serve the public routes the router declares, not the slugs the build saw |
|
|
| `24c01ae` | 2026-08-20 | feat: give the public surface an HTTP adapter, and a switch to reach it |
|
|
| `4566f2d` | 2026-08-20 | refactor: make the public read port async so a network adapter can implement it |
|
|
| `c362ec6` | 2026-08-20 | feat: vendor the public read contract, and give it its own source switch |
|
|
| `83409be` | 2026-08-20 | feat: give the frontend a deployment artifact, and show its logo |
|
|
|
|
### A.2 tech-log-backend
|
|
|
|
| 해시 | 날짜 | 제목 |
|
|
|---|---|---|
|
|
| `8cd8ee3` | 2026-09-02 | fix: 결정을 가리키는 링크가 열리는 주소를 갖는다 |
|
|
| `711b2c3` | 2026-09-02 | feat: 프로젝트 목록 행이 slug 를 싣는다 |
|
|
| `bd6db0f` | 2026-09-02 | fix: 결정의 consequences 를 실제 저장 모양대로 읽는다 |
|
|
| `22a65dc` | 2026-09-02 | feat: 주제 목록에 논지와 축을 싣는다 |
|
|
| `6d3b68b` | 2026-09-01 | feat: 축으로 기록을 거른다 |
|
|
| `911e8ba` | 2026-09-01 | fix: 편집기가 고를 목록을 서버가 실제로 준다 |
|
|
| `e185b87` | 2026-09-01 | feat: 질문 목록에도 주제를 싣는다 |
|
|
| `63eb177` | 2026-09-01 | fix: 축의 주소를 주제 화면 안의 자리로 적는다 |
|
|
| `78ec5f9` | 2026-09-01 | feat: 기록이 어느 축에 걸리는지를 저장한다 |
|
|
| `ee664fd` | 2026-09-01 | fix: 통합 테스트의 프로젝트 명령 인자를 thesis 만큼 맞춘다 |
|
|
| `d11cda8` | 2026-09-01 | feat: 주제 안의 접근/구조(Variant) 스키마를 세운다 |
|
|
| `92679f5` | 2026-08-31 | chore: 관계 요약을 담는 계약을 반입한다 |
|
|
| `926f058` | 2026-08-31 | feat: 개념 작업본을 지우는 경로를 연다 |
|
|
| `d34e42c` | 2026-08-29 | fix: 요약 상한을 2000자로 올린다 |
|
|
| `68db5f5` | 2026-08-29 | feat: 목록을 번호로 오가고, 요약이 문장 중간에서 멈추지 않게 한다 |
|
|
| `f0407d9` | 2026-08-29 | feat: 프로젝트 기록 목록이 요약·주제·게시일을 싣는다 |
|
|
| `026460f` | 2026-08-29 | feat: 결정 목록이 제목·요약·영향·근거를 싣는다 |
|
|
| `dd7c70e` | 2026-08-28 | fix: 관계 후보 목록이 개념을 실을 수 있게 하고, 그 대응을 테스트로 고정한다 |
|
|
| `3a226fb` | 2026-08-28 | fix: 공개 질의 파라미터의 허용 집합이 개념을 받아들이게 한다 |
|
|
| `ce2ef61` | 2026-08-28 | feat: 개념의 공개 조회 — 상세·탐색·최근 기록 |
|
|
| `fa5158d` | 2026-08-28 | feat: 개념(CONCEPT) 문서 종류 — 계약·스키마·작성·게시 |
|
|
| `edb0890` | 2026-08-26 | feat: 홈과 주제의 최근 기록이 게시된 Open Question 을 담는다 |
|
|
| `c6d9d2d` | 2026-08-25 | feat: 공개 Case·Reference 응답에 문서 요약을 싣는다 |
|
|
| `a5f93b9` | 2026-08-25 | feat: 공개 Reference 응답에 판단 기준과 예시를 싣는다 |
|
|
| `f1fd56f` | 2026-08-24 | chore: decision 미리보기기 계약 수정 |
|
|
| `bd66fb3` | 2026-08-24 | fix: Decision 미리보기가 열리도록 프로젝트 공개 경로를 catalog 에 싣는다 |
|
|
| `a7e2b7d` | 2026-08-24 | feat: 게시가 프로젝트 활동을 남기게 한다 |
|
|
| `6aa1400` | 2026-08-23 | feat: 프로젝트 주제를 저장하고 공개 응답에 싣는다 |
|
|
| `ca63d7d` | 2026-08-23 | fix: 스캔되는 스프링 컴포넌트의 생성자를 하나로 고정한다 |
|
|
| `4c14f1e` | 2026-08-23 | feat: 프로젝트 활동을 만들고 고치고 지울 수 있게 한다 |
|
|
| `561d02a` | 2026-08-23 | feat: 프로젝트 게시와 홈 focus, 그리고 기록 사이 연결을 실제로 가능하게 한다 |
|
|
| `23d82bd` | 2026-08-22 | fix: 오류 수정 |
|
|
| `857e6a9` | 2026-08-21 | fix: refuse deletion only while a record is live, and say why |
|
|
| `8d22825` | 2026-08-21 | style: apply the formatter to the image dimension reader |
|
|
| `e65b9e2` | 2026-08-21 | fix: record an uploaded image's dimensions |
|
|
| `96521a9` | 2026-08-21 | feat: serve uploaded media, delete a decision, and stop orphaning publications |
|
|
| `af91f7d` | 2026-08-21 | fix: accept a Question working copy with no resolution |
|
|
| `37f474a` | 2026-08-21 | fix: read the public projection by the columns it actually has |
|
|
| `1befdc3` | 2026-08-21 | feat: let an author delete a working copy |
|
|
| `386f360` | 2026-08-21 | feat: implement release authoring, so the changelog can be written |
|
|
| `48517b9` | 2026-08-21 | fix: cast the nullable uuid so Postgres can type the existence check |
|
|
| `0da7c7e` | 2026-08-20 | fix: use the Jackson 3 mapper the persistence module actually has |
|
|
| `bb6d233` | 2026-08-20 | feat: implement topic and project management, so documents can be authored |
|
|
| `bde5826` | 2026-08-20 | fix: ship the object storage adapter, so Studio asset uploads have a backend |
|
|
| `55a71fb` | 2026-08-20 | merge: develop — Tech Log 백엔드 계약 2종 완성 (studio-v1 19/19, public-v1 18/18) |
|
|
| `0854d42` | 2026-08-20 | merge: feature/techlog-public-v1 — Tech Log 공개 조회 백엔드 (public-v1 18/18) |
|
|
| `365560e` | 2026-08-20 | feat: Tech Log 공개 조회 백엔드 — public-v1 18개 operation 구현 |
|
|
| `e3254de` | 2026-08-20 | test: 미추적으로 남아 있던 Studio authz 배선 테스트 2개를 추적에 넣는다 |
|
|
|
|
### A.3 tech-log-design-package
|
|
|
|
| 해시 | 날짜 | 제목 |
|
|
|---|---|---|
|
|
| `1aae8dc` | 2026-09-02 | fix: 결정의 공개 주소가 앵커임을 항목에 담는다 |
|
|
| `ffa088b` | 2026-09-02 | feat: 프로젝트 목록 행에 slug 를 싣는다 |
|
|
| `559d04f` | 2026-09-02 | feat: 주제 목록에 논지와 축을 싣는다 |
|
|
| `b93d62a` | 2026-09-01 | feat: 축으로 기록을 거를 수 있게 한다 |
|
|
| `a58ad30` | 2026-09-01 | feat: 질문 목록에도 주제를 싣는다 |
|
|
| `71bab4c` | 2026-09-01 | fix: 축의 주소를 실제로 있는 자리로 적는다 |
|
|
| `0f4d2cd` | 2026-09-01 | feat: 공개 프로젝트에 논지를 싣는다 |
|
|
| `ca1bbfe` | 2026-09-01 | feat: 관계에 이유와 우선순위를, 주제에 접근/구조 관리를 연다 |
|
|
| `2d9672d` | 2026-09-01 | feat: 주제 안의 접근/구조(Variant)를 계약에 세운다 |
|
|
| `fa67a64` | 2026-08-31 | feat: 관계에 대상 요약을 실을 수 있게 한다 |
|
|
| `f56a03b` | 2026-08-31 | feat: 개념 작업본을 지울 수 있게 한다 |
|
|
| `5942a2e` | 2026-08-29 | fix: 요약 상한을 2000자로 올린다 |
|
|
| `617eb6a` | 2026-08-29 | fix: 요약이 문장 중간에서 멈추지 않도록 상한을 넓힌다 |
|
|
| `d445555` | 2026-08-29 | contract: Studio 목록이 페이지 번호를 그릴 수 있게 한다 |
|
|
| `76a7ccb` | 2026-08-29 | contract: 프로젝트 기록 목록이 요약·주제·게시일을 싣는다 |
|
|
| `987c1b8` | 2026-08-29 | contract: 결정 목록이 화면이 그리는 칸을 다 싣는다 |
|
|
| `2c25ccc` | 2026-08-28 | contract: 개념을 열거해야 하는 자리를 빠짐없이 채운다 |
|
|
| `32d1785` | 2026-08-28 | contract: 관계·근거 후보 목록이 개념을 실을 수 있게 한다 |
|
|
| `777aeaa` | 2026-08-28 | contract: 개념 envelope 을 기존 envelope 모양에 맞춘다 |
|
|
| `33c41cc` | 2026-08-28 | contract: 개념의 즉시 미리보기 렌더 모델을 더한다 |
|
|
| `04c791f` | 2026-08-28 | contract: 개념(CONCEPT) 문서 종류를 더한다 |
|
|
| `ef49d3a` | 2026-08-26 | contract: 홈과 주제의 최근 기록이 Open Question 을 담을 수 있게 한다 |
|
|
| `0ffbc28` | 2026-08-25 | contract: 공개 Case·Reference 에 문서 요약을 싣는다 |
|
|
| `ff0c12a` | 2026-08-25 | contract: 공개 Reference 에 판단 기준과 예시를 싣는다 |
|
|
| `83148b2` | 2026-08-24 | contract: Decision 렌더 모델의 결정일을 비울 수 있게 한다 |
|
|
| `06ae075` | 2026-08-23 | contract: 공개 프로젝트에 주제를 싣는다 |
|
|
| `436937f` | 2026-08-23 | contract: 프로젝트 활동을 envelope 으로 옮기고 지우기를 더한다 |
|
|
| `ed04872` | 2026-08-23 | contract: home focus 경로를 AIP-122 에 맞춘다 |
|
|
| `caa98be` | 2026-08-23 | contract: 프로젝트 게시와 홈 focus 를 envelope 으로 확정한다 |
|
|
| `b195b29` | 2026-08-21 | contract: let a list item be empty too |
|
|
| `fb36d56` | 2026-08-21 | contract: let a published record carry empty prose |
|
|
| `65a04fc` | 2026-08-21 | contract: let an author delete a project decision |
|
|
| `a30d61d` | 2026-08-21 | contract: declare the media path relative to the server prefix |
|
|
| `a981249` | 2026-08-21 | contract: declare the public media endpoint |
|
|
| `dc290b4` | 2026-08-21 | contract: stop requiring a resolution on an unresolved question |
|
|
| `332b11f` | 2026-08-21 | contract: name the in-use refusal for working-copy deletion |
|
|
| `356cb48` | 2026-08-21 | contract: convert the working-copy delete operations to the ADR-006 envelope |
|
|
| `0c10a4a` | 2026-08-21 | contract: convert the release operations to the ADR-006 envelope |
|
|
| `6ef5c1c` | 2026-08-20 | contract: TopicEdit에 id와 version을 싣는다 |
|
|
| `501bfb2` | 2026-08-20 | contract: studio-management-v1의 topics/projects 9개를 응답 봉투로 변환 (ADR-006) |
|
|
| `b98eaf9` | 2026-08-20 | merge: feature/public-v1-response-envelope — public-v1 계약을 응답 봉투로 재정의 (ADR-006) |
|
|
| `55a9599` | 2026-08-20 | contract: public-v1.yaml을 응답 봉투로 재정의 (ADR-006 갱신) |
|