genetics-mcp 2.14.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/bin/aarch64-apple-darwin/gcell +0 -0
  2. package/bin/aarch64-unknown-linux-musl/gcell +0 -0
  3. package/bin/manifest.json +4 -4
  4. package/bin/x86_64-apple-darwin/gcell +0 -0
  5. package/bin/x86_64-unknown-linux-musl/gcell +0 -0
  6. package/genetics.mjs +232 -10
  7. package/package.json +1 -1
  8. package/skills/gazette-report/README.md +71 -0
  9. package/skills/gazette-report/SKILL.md +479 -0
  10. package/skills/gazette-report/scripts/boundary.py +97 -0
  11. package/skills/gazette-report/scripts/contentdiff.sh +36 -0
  12. package/skills/gazette-report/scripts/cssaudit.sh +78 -0
  13. package/skills/gazette-report/scripts/extract.sh +84 -0
  14. package/skills/gazette-report/scripts/flexcheck.mjs +28 -0
  15. package/skills/gazette-report/scripts/fontchk.mjs +62 -0
  16. package/skills/gazette-report/scripts/kcheck.mjs +146 -0
  17. package/skills/gazette-report/scripts/package-lock.json +24 -0
  18. package/skills/gazette-report/scripts/package.json +5 -0
  19. package/skills/gazette-report/scripts/pdfcheck.sh +34 -0
  20. package/skills/gazette-report/scripts/read.mjs +85 -0
  21. package/skills/gazette-report/scripts/sentbreak.py +142 -0
  22. package/skills/gazette-report/scripts/tone.mjs +172 -0
  23. package/skills/gazette-report/scripts/tone.test.mjs +118 -0
  24. package/skills/gazette-report/scripts/typesurvey.mjs +48 -0
  25. package/skills/gazette-report/template/README.md +89 -0
  26. package/skills/gazette-report/template/gazette.css +2120 -0
  27. package/skills/gazette-report/template/skeleton.html +477 -0
  28. package/skills/genetics-cell-id/SKILL.md +9 -7
  29. package/skills/genetics-report/SKILL.md +53 -6
  30. package/skills/genetics-report/assets/example/cases-briefing.json +32 -0
  31. package/skills/genetics-report/assets/example/report-briefing.json +271 -0
  32. package/skills/genetics-report/assets/example/report.json +1 -1
  33. package/skills/genetics-report/references/content-pipeline.md +20 -0
  34. package/skills/genetics-report/references/lim-portal-format.md +100 -7
  35. package/skills/genetics-report/references/process.md +11 -0
@@ -0,0 +1,479 @@
1
+ ---
2
+ name: gazette-report
3
+ description: 정적 HTML 보고서를 한국 정부간행물(관보) 문서 문법으로 조판한다. 표지·차례·간지·측면 색인·여백 주석 단·표/그림 캡션·부록·판권지를 갖추고, 인쇄하면 실제 A4 페이지가 나오게 한다. 「웹페이지 같다 / 방향성만 보인다 / 완성도가 낮다」는 반려를 받은 문서에 쓴다.
4
+ ---
5
+
6
+ # gazette-report
7
+
8
+ ## 언제 쓰는가
9
+
10
+ - 정적 HTML 보고서·제안서·가이드를 **문서답게** 조판해야 할 때
11
+ - 「웹페이지 같다」 「카드만 나열했다」 「방향성만 보여준다」는 반려를 받았을 때
12
+ - 화면으로도 읽히고 **인쇄하면 진짜 A4 문서**가 나와야 할 때
13
+
14
+ 기준 원전: 행정안전부·KISA 『소프트웨어 개발보안 가이드』, 『공공 SW사업 제안요청서 작성을 위한 요구사항 상세화 실무가이드라인』 계열.
15
+
16
+ ---
17
+
18
+ ## 제0원칙 — 문서는 상자로 만들지 않는다
19
+
20
+ 가장 먼저, 그리고 가장 자주 틀리는 것. **관보는 괘선(rule)과 여백과 활자로 만든다.**
21
+ 라운드 카드에 담으면 아무리 문법을 갖춰도 「카드 UI 에 문서 옷을 입힌」 것으로 읽힌다.
22
+
23
+ 작업 전에 세어라 — 이 셋이 「싸구려」의 정량 지표다:
24
+
25
+ ```bash
26
+ grep -c 'border-radius' style.css # 관보 문서: 한 자릿수
27
+ grep -c 'border: 1px solid' style.css # 관보 문서: 한 자릿수
28
+ grep -oE 'font-size: *[0-9.]+px' style.css | sort -u | wc -l # 5~7단
29
+ ```
30
+
31
+ | | 흔한 상태 | 관보 |
32
+ |---|---|---|
33
+ | `border-radius` | 60회 · 라운드 카드 18종 | **0.** 컨트롤·포커스 링만 2px, 원(50%)은 **의미 기호에만** |
34
+ | 1px 테두리 상자 | 30곳 | **한 자릿수.** 발간번호 · 도해 노드 · 좌표 눈금 · 조작 컨트롤 · 도장 |
35
+ | 배경 tint | 40곳 | 간지 · 표 머리 · 강조에만. **본문은 백지** |
36
+ | 상태 표식 | 면+테두리 칩 여러 종 | **기호 + 색 글자** (● ◐ ○ ─ ■ □). 한 문서에 표식 체계는 하나 |
37
+
38
+ **표는 4괘선으로 만든다.** 감싸개 상자가 아니다.
39
+
40
+ ```css
41
+ --rule-thin: 1px solid; --rule-mid: 2px solid; --rule-thick: 4px solid;
42
+ --rule-edge: 2px solid var(--text); /* 표 위 · 표 맨 아래 */
43
+ --rule-head: 1px solid var(--border-strong); /* 헤더 아래 */
44
+ --rule-row: 1px solid var(--divider); /* 행 사이 */
45
+ /* 좌·우 바깥 테두리 없음. 세로선은 라벨-값 양식표에만 */
46
+ ```
47
+
48
+ **맨 아래 굵은 줄이 없으면 표가 아니라 목록으로 읽힌다.** `tr:last-child { border-bottom: none }`
49
+ 을 찾아 전부 `--rule-edge` 로 바꿔라 — 대개 모든 표가 이렇게 안 닫혀 있다.
50
+
51
+ ## 조판 3체계 — 값을 눈대중으로 찍지 않는다
52
+
53
+ ### 활자 — 1.2 배(Minor Third) 6단 + 표제 3단
54
+
55
+ > **CSS 가 전부 토큰이어도 화면은 들쭉날쭉할 수 있다.** `cssaudit.sh` 통과 뒤에도
56
+ > `fontchk.mjs` 로 **렌더된 값**을 여러 폭에서 세라. 원인 셋:
57
+ > ① **clamp 의 vw 구간** — 900px 에서 `27 · 32.4 · 37.8 · 48.6 · 57.6px` 처럼 스케일을 벗어난다.
58
+ > **문서는 고정 활자다** — clamp 를 버리고 고정값 + 미디어쿼리로 가라.
59
+ > ② **em/% 상대 크기** — 문맥에 따라 12.9 / 15.6px 로 갈린다. 스케일 토큰으로 고정.
60
+ > ③ **굵기 종수** — 700 이 요소의 절반을 넘으면 굵기가 위계를 못 나른다.
61
+
62
+ 한글 본문 17px 기준. 1.25는 작은 쪽이 10.9px 로 한글 판독 하한 미달, 1.333은 본문↔항목제목 +33% 로 포스터 문법.
63
+
64
+ **14px 미만은 쓰지 않는다.** 한글 판독 하한이고, 그 아래는 「작아서 넣은 것」이 된다.
65
+ 그래서 **가장 작은 등급은 하나뿐**이다 — 12px 등급을 따로 두면 라벨이 두 크기로 갈리는데
66
+ 어느 쪽인지 고를 근거가 없어 결국 눈대중으로 찍게 된다.
67
+
68
+ | 토큰 | px | line-height | 무엇이 여기 오는가 |
69
+ |---|---|---|---|
70
+ | `--t-sm` | **14** | 1.7 | **부속 활자** — 라벨·칩·번호 표기·표 안·각주·범례·여백 주석·머리띠·판권지 |
71
+ | `--t-base` | **17** | 1.75 | **읽는 글** — 문단 전부·리드·항목 본문·표 제목 |
72
+ | `--t-md` | 20 | 1.4 | 항목 제목 |
73
+ | `--t-lg` | 24 | 1.35 | 가구 제목(차례·부록·판권지) |
74
+ | `--t-xl` | 29 | 1.3 | 절 제목 |
75
+ | `--t-2xl` | 35 | 1.25 | PART 제목·수치 |
76
+ | `--t-d1/d2/d3` | 42/51/61 | 1.1 | 표제 register — 표지 대제목·간지 숫자 |
77
+
78
+ **0.5px 값(10.5 · 12.5 · 13.5 · 14.5)은 눈대중의 증거다.** 전량 제거.
79
+
80
+ ### 무엇이 어느 크기인가 — 크기는 역할로 정한다
81
+
82
+ **옛 px 값을 새 토큰에 매핑하지 마라.** `13px → --t-xs` 식으로 옮기면 눈대중으로 찍었던
83
+ 배정이 **토큰 이름만 바꿔 그대로 남는다.** 체계를 세운 게 아니라 이름표를 갈아 붙인 것이다.
84
+ 역할에서 다시 도출해야 한다.
85
+
86
+ 역할은 세 갈래뿐이다.
87
+
88
+ | 갈래 | 크기 | 판정 질문 |
89
+ |---|---|---|
90
+ | **읽는 글** | 17px | 문장인가? 마침표로 끝나는가? |
91
+ | **부속 활자** | 14px | 문장이 아니라 무언가에 딸린 표기인가? |
92
+ | **표제** | 20px 이상 | 차례에 오르는가? |
93
+
94
+ **어깨 라벨과 표제를 혼동하지 마라.** 「가장 시급한 요청」처럼 분류를 알리는 어깨 라벨은
95
+ 본문(17px)보다 작아도 된다 — 라벨은 제목이 아니다. 반면 「시간이 없다면 어디를 봐야 하는가?」
96
+ 처럼 **문장으로 된 것**은 그 행의 표제이므로 본문보다 작으면 위계 역전이다.
97
+ 대략 한두 낱말이면 라벨, 문장이면 표제다.
98
+
99
+ **전수 조사 없이는 못 찾는다.** 렌더된 크기·굵기를 클래스 서명별로 전부 뽑아 역할과 대조하라.
100
+ report-v2 144종을 이렇게 훑어 4건이 나왔다 — CSS 만 봐서는 하나도 안 보이던 것들이다.
101
+
102
+ | 어긋난 것 | 어떻게 | 왜 틀렸나 |
103
+ |---|---|---|
104
+ | 활용법의 질문 | 14px, 답은 17px | 질문은 그 행의 **표제**다. 표제가 본문보다 작으면 위계 역전 |
105
+ | `그림 5-1` | 17px(캡션 상속), `표 3-1`은 14px | **같은 역할이면 같은 크기.** 크기를 안 적으면 부모를 물려받아 짝이 어긋난다 |
106
+ | 차례의 `6건` | `6`=17px + `건`=14px | **한 낱말은 한 크기.** 숫자만 키우면 낱말 가운데에 이음매가 생긴다 |
107
+ | 확인 강도 표식 | `th` 안에서만 700 | 같은 표식이 자리에 따라 두 굵기면 표식이 아니라 **강조**로 읽힌다 |
108
+
109
+ **서명당 첫 표본으로 대표시키지 마라.** 같은 클래스가 변형(`--why`)이나 문맥(`th` 안)에 따라
110
+ 갈리는 경우를 통째로 놓친다 — 실제로 그렇게 읽어서 「여백 주석 18개가 전부 굵다」고 잘못 판정했다.
111
+ 크기·굵기는 **분포**로 뽑고, **평균 자수를 같이** 봐라. 자수가 역할을 가른다.
112
+
113
+ ### 굵기 — 3종
114
+
115
+ 굵기는 **위계를 나르는 신호**다. 종류가 많으면 신호가 죽는다.
116
+
117
+ ```
118
+ --w-light: 300; /* 표제 부제 — 표지 2행 · 간지 숫자 */
119
+ --w-body: 400; /* 본문 · 표 셀 값 · 설명 */
120
+ --w-bold: 700; /* 제목 · 라벨 · 번호 · 수치 · 강조 · 기호 */
121
+ ```
122
+
123
+ 500 은 Pretendard 에서 400 과 구분이 안 되고, 800/900 은 700 과 겹친다.
124
+ **굵게 쓸 수 있는 것: 제목 · 라벨 · 좌표 번호 · 수치 · 강조 · 기호. 그뿐.**
125
+
126
+ **본문 상호참조 링크를 굵게 하지 마라.** 밑줄이 이미 신호다 —
127
+ 실측에서 `.ref` 하나가 본문 **119군데**에 굵게 흩뿌려져 문단을 얼룩지게 하고 있었다.
128
+ 그 하나를 내리자 굵게 비율이 68% → 52% 로 떨어졌다.
129
+
130
+ 판정: `fontchk.mjs` 가 **굵기 3종 초과** 와 **굵게 55% 초과** 를 잡는다.
131
+ 「굵은 읽는 글(25자 이상)」은 제목·요지 첫 문장이 아니면 전부 내려라.
132
+
133
+ ### 간격 — 4px 격자 8값
134
+
135
+ `--s1:4 --s2:8 --s3:12 --s4:16 --s6:24 --s8:32 --s12:48 --s16:64`
136
+
137
+ 8px 격자는 12px 칩이 머리띠 2단(32px/단)에 안 들어가 기각. clamp 는 2종으로 줄이고
138
+ **min·max 를 둘 다 격자값으로** 고정한다 — 고정폭·인쇄에서 최종 조판은 언제나 격자값이 된다.
139
+
140
+ ## 제1원칙 — 가구를 옮기는 것과 페이지 문법을 옮기는 것은 다르다
141
+
142
+ 이 스킬이 존재하는 이유. **가구만 옮기면 「문서처럼 꾸민 웹페이지」에서 멈춘다.**
143
+
144
+ | 층 | 무엇 | 빠뜨리면 |
145
+ |---|---|---|
146
+ | **가구** | 표지 · 차례 · 간지 · 측면 색인 · 표 캡션 · 판권지 | 웹페이지로 읽힘 |
147
+ | **페이지 문법** | **두 단 본문 + 여백 주석 단** · 위치 브레드크럼 머리띠 · PART 층위 · 개조식 톤 | 「문서처럼 꾸민 웹페이지」에서 멈춤 |
148
+
149
+ 본문 쪽의 실제 골격:
150
+
151
+ ```
152
+ ┌──────────────────────────────────────────────┐
153
+ │ PART ▮ ▯ ▯ II · 경과 │ ← 머리띠 = 위치 브레드크럼
154
+ │ Chapter ▮ ▯ ▯ ▯ ▯ ▯ ⑥ 이번 회차 요지 │ (브랜드 바가 아니다)
155
+ ├──────────┬───────────────────────────────────┤
156
+ │ ? Why │ 제2절 │ 이번 회차 요지 │ ← 간지 밴드
157
+ │ …질문… ├─────────┴─────────────────────────┤
158
+ │ ! Key │ 본문… │
159
+ │ Point │ ┌───────────────────────────┐ │
160
+ │ …요지… │ │ 표 3-1 일곱 건의 처리… │ │ ← 표 제목 = 전폭 색 밴드
161
+ │ i Advice ├───┴───────────────────────────┴───┤
162
+ │ …참조… │ …표… │
163
+ │ │ ※ 한정 조건 │ ← 각주는 표 아래 행잉 인덴트
164
+ ├──────────┴───────────────────────────────────┤
165
+ │ 8 문서명 · 발행처 │ ← 인쇄에서만
166
+ └──────────────────────────────────────────────┘
167
+ 240px 나머지
168
+ ```
169
+
170
+ **좌측 여백 주석 단이 본체다.** 관보 가이드는 모든 본문 쪽 여백에 `? Why` / `Key Point` / `Advice` 를 고정으로 둔다. 장식이 아니라 읽기 장치 — 「이 절을 왜 읽는가 → 한 줄 요지 → 어디를 참조하라」가 본문 옆에 상시 붙어 있다.
171
+
172
+ 내용은 **독자가 실제로 던질 질문**이어야 한다. 채우려고 넣으면 안 넣느니만 못하다.
173
+ 쪽이 없는 화면에서는 쪽마다 반복할 수 없으므로 `position: sticky`. 900px 미만은 본문 위 가로 띠로 접는다.
174
+
175
+ ---
176
+
177
+ ## 문서 골격 — 순서와 필수 요소
178
+
179
+ | # | 요소 | 필수 내용 | 인쇄 |
180
+ |---|---|---|---|
181
+ | 1 | **표지** | 머리 스트립(발간번호 박스 | **고객사 마크**) · 대제목 2행(굵은 남색 + 가는 글자) · 빈 마당 · 닫는 괘선 · 메타 4칸 · 발행처 스트립 | 1쪽 단독 · `@page :first { margin: 0 }` |
182
+ | 2 | **발행 이력** | 회차 · 발행일 · 내용 (원문자 번호) | 새 쪽 |
183
+ | 3 | **차례** | PART 색 밴드 + 절 행(점선 리더 + 건수) + **하위 항목 개별 행** | 새 쪽 |
184
+ | 4 | **이 문서 읽는 법** | 좌측 질문 라벨 단 + 우측 ▶ 지시 | 새 쪽 |
185
+ | 5 | **요약** | 제목 밴드 + **현황 줄** + 상태 막대 + 수치 칸 + ※ | 새 쪽 |
186
+ | 6 | **PART 간지** | 전면 색 패널 · 사선 해칭 · 로마 숫자 · 절 목록 · **이 PART 가 답하는 질문** · 아이콘 | PART 마다 1쪽 단독 |
187
+ | 7 | **절 간지 + 본문** | 파스텔 면 + 「제N절」 + 세로 괘선 + 요약 → 두 단 본문 | 절마다 새 쪽 |
188
+ | 8 | **부록** | 확인 환경 · 판정 기준 등 본문에서 반복 참조되는 것 | 새 쪽 |
189
+ | 9 | **판권지** | 제목 · 라벨-값 · `내부 공유용` · ※ 고지 | 마지막 쪽 |
190
+
191
+ **떠 있는 알림 배너를 두지 마라.** 표지 위의 토스트는 관보 문법에 없다. 상태가 바뀌는 한 줄은
192
+ **요약면의 「현황」 줄**이 있을 자리다. 긴급 신호는 이미 해당 건 카드의 플래그·요지 절의 강조 박스·
193
+ 머리띠 배지에서 반복된다 — 네 번째로 띄울 이유가 없다.
194
+
195
+ **머리띠 우측 끝은 날짜가 아니라 위치다.** 관보 머리띠는 거기에 쪽번호를 둔다 — 문서 안 어디인지
196
+ 알려 주는 앵커. 화면에 쪽이 없으면 절 위치(`N / 6`)로 옮긴다. 날짜는 표지·이력·판권지의 몫이다.
197
+
198
+ ```
199
+ PART [Ⅰ][Ⅱ][Ⅲ] Ⅰ. 요청
200
+ Chapter [1][2][3][4][5][6] 지금 부탁드리는 것 │ 1 / 6 │ 요청 잔여 6건
201
+ ────────────────────────────────────────────────────────── ← 색 괘선
202
+ ```
203
+
204
+ **숫자만 있는 표식을 번호 칸 행 안에 두지 마라.** 잔여 건수 배지를 1번 칸 옆에 붙였더니
205
+ `1 6 2 3 4 5 6` 이 되어 **여섯 절 사이에 낀 일곱 번째 절**로 읽혔다. 원인은 위치가 아니라
206
+ **모양이 같다는 것** — 칸은 19px·굵게·`tabular-nums`, 배지는 17px·굵게·`tabular-nums` 였다.
207
+ 숫자 표식은 **낱말로 감싸(「요청 잔여 6건」) 우측 판독부**에 둔다. 거기가 표식의 자리다.
208
+
209
+ **차례의 하위 항목을 한 줄로 이어 붙이지 마라.** 개별 행 + 자기 리더가 있어야 목록이고, 이어 붙이면 설명문이다.
210
+
211
+ **표지 우상단은 고객사 마크의 자리다.** 문서를 짜기 전에 **로고 파일을 요청한다.**
212
+ 못 받으면 비워 두지 말고 **고객사명을 활자로 세운다** — 빈 자리는 「받을 것을 못 받은 문서」로 읽힌다.
213
+ 로고를 받으면 `<img class="cover__logo">` 로 갈아 끼우기만 하면 되게 자리를 미리 잡아 둔다.
214
+
215
+ **표지 가운데의 빈 마당은 미완성이 아니라 문법이다.** 다만 그 마당을 **괘선으로 닫아야**
216
+ 잘린 화면이 아니라 여백으로 읽힌다. 도해를 넣더라도 내용에서 도출된 것만 — 표지에
217
+ 전환 전/후를 다시 그리는 식으로 **본문·메타에 이미 있는 사실을 되풀이하지 마라.**
218
+
219
+ ---
220
+
221
+ ## 색 — 팔레트를 베끼지 말고 문서의 의미에서 뽑는다
222
+
223
+ PDF의 파스텔을 그대로 옮기면 「로봇처럼 베낀 것」이 된다. **원리만** 가져온다: 장마다 고유 정체성 → 색인이 항해 도구가 된다.
224
+
225
+ 색은 **그 문서가 이미 본문에서 쓰고 있는 상태색을 승격**한다.
226
+
227
+ > 예: 「완료」 절이 초록인 이유는 `.tag--live`(확인함)가 이미 초록이라서. 「대기」 절이 황갈인 이유는 `.tag--wait`가 이미 노랑이라서.
228
+
229
+ 체크:
230
+ - **원색 위 흰 글자 대비를 먼저 재라.** 노랑(#FFB114) 위 흰 글자는 1.8 — 면은 노랑으로 두고 글자 면만 어두운 동족색(#8A5C00)으로 내린다.
231
+ - 확정이 아니라 **유보**인 장은 채우지 말고 파선으로 (그 문서의 「준비되면 확인」 문법을 빌린다).
232
+ - 색 위에 **흰색을 얹으면 면이 밝아져 흰 글자가 죽는다.** 반투명 칩은 `rgba(0,0,0,…)` 로 얹는다.
233
+ - PART 층은 구조, 절 층은 의미. 두 축을 겹치지 마라.
234
+
235
+ ---
236
+
237
+ ## 한글 조판 — 라틴 기준 토큰을 그대로 쓰면 안 된다
238
+
239
+ **Pretendard 한글 자폭은 1em이 아니라 0.8625em이다(실측).** `--measure: 44em` 은 한글 44자가 아니라 **51자**가 된다.
240
+
241
+ | 항목 | 값 | 근거 |
242
+ |---|---|---|
243
+ | 글줄 길이 | **`--measure: 37em`** ≈ 43자 | 한글 적정 35~45자 |
244
+ | 본문 행간 | **1.75** | 1.5는 한글에 좁다 |
245
+ | 부속(각주·캡션) 행간 | 1.6~1.7 | |
246
+ | 본문 색 | **본문 색**(`#1E2124`) | 보조 회색은 캡션·라벨의 몫. 읽으라고 쓴 글을 회색으로 조판하지 마라 |
247
+ | 여백 주석 단 | 폭 240px · 14px · 19자 | 관보 여백 단은 12~19자 |
248
+ | 줄바꿈 | `word-break: keep-all` | |
249
+ | 경로·기술 문자열 | 별도 face + `<wbr>` | 본문 활자로 읽히면 안 되고 끊어 읽을 자리도 다르다 |
250
+
251
+ **측정하고 고쳐라.** 추측 금지 — `scripts/read.mjs` 로 글자/줄·행간·대비를 전수 실측한다.
252
+
253
+ ---
254
+
255
+ ## 시각 장치 — 글만 있으면 안 된다
256
+
257
+ 원전 PDF는 도해·흐름도·매트릭스·아이콘·썸네일이 없는 쪽이 거의 없다. 단 **내용에서 도출된 것만** 넣는다. 장식은 금지.
258
+
259
+ | 장치 | 언제 | 만드는 법 |
260
+ |---|---|---|
261
+ | **상태 막대** | 전체 분포를 숫자로만 말하고 있을 때 | flex 세그먼트 + `--w:%` |
262
+ | **확인 강도 표식** ●◐○— | 등급이 있는데 태그로만 나열될 때 | 원 4종(채움/반채움/테두리/가로줄). **파선 원은 쓰지 마라 — 13px에 2px 파선은 점 네 개가 되어 표식으로 안 읽힌다. 「해당 없음」은 가로줄(—)이 관례** |
263
+ | **매트릭스** | 항목 × 축(플랫폼·역할)이 있을 때 | 표 + 범례를 밴드 우측에. 행 그룹은 구분 행으로(rowspan은 좁은 화면에서 무너진다) |
264
+ | **의존 흐름도** | 「A가 되면 B가 되고 C가 된다」를 글로 반복 설명 중일 때 | 3단 grid + `::after` 삼각형. **좁은 화면에서 `display:block`으로 되돌릴 때 자식 `<span>`도 반드시 `display:block` 재지정** |
265
+ | **간지 아이콘** | 간지 원 무리가 비어 있을 때 | 인라인 SVG(외부 파일 아님). 빈 원은 장식, 표식은 안내 |
266
+ | **▶ 지시 마커** | 활용법·행동 지시 목록 | `::before` 삼각형 |
267
+
268
+ ---
269
+
270
+ ## 톤 — 개조식
271
+
272
+ `tone.mjs` 는 두 층위를 본다. **① 경어체 잔존(S1, 0이어야 함)** 과
273
+ **② 늘어짐 지표(S2, 천 자당 밀도)** — 문두 접속사 · 추측 종결 · 이중 완곡 ·
274
+ 「이는 ~」 지시 · 형식명사 결합 · 괄호 부연 · 낫표 · 대시 · 인라인 강조.
275
+
276
+ ②의 분류·처방은 [`epoko77-ai/im-not-ai`](https://github.com/epoko77-ai/im-not-ai) (MIT) 의
277
+ 「AI 한글 티 분류 체계」에서 **관보 보고서에도 그대로 해당하는 항목만** 골라 왔다.
278
+ 그쪽은 산문 윤문 도구이므로 **번역투(A) · 영어 인용(B) · 리듬 균일성(E)은 가져오지 않는다** —
279
+ 개조식은 리듬이 균일한 것이 정상이다. 심각도 3단계(S1/S2/S3)와 route 3경로도 같은 출처.
280
+
281
+ 논문·과제·공문 문체. `~합니다 / ~해 주세요 / ~했습니다` **아님**.
282
+
283
+ | 쓰지 마라 | 써라 |
284
+ |---|---|
285
+ | 인증서를 설치해 주셔야 합니다 | 인증서 설치 **필요** |
286
+ | 확인했습니다 | 확인**함** |
287
+ | 연결됩니다 | 연결**됨** |
288
+ | 거부합니다 | 거부하는 **상황** |
289
+ | 제거하겠습니다 | 제거 **예정** |
290
+ | 적었습니다 | 기재**함** |
291
+
292
+ 질문형(Why 블록, 활용법 라벨)은 `~하는가?` / `~인가?`. `~합니까?` 아님.
293
+
294
+ 검사: 본문에서 `습니다|십시오|합니다|입니다|됩니다|주세요` 정규식이 **0건**이어야 한다(스크립트 태그 제외).
295
+
296
+ ### 한 문장 한 줄 — 한글은 마침표에서 줄을 바꾼다
297
+
298
+ **문장을 이어 붙인 한글 문단은 기계가 쓴 글처럼 읽힌다.** 한글 보고서의 기본은 한 문장 한 줄이다.
299
+ 톤을 개조식으로 고쳐도 이걸 안 하면 AI 티가 남는다 — 낱말이 아니라 **조판**의 문제다.
300
+
301
+ **줄만 바꾸면 안 된다.** 실측(report-v2 문장 296개): **26%가 글줄 폭(43자)을 넘어 접힌다.**
302
+ 좁은 여백 주석 단(19자/줄)은 **54%**다. 접히면 둘째 줄과 다음 문장의 첫 줄이 같은 자리에서
303
+ 시작해 독자가 구별할 수 없다. 그래서 문장마다 **블록으로 세우고 이어지는 줄만 내어쓴다.**
304
+
305
+ ```css
306
+ .sn { display: block; padding-left: 1em; text-indent: -1em; }
307
+ ```
308
+
309
+ ```html
310
+ <p><span class="sn">전환 완료.</span><span class="sn">4건은 앱 작업만으로 종료.</span></p>
311
+ ```
312
+
313
+ `scripts/sentbreak.py <파일> --apply` 가 마크업을 새긴다. **낱말은 하나도 바뀌지 않는다** —
314
+ `contentdiff.sh` 로 확인할 것. 멱등이므로 여러 번 돌려도 안전하다.
315
+
316
+ **기각한 대안 — 전부 실측으로 떨어뜨렸다.**
317
+
318
+ | 후보 | 왜 안 되는가 |
319
+ |---|---|
320
+ | `<br />` 만 | 접힌 둘째 줄과 다음 문장을 구별할 수 없다 |
321
+ | `text-indent: -1em` | 첫 줄이 아니라 **다음 문장**이 들여써진다 — 방향이 거꾸로 |
322
+ | `text-indent: -1em each-line` | Chrome 이 `CSS.supports` 에 **true** 를 돌려주고 계산값도 `-17px each-line` 으로 보여 주면서 **렌더에서는 무시한다.** 명세만 있고 구현이 없다 |
323
+ | 문장마다 `<p>` 분리 | 한 문단이 여러 문단이 된다. 문단 사이 여백이 문장 사이에 생긴다 |
324
+
325
+ **어디에 적용하지 않는가.** 라벨-값 양식 행(표지 메타 `대상 | …`) · 차례 항목 · 색인 탭 ·
326
+ 상태 칩 · 짧은 표 셀. 판정 기준은 「산문인가」이고, 실무적으로는 **`display` 가 flex/grid 인
327
+ 블록은 산문이 아니다** — 그건 「라벨 | 값」 배치다.
328
+
329
+ **한 문장짜리 블록도 감싼다.** 안 그러면 접힐 때 「두 문장 블록은 들여쓰고 한 문장 블록은
330
+ flush」로 두 방식이 섞인다 — 실측: 여백 주석 단에서 Key Point 는 들여쓰고 Advice 는 flush 였다.
331
+
332
+ ---
333
+
334
+ ## 인쇄 = 진짜 페이지
335
+
336
+ ```css
337
+ @page {
338
+ size: A4;
339
+ margin: 20mm 18mm 18mm;
340
+ @bottom-left { content: counter(page); font-weight: 700; } /* Chrome 131+ */
341
+ @bottom-right { content: "문서명 · 발행처"; font-size: 8.5pt; }
342
+ }
343
+ @page :first { margin: 0; @bottom-left{content:""} @bottom-right{content:""} }
344
+
345
+ @media print {
346
+ *, *::before, *::after { transition: none !important; animation: none !important; } /* 첫 줄 */
347
+ html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
348
+ .cover { min-height: 100vh; break-after: page; }
349
+ .pt { break-before: page; break-after: page; }
350
+ .pt__panel { min-height: 84vh; }
351
+ .part, .apx, .eof { break-before: page; }
352
+ .part__grid { grid-template-columns: 46mm 1fr; } /* 여백 단은 인쇄에서도 유지 */
353
+ .marg { position: static; }
354
+ .sheet { break-inside: auto; overflow: visible; }
355
+ .board thead { display: table-header-group; }
356
+ .board-wrap, .flow__wrap { overflow: visible; }
357
+ }
358
+ ```
359
+
360
+ | 기능 | Chrome 131+ | Firefox | Safari |
361
+ |---|---|---|---|
362
+ | `@page size/margin` | O | O | O 18.2 |
363
+ | `@page` 마진 박스 + `counter(page)` | **O** | X (bug 856371) | X |
364
+ | `<thead>` 쪽 넘김 반복 | O | O | X (webkit 17205) |
365
+ | `break-after: avoid` | O | X (1972340) | X (294559) → `avoid-page` 병기 |
366
+ | `orphans`/`widows` | O | X (bug 137367) | O |
367
+ | **`counter-reset: page`** | **X** | X | X |
368
+
369
+ **함정:**
370
+ - 인쇄에서 `vh` 는 **용지 전체**(A4 297mm)이지 본문 영역이 아니다. 여백 20/18mm면 본문은 **87.2vh**. 이보다 크게 잡으면 쪽이 하나 더 생긴다. (표지는 `@page :first { margin: 0 }` 이라 100vh 가 맞다.)
371
+ - `counter-reset: page` 가 어느 브라우저에도 없으므로 **전면부 로마자 쪽번호(i·ii·iii)는 포기**한다. 넣으면 본문이 1이 아니라 7부터 시작해 앞뒤가 어긋난다.
372
+ - `overflow: hidden` 은 monolithic 후보(css-break-3 §4.1) — 인쇄에서 풀지 않으면 한 쪽보다 큰 요소가 **잘려 사라진다.**
373
+ - `position: fixed` 는 Chrome·FF 에서 매 쪽 반복되며 본문을 덮는다 — 측면 색인 레일은 반드시 `display:none`.
374
+ - 인쇄 대화상자의 「머리글 및 바닥글」을 체크 해제해야 브라우저 URL·날짜가 겹쳐 찍히지 않는다.
375
+ - **Paged.js 는 도입하지 마라.** 순증분은 사실상 「차례 쪽번호」 하나인데 921KB · 3년 미유지보수 · 런타임 DOM 재조립(sticky 레일·진행률과 충돌)이다.
376
+
377
+ ---
378
+
379
+ ## 하지 마라 — 싸구려로 보이는 수법
380
+
381
+ 디자인이 아니라 미봉책이다. 문서가 아마추어처럼 읽히는 직접 원인.
382
+
383
+ | 수법 | 왜 나쁜가 | 대신 |
384
+ |---|---|---|
385
+ | `box-shadow: inset 3px 0 0 <색>` 으로 강조 띠 | 보더를 못 쓰니 그림자로 그린 것. 인쇄·모바일에서 따로 지워야 하는 뒤처리가 붙는다 | **실제 보더**. 성격이 다른 표 행은 `border-top: 2px` 로 가른다(관보 표의 기본 문법) |
386
+ | `border-right-color` 같은 **한쪽 면만 색 땜질** | 이음매를 가리려는 것. 구조가 틀렸다는 신호 | 구조를 고쳐라 |
387
+ | `box-shadow: 0 1px 3px …` 드롭 섀도 | 웹앱 광택. 관보 문서에 그림자는 없다 | **보더 두께로 위계**. 1px 파선 → 2px 실선 |
388
+ | `opacity` · `filter: grayscale` 로 「가라앉히기」 | **글자 대비가 무너진다.** 실측 0.55 에서 2.26~3.61 (기준 4.5) | 흐리게 하지 말고 **강조를 내려놓아라** — 색을 중립으로, 번호 칩만 완료색으로. 글자는 그대로 읽히게 |
389
+
390
+ `grep -n 'box-shadow: inset\|border-[a-z]*-color' *.css` 로 전수 확인한다. 포커스 링·CSS 삼각형은 예외다.
391
+
392
+ ## 밟기 쉬운 함정 (전부 실제로 밟은 것)
393
+
394
+ | 함정 | 증상 | 원인·조치 |
395
+ |---|---|---|
396
+ | **괘선 두께 토큰에 `style` 누락** | 선이 아예 안 그려진다. 실측 **22곳 통째로** | `--rule-mid: 2px` 가 아니라 **`2px solid`**. `border-top: 2px` 는 style 이 없어 무효 |
397
+ | **표가 안 닫힘** | 표가 목록처럼 읽힌다 | `tr:last-child { border-bottom: none }` — 대개 **모든 표**가 이렇다. `--rule-edge` 로 |
398
+ | **`h2` 가 본문보다 작음** | 위계 역전 | 활자 스케일 없이 눈대중으로 찍은 결과. `font-size` 실사용 값을 세어 보면 드러난다 |
399
+ | **간지에 반투명 원 여러 겹** | 관보 간지가 아니라 랜딩페이지 히어로 | `radial-gradient` 겹수를 세라. 사선 해칭 한 겹 + 단색이면 충분 |
400
+ | **CSS 는 토큰인데 화면은 들쭉날쭉** | `cssaudit.sh` 통과인데도 크기가 제각각 | `clamp` 의 vw 구간이 중간 폭에서 스케일을 벗어난다. `fontchk.mjs` 로 **렌더 값**을 여러 폭에서 세라 |
401
+ | **면을 걷었는데 흰 글자가 남음** | 흰 바탕에 흰 글자 — 안 보인다 | 밴드를 제거할 때 그 위의 `#fff` 도 함께 내려라. 검사기 서명에 **부모 클래스를 포함**해야 클래스 없는 `<span>` 을 놓치지 않는다 |
402
+ | **조판 작업이 본문을 건드림** | 소리 없이 텍스트·id 가 바뀐다 | `contentdiff.sh` 로 전후를 대조. 조판은 「보이는 방식」만 바꾼다 |
403
+ | 대비 검사기가 조상 `opacity`/`filter` 를 무시 | 「가라앉힌」 요소의 미달을 통째로 놓침 | 조상 opacity 를 배경 위로 합성하고 grayscale 을 적용한 뒤 재라 |
404
+ | IntersectionObserver 진입 모션 문서 검사 | 요소가 `opacity: 0` 인 채로 남아 대비 1 로 나옴 | 끝까지 훑고 **멈춘 뒤**(≈500ms) 되돌아와라. 곧바로 되돌리면 배치된 IO 콜백이 「보이지 않음」으로 뒤집혀 진입 클래스가 안 붙는다 |
405
+ | 인쇄에서 화면용 `margin` 잔존 | `min-height: 100vh` 요소가 두 쪽으로 갈라짐 | 인쇄 블록에서 `margin: 0` 재지정 |
406
+ | `.note { display: flex }` | `04-2` 가 `04-` / `2` 로 쪼개짐 | flex 는 안의 `<a>` 를 별도 항목으로 떼어내 문장을 토막 낸다. **행잉 인덴트**를 써라 (`position:relative` + `::before`) |
407
+ | `.x span:not(.y)` | 숫자가 44px→17px 로 찌그러짐 | **자손** 선택자가 손자까지 잡는다. 자식 결합자 `>` |
408
+ | `[data-chapter]` 를 여러 층에 부여 | 활성 탭 계산 붕괴 | 스크립트에서 절만 잡으려면 `.part[data-chapter]` 로 한정 |
409
+ | `:first-of-type` | 아무것도 매치 안 됨 | 형제 중 **같은 태그**의 첫째를 요구한다. `id` 로 직접 지목 |
410
+ | 반투명 칩 대비 측정 | 흰↔검으로 오독 | 대비 검사기가 **알파를 합성**해야 한다 |
411
+ | 좁은 화면에서 grid → block | 셀이 서로 겹침 | grid 가 블록화하던 `<span>` 자식이 인라인으로 되돌아간다. `display:block` 재지정 |
412
+ | `thead` 숨기는 반응형 표 | 값의 뜻이 사라짐 | 셀에 `data-k` 라벨 + `::before content: attr(data-k)` |
413
+ | 새 표 추가 | 표 접근명 카운트 하락 | 모든 `<table>` 에 `aria-labelledby` 또는 `<caption>` |
414
+ | `--krds-text-muted` 를 색 면 위에 | 대비 4.0~4.1 | 색 면 위에서는 `--krds-text-subtle` 이상 |
415
+ | **숫자 배지를 번호 칸 행에 둠** | `1 6 2 3 4 5 6` — 일곱 번째 절로 읽힌다 | 칸과 배지가 같은 모양(굵게·`tabular-nums`·비슷한 크기)이다. 낱말로 감싸 판독부로 옮겨라 |
416
+ | **캡션 번호에 크기를 안 적음** | `그림 5-1`(17px)과 `표 3-1`(14px)이 어긋난다 | 부모 캡션 크기를 상속한다. **같은 역할이면 크기를 명시**해 못 박아라 |
417
+ | **낱말 가운데만 키움** | `6`(17px)+`건`(14px) 로 이음매가 보인다 | 한 낱말은 한 크기. 강조는 굵기·색으로 |
418
+ | **칩이 `th` 안에서 굵어짐** | 같은 표식이 자리마다 두 굵기 | 표식은 `font-weight` 를 스스로 못 박는다. 안 그러면 강조로 읽힌다 |
419
+ | **검사기가 서명당 첫 표본만 봄** | `--why` 변형·`th` 안 상속을 통째로 놓침 | 크기·굵기를 **분포**로 모아라. 대표값 하나는 거짓말한다 |
420
+ | **뼈대를 손으로 따라 고침** | 레퍼런스에서 `--t-xs` 를 없앴는데 뼈대엔 12px 이 47곳 남음 | 뼈대는 **추출물**이다. `extract.sh` 로 다시 뽑아라 |
421
+ | **문장을 이어 붙인 한글 문단** | 톤은 개조식인데 여전히 기계가 쓴 것처럼 읽힌다 | 한글은 마침표에서 줄을 바꾼다. 낱말이 아니라 **조판**의 문제다 |
422
+ | **줄만 바꾸고 끝냄** | 접힌 둘째 줄이 다음 문장처럼 보인다 | 실측 26%(좁은 단 54%)가 접힌다. 문장을 블록으로 세우고 **이어지는 줄을 내어써라** |
423
+ | `CSS.supports` 를 지원 근거로 삼음 | `text-indent: … each-line` 이 true 인데 렌더는 무시한다 | **지원 여부는 렌더로 확인하라.** 계산값(`-17px each-line`)도 거짓말한다 |
424
+ | **적용 대상을 긍정 목록으로 관리** | 검사기는 잡는데 적용기가 못 고치는 자리가 생긴다 | **전부 대상 + 예외만 명시**로 뒤집어라. 예외는 실측(flex/grid 인 라벨 행)으로 고른다 |
425
+ | 태그를 지우고 문장을 찾음 | `… 본 것. <small>…</small>` 의 경계를 놓치거나 태그 **안쪽**을 자른다 | 글자→원문 위치 **대응표**를 만들고, 인라인 요소 **바깥(깊이 0)**에서만 잘라라 |
426
+ | 무클래스 `<li>` 를 통째로 대상화 | 표지 메타·차례 항목까지 감싸진다 (`read.mjs` 에 `.sn > cover__mv` 가 뜸) | 산문 목록(`.guide__a` 등)으로 **범위를 한정**하라 |
427
+
428
+ ---
429
+
430
+ ## 검증 — 0표본 통과는 통과가 아니다
431
+
432
+ `scripts/` 의 스크립트를 프로젝트에 복사해 실행한다. 전부 Playwright(`playwright-core`) + 로컬 Chrome.
433
+
434
+ | 스크립트 | 무엇을 재는가 | 통과 기준 |
435
+ |---|---|---|
436
+ | `kcheck.mjs` | 선택자 전수 × 배너 상태별 **대비**(알파 합성) · 제목 레벨 · 표 접근명 · 링크·중복 id · 넘침(7폭) · 두 단 구조 · 인쇄 숨김 · reduced-motion | AA 미달 **0** · 넘침 0 · 표 접근명 전수 |
437
+ | `read.mjs` | 읽는 글 전수의 **글자/줄 · 행간 · 대비** | 43자 이하 · 행간 1.6+ · 지적 0 |
438
+ | `flexcheck.mjs` | flex/grid 안에서 **문장이 토막 나는 곳** | 0건 |
439
+ | `fontchk.mjs` | **렌더된** 활자 크기·굵기 분포를 4개 폭(1440·1100·900·390)에서. CSS 토큰화로는 안 잡히는 것 | 스케일 밖 **0** · 굵기 **3종** · 굵게 **≤55%** |
440
+ | `tone.mjs` | ① 본문 **경어체 잔존**(S1) ② **늘어짐 지표 9종**(S2, 천 자당 밀도) — 문두 접속사 · 추측 종결 · 이중 완곡 · 「이는 ~」 · 형식명사 결합 · 괄호 부연 · 낫표 · 대시 · 인라인 강조 ③ **한 문장 한 줄**(S1) — 마침표로 이어 붙인 블록 | 경어체 **0건** · 이어 붙인 블록 **0개** · 밀도 초과 **0항** |
441
+ | `sentbreak.py` | 검사가 아니라 **적용** — 문장마다 `<span class="sn">` 을 새긴다. 멱등 | 돌린 뒤 `contentdiff.sh` 통과 |
442
+ | `contentdiff.sh` | **내용 불변 게이트** — 조판이 본문 낱말 · `id` · `data-*` · 앵커를 바꿨는지 전후 대조 | 전 항목 차이 **0** |
443
+ | `pdfcheck.sh` | 실제 PDF 생성 → 쪽수 · 절별 새 쪽 시작 · 쪽번호 · 러닝 푸터 | 표지 단독 · 절마다 새 쪽 · 푸터 = 쪽수-1 |
444
+ | `cssaudit.sh` | 활자 스케일 · 간격 격자 · 라운드 · 상자 · 표 마감 · 미봉책 (브라우저 불필요). 끝에 **route 판정**(light/standard/heavy) | raw px 0 · 격자 밖 0 · 안 닫힌 표 0 · 굵기 ≤3종 · **지적 점수 0** |
445
+ | `extract.sh` | 검사가 아니라 **재추출** — 레퍼런스 CSS 에서 `template/gazette.css` 를 다시 뽑는다(문서 고유 규칙 제외) | 뼈대를 손으로 고치지 않았는가 |
446
+
447
+ **`fontchk.mjs` 는 「스케일 밖 0」으로 끝나지 않는다.** 그건 크기가 **몇 종**인지만 본다.
448
+ **어느 것이 어느 크기인지**는 기계가 판정할 수 없다 — 서명별 크기·굵기 **분포**와 **평균 자수**를
449
+ 뽑아 사람이 역할과 대조해야 한다. 「무엇이 어느 크기인가」 절 참조.
450
+
451
+ 보고할 때는 **표본 수와 방법**을 함께 적는다. 확인하지 못한 범위(다른 브라우저 실기 인쇄 등)는 명시한다.
452
+
453
+ ---
454
+
455
+ ## 작업 규모를 먼저 정한다
456
+
457
+ `cssaudit.sh` 의 지적 점수가 route 를 낸다. **재기 전에 시작하면 부분 수정을 여섯 번 반복하게 된다**
458
+ — 이 스킬이 나온 사례가 정확히 그랬다.
459
+
460
+ | route | 조건 | 할 일 |
461
+ |---|---|---|
462
+ | **light** | 지적 점수 0 | 마감만 |
463
+ | **standard** | 40 미만 | 상자 → 괘선 + 부분 규격화 |
464
+ | **heavy** | 40 이상 | **조판 3체계부터 새로 세운다** — 활자·간격·괘선 → 그 다음에 무엇이든 만진다 |
465
+
466
+ ## 작업 순서
467
+
468
+ 1. 원전 PDF/기준 문서의 **쪽 사진을 실제로 본다.** 장치를 표로 뽑고 현재 시안과 대조한다.
469
+ 2. **셈부터 한다** — `cssaudit.sh`. 라운드·상자·활자 단수·격자 밖 값. 「싸구려」는 정량 지표가 있다.
470
+ 3. **조판 3체계 수립** — 활자 스케일 · 간격 격자 · 괘선 3종. 그 다음에 무엇이든 만진다.
471
+ 4. **상자 → 괘선** — 라운드 0, 상자는 한 자릿수, 표는 4괘선
472
+ 5. **가구** 이식 — 표지 · 차례 · 간지 · 색인 · 캡션 · 부록 · 판권지
473
+ 6. **페이지 문법** 이식 — 두 단 + 여백 주석 · 머리띠 · PART 층위 ← *여기서 멈추면 반려된다*
474
+ 7. **색 도출** — 문서의 상태색을 승격
475
+ 8. **한글 조판 실측** — `read.mjs` 지적 0까지
476
+ 9. **시각 장치** — 내용에서 도출된 것만
477
+ 10. **개조식 톤** — `tone.mjs` 0건까지
478
+ 11. **인쇄 페이지 문법** — 실제 PDF 생성해 쪽 구조 확인
479
+ 12. **전수 검증** → 실측표로 기록
@@ -0,0 +1,97 @@
1
+ # -*- coding: utf-8 -*-
2
+ """문장 끝 마침표 판별 — 날짜·버전·경로·소수점·머리글자를 거른다.
3
+
4
+ 문장은 숫자로 시작할 수 있다("4건은 …", "04-2에는 …").
5
+ 그래서 「뒤에 숫자 금지」로는 못 거른다 — 날짜를 **먼저 가리고** 판별한다.
6
+ """
7
+ import re
8
+
9
+ # ① 날짜 꼴을 먼저 가린다: 2026. 8. 21. / 2026. 8.
10
+ DATE = re.compile(r'\d{4}\.\s*\d{1,2}\.(?:\s*\d{1,2}\.)?')
11
+ TAG = re.compile(r'<[^>]*>')
12
+
13
+ # ② 문장 끝
14
+ SENT_END = re.compile(
15
+ r'(?<=[가-힣0-9\)\]」』”\'"A-Za-z])' # 앞: 한글·숫자·닫는 괄호/따옴표·라틴
16
+ r'([.!?])'
17
+ r'(\s+)'
18
+ r'(?=[가-힣0-9「『※(\[]|[A-Z])' # 뒤: 다음 문장의 첫 글자 (숫자 허용)
19
+ )
20
+ # ③ 머리글자 배제: 앞이 「공백 뒤 한 글자짜리 라틴」이면 문장 끝이 아니다 (A. B. 등)
21
+ INITIAL = re.compile(r'(?:^|[\s(\[])[A-Za-z]$')
22
+
23
+ def _mask(s, keep_len=False):
24
+ """날짜 꼴을 가린다.
25
+ keep_len=True 면 **길이를 보존**한다 — 원문 위치 대응표를 쓰는 쪽에서 필요하다."""
26
+ if keep_len:
27
+ return DATE.sub(lambda m: '\x02' * len(m.group(0)), s), []
28
+ keep = []
29
+ def sub(m):
30
+ keep.append(m.group(0)); return f'\x00{len(keep)-1}\x01'
31
+ return DATE.sub(sub, s), keep
32
+
33
+ def _unmask(s, keep):
34
+ return re.sub(r'\x00(\d+)\x01', lambda m: keep[int(m.group(1))], s)
35
+
36
+ def _bounds(text):
37
+ """text 안 문장 경계의 (부호끝, 공백끝) 위치 목록"""
38
+ masked, keep = _mask(text)
39
+ out = []
40
+ for m in SENT_END.finditer(masked):
41
+ if INITIAL.search(masked[:m.start(1)]): continue
42
+ out.append((m.start(1), m.end(1), m.end(2)))
43
+ return out, masked, keep
44
+
45
+ def split_text(html):
46
+ out, pos = [], 0
47
+ for m in TAG.finditer(html):
48
+ if m.start() > pos: out.append((False, html[pos:m.start()]))
49
+ out.append((True, m.group(0))); pos = m.end()
50
+ if pos < len(html): out.append((False, html[pos:]))
51
+ return out
52
+
53
+ def count(html):
54
+ n = 0
55
+ for is_tag, c in split_text(html):
56
+ if not is_tag:
57
+ b, _, _ = _bounds(c); n += len(b)
58
+ return n
59
+
60
+ def insert(html, mark='<br />'):
61
+ parts = []
62
+ for is_tag, c in split_text(html):
63
+ if is_tag: parts.append(c); continue
64
+ b, masked, keep = _bounds(c)
65
+ res, prev = [], 0
66
+ for s, e, ws in b:
67
+ res.append(masked[prev:e]); res.append(mark); prev = e
68
+ res.append(masked[prev:])
69
+ parts.append(_unmask(''.join(res), keep))
70
+ return ''.join(parts)
71
+
72
+ if __name__ == '__main__':
73
+ T = [
74
+ ("앱 실행 시 기존 사이트가 열림. 앱 내 화면 이동과 하단 홈 단추도 동작함.", 1),
75
+ ("2026. 8. 21.", 0),
76
+ ("작성 2026. 8.", 0),
77
+ ("최초 발행 2026. 8. 21. 이후 변경 없음.", 0),
78
+ ("아이폰 확인은 시뮬레이터(iPhone 17 Pro · iOS 26.4) 기준임. 실제 단말은 차기 회차 예정.", 1),
79
+ ("/wp-content/plugins/kboard/api.php 를 확인함.", 0),
80
+ ("잔존 없음(04-1). 아이폰에서도 정상 표시됨(04-2).", 1),
81
+ ("「앱 쪽 완료」는 앱 작업이 끝났다는 뜻임. 04-2에는 미결 행이 남음.", 1),
82
+ ("무엇을 조치해야 하는가?", 0),
83
+ ("A. 첫째. B. 둘째.", 1), # A. B. 는 항목 머리글자 — 경계는 「첫째.」 뒤 하나뿐
84
+ ("실기기. 배포용 설치 파일을 실제 단말에 설치하여 확인함.", 1),
85
+ ("Chrome 131 이상에서 인쇄. 대화상자의 「머리글 및 바닥글」 체크 해제 필요.", 1),
86
+ ("전환 완료. 4건은 앱 작업만으로 종료. 3건은 서버 준비 대기.", 2),
87
+ ("<b>표 3-2</b> 참조함. 판정 기준은 <a href='#x'>부록 2</a>.", 1),
88
+ ("판정 기준은 부록 2. 자세한 것은 06절 참조.", 1),
89
+ ]
90
+ bad = 0
91
+ for s, want in T:
92
+ got = count(s)
93
+ ok = '✓' if got == want else '✗'
94
+ if got != want: bad += 1
95
+ print(f'{ok} 기대{want} 실제{got} {s[:58]}')
96
+ print(f'\n오판 {bad}건 / {len(T)}')
97
+ print('\n삽입 예:', insert("전환 완료. 4건은 앱 작업만으로 종료. 3건은 서버 대기."))
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # contentdiff.sh — 내용 불변 게이트
3
+ # 사용: ./contentdiff.sh <이전.html> <이후.html>
4
+ #
5
+ # 조판 작업은 「보이는 방식」만 바꾼다. 본문·앵커 id·data 속성·상태 클래스가
6
+ # 소리 없이 바뀌면 그건 조판이 아니라 내용 변경이다.
7
+ # 6차에 걸쳐 고치는 동안 실제로 여러 번 HTML 을 만졌으므로, 기계로 확인한다.
8
+ set -uo pipefail
9
+ A="${1:?usage: contentdiff.sh <before.html> <after.html>}"
10
+ B="${2:?usage: contentdiff.sh <before.html> <after.html>}"
11
+ T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
12
+ FAIL=0
13
+
14
+ textof() {
15
+ sed -e 's/<script[^>]*>.*<\/script>//g' -e 's/<style[^>]*>.*<\/style>//g' "$1" \
16
+ | sed -e 's/<[^>]*>/ /g' | tr -s ' \n' ' \n' | tr ' ' '\n' | grep -v '^$'
17
+ }
18
+ attrof() { grep -oE "$2" "$1" | sort -u; }
19
+
20
+ say() { printf '%-22s %s\n' "$1" "$2"; }
21
+
22
+ textof "$A" > "$T/a.txt"; textof "$B" > "$T/b.txt"
23
+ D=$(diff "$T/a.txt" "$T/b.txt" | grep -c '^[<>]' || true)
24
+ say "본문 낱말 차이" "$D"
25
+ [ "$D" -ne 0 ] && { FAIL=1; diff "$T/a.txt" "$T/b.txt" | grep '^[<>]' | head -20; }
26
+
27
+ for label in 'id="[^"]*"' 'data-[a-z-]*="[^"]*"' 'href="#[^"]*"'; do
28
+ attrof "$A" "$label" > "$T/x"; attrof "$B" "$label" > "$T/y"
29
+ N=$(diff "$T/x" "$T/y" | grep -c '^[<>]' || true)
30
+ say "$label" "$N"
31
+ [ "$N" -ne 0 ] && { FAIL=1; diff "$T/x" "$T/y" | grep '^[<>]' | head -10; }
32
+ done
33
+
34
+ echo
35
+ [ "$FAIL" -eq 0 ] && echo "== 내용 불변: 통과 (조판만 바뀜) ==" || echo "== 내용 불변: 위반 — 위 차이를 의도했는지 확인 =="
36
+ exit $FAIL