feat: 문서 구조 변경 및 tech-visual 스킬 추가
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# writing-tech-log-records
|
||||
|
||||
Tech Log Studio에 올릴 기록을 쓰는 스킬. Case · Concept · Reference · Question · Decision 다섯 종류의
|
||||
종류 선택, 칸 채우기, Case 본문 작성, 게시 전 대조를 다룬다.
|
||||
|
||||
## 파일
|
||||
|
||||
| 파일 | 무엇 |
|
||||
|---|---|
|
||||
| `SKILL.md` | 진입점. 종류 선택과 절차 |
|
||||
| `references/record-kinds.md` | 다섯 종류의 칸·상한·게시 조건 |
|
||||
| `references/from-ssot-to-records.md` | 긴 글에서 글감을 뽑는 기준과 `tech-log-tree.json` |
|
||||
| `references/writing-each-kind.md` | 종류마다 무엇을 어떤 순서로 쓰나 |
|
||||
| `references/body-syntax.md` | Case 본문의 허용·금지 문법 |
|
||||
| `references/code-tables-diagrams.md` | 코드블록·표·SVG·이미지 |
|
||||
| `references/explaining.md` | 설명의 깊이와 말투 |
|
||||
| `references/review-checklist.md` | 게시 전 대조 |
|
||||
| `examples/case-body.md` | 통과하는 본문 예시 |
|
||||
| `scripts/check_body.mjs` | 본문을 Studio 파서로 미리 검사 |
|
||||
|
||||
## 본문 미리 검사
|
||||
|
||||
Studio에 붙여넣기 전에 확인한다. Studio가 쓰는 파서를 그대로 부르므로, 통과하면 저장도
|
||||
통과한다.
|
||||
|
||||
```bash
|
||||
node --experimental-transform-types \
|
||||
.agents/skills/writing-tech-log-records/scripts/check_body.mjs 초안.md
|
||||
```
|
||||
|
||||
`tech-log-frontend` 체크아웃이 기본 경로에 없으면 알려 준다.
|
||||
|
||||
```bash
|
||||
node --experimental-transform-types scripts/check_body.mjs 초안.md \
|
||||
--frontend /path/to/tech-log-frontend
|
||||
# 또는 TECH_LOG_FRONTEND 환경변수
|
||||
```
|
||||
|
||||
통과하면 블록 구성을, 실패하면 줄·칸과 이유를 낸다.
|
||||
|
||||
```text
|
||||
PASS 14개 블록 — CALLOUT 2 · CODE_BLOCK 1 · DATA_TABLE 1 · …
|
||||
FAIL 초안.md:3:1 unsupported block syntax: html
|
||||
```
|
||||
|
||||
## 계약 기준
|
||||
|
||||
| 계약 | 버전 |
|
||||
|---|---|
|
||||
| `@tech-log/studio-contract` | 3.1.0 |
|
||||
| `@tech-log/public-contract` | 2.1.0 |
|
||||
|
||||
계약이 올라가면 `body-syntax.md`의 허용 목록과 `record-kinds.md`의 상한을 다시 맞춘다. 특히
|
||||
블록 유니온(`CaseRenderBlock`)에 타입이 늘면 쓸 수 있는 문법이 늘어난다.
|
||||
|
||||
## 알아둘 제약
|
||||
|
||||
코드·표·다이어그램·이미지는 **본문에만** 들어간다. 본문이 있는 종류는 Case 와 Concept 이다. Reference·Question·Decision의 모든
|
||||
칸은 평문으로 렌더링된다. 설계상 그렇다 — 본문을 가진 종류는 Case뿐이다.
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: writing-tech-log-records
|
||||
description: Use when writing or revising a Tech Log Studio record — Case, Concept, Reference, Question, or Decision — including choosing the right kind, filling each kind's fields, authoring body Markdown with code blocks, tables, callouts, diagrams and evidence images, and linking records so a published document renders correctly on the public site.
|
||||
metadata:
|
||||
version: "1.1.0"
|
||||
language: "ko-KR"
|
||||
studioContract: "@tech-log/studio-contract@3.1.0"
|
||||
publicContract: "@tech-log/public-contract@2.1.0"
|
||||
---
|
||||
|
||||
# Tech Log 기록 작성
|
||||
|
||||
## 개요
|
||||
|
||||
Studio는 다섯 종류를 구분한다. 종류를 잘못 고르면 채울 칸이 맞지 않는다.
|
||||
|
||||
| 종류 | 쓰는 때 | 본문 |
|
||||
|---|---|---|
|
||||
| **Case** | 내가 재현하고 검증해 결론을 냈다 | 있음 |
|
||||
| **Concept** | 남의 것이 어떻게 동작하는지 읽고 정리했다 | 있음 |
|
||||
| **Reference** | 반복 적용할 기준을 굳혔다 | 없음 |
|
||||
| **Question** | 아직 판단이 안 끝났다 | 없음 |
|
||||
| **Decision** | 프로젝트가 방향을 정했다 (`PROJECT_DECISION`) | 없음 |
|
||||
|
||||
## 절대 규칙
|
||||
|
||||
**코드·표·다이어그램·이미지는 본문에만 들어간다. 본문이 있는 종류는 Case 와 Concept 둘뿐이다.**
|
||||
본문이 없는 세 종류의 칸은 평문으로 렌더링돼 백틱과 파이프가 글자 그대로 보인다. 그런 자료는 Case 나
|
||||
Concept 에 담고 `관계`로 가리킨다. `references/record-kinds.md`
|
||||
|
||||
## 필수 절차
|
||||
|
||||
0. **글감 나누기** — 긴 글(SSOT)에서 옮겨 오는 것이면 먼저 나눈다. 기준과 `tech-log-tree.json`
|
||||
형식은 `references/from-ssot-to-records.md`. 나눈 뒤 글을 쓴다.
|
||||
1. **종류 선택** — 위 표. 애매하면 "재현했나"를 묻는다.
|
||||
2. **칸 채우기** — 칸과 게시 조건은 `references/record-kinds.md`.
|
||||
3. **본문 작성**(Case·Concept) — **종류마다 무엇을 어떤 순서로 쓰는지는
|
||||
`references/writing-each-kind.md`.** 문법은 `references/body-syntax.md`, 표·코드·그림은
|
||||
`references/code-tables-diagrams.md`. **문장은 `references/explaining.md`.**
|
||||
**문서군 전체의 리듬은 `references/ai-tells.md`.**
|
||||
그림이 필요하면 손으로 그리지 말고 `technical-visualizer` 스킬로 만든다.
|
||||
이미 쓴 문장이 AI가 쓴 것처럼 읽히면 `rewriting-technical-prose-naturally` 로 다시 쓴다.
|
||||
4. **검사** — 둘 다 돌린다. 파서와 문장은 다른 것을 본다.
|
||||
- `scripts/check_body.mjs` — Case 본문이 Studio 파서를 통과하는지. 같은 파서를 그대로 부른다.
|
||||
- `rewriting-technical-prose-naturally/scripts/check_prose.mjs` — 문장 규범. **error 0 이 될 때까지 고친다.**
|
||||
칸 하나나 한 절만 고쳤으면 `--doc` 없이 부른다. 이어서 `style_profile.mjs` 로 문체 수치를 본다.
|
||||
5. **관계 연결** — Decision은 근거가 **1개 이상** 없으면 게시가 거절된다.
|
||||
6. **Studio에서 확인** — 넣고 **저장까지만** 한 뒤 미리보기로 읽는다.
|
||||
절차는 `references/studio-draft-review.md`. **게시하지 않는다.**
|
||||
7. **게시** — 고칠 것이 없을 때만. 못 채운 칸은 그 칸 아래에 표시된다.
|
||||
|
||||
## 보호 구간
|
||||
|
||||
수치, 날짜, 버전, 단위, 코드, 명령어, URL, 인용, 공식 명칭은 원문과 한 글자도 달라지면 안 된다.
|
||||
측정하지 않은 값을 채우지 않는다 — 검증일은 실제로 확인한 날이다.
|
||||
|
||||
## 쓰지 않는 것
|
||||
|
||||
- 자료가 뒷받침하지 않는 선택 이유. 썼다는 사실을 왜 골랐는지로 바꾸지 않는다.
|
||||
- 지어낸 경험·실패·감정. 자료에 없는 1인칭 서술.
|
||||
- 가능성을 확정으로, 한 구조에서 본 것을 protocol 전체로 넓히기.
|
||||
|
||||
## 흔한 실패
|
||||
|
||||
| 실패 | 대응 |
|
||||
|---|---|
|
||||
| Reference 규칙에 코드블록 | Case로 옮겨 관계 연결 |
|
||||
| 본문 밖 칸에 백틱 | 평문이다. 백틱을 뺀다 |
|
||||
| 평문 칸에 쉼표 나열 | `이름 : 값`으로 줄 나눔. 있음·없음은 `o`·`x` |
|
||||
| 표 머리글이 명사뿐 | 질문으로. 비교 축은 `전`·`후` 시점으로 |
|
||||
| 그림 안에 문장·숫자 | `<text>`는 이름만. 문장은 `<desc>`·옆 문단에 |
|
||||
| 이름만 대고 넘어감 · 「역할이 다르다」로 끝냄 | 왜 있는지·왜 못 합치는지까지 |
|
||||
| 산문에 내부 코드명 | 구조 이름으로. 번호는 표 축·식별자에만 |
|
||||
| `**굵게**` 남발 · 끊어 나열 · 되풀이 강조 | 한 절에 하나, 한 문단으로, 한 번만 |
|
||||
| `~하는 것은 ~이다` · 「~한 것은 아니다」로 시작 | 번역투다. 문제 → 할 일 → 확인 |
|
||||
| `싣는다`·`낸다`·`둘이` · `자리`·`떠안다` | 동작을 풀고, 비유 없이 그대로 |
|
||||
| 읽는 법을 지시 | 「봐야 한다」·「여기까지다」 삭제 |
|
||||
| `..?`·`~해보자` 억지 구어체 | 명사구 제목으로. 본 것을 먼저 쓴다 |
|
||||
| 문서마다 같은 문형·같은 길이 | `references/ai-tells.md` |
|
||||
| 표를 `:::table`로 감쌈 | 그냥 파이프로 쓴다 |
|
||||
| `` | Asset으로 올려 `/api/v1/public/media/…` |
|
||||
| Decision에 근거 없음 | 관계 1개 이상 연결 |
|
||||
| 측정 안 한 검증일 | 비워 둔다 |
|
||||
|
||||
작성 후 `references/review-checklist.md`로 대조한다.
|
||||
@@ -0,0 +1,49 @@
|
||||
## 측정값 — 직접 측정
|
||||
|
||||
초기화 컬렉션 수와 총 PreparedStatement는 Hibernate `Statistics`, 지연은 `System.nanoTime`으로 읽은 값이다.
|
||||
|
||||
:::table id="direct-measurement" caption="N별 초기화 컬렉션 수와 총 PreparedStatement" rowHeaderColumn="1"
|
||||
|
||||
| N | 초기화 Highlight 컬렉션 | 총 PreparedStatement | 지연 중앙값 |
|
||||
|---:|---:|---:|---:|
|
||||
| 10 | 10 | 25 | 32.8 ms |
|
||||
| 100 | 100 | 222 | 85.9 ms |
|
||||
| 1,000 | 1,000 | 2,022 | 193.7 ms |
|
||||
|
||||
:::
|
||||
|
||||
조회량은 N에 정확히 비례했다. 여기서 N은 전체 테이블 크기가 아니라 한 요청이 반환한 FeedItem 수다.
|
||||
|
||||
## 문제가 된 조회
|
||||
|
||||
```java label="컬렉션을 지연 로딩하는 최초 구현"
|
||||
@Query("select fi from FeedItem fi where fi.visibility = :visibility")
|
||||
Page<FeedItem> loadFeed(@Param("visibility") Visibility visibility, Pageable pageable);
|
||||
```
|
||||
|
||||
:::note
|
||||
|
||||
같은 `@ManyToOne(EAGER)`라도 실행 횟수는 Persistence Context 안의 distinct 대상 수가 정한다. 애너테이션 하나로 갈리지 않는다.
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 확인한 실행 계획
|
||||
|
||||
:::evidence key="feed-explain-plan" alt="Index Scan 뒤 rows=500이 찍힌 EXPLAIN 출력" caption="반복되는 자식 조회의 실행 계획" zoom="true"
|
||||
:::
|
||||
|
||||
## 측정의 범위와 한계
|
||||
|
||||
:::warning
|
||||
|
||||
지연 값은 이미 열린 테스트 트랜잭션 안에서 어댑터 호출만 잰 단일 스레드·warm-cache 로컬 비교값이다. HTTP 종단 지연도 운영 p99도 아니다.
|
||||
|
||||
:::
|
||||
|
||||
Hibernate `Statistics`가 주는 값은 획득한 PreparedStatement 수이지 SQL shape별 실행 횟수가 아니다. shape별 횟수를 원문 SQL 수준에서 확정하려면 다음 중 하나로 따로 수집해야 한다.
|
||||
|
||||
- SQL 로그 또는 `StatementInspector`
|
||||
- datasource-proxy 또는 p6spy
|
||||
- PostgreSQL statement logging
|
||||
@@ -0,0 +1,147 @@
|
||||
# AI가 쓴 티
|
||||
|
||||
문장 하나하나는 멀쩡한데 문서군 전체가 같은 리듬으로 굴러갈 때 티가 난다. 개별 문장을 일부러
|
||||
어눌하게 만드는 방향은 역효과다. 아래는 실제 리뷰에서 지적된 것들이다.
|
||||
|
||||
## 억지 구어체를 만들지 않는다
|
||||
|
||||
AI 티를 지우려고 넣은 질문체와 청유형이 오히려 「AI 문장을 억지로 인간화한 것」으로 읽힌다.
|
||||
제목은 명사구로 두고, 본문은 무엇을 봤는지로 시작한다.
|
||||
|
||||
| 억지로 사람처럼 | 그냥 제목 |
|
||||
|---|---|
|
||||
| `무엇이 서버로 책임 이전을 했지?` | `서버로 옮겨진 책임` |
|
||||
| `AP2_SESSION은 언제 생기지?` | `AP2_SESSION이 생성되는 시점` |
|
||||
| `one-time handoff인가..?` | `/token/access는 일회성 전달이 아니다` |
|
||||
| `memory-only가 위험을 막아주나..?` | `memory-only가 줄이는 위험` |
|
||||
| `세 겹으로 나눠서 막아보자` | `세 개의 독립된 경계` |
|
||||
| `그래서 이 패턴의 문제는 받은 헤더를 어떻게 믿지?` | `upstream은 헤더의 출처를 구분할 수 없다` |
|
||||
|
||||
`..?`, `~하지?`, `~해보자`, `~하나?`, `확인하자`가 보이면 지운다.
|
||||
|
||||
## 결론을 먼저 정리하지 않는다
|
||||
|
||||
관측한 사실이 결론을 만들게 둔다. 정리된 대구 문장은 한 문서에 한 번이면 충분하다.
|
||||
|
||||
```text
|
||||
✗ BFF는 token을 브라우저에서 제거하는 대신 session과 CSRF 책임을 갖게 된다.
|
||||
|
||||
○ 브라우저 network에서 token endpoint 호출과 Authorization Bearer가 사라졌다.
|
||||
대신 `/bff/api/me` 요청에는 AP3_SESSION이 자동으로 붙었다.
|
||||
상태 변경 요청을 추가하면서 이 cookie 때문에 CSRF 검증이 필요해졌다.
|
||||
```
|
||||
|
||||
Case는 튜토리얼이 아니라 사건의 순서를 따라간다.
|
||||
|
||||
```text
|
||||
처음 예상 → 실제 요청·코드에서 본 것 → 예상과 달랐던 지점 → 왜 그런지 → 확인한 범위
|
||||
```
|
||||
|
||||
「처음에는 ~라고 봤다. 그런데 ~를 따라가 보니 ~였다」는 **실제로 그렇게 생각한 기록이 있을
|
||||
때만** 쓴다. 없으면 지어낸 1인칭이다.
|
||||
|
||||
## 같은 문형을 문서마다 되풀이하지 않는다
|
||||
|
||||
한 문서군에서 아래 두 구조가 반복되면 그것 자체가 티다.
|
||||
|
||||
```text
|
||||
A를 얻는다. 대신 B를 내준다. 그래서 C를 해야 한다.
|
||||
A와 B는 다르다. 둘을 나눠야 한다. 같은 이름으로 부르면 안 된다.
|
||||
```
|
||||
|
||||
다 쓰고 나면 세어 본다. `대신`·`그래서`·`함께`·`그대로`·`따로`·`하게 된다`·`정해야`가 문서마다
|
||||
비슷한 횟수로 나오면 문형이 굳은 것이다.
|
||||
|
||||
```bash
|
||||
grep -o '대신\|그래서\|함께\|그대로\|따로\|하게 된다' *.md | sort | uniq -c | sort -rn
|
||||
```
|
||||
|
||||
## 길이를 고르게 맞추지 않는다
|
||||
|
||||
규칙 11개를 같은 길이로 쓰면 사람이 고른 것으로 읽히지 않는다. 중요한 규칙은 길게 쓰고 자명한
|
||||
규칙은 한 줄로 끝낸다. 항목이 축으로 정리되는 내용이면 산문 대신 표 하나가 낫다.
|
||||
|
||||
```text
|
||||
endpoint | 브라우저가 접근 | credential | secret | 검증 주체
|
||||
```
|
||||
|
||||
표로 정리한 뒤 특이사항만 문장으로 쓴다.
|
||||
|
||||
## Question은 균형 잡힌 비교표가 아니다
|
||||
|
||||
선택지마다 「장점. 대신 단점.」을 똑같이 붙여 놓으면 아직 모르는 문제가 아니라 비교를 요청받고
|
||||
답한 문서가 된다. 실제 설계 기록은 이렇게 생겼다.
|
||||
|
||||
```text
|
||||
지금 확인한 사실
|
||||
지금 모르는 것
|
||||
유력한 후보와 그 후보에서 확인할 항목
|
||||
제외한 후보와 제외한 이유
|
||||
무엇으로 결정할지
|
||||
```
|
||||
|
||||
후보를 균등하게 나열하는 대신 지금 위치에서 **한 단계 앞의 결정만** 본다. 그리고 저장소 선택과
|
||||
구조 변경처럼 층이 다른 선택지는 같은 목록에 넣지 않는다.
|
||||
|
||||
## 검증 전 결과를 결론으로 쓰지 않는다
|
||||
|
||||
Question이 재현하지 않은 일을 단정하면 답을 이미 아는 문서가 된다.
|
||||
|
||||
```text
|
||||
✗ 두 replica가 같은 refresh token으로 동시에 갱신하면 한쪽은 거부되게 된다.
|
||||
○ 두 replica가 같은 refresh token으로 동시에 갱신할 수 있다. rotation 정책 때문에 두 번째
|
||||
사용이 거부될 가능성이 있고, 실제 응답과 session 영향은 아직 재현하지 않았다.
|
||||
```
|
||||
|
||||
제약에 「이 전제는 바꾸지 않는다」고 써 놓고 그 전제를 바꾸는 선택지를 나란히 두지 않는다.
|
||||
비교용으로 남기려면 「제약상 제외」로 따로 뺀다.
|
||||
|
||||
## 가짜 정량성을 만들지 않는다
|
||||
|
||||
측정할 수 없는 것을 숫자처럼 쓰지 않는다.
|
||||
|
||||
```text
|
||||
✗ 헤더 계약 수가 BFF 계약 수를 넘는 지점이 되돌릴 기준이다.
|
||||
○ 전달하려는 claim이 계속 늘어나는가. role·tenant 변경이 즉시 반영돼야 하는가.
|
||||
정책이 애플리케이션 도메인을 알아야 하는가.
|
||||
```
|
||||
|
||||
## 범위를 넓히는 단정을 쓰지 않는다
|
||||
|
||||
| 넓힌 것 | 좁힌 것 |
|
||||
|---|---|
|
||||
| token이 memory 밖으로 나가는 **유일한** 구간 | 현재 SPA 코드에서 access token이 외부 요청으로 나가는 지점 |
|
||||
| 짧은 수명이 **사실상 유일한** 방어 | 이 구성에는 denylist도 introspection도 없다. 그래서 노출 시간을 줄이는 주된 수단이 짧은 TTL이다 |
|
||||
| 배포 한 번에 **전원이** 로그아웃된다 | 상태가 process-local이라 그 instance가 종료되면 그 instance가 들고 있던 session과 authorized client가 사라진다 |
|
||||
| JDBC는 컬럼 암호화 수단이 **대부분 이미** 갖춰져 있다 | (환경마다 다르다. 확인한 것만 쓴다) |
|
||||
|
||||
「~하면 ~을 우회할 수 있다」도 실제 구성에서 확인한 범위까지만 쓴다.
|
||||
|
||||
## 기록되지 않은 과거를 만들지 않는다
|
||||
|
||||
`~하던 관행을 버리게 된다`, `그동안 ~라고 불러 왔다`. 그런 이력이 자료에 없으면 지운다.
|
||||
|
||||
## Reference는 Case의 재설명이 아니다
|
||||
|
||||
같은 프로젝트의 Case를 문장만 바꿔 옮기면 규칙 수만 늘어난다. 다른 프로젝트에서 다시 적용할 수
|
||||
있는 기준만 남기고, 사건은 Case에 두고 관계로 가리킨다. 규칙이 10개를 넘으면 축이 겹치는지 본다.
|
||||
|
||||
## 현재 검증과 운영 권고를 섞지 않는다
|
||||
|
||||
secret manager, network policy, mTLS처럼 지금 구성에 없는 것을 규칙에 그냥 적으면 Best
|
||||
Practice를 덧붙인 문서가 된다. 두 묶음으로 나눈다.
|
||||
|
||||
```text
|
||||
현재 확인한 것
|
||||
운영에서 추가로 필요한 것
|
||||
```
|
||||
|
||||
## 테스트가 무엇을 단정하는지 쓴다
|
||||
|
||||
```text
|
||||
✗ 이 요청이 200을 받는지 아닌지는 중요하지 않다.
|
||||
○ 이 테스트의 assertion은 status code가 아니다. 정상 session을 함께 보냈으니 요청은 200이
|
||||
될 수 있다. 확인할 값은 응답의 `user`가 `spoofed-admin`으로 바뀌지 않았는지다.
|
||||
```
|
||||
|
||||
`설정 → 실제 요청 → 실제 status`를 잇는 문장을 늘리고 일반론을 줄인다.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Case 본문 문법
|
||||
|
||||
`본문 Markdown` 칸에만 해당한다. 다른 종류의 칸은 평문이다.
|
||||
|
||||
본문은 자유 Markdown이 아니라 **화이트리스트로 좁힌 Markdown**이다. 공개 사이트가 raw HTML이
|
||||
아니라 타입 블록을 렌더링하기 때문에, 유니온에 없는 문법은 렌더링할 대상이 없어 거절된다.
|
||||
벗어나면 `CONTENT_FORMAT_INVALID`로 게시가 막힌다.
|
||||
|
||||
## 쓸 수 있는 것
|
||||
|
||||
| 문법 | 예 |
|
||||
|---|---|
|
||||
| 제목 | `#` ~ `######` (1~6단계) |
|
||||
| 문단 | 그냥 쓴다 |
|
||||
| 강조 | `**굵게**` `*기울임*` `` `코드` `` |
|
||||
| 링크 | `[문구](/경로)` · `[문구](https://…)` |
|
||||
| 목록 | `- 항목` · `1. 항목` |
|
||||
| 인용 | `> 한 문단` |
|
||||
| 수평선 | `---` |
|
||||
| 코드블록 | ```` ```java ```` |
|
||||
| 표 | 파이프 표. **감싸지 않는다** |
|
||||
| 그림 | `` |
|
||||
| callout | `:::note` `:::tip` `:::warning` `:::danger` |
|
||||
| 증거 이미지 | `:::evidence key="…" alt="…" caption="…" zoom="true"` |
|
||||
|
||||
제목에 고정 id를 주려면 `## 측정 결과 {#measurement}`.
|
||||
|
||||
## 쓸 수 없는 것
|
||||
|
||||
- **raw HTML** — `<div>`, `<br>`, `<img>` 모두 거절
|
||||
- **각주** — `[^1]`
|
||||
- **체크박스 목록** — `- [ ] 할 일`
|
||||
- **중첩 목록** — 목록 항목 안에 목록
|
||||
- **중첩 인용** — `> >`
|
||||
- **취소선** — `~~지움~~`
|
||||
- **링크 title** — `[문구](/경로 "설명")`
|
||||
- **외부 스킴** — `javascript:`, `data:`, `//다른호스트`
|
||||
|
||||
인용과 callout은 **문단을 정확히 하나만** 담는다. 목록 항목도 문단 하나만 담는다. 여러 문단이
|
||||
필요하면 블록을 나눈다.
|
||||
|
||||
## 링크와 이미지 주소
|
||||
|
||||
허용되는 주소는 넷뿐이다.
|
||||
|
||||
```text
|
||||
#앵커
|
||||
/상대경로
|
||||
https://… 또는 http://…
|
||||
mailto:…
|
||||
```
|
||||
|
||||
`//호스트`로 시작하는 주소는 거절된다. 프로토콜 상대 주소는 어느 사이트를 가리키는지 원문만
|
||||
보고 알 수 없기 때문이다.
|
||||
|
||||
**object storage 주소를 본문에 직접 쓰지 않는다.** 만료되는 presigned URL이 원문에 박히면 나중에
|
||||
깨진다. 이미지는 Asset으로 올리고 `/api/v1/public/media/{assetId}` 또는 `:::evidence`로 가리킨다.
|
||||
|
||||
## directive 쓰는 법
|
||||
|
||||
세 개뿐이다: `table`, `callout`, `evidence`. 그리고 서버가 아는 이름 넷: `note`, `tip`,
|
||||
`warning`, `danger`.
|
||||
|
||||
속성은 **정확히 맞아야 한다** — 하나라도 빠지거나 남으면 거절된다.
|
||||
|
||||
```text
|
||||
:::table id="…" caption="…" rowHeaderColumn="1"|"none"
|
||||
:::callout tone="warning"|"info" label="…"
|
||||
:::evidence key="…" alt="…" caption="…" zoom="true"|"false"
|
||||
```
|
||||
|
||||
`note`·`tip`·`warning`·`danger`는 이름이 곧 성격이라 속성을 받지 않는다.
|
||||
|
||||
```text
|
||||
:::note
|
||||
|
||||
참고할 내용 한 문단.
|
||||
|
||||
:::
|
||||
```
|
||||
|
||||
여는 줄과 닫는 `:::` 사이에 **빈 줄**을 둔다. 붙여 쓰면 문단으로 인식되지 않는다.
|
||||
|
||||
## 자주 나오는 거절과 원인
|
||||
|
||||
| 메시지 | 원인 |
|
||||
|---|---|
|
||||
| `unsupported block syntax: html` | raw HTML을 썼다 |
|
||||
| `unsupported inline syntax: image` | 문단 안에 글과 그림을 섞었다 |
|
||||
| `unknown block directive: …` | 위 일곱 이름이 아니다 |
|
||||
| `table directive must contain exactly one GFM table` | `:::table` 안에 표가 없거나 둘이다 |
|
||||
| `callout directive must contain exactly one paragraph` | callout에 문단이 없거나 둘 이상이다 |
|
||||
| `list items must contain exactly one paragraph` | 목록을 중첩했다 |
|
||||
| `unsafe link URL: …` | 허용되지 않는 스킴이나 `//` 주소 |
|
||||
| `duplicate explicit ID: …` | 같은 id를 두 번 썼다 |
|
||||
|
||||
거절 메시지에는 줄·칸 번호가 붙는다. 그 줄을 먼저 본다.
|
||||
@@ -0,0 +1,188 @@
|
||||
# 코드·표·다이어그램·이미지
|
||||
|
||||
본문(`bodyMarkdown`)에만 해당한다. 본문이 있는 종류는 Case 와 Concept 이다.
|
||||
|
||||
## 코드블록
|
||||
|
||||
````text
|
||||
```java
|
||||
@Query("select fi from FeedItem fi join fetch fi.user")
|
||||
List<FeedItem> findFeed(Pageable pageable);
|
||||
```
|
||||
````
|
||||
|
||||
언어는 `^[A-Za-z0-9][A-Za-z0-9_.+-]*$`만 쓴다 — `java`, `kotlin`, `sql`, `yaml`, `bash`,
|
||||
`text`. 언어를 모르면 `text`.
|
||||
|
||||
설명을 붙이려면 `label` 하나만 쓴다. 다른 속성은 거절된다.
|
||||
|
||||
````text
|
||||
```sql label="N+1이 발생하는 조회"
|
||||
SELECT * FROM highlights WHERE feed_item_id = ?;
|
||||
```
|
||||
````
|
||||
|
||||
**코드에 자격증명·토큰·내부 호스트를 남기지 않는다.** 지울 때는 지웠다는 사실이 보이게 한다 —
|
||||
`Authorization: Bearer <생략>`처럼. 조용히 빼면 다음 사람이 그 헤더가 없었다고 읽는다.
|
||||
|
||||
붙여넣은 코드는 원문 그대로 둔다. 줄바꿈과 들여쓰기를 손보면 재현이 달라진다.
|
||||
|
||||
## 표
|
||||
|
||||
파이프로 그냥 쓴다. **감싸지 않는다.**
|
||||
|
||||
```text
|
||||
| N | 초기화 컬렉션 | 총 쿼리 |
|
||||
|---|---|---|
|
||||
| 10 | 10 | 25 |
|
||||
| 100 | 100 | 222 |
|
||||
| 1,000 | 1,000 | 2,022 |
|
||||
```
|
||||
|
||||
설명이나 행 머리글이 필요할 때만 `:::table`로 감싼다. 세 속성이 전부 있어야 한다.
|
||||
|
||||
```text
|
||||
:::table id="direct-measurement" caption="N별 직접 측정값" rowHeaderColumn="1"
|
||||
|
||||
| N | 초기화 컬렉션 | 총 쿼리 |
|
||||
|---|---|---|
|
||||
| 10 | 10 | 25 |
|
||||
|
||||
:::
|
||||
```
|
||||
|
||||
- `id` — 문서 안에서 유일해야 한다. 제목 id와도 겹치면 안 된다
|
||||
- `rowHeaderColumn` — `"1"`이면 첫 열이 행 머리글, 아니면 `"none"`
|
||||
- 정렬은 구분줄로 준다: `|---:|` 오른쪽, `|:---:|` 가운데
|
||||
|
||||
**측정값과 파생값을 한 표에 섞지 않는다.** 섞어야 한다면 성격 열을 두어 어느 것이 잰 값이고
|
||||
어느 것이 계산한 값인지 밝힌다.
|
||||
|
||||
### 머리글이 질문이면 행이 답이 된다
|
||||
|
||||
열 이름을 명사로 두면 독자가 표를 훑는다. 질문으로 두면 읽고 답을 얻는다.
|
||||
|
||||
| 쓰지 않는다 | 쓴다 |
|
||||
|---|---|
|
||||
| `memory-only가 줄이는가` | `memory-only가 위험을 막아주나..?` |
|
||||
| `남는 데이터` · `reload 뒤` | `reload 전` · `reload 후` |
|
||||
| `확인함` · `확인 안 함` | `o` · `x` |
|
||||
|
||||
비교 표의 축은 **시점이나 상태**로 못 박는다. `전`·`후`, `켬`·`끔`처럼 어느 때의 값인지 열
|
||||
이름이 말해야 한다. `남는 데이터` 같은 두루뭉술한 이름을 쓰면 그 열이 언제 얘긴지 본문을 다시
|
||||
봐야 한다.
|
||||
|
||||
있음·없음을 가르는 열은 `o`·`x`로 채운다. 열이 좁아지고 값이 눈에 띄게 갈린다. 문장 안이 아니라
|
||||
값 자리에서만 쓴다.
|
||||
|
||||
**열을 줄인다.** 감싸개가 `--body-copy`(672px)에 묶여 있고 표에는 `min-width: 780px`이 걸려
|
||||
있어서, 열이 몇 개든 오른쪽 108px은 늘 잘린다. 3열을 2열로 줄일 수 있으면 줄이고, 첫 열
|
||||
문구를 짧게 해서 값 열을 왼쪽으로 당긴다.
|
||||
|
||||
```text
|
||||
3열 2열로
|
||||
| 방어선 | 무엇을 막나 | 뚫리면 | → | 위치 | 여기서 어떻게 막지? |
|
||||
```
|
||||
|
||||
세 번째 열에 있던 내용은 표 다음 문단에서 이어 쓴다.
|
||||
|
||||
## 다이어그램·SVG
|
||||
|
||||
### 그림에 문장을 넣지 않는다
|
||||
|
||||
가장 흔한 실패다. 설명을 그림 안으로 밀어 넣으면 라벨이 길어지고, 글자는 작아지고, 검색도
|
||||
복사도 화면 낭독도 안 되는 텍스트가 된다. 그림은 **관계**를 보이고 문장은 그 옆 문단에 쓴다.
|
||||
|
||||
지킬 선:
|
||||
|
||||
| 항목 | 기준 |
|
||||
|---|---|
|
||||
| 상자 | 7개 이하. 넘으면 그림을 나눈다 |
|
||||
| 라벨 | 한 줄 40자 이하. 문장이 아니라 이름 |
|
||||
| 글자 크기 | 2종 (제목·보조). 3종부터는 위계가 아니라 소음이다 |
|
||||
| 색 | 의미를 색에만 싣지 않는다. 빗금·테두리·위치를 함께 쓴다 |
|
||||
| 화살표 | 방향이 논지일 때만. 장식으로 긋지 않는다 |
|
||||
| 숫자 | 넣지 않는다. 산문에 쓴다 |
|
||||
|
||||
빨강·초록 조합은 피한다. 색각 이상에서 구분되지 않는다.
|
||||
|
||||
**자가 점검.** 올리기 전에 `<text>`를 전부 뽑아 읽는다.
|
||||
|
||||
```bash
|
||||
grep -o '<text[^>]*>[^<]*</text>' 그림.svg
|
||||
```
|
||||
|
||||
하나라도 아래에 걸리면 문장이므로 뺀다. 길이가 아니라 **서술하느냐**가 기준이다 — 40자 이하여도
|
||||
문장은 문장이다.
|
||||
|
||||
- 마침표나 물음표로 끝난다
|
||||
- 서술어가 있다 — `~있다`, `~막는다`, `~바꿔도`
|
||||
- 조사로 두 대상을 잇는다 — `A를 B로`, `A에서 B까지`
|
||||
|
||||
`세 가지가 한 영역 안에 있다`는 그림이 이미 보여 주는 것을 글로 다시 쓴 것이다. 상자를 한
|
||||
영역 안에 그렸으면 그 문장은 필요 없다. 지우면 그림이 더 명확해진다.
|
||||
|
||||
`<title>`과 `<desc>`는 예외다. 화면을 못 보는 사람이 듣는 자리이므로 여기에는 문장을 쓴다.
|
||||
그림 안에서 뺀 설명이 갈 곳이기도 하다.
|
||||
|
||||
### 무엇을 그릴지 정하는 법
|
||||
|
||||
그림 하나에 주장 하나다. "이 그림이 없으면 독자가 무엇을 못 보나"에 한 문장으로 답할 수
|
||||
없으면 그리지 않는다. 표로 되는 것을 그림으로 그리지 않는다 — 표는 값을 비교하고, 그림은
|
||||
**포함·순서·경계**처럼 자리로만 보이는 것을 맡는다.
|
||||
|
||||
Studio는 다이어그램을 그려 주지 않는다. **파일로 만들어 Asset으로 올린다.**
|
||||
|
||||
1. `본문에 Asset 삽입` → `업로드 종류`를 **다이어그램**으로 → `Asset 업로드`
|
||||
2. 목록에서 고르면 커서 자리에 `:::evidence` 구문이 삽입된다
|
||||
|
||||
SVG는 `image/svg+xml`로 올라가고 다른 이미지와 같게 다뤄진다. 벡터라 확대해도 깨지지 않으니
|
||||
구조도·흐름도에 맞다.
|
||||
|
||||
올리기 전에 SVG에서 지울 것:
|
||||
|
||||
- `<script>` 요소와 `on*` 속성
|
||||
- 외부 폰트·이미지 참조 — 열람자 환경에서 안 불러온다. 글자는 path로 변환하거나 일반 폰트만
|
||||
- 절대 좌표에 의존하는 고정 크기 — `viewBox`를 두어 늘어나게 한다
|
||||
|
||||
## 증거 이미지
|
||||
|
||||
```text
|
||||
:::evidence key="asset-key" alt="무엇을 보여 주는 그림인가" caption="설명" zoom="true"
|
||||
:::
|
||||
```
|
||||
|
||||
- 네 속성이 전부 있어야 한다. 본문은 비운다
|
||||
- `key`는 `^[a-z0-9]+(?:-[a-z0-9]+)*$`
|
||||
- `zoom="true"`면 눌러서 확대할 수 있다. 표·로그처럼 글자가 작으면 켠다
|
||||
- `alt`는 화면을 못 보는 사람이 읽는 문장이다. "스크린샷"이 아니라 **무엇이 보이는지** 쓴다
|
||||
- 장식용 그림은 Asset의 `decorative`를 켜고 `alt`를 비운다
|
||||
|
||||
`READY`가 아닌 Asset은 게시 시 거절된다.
|
||||
|
||||
## 일반 이미지
|
||||
|
||||
Asset이 아닌 그림은 Markdown으로 쓴다.
|
||||
|
||||
```text
|
||||

|
||||
```
|
||||
|
||||
**문단 하나가 그림 하나로만 이루어져야** 그림으로 인식된다. 글과 섞으면 인라인 이미지가 되어
|
||||
거절된다.
|
||||
|
||||
설명이 필요하면 Markdown title을 쓰지 말고 — 거절된다 — `:::evidence`의 `caption`을 쓰거나
|
||||
그림 다음 문단에 쓴다.
|
||||
|
||||
## 무엇을 어디에 쓰나
|
||||
|
||||
| 담을 것 | 쓸 것 |
|
||||
|---|---|
|
||||
| 명령어·설정·소스 | 코드블록 |
|
||||
| 숫자 비교 | 표 |
|
||||
| 구조·흐름 | SVG 다이어그램 Asset |
|
||||
| 화면·로그 캡처 | 이미지 Asset + `zoom="true"` |
|
||||
| 놓치면 안 되는 단서 | `:::warning` |
|
||||
| 곁가지 설명 | `:::note` |
|
||||
|
||||
캡처로 표를 대신하지 않는다. 그림 속 숫자는 검색도 복사도 안 되고 화면 낭독기가 읽지 못한다.
|
||||
@@ -0,0 +1,413 @@
|
||||
# 설명의 깊이와 말투
|
||||
|
||||
가장 자주 나오는 지적은 **설명이 짧다**는 것이다. 사실은 맞는데 독자가 따라오지 못한다.
|
||||
|
||||
## 이름을 댔으면 왜 있는지도 댄다
|
||||
|
||||
낯선 클래스·기법·설정 이름을 적고 다음 문장으로 넘어가지 않는다. **왜 그것이 존재하는지**를 한
|
||||
문장 붙인다.
|
||||
|
||||
```text
|
||||
쓰지 않는다
|
||||
XorCsrfTokenRequestAttributeHandler가 token을 XOR와 Base64로 가린다.
|
||||
|
||||
쓴다
|
||||
XorCsrfTokenRequestAttributeHandler가 token을 XOR와 Base64로 가린다.
|
||||
BREACH 공격을 줄이기 위해서다. 여기서 깊게 다루지는 않는다 — HTTP 응답 압축 크기의
|
||||
차이로 응답 안의 비밀값을 조금씩 추측하는 공격이고, 그래서 응답에 실리는 값을 매번
|
||||
다르게 만든다.
|
||||
```
|
||||
|
||||
깊게 안 갈 것이면 **안 간다고 밝히고 한 문장 요약을 준다.** 이름만 던지고 넘어가면 독자는 그
|
||||
자리에서 검색하러 나간다.
|
||||
|
||||
## 「역할이 다르다」로 끝내지 않는다
|
||||
|
||||
두 값이 왜 하나로 합쳐질 수 없는지 **메커니즘**을 적는다.
|
||||
|
||||
```text
|
||||
쓰지 않는다
|
||||
첫 줄은 claim 검증 기준이고 셋째·넷째 줄은 network 경로다. 역할이 다르다.
|
||||
|
||||
쓴다
|
||||
issuer는 요청을 보내는 주소가 아니라 발급된 token의 iss claim이 기대한 값과 같은지
|
||||
확인하는 기준값이다. token URL과 userinfo URL은 실제로 요청을 보내는 내부 주소다.
|
||||
브라우저는 docker 내부 호스트명에 접근할 수 없어 로그인에는 외부 주소를 쓰고,
|
||||
컨테이너는 자기 localhost가 그 서버가 아니므로 내부 통신에는 service 이름을 쓴다.
|
||||
```
|
||||
|
||||
## 값이 합쳐지면 합쳐진 결과를 보여 준다
|
||||
|
||||
두 곳에서 온 값이 하나의 요청이 되는 흐름은 **조립된 실물**까지 보여 준다. 대응표만 두면
|
||||
독자가 머릿속으로 조립해야 한다.
|
||||
|
||||
```text
|
||||
대응표만 두지 않는다
|
||||
body.token = masked token
|
||||
cookie XSRF-TOKEN = raw token
|
||||
POST X-XSRF-TOKEN = same raw token
|
||||
|
||||
조립된 요청을 이어서 보여 준다
|
||||
POST /bff/theme HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
Cookie: SESSION=abc123; XSRF-TOKEN=xyz789
|
||||
X-XSRF-TOKEN: xyz789
|
||||
```
|
||||
|
||||
## 직접 본 것은 따로 절을 만든다
|
||||
|
||||
테스트 계약을 인용하는 것과 **직접 열어서 본 것**은 다른 증거다. 화면을 열어 확인했다면 그
|
||||
사실을 따로 적는다.
|
||||
|
||||
```text
|
||||
로그인 뒤 브라우저 개발자 도구에서 요청과 저장소를 확인했다. Keycloak token endpoint를
|
||||
직접 호출하지 않았고 Resource Server 포트도 직접 호출하지 않았다. localStorage와
|
||||
sessionStorage에도 accessToken과 refreshToken이 없었다.
|
||||
```
|
||||
|
||||
확인하지 않았으면 쓰지 않는다. 계약 인용은 계약 인용이라고 적는다.
|
||||
|
||||
## 내부 코드명을 산문에 쓰지 않는다
|
||||
|
||||
독자는 `AP1`~`AP4` 같은 내부 번호를 모른다. 산문에서는 **구조 이름**으로 부른다.
|
||||
|
||||
| 쓰지 않는다 | 쓴다 |
|
||||
|---|---|
|
||||
| AP1은 | SPA 구조는 |
|
||||
| AP2가 맞는 경우는 | Mediator를 고를 수 있다 |
|
||||
| AP3은 | BFF에서는 · BFF 구조에선 |
|
||||
| AP4는 | OAuth2-Proxy 구조는 |
|
||||
|
||||
번호는 **표의 축과 식별자에만** 남긴다 — `AP1~AP3 | AP4` 같은 비교 열, `AP3_SESSION` 같은
|
||||
실제 값. slug에도 넣지 않는다.
|
||||
|
||||
```text
|
||||
쓰지 않는다 ap4-identity-header-trust
|
||||
쓴다 identity-header-trust
|
||||
```
|
||||
|
||||
## 제목은 묻고 본문은 답한다
|
||||
|
||||
절 제목에 `~해보자` `~하지?` `~일까?`를 쓴다. 그리고 **첫 문장에서 그 질문을 다시 던지고**
|
||||
답한다.
|
||||
|
||||
| 쓰지 않는다 | 쓴다 |
|
||||
|---|---|
|
||||
| 브라우저에 남은 것 | 브라우저에 관리 대상 |
|
||||
| 위조 요청은 어떻게 생겼나 | 위조 요청은 어떻게 생겼을까? |
|
||||
| 세 겹으로 나눠서 막는다 | 세 겹으로 나눠서 막아보자 |
|
||||
| 무엇이 서버로 넘어왔나 | 무엇이 서버로 책임이 넘어왔지? |
|
||||
| upstream이 JWT를 받지 않는다는 뜻 | upstream이 JWT를 받지 않는다? |
|
||||
|
||||
```text
|
||||
## 브라우저에 관리 대상
|
||||
|
||||
브라우저에 관리 대상은 그럼 어떤 게 될까?
|
||||
|
||||
| 무엇 | 브라우저에 있나..? | JavaScript가 읽나..? |
|
||||
```
|
||||
|
||||
## 굵게를 걷어낸다
|
||||
|
||||
`**굵게**`는 거의 쓰지 않는다. 한 절에 하나를 넘기면 강조가 아니라 얼룩이 된다. 강조는
|
||||
**자리**로 한다 — 절을 따로 떼거나, 표에서 그 행을 첫 줄에 두거나, 짧은 문단으로 끊는다.
|
||||
|
||||
## 단정을 좁힌다
|
||||
|
||||
「항상 그렇다」로 적기 전에 예외를 센다.
|
||||
|
||||
```text
|
||||
틀렸다 token endpoint는 server-to-server 호출이다
|
||||
맞다 server-to-server 호출일 수도 있고 browser-to-server 호출일 수도 있다
|
||||
```
|
||||
|
||||
public client는 브라우저가 직접 token endpoint를 부른다. 한 구조에서 본 것을 protocol
|
||||
전체의 성질로 넓히지 않는다.
|
||||
|
||||
## 흐름은 끊지 않고 이어 간다
|
||||
|
||||
한 흐름은 한 문단으로 이어 간다. `이후` · `그리고` · `~하면` · `~한 뒤`로 다음 단계를 붙인다.
|
||||
단계마다 문장을 끊고 각각 결론을 다는 방식은 쓰지 않는다.
|
||||
|
||||
```text
|
||||
쓰지 않는다
|
||||
브라우저가 로그인 요청을 보낸다. mediator가 세션을 확인한다. Keycloak으로 302를 준다.
|
||||
브라우저가 URI를 조립해 Keycloak을 부른다. code를 받는다. mediator가 교환한다.
|
||||
|
||||
쓴다
|
||||
브라우저가 로그인 요청을 보내면 mediator에서 세션을 확인한 뒤 Keycloak으로 302
|
||||
리다이렉트를 하게 된다. 그리고 브라우저가 로그인 요청과 관련된 값을 조립해 URI를 만들고
|
||||
Keycloak으로 요청을 보낸다. 이후 로그인 화면에서 아이디와 비밀번호를 넣어 전달하면
|
||||
authorization code와 함께 redirect되고, 그 code를 mediator가 token으로 교환하게 된다.
|
||||
```
|
||||
|
||||
짧은 문장을 나열하면 각 문장이 다 결론처럼 읽힌다. 읽는 사람은 어디가 흐름이고 어디가
|
||||
판단인지 구분하지 못한다.
|
||||
|
||||
## 앞 구조와 무엇이 달라졌는지로 연다
|
||||
|
||||
절을 열 때 이전 구조를 먼저 세우고 무엇이 옮겨졌는지 말한다. 그러면 비교 축이 문단 안에서
|
||||
고정된다.
|
||||
|
||||
```text
|
||||
앞선 구조에서는 브라우저가 token 교환과 관리, API 요청까지 전부 맡았다.
|
||||
이 구조에서는 token 관리와 교환의 위치가 브라우저에서 Spring backend로 옮겨지게 된다.
|
||||
```
|
||||
|
||||
같은 대조를 절 끝에서 한 번 더 쓴다 — 「앞선 구조에서는 브라우저가 OIDC client였다면 이
|
||||
구조에서는 mediator가 OIDC client가 된다」처럼.
|
||||
|
||||
## 결론은 문장 끝에 붙인다
|
||||
|
||||
한 줄짜리 단정문을 따로 떼어 강조하지 않는다. 앞 문장에서 `그래서` · `그렇기 때문에`로
|
||||
이어 간다.
|
||||
|
||||
```text
|
||||
쓰지 않는다
|
||||
code 교환과 API 호출을 둘 다 server가 대신해야 한다. 하나만 옮겨서는 안 된다.
|
||||
원문이 응답 본문과 지역 변수와 헤더를 지난다. 세 자리 모두 같은 실행 영역이라
|
||||
「브라우저에 token 없음」을 만족하지 못한다.
|
||||
|
||||
쓴다
|
||||
refresh token만 옮기는 구조에서는 브라우저가 Resource Server를 직접 부르기 때문에
|
||||
access token이 필요하고, 그것을 응답 본문으로 받게 된다. 그래서 원문이 응답 본문과
|
||||
지역 변수, Authorization 헤더를 차례로 지나게 되고 결국 브라우저에 token이 없다고
|
||||
말할 수 없게 된다.
|
||||
```
|
||||
|
||||
**같은 말을 다시 말해 강조하지 않는다.** 앞 문장이 이미 말했으면 거기서 끝낸다.
|
||||
`하나만 옮겨서는 안 된다` 같은 덧붙임이 그것이다.
|
||||
|
||||
## `~하게 된다`는 상태가 실제로 바뀌는 자리에만 쓴다
|
||||
|
||||
무엇이 바뀌는 대목에서는 `~한다`보다 `~하게 된다`가 맞다. 흐름을 따라가는 자리이기
|
||||
때문이다.
|
||||
|
||||
| 쓰지 않는다 | 쓴다 |
|
||||
|---|---|
|
||||
| 위치가 바뀐다 | 위치가 옮겨지게 된다 |
|
||||
| mediator가 code를 교환한다 | mediator가 code를 token으로 교환하게 된다 |
|
||||
| session cookie를 발급한다 | 저장이 끝나고 session cookie를 발급하게 된다 |
|
||||
|
||||
**정의·분류·사실·지시에는 붙이지 않는다.** 어미만 바꾸면 문장이 어색해지고 무엇이 흐름이고
|
||||
무엇이 기준인지도 흐려진다.
|
||||
|
||||
| 자리 | 틀렸다 | 맞다 |
|
||||
|---|---|---|
|
||||
| 정의 | 네 구조는 서로 다른 운영 계약이 된다 | 운영 계약이다 |
|
||||
| 분류 | 이 흐름은 별도 client가 된다 | 별도 client다 |
|
||||
| 사실 | upstream은 role 판단을 하지 않게 된다 | 하지 않는다 |
|
||||
| 지시 | edge에 인증을 맡기지 않게 된다 | 맡기지 않는다 |
|
||||
|
||||
가릴 때는 **정말로 무엇이 되는지**를 묻는다. `refresh token이 무효가 된다`, `lock 자체가 새
|
||||
장애 지점이 된다`는 상태가 바뀌므로 맞다. `배치가 된다`는 원래 배치였으므로 틀렸다.
|
||||
|
||||
Reference의 규칙 제목과 Decision의 결정문도 `~한다`로 끊는다. 그 자리는 기준이다.
|
||||
|
||||
## 그림은 한 줄로 예고한다
|
||||
|
||||
`전체적인 구조를 보면 다음과 같다` 같은 한 줄을 두고 그림을 넣는다. 문단 사이에 말없이
|
||||
끼우지 않는다.
|
||||
|
||||
## 번역투를 걷어낸다
|
||||
|
||||
가장 자주 나오는 지적 두 번째다. 어미는 한국어인데 **문장 구조가 영어**여서 읽기 힘들다.
|
||||
|
||||
### `~하는 것은 ~이다`를 쓰지 않는다
|
||||
|
||||
영어의 `What matters is …`를 그대로 옮긴 구조다. 한국어는 동사로 바로 간다.
|
||||
|
||||
| 쓰지 않는다 | 쓴다 |
|
||||
|---|---|
|
||||
| 고르기 전에 답할 것은 세 가지 배치다 | 고르기 전에 누가 code를 바꾸고 누가 token을 드는지부터 정한다 |
|
||||
| 실제로 갈리는 것은 보안 수준이 아니다 | 보안 수준으로는 구조가 갈리지 않았다 |
|
||||
| 요구로 들어오면 남는 것은 BFF다 | 요구로 들어오면 BFF만 남는다 |
|
||||
| 여기서 줄어드는 것은 재사용 반경이다 | 여기서는 재사용 반경만 줄어든다 |
|
||||
|
||||
### 지시대명사를 주어로 세우지 않는다
|
||||
|
||||
`그것이` · `이것이`로 문장을 시작하면 앞 문장을 되짚어야 읽힌다.
|
||||
|
||||
| 쓰지 않는다 | 쓴다 |
|
||||
|---|---|
|
||||
| 그것이 더 안전한 순서는 아니다 | 그렇다고 뒤로 갈수록 더 안전해지지는 않는다 |
|
||||
| 그것이 학습 환경임을 문서에 남긴다 | 학습 환경이라고 문서에 적어 둔다 |
|
||||
| 정책상 그것이 금지라면 | 정책상 브라우저 token이 금지라면 |
|
||||
|
||||
### `~라는 뜻은 아니다` · `~는 것은 아니다`를 쓰지 않는다
|
||||
|
||||
부정을 두 겹으로 쌓지 않는다. 긍정으로 뒤집고 조건을 붙인다.
|
||||
|
||||
| 쓰지 않는다 | 쓴다 |
|
||||
|---|---|
|
||||
| confidential이라고 token이 안 가는 것은 아니다 | confidential이어도 token이 갈 수 있다 |
|
||||
| client 인증이 있다고 PKCE가 필요 없어지는 것은 아니다 | client 인증이 있어도 PKCE는 여전히 쓸모가 있다 |
|
||||
| 도메인 정책을 통과했다는 뜻은 아니다 | 도메인 정책은 아직 통과해 보지 않았다 |
|
||||
| 이름이 장애 복구를 갖췄다는 뜻은 아니다 | 이름만 봐서는 장애 복구가 갖춰졌는지 알 수 없다 |
|
||||
|
||||
### 추상 공간 은유를 쓰지 않는다
|
||||
|
||||
`그 자리에 무엇이 들어오는지` 같은 표현은 실체가 없다. 무엇을 누가 하는지로 바꾼다.
|
||||
|
||||
```text
|
||||
쓰지 않는다 token을 없앨 때 그 자리에 무엇이 들어오는지 적는다
|
||||
쓴다 token을 없앤 대신 무엇을 관리해야 하는지 적는다
|
||||
|
||||
쓰지 않는다 CSRF와 공유 저장소가 그 자리에 들어오게 된다
|
||||
쓴다 server로 옮기면 server session과 CSRF, 공유 저장소를 관리해야 한다
|
||||
```
|
||||
|
||||
### 비유로 설명하지 않는다
|
||||
|
||||
기록하는 글이지 수필이 아니다. 그림이 떠오르는 표현을 쓰면 읽는 사람마다 다르게 읽는다.
|
||||
|
||||
| 쓰지 않는다 | 쓴다 |
|
||||
|---|---|
|
||||
| 얻은 것 옆에 내준 것을 같이 둔다 | 얻은 것과 내준 것을 함께 적는다 |
|
||||
| 판단 자료가 아니라 홍보문이 되어 버린다 | 무엇을 감수해야 하는지 알 수 없다 |
|
||||
| 그 자리를 PKCE가 메운다 | secret 대신 PKCE를 쓴다 |
|
||||
| 짧은 수명이 그 자리를 대신한다 | token 수명을 짧게 두는 것이 유일한 방어가 된다 |
|
||||
| BFF가 떠안게 된다 | BFF가 맡아야 한다 |
|
||||
| session ID마다 독립된 금고 | session ID마다 token을 따로 보관 |
|
||||
| 401을 그대로 흘리면 | 401을 그대로 내려보내면 |
|
||||
| 재사용되는 반경 | 재사용될 범위 |
|
||||
| 그 뒤 검사가 다 무의미해진다 | 그 뒤에 무엇을 검사해도 소용이 없다 |
|
||||
| 값이 새면 | 값이 유출되면 |
|
||||
| 덮어쓰기가 도는지 | 덮어쓰기가 실제로 동작하는지 |
|
||||
| 인스턴스가 죽으면 | 인스턴스가 내려가면 |
|
||||
| 경쟁이 자연히 흡수된다 | 경쟁이 저절로 해소된다 |
|
||||
| 통째로 건너뛰어진다 | 함께 빠지게 된다 |
|
||||
| 다른 사람이 되어 버린다 | 다른 사람으로 인식된다 |
|
||||
| token을 치운다 | token을 없앤다 |
|
||||
| 배치가 구조를 가른다 | 어디에 두느냐에 따라 구조가 달라진다 |
|
||||
| 비교표의 축에 끼어든다 | 성격이 다른 항목이 섞인다 |
|
||||
|
||||
`자리` · `옆` · `칸` · `축` 같은 **공간 말**, `떠안다` · `죽다` · `흡수하다` 같은 **의인·비유**,
|
||||
`무의미해진다` · `되어 버린다` 같은 **과장**을 지운다. 지우고 나면 무슨 일이 일어나는지만
|
||||
남는다.
|
||||
|
||||
### 그 밖에 자주 나오는 것
|
||||
|
||||
- `~에 대한` → 조사로 푼다. `학생들에 대한 관심` → `학생에게 관심이 많다`
|
||||
- `~에 있어서` → `~에서` · `~할 때`
|
||||
- `~에 의해` · `~로 인해` → `~ 때문에` · `~가`
|
||||
- 피동 → 능동. `결정이 내려졌다` → `결정했다`
|
||||
- `~들` → 복수가 문맥으로 분명하면 뺀다
|
||||
|
||||
의미를 바꾸지 않는 단어를 먼저 지운다. 글자 수를 줄이는 것보다 이것이 앞선다.
|
||||
|
||||
## 읽는 법을 지시하지 않는다
|
||||
|
||||
「무엇을 막는지를 좁혀서 봐야 한다」, 「여기까지다」, 「먼저 본다」, 「~로 읽으면 안 된다」,
|
||||
「두 질문을 따로 답한다」. 전부 독자에게 읽는 방법을 알려 주는 문장이다. 문서는 대상을 설명하지
|
||||
독자의 읽기를 지시하지 않는다.
|
||||
|
||||
지울 자리를 찾는 법은 간단하다. 그 문장을 빼도 남은 내용이 그대로면 곁가지다.
|
||||
|
||||
```text
|
||||
✗ 무엇을 막는지를 좁혀서 봐야 한다. code를 누가 훔쳐 가도 verifier가 없으면
|
||||
token으로 바꾸지 못한다. 여기까지다.
|
||||
○ code를 누가 훔쳐 가도 verifier가 없으면 token으로 바꾸지 못한다.
|
||||
```
|
||||
|
||||
앞 문장이 빠져도 뒷 문장의 뜻은 하나도 줄지 않는다. 「먼저 본다」도 마찬가지다. 순서를 지시하는
|
||||
대신 왜 그런지를 쓴다.
|
||||
|
||||
```text
|
||||
✗ 여기서 걸리면 나머지 비교가 필요 없어지기 때문에 먼저 본다.
|
||||
○ 이 조건에 걸리면 다른 항목은 볼 필요가 없다.
|
||||
```
|
||||
|
||||
규칙 제목이 이미 말한 것을 본문에서 다시 지시하는 것도 같은 문제다. 제목이 「얻은 것과 내준 것을
|
||||
함께 적는다」면 본문 끝에 「함께 적는다」를 또 쓰지 않는다.
|
||||
|
||||
## 동작을 압축하지 않는다
|
||||
|
||||
`싣는다`, `낸다`, `친다`, `짠다`처럼 한 글자로 줄인 동사는 무엇을 어디로 하는지를 지운다. 실제
|
||||
동작으로 풀어 쓰고, 목적어를 빼지 않는다.
|
||||
|
||||
| 압축 | 푼 것 |
|
||||
|---|---|
|
||||
| code_challenge를 싣고 | code_challenge를 담아서 보내고 |
|
||||
| code_verifier를 낸다 | code_verifier를 보낸다 |
|
||||
| secret을 함께 낸다 | secret을 함께 보낸다 |
|
||||
| 401을 낸다 | 401을 돌려준다 |
|
||||
| 요청마다 DB를 친다 | 요청마다 DB를 조회한다 |
|
||||
| 같다고 가정하고 짜면 | 같다고 가정하고 구현하면 |
|
||||
| 교환이 끝난다 | 토큰 교환이 된다 |
|
||||
|
||||
`교환이 끝난다`는 무엇의 교환인지가 없다. 앞 문장에서 짐작할 수 있어도 그 자리에 다시 쓴다.
|
||||
|
||||
## 앞에서 말한 것을 다시 짚는다
|
||||
|
||||
`둘이`, `셋이`, `그 둘은`은 무엇을 가리키는지 독자가 되짚게 만든다. 대명사 대신 세어서 가리키거나
|
||||
이름을 다시 쓴다.
|
||||
|
||||
```text
|
||||
✗ 둘이 맞아야 교환이 끝난다.
|
||||
○ 이 두 값이 일치해야 토큰 교환이 된다.
|
||||
|
||||
✗ upstream이 받는 요청에서는 둘이 구분되지 않는다.
|
||||
○ upstream이 받는 요청에서는 이 두 헤더가 구분되지 않는다.
|
||||
```
|
||||
|
||||
가리키는 대상이 바로 앞 문장에 있어도 마찬가지다. 문장 하나만 떼어 읽어도 뜻이 서는 쪽을 고른다.
|
||||
|
||||
## 한 문장에 동작을 두 개 넣지 않는다
|
||||
|
||||
확인하는 동작과 그 결과는 서로 다른 일이다. 한 문장에 이어 붙이면 조건과 결론이 뭉개진다.
|
||||
|
||||
```text
|
||||
✗ 처음 요청에 code_challenge를 싣고, 교환할 때 원본인 code_verifier를 낸다. 둘이 맞아야
|
||||
교환이 끝난다.
|
||||
○ 처음 요청에 code_challenge를 담아서 보내고, 교환할 때 원본인 code_verifier를 보내서
|
||||
이 두 값이 일치하는지 확인한다.
|
||||
두 값이 일치해야 토큰 교환이 된다.
|
||||
```
|
||||
|
||||
## 못 하는 것이 아니라 할 것을 쓴다
|
||||
|
||||
「~라고 말할 수는 없다」, 「~를 확인한 것은 아니다」, 「~라고 단정하면 안 된다」로 문단을 열면
|
||||
독자가 할 일을 스스로 뽑아내야 한다. 무엇이 문제인지, 무엇을 하면 되는지, 어떻게 확인하는지
|
||||
순서로 쓴다.
|
||||
|
||||
```text
|
||||
✗ wildcard allowlist는 학습 환경에서 편하다. 다만 그것으로 exact callback만 허용하는
|
||||
가드레일을 확인했다고 말할 수는 없다.
|
||||
허용 범위가 넓으면 같은 호스트의 다른 경로로 code를 흘릴 여지가 생긴다. 잘못된 redirect를
|
||||
거부하는지 확인하는 검사도 따로 둔다.
|
||||
|
||||
○ wildcard allowlist는 학습 환경에서 편하다. 다만 허용 범위가 넓으면 같은 호스트의 다른
|
||||
경로로도 code가 갈 수 있다.
|
||||
|
||||
실제로 쓰는 callback 주소만 등록해 두면 code가 도착할 수 있는 곳이 그 하나로 줄어든다.
|
||||
|
||||
등록하지 않은 redirect_uri를 보냈을 때 거부하는지 확인하는 검사도 따로 둔다.
|
||||
```
|
||||
|
||||
고친 쪽은 문단이 셋으로 나뉜다. 첫 문단은 무엇이 문제인지, 둘째는 무엇을 하면 되는지, 셋째는
|
||||
어떻게 확인하는지다. 원래 글은 이 셋이 두 문장 안에 뭉쳐 있어서 가운데 「그래서 좁힌다」가
|
||||
빠져 있었다.
|
||||
|
||||
「여지가 생긴다」도 같이 지운다. 무엇이 어디로 가는지 그대로 쓰면 된다.
|
||||
|
||||
| 돌려 말한 것 | 그대로 말한 것 |
|
||||
|---|---|
|
||||
| code를 흘릴 여지가 생긴다 | 다른 경로로도 code가 갈 수 있다 |
|
||||
| 확인한 것은 아니다 | (무엇이 실제로 일어나는지) |
|
||||
| 단정하면 안 된다 | (그렇게 하려면 무엇이 필요한지) |
|
||||
| token이 없다고 말할 수 없게 된다 | 브라우저에 token이 남는다 |
|
||||
|
||||
부정형이 규칙의 논지 자체일 때는 그대로 둔다. 「network 격리와 헤더 검증을 서로 대신하지
|
||||
않는다」는 대체할 수 없다는 것이 규칙이고, 본문도 「두 가지를 다 둔다」로 끝난다.
|
||||
|
||||
## 문장 끝
|
||||
|
||||
`요` · `습니다`가 아니라 `한다` · `이다`로 끝낸다. 그 밖에는 이렇게 쓴다.
|
||||
|
||||
- **`~하면 된다`를 쓰지 않는다.** 조언하는 말투이지 기록하는 말투가 아니다.
|
||||
`정하면 된다` → `정한다`, `적으면 된다` → `적는다`, `두면 된다` → `둔다`
|
||||
- `A는 B다`보다 `A는 B라는 점이 문제가 된다` — 무엇이 걸리는지까지 말한다
|
||||
- 무엇을 하자고 이끌 때는 `~해 보자`를 쓴다. 절 제목과 여는 문장에만 쓰고 규칙에는 쓰지 않는다
|
||||
- 명령조를 줄인다. 검사 항목이 아니라 같이 읽는 사람의 말로 쓴다
|
||||
@@ -0,0 +1,92 @@
|
||||
# SSOT에서 글감을 뽑는 기준
|
||||
|
||||
`final/`의 긴 글 하나가 정본(SSOT)이다. 거기서 Studio에 올릴 기록을 뽑아낸다. 이 문서는 **무엇을
|
||||
몇 건으로 나눌지**를 정하는 기준이다. 나눈 결과는 `tech-log-tree.json`에 제목만 먼저 적고, 글은
|
||||
그다음에 쓴다.
|
||||
|
||||
## 왜 먼저 나누는가
|
||||
|
||||
긴 글을 앞에서부터 잘라 기록으로 만들면 절 하나가 기록 하나가 된다. 그러면 Case의 칸도
|
||||
Reference의 칸도 반쯤만 맞는 기록이 나온다. 종류를 먼저 정하고 그 종류가 요구하는 것이 SSOT에
|
||||
있는지 확인해야 한다.
|
||||
|
||||
## 한 건으로 자르는 단위
|
||||
|
||||
**절이 아니라 주장이다.** SSOT의 `##` 하나가 기록 하나가 아니다. 다음 넷 중 하나가 한 건이다.
|
||||
|
||||
| 단위 | 무엇 | 종류 |
|
||||
|---|---|---|
|
||||
| 재현한 관측 하나 | 조건을 갖추고 돌려서 얻은 수치·실행계획·로그 | Case |
|
||||
| 남의 것의 동작 하나 | 문서와 코드를 읽어 정리한 규칙·흐름 | Concept |
|
||||
| 반복 적용할 기준 하나 | 다음에도 같게 하기로 한 규칙 | Reference |
|
||||
| 프로젝트가 고른 방향 하나 | 대안을 두고 정한 것 | Decision |
|
||||
| 닫히지 않은 판단 하나 | 아직 모르는 것과 무엇을 하면 닫히는지 | Question |
|
||||
|
||||
## 종류를 정하는 물음
|
||||
|
||||
순서대로 묻는다. 먼저 걸리는 것이 그 글감의 종류다.
|
||||
|
||||
1. **내가 돌려서 얻은 결과인가** → Case. 수치·실행계획·로그가 SSOT에 있어야 한다. 없으면 Case가 아니다
|
||||
2. **남의 것이 어떻게 동작하는지인가** → Concept. 무엇을 보고 썼는지(버전)가 있어야 한다
|
||||
3. **다음에도 같게 하기로 한 규칙인가** → Reference
|
||||
4. **대안을 두고 고른 것인가** → Decision. 근거로 걸 기록이 최소 하나 필요하다
|
||||
5. **아직 모르는 것인가** → Question. 미지수가 최소 하나 필요하다
|
||||
|
||||
## 나눌 때 지키는 것
|
||||
|
||||
**한 건에 종류를 섞지 않는다.** N+1을 재현해 고쳤고 그 과정에서 조회 기준을 굳혔다면 Case 하나와
|
||||
Reference 하나로 나누고 `관계`로 잇는다.
|
||||
|
||||
**증거가 없는 Case는 만들지 않는다.** SSOT에 그 수치가 없으면 글감 목록에는 남기되 `file` 없이
|
||||
두고, 측정을 먼저 한다. 없는 수치를 쓰지 않는다.
|
||||
|
||||
**Decision은 근거 없이 만들지 않는다.** 게시가 거절된다. 근거로 걸 Case나 Concept이 먼저 있어야
|
||||
한다. 그래서 Decision은 대개 마지막에 뽑는다.
|
||||
|
||||
**같은 관측을 두 건으로 쪼개지 않는다.** 「N+1이 났다」와 「그래서 몇 개가 나갔다」는 한 건이다.
|
||||
쪼개면 둘 다 반쪽이 된다.
|
||||
|
||||
**주제를 먼저 정한다.** Studio는 주제 아래에 다섯 종류를 나눠 보여 준다. 주제가 다르면 같은
|
||||
프로젝트여도 폴더가 갈린다. 주제 slug는 Studio의 것을 그대로 쓴다.
|
||||
|
||||
## `tech-log-tree.json`
|
||||
|
||||
주제 → 종류 → 글감 순서로 담는다. 아직 쓰지 않은 글감은 `file` 없이 제목만 둔다.
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "n+1liner",
|
||||
"ssot": "final/document.md",
|
||||
"topics": {
|
||||
"jpa-feed-query-performance": {
|
||||
"topic": "jpa-feed-query-performance",
|
||||
"kinds": {
|
||||
"case": [
|
||||
{ "title": "Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1",
|
||||
"slug": "eager-toone-nplus1-without-access",
|
||||
"file": "jpa-feed-query-performance/case/case-eager-toone-nplus1-without-access.md",
|
||||
"status": "게시 전", "studioId": "32d0be7d-…", "assets": 1, "evidence": 3 },
|
||||
{ "title": "아직 쓰지 않은 글감" }
|
||||
],
|
||||
"concept": [], "reference": [], "question": [], "decision": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
기록을 쓰거나 지운 뒤에는 다시 만든다. 스크립트는 기록 파일에서 값을 읽어 채우고, `file`이 없는
|
||||
글감은 지우지 않는다.
|
||||
|
||||
```bash
|
||||
python3 scripts/build-tech-log-tree.py [프로젝트]
|
||||
```
|
||||
|
||||
## 순서
|
||||
|
||||
1. SSOT를 끝까지 읽는다. 절 제목만 훑지 않는다 — 수치와 근거가 어디 있는지 알아야 종류를 정한다
|
||||
2. 위 물음으로 글감을 나누고 주제를 정한다
|
||||
3. `tech-log-tree.json`에 제목만 적는다. 이때 글은 쓰지 않는다
|
||||
4. 글감 하나를 골라 `<주제>/<종류>/`에 기록을 쓴다. 형식은 `writing-each-kind.md`
|
||||
5. 트리를 다시 만든다
|
||||
6. Studio에 넣고 저장한다
|
||||
@@ -0,0 +1,205 @@
|
||||
# 다섯 종류의 칸과 게시 조건
|
||||
|
||||
칸 이름은 Studio 편집 화면 그대로, 상한과 필수 여부는 `studio-api.openapi.yaml` 그대로다.
|
||||
계약 필드명을 괄호에 적는다. 계약은 `tech-log-frontend/src/features/tech-log/contracts/studio/`
|
||||
에 있고, 이 파일과 계약이 어긋나면 계약이 맞다.
|
||||
|
||||
`RecordKind` 는 다섯이다 — `CASE` · `REFERENCE` · `QUESTION` · `CONCEPT` · `PROJECT_DECISION`.
|
||||
|
||||
## 종류별 칸 (`새 문서` 화면이 적어 주는 그대로)
|
||||
|
||||
| 화면 이름 | 계약 `kind` | 그 종류만의 칸 |
|
||||
|---|---|---|
|
||||
| Case | `CASE` | 문제 · 결론 · 환경 · 재현 · 본문 |
|
||||
| Reference | `REFERENCE` | 목적 · 규칙 · 적용 조건 · 예외 · 예시 |
|
||||
| **개념** | `CONCEPT` | 기준 버전 · 본문 |
|
||||
| Question | `QUESTION` | 상태 · 사실 · 가정 · 미지수 · 선택지 |
|
||||
| Decision | `PROJECT_DECISION` | 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거 |
|
||||
|
||||
여기에 아래 공통 칸이 더해진다.
|
||||
|
||||
## 공통 (다섯 종류 모두 — `WorkingCopyInputBase`)
|
||||
|
||||
| 칸 | 필드 | 상한 | 게시 조건 |
|
||||
|---|---|---|---|
|
||||
| 제목 | `title` | 120자 | **필수** — 없으면 게시 거절 |
|
||||
| slug | `slug` | 3~100자, `^[a-z0-9]+(?:-[a-z0-9]+)*$` | **필수** — 비우면 제목에서 만든다 |
|
||||
| 요약 | `summary` | **2,000자** | 경고. 목록 카드에는 약 90자까지 보인다 |
|
||||
| Topic | `topicId` | — | 경고 |
|
||||
| 축 | `variantIds` | 8개 | 선택. 고르지 않으면 그 주제의 **공통 기록**이 된다 |
|
||||
| Project | `projectId` | — | `PROJECT_DECISION`은 게시 시 필수 |
|
||||
| 관계 | `relations` | 20개 | `PROJECT_DECISION`은 **1개 이상 필수**. 그 종류에서는 절 이름이 「근거」다 |
|
||||
|
||||
편집 화면에서 축은 체크박스 묶음으로 나오고 묶음 이름이 「축 — 고르지 않으면 이 주제의 공통
|
||||
기록이 됩니다」다. **보이는 축은 고른 Topic 이 정한다** — OAuth/OIDC 인증 경계를 고르면
|
||||
SPA·Mediator·BFF·Forward-Auth 가 나온다.
|
||||
|
||||
**축(`variantIds`)은 주제 안의 접근·구조다.** 인증 경계 주제의 축은 SPA·Mediator·BFF·Forward-Auth,
|
||||
조회 성능 주제의 축은 조회 전략이다. 한 기록이 여러 축에 걸릴 수 있다 — PKCE 는 SPA 와 BFF 양쪽에
|
||||
관계된다. 아무 축도 고르지 않으면 「공통」 축이 따로 있는 것이 아니라 그 주제의 공통 기록으로 읽힌다.
|
||||
|
||||
slug를 비우면 제목에서 만든다. 한글 제목도 로마자로 옮겨 유효한 slug가 된다. 직접 쓸 때는
|
||||
영문 소문자·숫자·하이픈만 쓴다.
|
||||
|
||||
**Project에 slug가 없으면 공개 화면에 프로젝트가 표시되지 않는다.** 공개 계약의
|
||||
`ProjectSummary`가 `slug`와 `path`를 요구하기 때문이다. `주제·프로젝트` 화면에서 확인한다.
|
||||
|
||||
## 기록이 가리키는 로컬 파일
|
||||
|
||||
기록은 `docs/<프로젝트>/tech-log-studio/<주제>/<종류>/` 에 있고, 그림과 증거는 같은 프로젝트의
|
||||
`final/` 에 있다. 같은 파일을 양쪽에 두지 않고 frontmatter 로 잇는다.
|
||||
|
||||
```yaml
|
||||
assets:
|
||||
- key: eager-lazy-query-sequence # 본문의 :::evidence key 와 같은 값
|
||||
file: ../../../final/assets/tech-log-studio/eager-lazy-query-sequence.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/explain/highlights-child-plan-A.txt
|
||||
```
|
||||
|
||||
`assets` 는 Studio 에 올릴 파일이다. 아직 안 올렸으면 key 가 파일 이름과 같고, 올린 뒤에는
|
||||
서버가 준 `<이름>-<해시8>` 로 바뀐다. **Studio 에 넣을 때 이 목록을 보고 Asset 을 올리고,
|
||||
본문의 `:::evidence key` 를 서버가 준 키로 바꾼다.**
|
||||
|
||||
`evidence` 는 그 기록이 인용한 측정 자료다. 실행계획·csv·터미널 기록·스크린샷이 여기 온다.
|
||||
본문에 값을 옮겨 적었으면 그 값이 어느 파일에서 나왔는지 이 줄이 말해 준다.
|
||||
|
||||
## 평문 칸 쓰는 법
|
||||
|
||||
본문(`bodyMarkdown`)을 뺀 모든 칸은 평문이다. 백틱·파이프·`#`는 글자 그대로 보이고 줄바꿈만
|
||||
살아난다. 그 줄바꿈이 유일한 서식이므로 아끼지 않는다.
|
||||
|
||||
**나열은 `이름 : 값`으로 끊는다.** 쉼표로 이으면 읽는 사람이 항목을 세어야 한다.
|
||||
|
||||
```text
|
||||
쓰지 않는다
|
||||
access token 300초, refresh token rotation과 재사용 허용 0회, 그리고 issuer·audience 검증이다
|
||||
|
||||
쓴다
|
||||
access token : 300초
|
||||
refresh token rotation, 재사용 허용 : x
|
||||
issuer · audience : 검증
|
||||
```
|
||||
|
||||
있음·없음은 `o`·`x`로 적는다. `확인함`·`확인 안 함`보다 훑을 때 빨리 잡힌다.
|
||||
|
||||
**한 문장이 화면에서 두 줄을 넘으면 끊는다.** 편집 화면의 칸은 좁고 공개 화면은 넓다. 여기서
|
||||
한 줄로 보이는 문장이 저기서는 덩어리가 된다. 절차·조건을 한 문단에 이어 쓰지 않는다.
|
||||
|
||||
## Case — 문제를 재현하고 검증한 결론
|
||||
|
||||
| 칸 | 필드 | 비고 |
|
||||
|---|---|---|
|
||||
| 문제 | `problem` | 무엇이 왜 문제였나 |
|
||||
| 결론 | `conclusion` | 검증으로 확정한 것 |
|
||||
| 검증 환경 | `environment` | 런타임·버전·DB·도구 |
|
||||
| 재현 조건 | `reproduction` | 다른 사람이 같은 결과를 얻는 방법 |
|
||||
| 마지막 검증일 | `lastVerifiedOn` | 실제로 확인한 날. 30일 지나면 경고 |
|
||||
| 본문 Markdown | `bodyMarkdown` | 10만 자. 본문을 갖는 두 종류 중 하나 |
|
||||
|
||||
공개 화면에서 `검증 환경`과 `재현 조건`은 `environmentSummary` 배열에 그 순서로 실린다.
|
||||
|
||||
## Concept — 남의 것이 어떻게 동작하는지
|
||||
|
||||
`새 문서` 화면에서 이 종류만 이름이 한글이다. **「개념」을 고른다.** 나머지 넷은 Case·Reference·
|
||||
Question·Decision 으로 적혀 있다. 화면 설명은 「개념의 내부 동작과 아키텍처를 처음부터 풀어 씁니다」다.
|
||||
|
||||
| 칸 | 필드 | 비고 |
|
||||
|---|---|---|
|
||||
| 본문 Markdown | `bodyMarkdown` | 10만 자. **Case 와 함께 본문을 갖는 두 종류 중 하나** |
|
||||
| 기준 버전 | `basisVersion` | 120자. 무엇을 보고 쓴 글인지 한 줄 |
|
||||
|
||||
칸이 둘뿐이다. 문제도 결론도 재현 조건도 없다. 내가 재현한 결과가 아니라 **이미 그렇게 동작하는
|
||||
것**을 적기 때문이다. `Authorization Code Flow 가 code 를 한 번 더 교환하는 이유`, `Forward-Auth
|
||||
subrequest 가 실어 보내는 것` 같은 것이 여기 온다.
|
||||
|
||||
**`lastVerifiedOn` 이 없고 `basisVersion` 이 그 자리를 대신한다.** 개념은 날짜로 낡지 않고 버전으로
|
||||
낡기 때문이다. `Keycloak 26.7.0 identity brokering`, `Kubernetes 1.31` 처럼 적는다. 비워도 게시된다.
|
||||
|
||||
공개 주소는 `/concepts/{slug}` 다.
|
||||
|
||||
**`기준 버전`을 비워도 게시된다.** 계약의 `required` 에 들어 있지만 빈 문자열을 허용하고, 게시
|
||||
검증도 막지 않는다(2026-09-04 에 임시 개념 기록을 만들어 비운 채로 게시해 확인했다). 그래도
|
||||
채우는 편이 낫다 — 개념이 언제 낡았는지 읽는 사람이 알 방법이 이 칸뿐이다.
|
||||
|
||||
편집 화면 오른쪽 `작업 상태` 는 이 종류를 「동작 원리」라고 부른다. `새 문서` 의 「개념」과 같은
|
||||
것이다.
|
||||
|
||||
Case 와 헷갈리면 **내가 무엇을 했는지**를 묻는다. 내가 돌려 보고 수치를 얻었으면 Case, 남의 문서와
|
||||
코드를 읽고 동작을 정리했으면 Concept 이다.
|
||||
|
||||
## Reference — 반복 적용할 기준
|
||||
|
||||
| 칸 | 필드 | 비고 |
|
||||
|---|---|---|
|
||||
| 목적 | `purpose` | 이 기준이 무엇을 막는가 |
|
||||
| 규칙 | `rules[]` | 제목(120자) + 본문. **평문** |
|
||||
| 적용 조건 | `applyWhen[]` | 언제 적용되는가 |
|
||||
| 예외 | `exceptions[]` | 적용되지 않는 경우 |
|
||||
| 예시 | `examples[]` | 짧은 문장. 코드가 아니다 |
|
||||
| 마지막 검증일 | `verifiedOn` | |
|
||||
|
||||
규칙 본문에 코드를 쓰고 싶으면 그 코드가 있는 Case를 만들고 `관계`로 가리킨다.
|
||||
|
||||
## Question — 아직 닫히지 않은 판단
|
||||
|
||||
| 칸 | 필드 | 비고 |
|
||||
|---|---|---|
|
||||
| 질문 상태 | `questionStatus` | `OPEN` / `RESOLVED` / 미정 |
|
||||
| 사실 | `facts[]` | 확인된 것 |
|
||||
| 가정 | `assumptions[]` | 확인하지 않고 전제한 것 |
|
||||
| 미지수 | `unknowns[]` | `OPEN`이면 **1개 이상 필수** |
|
||||
| 제약 | `constraints[]` | 선택을 좁히는 조건 |
|
||||
| 선택지 | `options[]` | 제목(120자) + 설명. 50개까지 |
|
||||
| 다음 검증 | `nextValidation` | 무엇을 하면 판단이 끝나는가 |
|
||||
|
||||
`OPEN`인데 해결 내용을 채우면 게시가 거절된다. 상태와 내용이 어긋나기 때문이다.
|
||||
|
||||
사실과 가정을 섞지 않는다. 확인했으면 사실, 아니면 가정이다. 그 구분이 이 종류의 존재 이유다.
|
||||
|
||||
## Decision — 프로젝트가 정한 방향 (`PROJECT_DECISION`)
|
||||
|
||||
| 칸 | 필드 | 비고 |
|
||||
|---|---|---|
|
||||
| 결정 상태 | `decisionStatus` | `PROPOSED` / `ADOPTED` / 미정 |
|
||||
| 결정일 | `decidedOn` | |
|
||||
| 결정문 | `statement` | 무엇을 정했는가. 한 문장 |
|
||||
| 판단 이유 | `rationale` | 왜 그렇게 정했는가 |
|
||||
| 영향 | `consequences[]` | 이 결정으로 감수하는 것 |
|
||||
| 근거 기록 | `relations` | **1개 이상 필수** |
|
||||
|
||||
근거가 없는 Decision은 게시되지 않는다(`DECISION_EVIDENCE_REQUIRED`). 무엇을 보고 정했는지
|
||||
가리키지 못하면 그것은 결정이 아니라 선언이다.
|
||||
|
||||
`영향`에는 좋은 것만 적지 않는다. 감수한 비용이 빠지면 다음 사람이 같은 판단을 다시 못 한다.
|
||||
|
||||
## 종류 고르기
|
||||
|
||||
```text
|
||||
내가 직접 돌려 보고 결과를 얻었나 ── 예 ──→ Case
|
||||
│
|
||||
아니오
|
||||
│
|
||||
남의 것이 어떻게 동작하는지 적나 ── 예 ──→ Concept
|
||||
│
|
||||
아니오
|
||||
│
|
||||
판단이 끝났나 ──── 아니오 ──→ Question
|
||||
│
|
||||
예
|
||||
│
|
||||
프로젝트의 방향인가 ── 예 ──→ Decision
|
||||
│
|
||||
아니오
|
||||
│
|
||||
└──→ Reference
|
||||
```
|
||||
|
||||
Case 와 Concept 이 가장 자주 헷갈린다. **수치와 재현 조건이 있으면 Case**, 읽고 정리한 동작이면
|
||||
Concept 이다. Concept 과 Reference 는 「무엇이 그런가」와 「우리가 어떻게 할 것인가」로 갈린다 —
|
||||
`PKCE 가 code 가로채기를 막는 방식`은 Concept, `우리는 public client 에 PKCE 를 필수로 쓴다`는
|
||||
Reference 다.
|
||||
|
||||
한 자료가 여러 종류에 걸치면 나눈다. 예를 들어 N+1을 재현해 고쳤고 그 과정에서 조회 기준을
|
||||
굳혔다면, Case 하나와 Reference 하나를 만들고 서로 관계로 잇는다. 한 기록에 몰아넣으면 Case의
|
||||
칸도 Reference의 칸도 반쯤만 맞는다.
|
||||
@@ -0,0 +1,110 @@
|
||||
# 게시 전 대조
|
||||
|
||||
위에서 아래로 훑는다. 하나라도 걸리면 게시하지 않는다.
|
||||
|
||||
## 사실
|
||||
|
||||
- [ ] 수치·날짜·버전·단위·명령어·URL·인용이 원자료와 한 글자도 다르지 않다
|
||||
- [ ] 측정하지 않은 값이 없다. 검증일은 실제로 확인한 날이다
|
||||
- [ ] 측정값과 파생값이 구분돼 있다. 역산한 값을 잰 값처럼 적지 않았다
|
||||
- [ ] 자료에 없는 선택 이유를 만들지 않았다
|
||||
- [ ] 지어낸 경험·실패·감정이 없다
|
||||
- [ ] 가능성을 확정으로, 상관을 인과로 넓히지 않았다
|
||||
|
||||
## 범위와 한계
|
||||
|
||||
- [ ] 이 측정으로 **말할 수 없는 것**을 적었다
|
||||
- [ ] 로컬에서 본 것을 운영에서 본 것으로 올리지 않았다
|
||||
- [ ] 감수한 비용·위험이 빠지지 않았다
|
||||
- [ ] 적용되지 않는 조건을 적었다
|
||||
|
||||
## 종류
|
||||
|
||||
- [ ] 종류가 내용과 맞는다 (`record-kinds.md`의 판단 흐름)
|
||||
- [ ] Question의 사실과 가정이 섞이지 않았다
|
||||
- [ ] Question이 `OPEN`이면 미지수가 있다
|
||||
- [ ] Decision에 근거 기록이 1개 이상 연결됐다
|
||||
- [ ] Decision의 영향에 감수한 비용이 있다
|
||||
- [ ] Reference의 예시가 문장이다. 코드를 밀어 넣지 않았다
|
||||
|
||||
## 설명
|
||||
|
||||
- [ ] 처음 나오는 클래스·기법 이름에 왜 있는지가 붙었다
|
||||
- [ ] 깊게 다루지 않는 주제는 다루지 않는다고 밝히고 한 문장 요약을 줬다
|
||||
- [ ] 「역할이 다르다」로 끝난 자리에 메커니즘을 적었다
|
||||
- [ ] 두 값이 합쳐지는 흐름에 조립된 실물을 보여 줬다
|
||||
- [ ] 직접 열어 본 것과 계약 인용을 나눠 적었다
|
||||
- [ ] 산문에 내부 코드명이 없다. slug에도 없다
|
||||
- [ ] 한 구조에서 본 것을 protocol 전체의 성질로 넓히지 않았다
|
||||
- [ ] `**굵게**`가 한 절에 하나를 넘지 않는다
|
||||
- [ ] 한 흐름이 한 문단으로 이어진다. 단계마다 끊어 나열하지 않았다
|
||||
- [ ] 앞 문장을 다시 말해 강조한 자리가 없다
|
||||
- [ ] 흐름을 말하는 자리에만 `~하게 된다`를 썼다. 정의·분류·사실·지시에는 붙이지 않았다
|
||||
- [ ] `~하면 된다`가 없다
|
||||
- [ ] `~하는 것은 ~이다` 구문이 없다. 동사로 바로 간다
|
||||
- [ ] `그것이`·`이것이`로 시작하는 문장이 없다
|
||||
- [ ] `~라는 뜻은 아니다`·`~는 것은 아니다` 이중부정이 없다
|
||||
- [ ] `그 자리에` 같은 추상 공간 은유가 없다
|
||||
- [ ] `자리`·`옆`·`칸`·`축` 같은 공간 말로 설명하지 않았다
|
||||
- [ ] `떠안다`·`죽다`·`흡수하다`·`홍보문` 같은 비유가 없다
|
||||
- [ ] `무의미해진다`·`되어 버린다` 같은 과장이 없다
|
||||
- [ ] 「~라고 말할 수는 없다」·「~한 것은 아니다」로 문단을 열지 않았다
|
||||
- [ ] 문제 → 할 일 → 확인 순서로 이어진다. 가운데 「그래서 무엇을 한다」가 빠지지 않았다
|
||||
- [ ] `여지가 생긴다`·`소지가 있다` 대신 무엇이 어디로 가는지 썼다
|
||||
- [ ] 읽는 법을 지시하는 문장이 없다. `봐야 한다`·`여기까지다`·`먼저 본다`·`읽으면 안 된다`
|
||||
- [ ] 빼도 남은 뜻이 그대로인 문장이 없다
|
||||
- [ ] 규칙 제목이 말한 것을 본문 끝에서 다시 지시하지 않았다
|
||||
- [ ] `싣는다`·`낸다`·`친다`·`짠다`를 실제 동작으로 풀어 썼다
|
||||
- [ ] 동사마다 목적어가 있다. `교환이 끝난다`처럼 무엇인지 빠지지 않았다
|
||||
- [ ] `둘이`·`셋이`·`그 둘은` 대신 무엇인지 다시 짚었다
|
||||
- [ ] 확인하는 동작과 그 결과를 한 문장에 이어 붙이지 않았다
|
||||
- [ ] 절을 열 때 앞 구조와 무엇이 달라졌는지 먼저 말했다
|
||||
|
||||
## AI가 쓴 티 (`references/ai-tells.md`)
|
||||
|
||||
- [ ] `..?`·`~하지?`·`~해보자`·`확인하자` 같은 억지 구어체가 없다
|
||||
- [ ] 결론을 먼저 정리한 대구 문장이 문서에 한 번을 넘지 않는다
|
||||
- [ ] `대신`·`그래서`·`함께`·`따로`·`그대로`를 세어 봤다. 같은 문형이 문서마다 되풀이되지 않는다
|
||||
- [ ] 규칙·선택지 길이가 고르게 맞춰져 있지 않다. 축이 겹치는 것은 표로 옮겼다
|
||||
- [ ] Question이 재현하지 않은 결과를 단정하지 않았다
|
||||
- [ ] 제약에서 바꾸지 않겠다고 한 전제를 선택지에서 바꾸지 않았다
|
||||
- [ ] 측정할 수 없는 것을 숫자처럼 쓰지 않았다
|
||||
- [ ] `유일한`·`사실상`·`전원`·`대부분 이미` 같은 범위 확장이 없다
|
||||
- [ ] 자료에 없는 과거(`~하던 관행`)를 만들지 않았다
|
||||
- [ ] Reference가 Case를 문장만 바꿔 옮기지 않았다
|
||||
- [ ] 현재 확인한 것과 운영에서 추가로 필요한 것을 나눴다
|
||||
- [ ] 테스트를 말할 때 무엇을 단정하는지 적었다
|
||||
|
||||
## 본문 (Case)
|
||||
|
||||
- [ ] raw HTML·각주·체크박스·중첩 목록·취소선이 없다
|
||||
- [ ] 표를 파이프로 썼다. 감쌌다면 세 속성이 다 있다
|
||||
- [ ] 표 머리글이 무엇을 묻는지 말한다. 비교 표의 축이 시점·상태로 적혔다
|
||||
- [ ] 코드블록 언어가 허용 문자만 쓴다
|
||||
- [ ] 이미지 주소가 `/api/v1/public/media/…`다. object storage 주소가 아니다
|
||||
- [ ] 그림이 있는 문단에 글을 섞지 않았다
|
||||
- [ ] `alt`가 무엇이 보이는지 말한다
|
||||
- [ ] 그림 안 `<text>`가 전부 이름이다. 문장도 숫자도 없다 (`<title>`·`<desc>`는 예외)
|
||||
- [ ] 코드에 자격증명·토큰·내부 호스트가 없다. 지운 자리가 보인다
|
||||
- [ ] 제목 id와 표 id가 겹치지 않는다
|
||||
|
||||
## 평문 칸
|
||||
|
||||
- [ ] 본문 밖 칸에 백틱·파이프가 없다
|
||||
- [ ] 나열을 쉼표로 잇지 않고 `이름 : 값`으로 끊었다
|
||||
- [ ] 있음·없음을 `o`·`x`로 적었다
|
||||
- [ ] 한 문장이 화면에서 두 줄을 넘지 않는다
|
||||
|
||||
## 연결
|
||||
|
||||
- [ ] Topic이 지정됐다
|
||||
- [ ] Project가 필요하면 지정됐고, 그 Project에 slug가 있다
|
||||
- [ ] 관계의 대상이 실제로 있는 공개 기록이다
|
||||
- [ ] 코드·표·그림이 필요한 내용을 Reference나 Question에 밀어 넣지 않았다
|
||||
|
||||
## 마지막
|
||||
|
||||
- [ ] `저장`을 누른 뒤 `게시`를 눌렀다
|
||||
- [ ] 게시 후 공개 페이지를 열어 표·코드·그림이 의도대로 나오는지 봤다
|
||||
|
||||
마지막 항목을 건너뛰지 않는다. 저장은 통과해도 공개 화면에서 다르게 보이는 경우가 있다.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Studio 초안 검토
|
||||
|
||||
초안을 Studio에 넣고 **저장까지만** 한 뒤 미리보기로 읽는다. 게시하지 않는다.
|
||||
|
||||
파서를 통과한 본문도 화면에서는 다르게 보인다. 표가 가로로 넘치거나, 설명 없이 코드만 있거나,
|
||||
callout이 연달아 나와 읽는 흐름이 끊기는 것은 파서가 잡지 못한다.
|
||||
|
||||
## 왜 저장까지만인가
|
||||
|
||||
게시는 공개 사이트에 올린다. 되돌리려면 `unpublish`를 해야 하고 그 사이에 누구나 볼 수 있다.
|
||||
저장은 Studio 안에만 남는다 — 불완전한 초안도 저장할 수 있게 만들어진 이유가 이것이다.
|
||||
|
||||
`즉시 미리보기` 탭은 저장한 값이 아니라 **화면에 입력한 값**을 렌더링한다. 그래서 게시 없이도
|
||||
공개 화면과 같은 블록 렌더러로 본문을 볼 수 있다.
|
||||
|
||||
## 절차
|
||||
|
||||
1. `새 문서`에서 종류를 고르고 `작업본 만들기`
|
||||
2. 칸을 채운다. 본문은 미리 `check_body.mjs`를 통과시킨 것을 넣는다
|
||||
3. **`저장`** — `게시`가 아니다
|
||||
4. `즉시 미리보기` 탭으로 옮겨 아래 항목을 읽는다
|
||||
5. 고칠 것이 있으면 `편집` 탭으로 돌아가 고치고 다시 저장
|
||||
|
||||
Playwright로 할 때는 편집 화면의 `aside`에 버튼이 `저장`·`게시` 둘뿐이라는 점에 주의한다.
|
||||
**`게시`를 누르면 저장·검증·미리보기·게시가 한 번에 돈다.** 검토 단계에서는 `저장`만 누른다.
|
||||
|
||||
```js
|
||||
// 저장만 — aside 의 첫 버튼
|
||||
await page.locator('aside button').first().click();
|
||||
// 미리보기 탭
|
||||
await page.getByRole('tab', { name: '즉시 미리보기' }).click();
|
||||
```
|
||||
|
||||
## evidence를 넣었다면 화면을 새로 고친다
|
||||
|
||||
`즉시 미리보기`는 편집 화면이 들고 있는 asset 목록에서만 evidence 키를 찾는다. Asset을 방금
|
||||
올렸다면 그 목록에 아직 없어서 본문 전체가 이렇게 막힌다.
|
||||
|
||||
```text
|
||||
초안을 미리 볼 수 없습니다
|
||||
1:1 supported local evidence key not found: <asset-key>
|
||||
```
|
||||
|
||||
Asset 업로드 패널로 올렸으면 화면이 바로 알지만, 다른 경로로 올렸다면 편집 화면을 한 번 새로
|
||||
고쳐야 목록을 다시 받는다. 본문 문법 문제로 오해하기 쉽다.
|
||||
|
||||
## 미리보기에서 읽을 것
|
||||
|
||||
### 읽는 흐름
|
||||
|
||||
- [ ] 제목만 훑어도 무슨 이야기인지 따라가는가
|
||||
- [ ] 첫 문단이 무엇을 다루는지 말하는가. 배경부터 길게 시작하지 않는가
|
||||
- [ ] 문단이 너무 길어 화면에서 덩어리로 보이지 않는가
|
||||
- [ ] 같은 말을 다른 자리에서 반복하지 않는가
|
||||
|
||||
### 본문 밖 칸
|
||||
|
||||
Case의 `문제`·`결론`·`검증 환경`·`재현 조건`도 본문이 아니라 **평문**이다. 백틱과 파이프가
|
||||
글자 그대로 보인다.
|
||||
|
||||
- [ ] 백틱이 화면에 그대로 나오지 않는가
|
||||
- [ ] 절차를 한 문단에 이어 쓰지 않았는가. 줄바꿈은 `<br>` 로 살아난다
|
||||
- [ ] 한 칸이 화면에서 덩어리로 보이지 않는가
|
||||
- [ ] 나열을 쉼표로 잇지 않았는가. `이름 : 값`으로 줄을 나눴는가
|
||||
- [ ] 한 문장이 화면에서 두 줄을 넘지 않는가
|
||||
|
||||
### 설명이 빠진 곳
|
||||
|
||||
- [ ] 표 바로 앞이나 뒤에 그 표를 어떻게 읽는지 적었는가
|
||||
- [ ] 코드블록에 무엇을 보라는 설명이 있는가. 붙여 놓기만 하지 않았는가
|
||||
- [ ] 처음 나오는 약어와 고유명사를 풀었는가. 왜 있는지까지 말했는가
|
||||
- [ ] 이름만 대고 다음 문단으로 넘어간 자리가 없는가
|
||||
- [ ] 두 값이 합쳐지는 곳에 합쳐진 결과가 있는가
|
||||
- [ ] 직접 본 것과 테스트 계약이 구분돼 있는가
|
||||
- [ ] 수치에 단위와 측정 조건이 붙었는가
|
||||
- [ ] 그림의 `alt`와 `caption`이 무엇이 보이는지 말하는가
|
||||
|
||||
### 화면에서만 드러나는 것
|
||||
|
||||
- [ ] 표 머리글만 읽어도 그 표가 무엇을 묻는지 아는가
|
||||
- [ ] 열을 더 줄일 수 있는가. 3열이 2열로 되는가
|
||||
- [ ] 비교 표의 열 이름이 어느 시점·상태의 값인지 말하는가
|
||||
- [ ] 표가 가로로 넘치지 않는가. 열이 너무 많지 않은가
|
||||
- [ ] 코드블록이 가로 스크롤을 만들지 않는가. 긴 줄을 줄일 수 있는가
|
||||
- [ ] callout이 연달아 나와 본문 흐름을 끊지 않는가
|
||||
- [ ] 제목 단계가 건너뛰지 않는가 (`##` 다음에 바로 `####`)
|
||||
- [ ] 그림이 의도한 자리에 있는가. 글과 섞여 사라지지 않았는가
|
||||
- [ ] 그림 안에 문장이 없는가. `<text>`가 전부 이름인가
|
||||
|
||||
### 종류별
|
||||
|
||||
- [ ] **Case** — 문제·결론·검증 환경·재현 조건 네 칸이 본문 없이도 이해되는가
|
||||
- [ ] **Reference** — 규칙 제목만 읽어도 무엇을 금지하는지 아는가
|
||||
- [ ] **Question** — 사실과 가정이 화면에서 구분돼 보이는가
|
||||
- [ ] **Decision** — 결정문이 한 문장인가. 영향에 감수한 비용이 있는가
|
||||
|
||||
## 고칠 것이 없을 때
|
||||
|
||||
`references/review-checklist.md`를 마지막으로 훑고 게시한다. 게시 뒤에는 공개 페이지를 열어
|
||||
미리보기와 같게 보이는지 다시 확인한다 — 공개 경로는 미리보기와 다른 데이터를 쓴다.
|
||||
|
||||
## 초안을 남기지 않는다
|
||||
|
||||
검토용으로 만든 작업본은 지운다. Decision은 계약에 삭제 경로가 없으므로 확인용으로 만들지
|
||||
않는 편이 낫다.
|
||||
|
||||
**한 번이라도 게시한 문서는 게시를 취소해도 지워지지 않는다.** 게시를 취소해
|
||||
`publicationStatus` 가 `UNPUBLISHED` 가 된 뒤에도 삭제는 409 로 거절되고, 화면에는
|
||||
「공개된 기록은 삭제할 수 없습니다. 먼저 공개를 취소해 주세요」가 뜬다. 이미 취소했는데도 그렇다.
|
||||
그러므로 **시험 삼아 게시하지 않는다.** 게시 동작을 확인해야 하면 지워도 되는 문서를 따로
|
||||
만들고, 그것이 목록에 영구히 남는다는 것을 감수한다.
|
||||
|
||||
관계로 참조된 문서도 지워지지 않는다(`DOCUMENT_IN_USE`). 참조하는 쪽의 관계를 먼저 끊는다.
|
||||
끊어도 막히면 그 문서들의 지난 Public Preview 스냅샷에 옛 관계가 남아 있는 경우다.
|
||||
@@ -0,0 +1,133 @@
|
||||
# 종류마다 무엇을 어떤 순서로 쓰나
|
||||
|
||||
칸 목록과 상한은 `record-kinds.md`, 문장 규칙은 `explaining.md`, 문서군의 리듬은 `ai-tells.md`에
|
||||
있다. 이 문서는 **그 칸을 무엇으로 채우는가**다.
|
||||
|
||||
**이미 쓴 47건에서 뽑았다.** keycloak 23건(Case 4·Concept 6·Reference 7·Question 4·Decision 2),
|
||||
n+1liner 24건(Case 5·Reference 7·Question 5·Decision 7). 「대개 이렇게 쓴다」는 말은 그 47건이
|
||||
그렇게 돼 있다는 뜻이다.
|
||||
|
||||
## 파일 뼈대 — 다섯 종류가 같다
|
||||
|
||||
```markdown
|
||||
---
|
||||
id · kind · slug · title · topic · project · status · studio
|
||||
(종류별) basisVersion · decisionStatus · questionStatus · lastVerifiedOn
|
||||
(있으면) assets · evidence
|
||||
---
|
||||
|
||||
# 제목
|
||||
|
||||
리드 문단. 이것이 Studio 의 `요약` 칸이다. ← 47건 모두 있다
|
||||
|
||||
## 관계 ← Decision 만 「근거」다
|
||||
## <칸 이름> ← 종류마다 다르다
|
||||
## 본문 ← Case · Concept 만
|
||||
<!-- body:start -->
|
||||
...
|
||||
<!-- body:end -->
|
||||
```
|
||||
|
||||
frontmatter 는 메타데이터, `##` 는 Studio 의 칸, 제목 아래 첫 문단은 요약이다. 47건 모두 이 모양이다.
|
||||
|
||||
**관계 항목은 굵은 제목 한 줄 + 이유 한 줄**이다.
|
||||
|
||||
```markdown
|
||||
- **Keyset Pagination 설계 기준**
|
||||
이 결정을 규칙으로 편 기준이다.
|
||||
```
|
||||
|
||||
## Case — 9건
|
||||
|
||||
칸은 `관계` · `문제` · `결론` · `검증 환경` · `재현 조건` · `본문`. 9건 모두 여섯 칸을 채웠다.
|
||||
|
||||
| 칸 | 무엇을 |
|
||||
|---|---|
|
||||
| 문제 | 무엇이 어떠해야 했는데 어떻게 됐나. 요구를 먼저, 실제를 다음에 |
|
||||
| 결론 | 재현해서 확정한 것. 수치를 그대로. 「~일 것이다」가 아니라 「~였다」 |
|
||||
| 검증 환경 | 런타임·버전·DB·측정 도구. `이름 : 값`으로 줄을 나눈다 |
|
||||
| 재현 조건 | 다른 사람이 같은 값을 얻는 순서. 번호를 매긴다 |
|
||||
|
||||
**본문은 5~12절, 대개 6절이다.**
|
||||
|
||||
- **첫 절은 무대를 세운다.** 잰 코드나 구조를 먼저 보여 준다 — 「측정한 loadFeed 구현」,
|
||||
「무대 — 페이징 한 줄만 추가」, 「가시성을 얹기 전 — 인덱스로 커서 이후만」,
|
||||
「Mediator에서 Access Token과 Refresh Token을 관리하는 위치」
|
||||
- 가운데는 측정값(표) → 그 값을 어떻게 읽나 → 실행계획이나 로그 순서다
|
||||
- **마지막 절은 범위나 다음이다.** 9건 중 4건이 확인 범위(「증명하지 않는 것」, 「현재 자동
|
||||
테스트로 확인한 범위」, 「Redirect URI와 CORS에서 아직 확인하지 않은 부분」), 2건이 「다음 선택」,
|
||||
나머지가 지표 읽는 법이나 남긴 이유다. **재지 않은 것을 적지 않고 닫는 Case 는 없다**
|
||||
|
||||
코드블록에는 무엇을 보라는 한 줄을 붙인다. 표 앞이나 뒤에 그 표를 어떻게 읽는지 적는다. 예시는
|
||||
한 규모로 고정한다 — 표가 전체 계열을 이미 보여 준다.
|
||||
|
||||
## Concept — 6건
|
||||
|
||||
칸은 `관계` · `본문` 둘뿐이다. 문제도 결론도 재현 조건도 없다. 내가 잰 결과가 아니라 이미 그렇게
|
||||
동작하는 것을 적기 때문이다.
|
||||
|
||||
**`basisVersion` 은 frontmatter 에 있고 본 것을 `·` 로 잇는다.**
|
||||
|
||||
```yaml
|
||||
basisVersion: Keycloak 26.7.0 · oidc-client-ts 3.3.0
|
||||
basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request
|
||||
```
|
||||
|
||||
**본문은 5~8절, 대개 6절이다.**
|
||||
|
||||
- 첫 절은 무엇이 무엇을 주고받는지다 — 「두 개의 OAuth 왕복이 이어진다」,
|
||||
「Resource Server가 받는 입력」, 「요청 하나가 두 번 평가된다」
|
||||
- 가운데는 단계마다 실제로 일어나는 일이다
|
||||
- **마지막 절은 막지 않는 것이나 확인한 범위다** — 「PKCE가 막지 않는 것」,
|
||||
「CSRF가 XSS를 대신하지 않는다」, 「현재 검증한 범위」, 「지금 구성이 보여 주지 않는 것」
|
||||
|
||||
「확인했다」는 Case 의 말이다. Concept 은 「~한다」로 적는다. 규격이 정한 것과 그 구현이 그렇게 한
|
||||
것을 구분한다.
|
||||
|
||||
## Reference — 14건
|
||||
|
||||
칸은 `관계` · `목적` · `규칙` · `적용 조건` · `예외` · `예시`. 14건 모두 여섯 칸을 채웠다. 본문이 없다.
|
||||
|
||||
| 칸 | 무엇을 |
|
||||
|---|---|
|
||||
| 목적 | 이 기준이 무엇을 막는가. 막으려는 실패를 먼저 |
|
||||
| 규칙 | 제목은 무엇을 하는지/하지 않는지로. 본문에 왜인지 |
|
||||
| 적용 조건 | 언제 이 기준이 걸리는가 |
|
||||
| 예외 | 걸리지 않는 경우. 이 칸이 비면 규칙이 과잉 적용된다 |
|
||||
| 예시 | 짧은 문장. 코드가 아니다 |
|
||||
|
||||
규칙 제목만 읽어도 무엇을 금지하는지 알아야 한다. 「지표를 정확히 읽는다」보다
|
||||
「지표 이름이 뜻하는 것을 그대로 읽는다」가 낫다. 코드가 필요하면 그 코드가 있는 Case 를 만들고
|
||||
`관계`로 가리킨다 — Reference 칸은 평문이라 백틱이 글자로 보인다.
|
||||
|
||||
## Question — 9건
|
||||
|
||||
칸은 `관계` · `사실` · `가정` · `미지수` · `제약` · `선택지` · `다음 검증`. 9건 모두 일곱 칸을 채웠다.
|
||||
`questionStatus` 는 frontmatter 에 있다.
|
||||
|
||||
**가정을 사실 칸에 넣지 않는 것이 이 종류의 전부다.** 사실은 확인한 것, 가정은 확인하지 않고
|
||||
전제한 것이다.
|
||||
|
||||
`다음 검증`은 실행할 수 있는 문장으로 적는다. 「더 알아본다」로는 닫히지 않는다 —
|
||||
「seed(1000) 뒤 `ANALYZE highlights` 를 돌리고 Plan B 를 다시 잰다」처럼 적는다.
|
||||
|
||||
## Decision — 9건
|
||||
|
||||
칸은 **`근거`** · `결정문` · `판단 이유` · `영향`. **다른 넷과 달리 관계 절 이름이 「근거」다.**
|
||||
`decisionStatus` 는 frontmatter 에 있다. 근거가 1개 이상 없으면 게시가 거절된다.
|
||||
|
||||
| 칸 | 무엇을 |
|
||||
|---|---|
|
||||
| 결정문 | 「~한다」로 끝나는 문장. 조건이 있으면 한 문단 더 |
|
||||
| 판단 이유 | 무엇을 보고 그렇게 정했나. 근거로 건 기록을 가리킨다 |
|
||||
| 영향 | **감수한 비용을 포함한다.** 좋아진 것만 적지 않는다 |
|
||||
|
||||
대안을 적고 왜 고르지 않았는지 적는다. 대안이 없으면 결정이 아니라 사실이다. 자료가 뒷받침하지
|
||||
않는 이유를 만들지 않는다.
|
||||
|
||||
## 본문이 있는 두 종류의 공통 규칙
|
||||
|
||||
- 그림은 `technical-visualizer` 로 만든다. 손으로 SVG 를 그리지 않는다
|
||||
- 그림 안에는 이름만 넣는다. 문장은 `<desc>` 와 옆 문단에 둔다
|
||||
- 그림과 증거는 frontmatter 의 `assets` · `evidence` 로 잇는다. 같은 파일을 기록 옆에 복사하지 않는다
|
||||
- 본문은 `<!-- body:start -->` 와 `<!-- body:end -->` 사이다. 그 밖은 Studio 로 가지 않는다
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Case 본문이 Studio 파서를 통과하는지 게시 전에 확인한다.
|
||||
*
|
||||
* Studio 에 붙여넣고 저장한 뒤에야 거절을 알게 되면, 어느 줄이 문제인지 찾느라 화면을 오가게
|
||||
* 된다. 같은 파서를 그대로 부르므로 여기서 통과하면 저장도 통과한다.
|
||||
*
|
||||
* node check_body.mjs <파일> [--frontend <경로>]
|
||||
*
|
||||
* `--frontend` 는 tech-log-frontend 체크아웃 경로다. 생략하면 TECH_LOG_FRONTEND 환경변수를
|
||||
* 쓰고, 그것도 없으면 기본 경로를 쓴다.
|
||||
*/
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const DEFAULT_FRONTEND =
|
||||
"/home/donghyeon/workspace/desktop-server-git/tech-log-frontend";
|
||||
|
||||
function optionValue(name) {
|
||||
const index = process.argv.indexOf(name);
|
||||
return index >= 0 ? process.argv[index + 1] : undefined;
|
||||
}
|
||||
|
||||
const target = process.argv[2];
|
||||
if (!target || target.startsWith("--")) {
|
||||
console.error("usage: node check_body.mjs <파일> [--frontend <경로>]");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const frontend = path.resolve(
|
||||
optionValue("--frontend") ?? process.env.TECH_LOG_FRONTEND ?? DEFAULT_FRONTEND,
|
||||
);
|
||||
const parserPath = path.join(
|
||||
frontend,
|
||||
"src/features/tech-log/domain/content-format/parse-case-content.ts",
|
||||
);
|
||||
|
||||
let parseCaseContent;
|
||||
let ContentFormatError;
|
||||
try {
|
||||
({ parseCaseContent, ContentFormatError } = await import(
|
||||
pathToFileURL(parserPath).href
|
||||
));
|
||||
} catch (error) {
|
||||
console.error(`파서를 불러오지 못했습니다: ${parserPath}`);
|
||||
console.error(
|
||||
"tech-log-frontend 경로를 --frontend 또는 TECH_LOG_FRONTEND 로 알려 주세요.",
|
||||
);
|
||||
console.error(
|
||||
"TypeScript 를 그대로 읽으므로 node 는 --experimental-transform-types 가 필요합니다.",
|
||||
);
|
||||
console.error(String(error instanceof Error ? error.message : error));
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const raw = await readFile(target, "utf8");
|
||||
|
||||
// 기록 파일을 통째로 넣으면 칸의 <br> 과 주석까지 파서에 걸린다. Studio 가 받는 것은
|
||||
// body:start ~ body:end 사이뿐이므로 그 구간만 잘라 검사한다. 마커가 없으면 파일 전체를
|
||||
// 본문으로 본다 — 본문만 담은 초안을 그대로 넣는 경우다.
|
||||
const BODY_START = "<!-- body:start -->";
|
||||
const BODY_END = "<!-- body:end -->";
|
||||
let source = raw;
|
||||
let offset = 0;
|
||||
const startIndex = raw.indexOf(BODY_START);
|
||||
const endIndex = raw.indexOf(BODY_END);
|
||||
if (startIndex !== -1 && endIndex > startIndex) {
|
||||
const bodyStart = startIndex + BODY_START.length;
|
||||
source = raw.slice(bodyStart, endIndex);
|
||||
offset = raw.slice(0, bodyStart).split("\n").length - 1;
|
||||
console.log(`본문 구간만 검사합니다 — ${BODY_START} ~ ${BODY_END}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const blocks = parseCaseContent(source);
|
||||
const counts = new Map();
|
||||
for (const block of blocks) {
|
||||
counts.set(block.type, (counts.get(block.type) ?? 0) + 1);
|
||||
}
|
||||
const summary = [...counts]
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([type, count]) => `${type} ${count}`)
|
||||
.join(" · ");
|
||||
console.log(`PASS ${blocks.length}개 블록 — ${summary}`);
|
||||
} catch (error) {
|
||||
if (error instanceof ContentFormatError) {
|
||||
for (const issue of error.issues) {
|
||||
console.error(`FAIL ${target}:${issue.line + offset}:${issue.column} ${issue.detail}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
Reference in New Issue
Block a user