genetics-mcp 2.9.1 → 2.9.3
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.
- package/bin/aarch64-apple-darwin/gcell +0 -0
- package/bin/aarch64-unknown-linux-musl/gcell +0 -0
- package/bin/manifest.json +4 -4
- package/bin/x86_64-apple-darwin/gcell +0 -0
- package/bin/x86_64-unknown-linux-musl/gcell +0 -0
- package/genetics.mjs +658 -26
- package/package.json +1 -1
- package/skills/genetics-cell-id/SKILL.md +204 -0
- package/skills/genetics-report/SKILL.md +217 -93
- package/skills/genetics-report/assets/example/cases.json +19 -22
- package/skills/genetics-report/assets/example/report.json +198 -53
- package/skills/genetics-report/assets/render.py +21 -10
- package/skills/genetics-report/assets/subset_font.py +24 -8
- package/skills/genetics-report/references/content-pipeline.md +316 -733
- package/skills/genetics-report/references/design-system.md +20 -723
- package/skills/genetics-report/references/font-embedding.md +132 -108
- package/skills/genetics-report/references/illustrations.md +20 -462
- package/skills/genetics-report/references/lim-portal-format.md +756 -182
- package/skills/genetics-report/references/process.md +414 -403
- package/skills/genetics-report/references/qa.md +28 -712
- package/skills/genetics-report/assets/generate-illustrations.py +0 -159
- package/skills/genetics-report/assets/qa.py +0 -198
- package/skills/genetics-report/assets/visual-baseline.py +0 -459
- package/skills/genetics-report/references/textbook-mode.md +0 -111
- package/skills/genetics-report/references/wysiwyg.md +0 -751
|
@@ -1,55 +1,200 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
2
|
+
"out": "예시-보고서.html",
|
|
3
|
+
"cases": "cases.json",
|
|
4
|
+
"lang": "ko",
|
|
5
|
+
|
|
6
|
+
"title_tag": "시스템 수정 내역 보고 · 예시 고객사",
|
|
7
|
+
"description": "이번 회차에 처리한 2건의 수정 내역과, 확인이 필요한 1건을 정리한 것임",
|
|
8
|
+
|
|
9
|
+
"doc_no": "LIM-2026-0824-01",
|
|
10
|
+
"client": "예시 고객사",
|
|
11
|
+
"kicker": "우리회사 · 예시 고객사",
|
|
12
|
+
"title": "시스템 수정 내역",
|
|
13
|
+
"title_sub": "1차 보고",
|
|
14
|
+
"subtitle": "이번 회차에 처리한 2건과, 확인이 필요한 1건을 정리한 것임. 검수 결과는 별도 회신 필요.",
|
|
15
|
+
"cover_date": "2026.08.24",
|
|
16
|
+
"issuer": ["우리회사", "예시 고객사", "2026. 8."],
|
|
17
|
+
"running_footer": "예시 고객사 · LIM-2026-0824-01",
|
|
18
|
+
"remain": "1",
|
|
19
|
+
|
|
20
|
+
"meta": [
|
|
21
|
+
["보고 기간", "2026.08.17 – 08.24"],
|
|
22
|
+
["처리 건수", "2건 (접수 1 · 자체 발견 1)"],
|
|
23
|
+
["수신", "예시 고객사 검수 담당"],
|
|
24
|
+
["작성", "2026.08.24 · 우리회사"]
|
|
25
|
+
],
|
|
26
|
+
|
|
27
|
+
"history": [
|
|
28
|
+
{ "no": 1, "date": "2026. 8. 24.", "note": "최초 발행. 접수 1건과 자체 발견 1건의 처리 결과, 확인 요청 1건을 정리함." }
|
|
29
|
+
],
|
|
30
|
+
|
|
31
|
+
"guide": [
|
|
32
|
+
{ "q": "시간이 없다면 어디를 봐야 하는가?",
|
|
33
|
+
"a": ["01절 확인 요청 1건이 조치 사항의 전부임.", "나머지 절은 그 근거."] },
|
|
34
|
+
{ "q": "어디까지 확인한 것인가?",
|
|
35
|
+
"a": ["03절 대조표의 「확인 방법」 열 참조.", "판정 기준은 부록 1.", "확인하지 않은 범위는 06절에 따로 적어 둠."] },
|
|
36
|
+
{ "q": "각 절 왼쪽 여백의 표식은?",
|
|
37
|
+
"a": ["Why 는 그 절이 답하는 질문.", "Key Point 는 이 절에서 하나만 가져간다면 그것.", "Advice 는 어디를 더 볼 것인가."] },
|
|
38
|
+
{ "q": "인쇄하려면?",
|
|
39
|
+
"a": ["브라우저 인쇄 대화상자에서 「머리글 및 바닥글」 체크 해제 필요.", "그대로 두면 브라우저 주소가 페이지마다 찍힘."] }
|
|
40
|
+
],
|
|
41
|
+
|
|
42
|
+
"notice": { "state": "info", "text": "접수 1건 처리 완료. 확인 요청 1건이 남아 있음." },
|
|
43
|
+
"bar": [
|
|
44
|
+
{ "value": 2, "label": "처리 완료", "tone": "done" },
|
|
45
|
+
{ "value": 1, "label": "확인 요청", "tone": "wait" }
|
|
46
|
+
],
|
|
47
|
+
"bar_alt": "처리 완료 2건과 확인 요청 1건의 분포",
|
|
48
|
+
"bar_note": "「처리 완료」는 운영 반영까지 확인한 것을 뜻함. 남은 1건은 01절에 모아 둠.",
|
|
49
|
+
"stats": [
|
|
50
|
+
{ "value": "1", "unit": "가지", "label": "확인·처리가 필요한 것", "accent": true, "href": "#sec-1", "go": "01절로" },
|
|
51
|
+
{ "value": "2", "unit": "건", "label": "이번 회차에 끝난 것", "href": "#sec-4", "go": "04절로" },
|
|
52
|
+
{ "value": "2", "unit": "종", "label": "적용한 확인 등급" }
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
"grades": {
|
|
56
|
+
"운영 환경 확인": ["m-live", "운영 환경에서 실제로 동작시켜 결과까지 확인함"],
|
|
57
|
+
"실데이터 대조": ["m-data", "운영 데이터를 건별로 대조해 확인함. 화면 재현은 미실시"]
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
"parts": [
|
|
61
|
+
{ "no": "1", "title": "요청", "tone": "you",
|
|
62
|
+
"lede": "확인·처리가 필요한 사항. 이 PART 가 수신 측 조치 사항의 전부임.",
|
|
63
|
+
"questions": ["무엇을 확인해야 하는가?", "그 판단의 근거는 어디에 있는가?"] },
|
|
64
|
+
{ "no": "2", "title": "경과", "tone": "neutral",
|
|
65
|
+
"lede": "이번 회차에 처리한 것과 그 근거.",
|
|
66
|
+
"questions": ["무엇이 어떻게 달라졌는가?", "어디까지 확인한 것인가?"] },
|
|
67
|
+
{ "no": "3", "title": "한계", "tone": "hold",
|
|
68
|
+
"lede": "이번 확인이 닿지 않은 범위. 다음 회차로 넘기는 몫임.",
|
|
69
|
+
"questions": ["확인하지 않은 것은 무엇인가?", "그것은 언제 확인되는가?"] }
|
|
70
|
+
],
|
|
71
|
+
|
|
72
|
+
"groups": [
|
|
73
|
+
{ "key": "reported", "no": "4", "name": "접수하신 건" },
|
|
74
|
+
{ "key": "own", "no": "5", "name": "함께 발견해 처리한 건" }
|
|
75
|
+
],
|
|
76
|
+
|
|
77
|
+
"sections": [
|
|
78
|
+
{ "no": "01", "id": "sec-1", "label": "01", "name": "지금 확인이 필요한 것", "kind": "asks", "part": "1",
|
|
79
|
+
"lede": "총 건수 표시 1건에 대해 확인 필요.",
|
|
80
|
+
"sub": "수신 측 조치 사항의 전부",
|
|
81
|
+
"short": "확인 요청",
|
|
82
|
+
"marg": {
|
|
83
|
+
"why": "무엇을 확인해야 하는가?",
|
|
84
|
+
"key": "이 절이 조치 사항의 전부임. 나머지 절은 근거.",
|
|
85
|
+
"advice": "각 건의 「근거」 단추는 04·05절 상세로 연결됨."
|
|
86
|
+
} },
|
|
87
|
+
|
|
88
|
+
{ "no": "02", "id": "sec-2", "label": "02", "name": "이번 회차 요지", "kind": "summary", "part": "2",
|
|
89
|
+
"lede": "접수 1건과 자체 발견 1건의 처리 결과 요약.",
|
|
90
|
+
"sub": "네 문단으로 훑는 이번 회차",
|
|
91
|
+
"short": "요지",
|
|
92
|
+
"marg": {
|
|
93
|
+
"why": "이번 회차에 무엇이 달라졌는가?",
|
|
94
|
+
"key": "두 건 모두 운영 반영까지 확인함.",
|
|
95
|
+
"advice": "각 문단 끝의 「해당 절」이 근거의 출처."
|
|
96
|
+
} },
|
|
97
|
+
|
|
98
|
+
{ "no": "03", "id": "sec-3", "label": "03", "name": "두 건 한눈에", "kind": "status", "part": "2",
|
|
99
|
+
"lede": "처리한 두 건의 상태와 확인 방법 대조.",
|
|
100
|
+
"sub": "상태 · 확인 방법 대조표",
|
|
101
|
+
"short": "한눈에",
|
|
102
|
+
"board_title": "두 건의 처리 결과와 확인 방법",
|
|
103
|
+
"board_hint": "항목을 누르면 해당 건의 상세로 이동함.",
|
|
104
|
+
"note": "「확인 방법」의 판정 기준은 부록 1에 있음.",
|
|
105
|
+
"marg": {
|
|
106
|
+
"why": "어디까지 확인한 것인가?",
|
|
107
|
+
"key": "두 건 모두 운영 반영까지 확인함.",
|
|
108
|
+
"advice": "판정 기준은 부록 1."
|
|
109
|
+
} },
|
|
110
|
+
|
|
111
|
+
{ "no": "04", "id": "sec-4", "label": "04", "name": "접수하신 1건", "kind": "cases", "group": "reported", "part": "2",
|
|
112
|
+
"lede": "제보해 주신 건. 증상·원인·조치와 직접 확인 가능한 방법을 기재함.",
|
|
113
|
+
"sub": "제보해 주신 건의 처리 상세",
|
|
114
|
+
"short": "접수 건",
|
|
115
|
+
"marg": {
|
|
116
|
+
"why": "접수하신 건은 어떻게 되었는가?",
|
|
117
|
+
"key": "저장이 일어나는 모든 경로를 수정함.",
|
|
118
|
+
"advice": "확인 방법은 표의 해당 행."
|
|
119
|
+
} },
|
|
120
|
+
|
|
121
|
+
{ "no": "05", "id": "sec-5", "label": "05", "name": "함께 발견해 처리한 1건", "kind": "cases", "group": "own", "part": "2",
|
|
122
|
+
"tone": "wait",
|
|
123
|
+
"lede": "접수 건 확인 과정에서 함께 발견해 처리한 건. 접수 목록에는 없던 건임.",
|
|
124
|
+
"sub": "자체 발견분",
|
|
125
|
+
"short": "자체 발견",
|
|
126
|
+
"marg": {
|
|
127
|
+
"why": "요청하지 않은 것을 왜 고쳤는가?",
|
|
128
|
+
"key": "같은 화면에서 함께 드러난 건이라 함께 처리함.",
|
|
129
|
+
"advice": "03절 대조표와 함께 읽을 것."
|
|
130
|
+
} },
|
|
131
|
+
|
|
132
|
+
{ "no": "06", "id": "sec-6", "label": "06", "name": "이번 확인의 한계", "kind": "note", "part": "3",
|
|
133
|
+
"lede": "확인이 닿은 범위와 닿지 않은 범위.",
|
|
134
|
+
"sub": "확인하지 않은 범위",
|
|
135
|
+
"short": "한계",
|
|
136
|
+
"count": "3",
|
|
137
|
+
"count_unit": "가지",
|
|
138
|
+
"limit_label": "이번 회차에서 확인하지 않은 것",
|
|
139
|
+
"paragraphs": [
|
|
140
|
+
"확인 범위는 두 건이 일어나는 주문 목록 화면까지임. 같은 데이터를 쓰는 다른 화면은 대조하지 않음.",
|
|
141
|
+
"§5.1 의 속도 개선은 운영 데이터 대조까지임. 화면을 실제로 열어 재현하지는 않음.",
|
|
142
|
+
"총 건수 표시는 기준을 아직 못 맞춘 상태임. 01절 회신을 받은 뒤 다시 확인 예정."
|
|
143
|
+
],
|
|
144
|
+
"marg": {
|
|
145
|
+
"why": "이 보고가 확인하지 않은 것은 무엇인가?",
|
|
146
|
+
"key": "확인 범위는 주문 목록 화면 한 곳까지임.",
|
|
147
|
+
"advice": "판정 기준은 부록 1."
|
|
148
|
+
} }
|
|
149
|
+
],
|
|
150
|
+
|
|
151
|
+
"summary": [
|
|
152
|
+
{ "lead": "접수해 주신 1건은 조치를 마치고 운영에 반영함.",
|
|
153
|
+
"body": "저장 순서가 어긋나 목록에 반영되지 않던 상황이었음(§4.1). 확인 방법은 각 건에 기재함.",
|
|
154
|
+
"refs": ["4.1"] },
|
|
155
|
+
{ "lead": "확인 과정에서 목록 속도 문제를 함께 찾아 처리함.",
|
|
156
|
+
"body": "주문이 많은 지점에서 첫 화면이 10초 넘게 걸리던 것을 0.3초로 줄임(§5.1). 접수 목록에는 없던 건임.",
|
|
157
|
+
"refs": ["5.1"] },
|
|
158
|
+
{ "lead": "두 건은 같은 화면에서 함께 드러난 것임.",
|
|
159
|
+
"body": "목록을 그리는 자리가 하나라, 저장 반영과 표시 속도가 같은 지점에서 갈렸음(§4.1). 그래서 함께 처리함.",
|
|
160
|
+
"refs": ["4.1"] },
|
|
161
|
+
{ "lead": "확인이 필요한 것은 1건임.",
|
|
162
|
+
"body": "총 건수 표시가 실제와 다르게 나올 수 있어 확인 필요(§5.1). 01절에 모아 둠.",
|
|
163
|
+
"refs": ["5.1"] }
|
|
164
|
+
],
|
|
165
|
+
|
|
166
|
+
"open_items": [
|
|
167
|
+
{ "title": "목록 위 총 건수 표시 확인 필요",
|
|
168
|
+
"priority": "must",
|
|
169
|
+
"body": "총 건수가 예전 데이터 때문에 실제보다 몇 건 많게 나올 수 있는 상황. 목록에 표시되는 내용 자체는 정상임. 실제 수치와 대조해 알려 주시면 기준을 맞출 예정.",
|
|
170
|
+
"refs": ["5.1"] }
|
|
171
|
+
],
|
|
172
|
+
|
|
173
|
+
"cta": [
|
|
174
|
+
"검수는 각 건에 기재한 <strong>확인 방법</strong>대로 한 번씩 열어 보는 것으로 충분함."
|
|
175
|
+
],
|
|
176
|
+
|
|
177
|
+
"appendix": [
|
|
178
|
+
{ "no": "부록 1", "title": "「확인 방법」 판정 기준",
|
|
179
|
+
"rows": [
|
|
180
|
+
["운영 환경 확인", "운영 환경에서 실제로 동작시켜 결과까지 확인함. 가장 강한 근거."],
|
|
181
|
+
["실데이터 대조", "운영 데이터를 건별로 대조해 확인함. 화면 재현은 미실시."]
|
|
182
|
+
],
|
|
183
|
+
"note": "이 기준은 03절 대조표의 「확인 방법」 열에 그대로 적용됨." }
|
|
184
|
+
],
|
|
185
|
+
|
|
186
|
+
"colophon": {
|
|
187
|
+
"rows": [
|
|
188
|
+
["발행", "우리회사"],
|
|
189
|
+
["문서번호", "LIM-2026-0824-01"],
|
|
190
|
+
["수신", "예시 고객사 검수 담당"],
|
|
191
|
+
["회신", "이 문서를 보낸 담당자에게 회신"]
|
|
192
|
+
],
|
|
193
|
+
"mark": "이 문서는 2026년 8월 24일 기준임.",
|
|
194
|
+
"badge": "고객 공유용",
|
|
195
|
+
"note": "※ 이 문서에는 입력·저장 기능이 없음. 검수 결과는 별도 회신 필요.",
|
|
196
|
+
"tail": "이후 진행 사항은 차기 회차 보고로 공유 예정."
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
"forbid": ["티켓", "스프린트", "레포지토리", "커밋", "(?<!나)머지"]
|
|
55
200
|
}
|
|
@@ -41,7 +41,10 @@ import sys
|
|
|
41
41
|
DEFAULT_REPO = pathlib.Path.home() / 'project/FCG/report-portal'
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
# 관보 전환(2026-08-24) 뒤로는 gazette 계열이 렌더러의 본체다 — 그 사본도 막는다.
|
|
45
|
+
# 옛 서식 파일도 목록에 남긴다: 다시 생기면 그것도 「사본이 갈라진다」의 재발이다.
|
|
46
|
+
FORKABLE = ('build.py', 'gazette.py', 'gazette.css', 'sentbreak.py', 'boundary.py',
|
|
47
|
+
'report.css', 'lim-report.css', 'palette.py', 'editor.js', 'editor.css', 'fonts')
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
def refuse_fork() -> None:
|
|
@@ -105,15 +108,24 @@ def templates_from_server(repo: pathlib.Path) -> dict:
|
|
|
105
108
|
lim = re.search(r"'lim-report'\s*=>\s*\[(.*?)\n \],", body, re.S)
|
|
106
109
|
css = re.findall(r"'([\w.-]+\.css)'", lim.group(1)) if lim else []
|
|
107
110
|
family = re.search(r"'family'\s*=>\s*'([^']+)'", lim.group(1)) if lim else None
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
# ⚠ 굵기는 **범위**일 수 있다(가변 폰트: `'300 700'`). `(\d+)` 로만 받으면 공백에서
|
|
112
|
+
# 끊겨 정규식이 통째로 안 맞고, 그러면 faces 가 비어 **폰트가 안 실린 문서**가 나간다.
|
|
113
|
+
# 육안으로는 「글꼴이 좀 다르네」로만 보이는 종류의 사고다(2026-08-24 에 실제로 밟았다).
|
|
114
|
+
faces = re.findall(r"'file'\s*=>\s*'([^']+)'.*?'weight'\s*=>\s*'([\d ]+)'", lim.group(1)) if lim else []
|
|
115
|
+
# 라이선스 문구는 PHP 에서 `. '…'` 로 여러 줄에 걸쳐 이어진다. 첫 조각만 받으면
|
|
116
|
+
# 「… SIL Open Font License 1.1 ·」 에서 잘려 **출처 URL 이 없는 고지**가 나간다.
|
|
117
|
+
_lic = re.search(r"'license'\s*=>\s*((?:'[^']*'\s*\.?\s*)+)", lim.group(1)) if lim else None
|
|
118
|
+
lic = ''.join(re.findall(r"'([^']*)'", _lic.group(1))) if _lic else ''
|
|
119
|
+
# 렌더 경로도 서식이 정한다 — 안 실어 오면 관보 서식 문서가 옛 렌더러로 구워진다.
|
|
120
|
+
render = re.search(r"'render'\s*=>\s*'([a-z-]+)'", lim.group(1)) if lim else None
|
|
110
121
|
return {
|
|
111
122
|
'names': list(out),
|
|
112
123
|
'lim-report': {
|
|
113
124
|
'css': css,
|
|
125
|
+
**({'render': render.group(1)} if render else {}),
|
|
114
126
|
'font': {
|
|
115
127
|
'family': family.group(1) if family else '',
|
|
116
|
-
'license':
|
|
128
|
+
'license': lic,
|
|
117
129
|
'faces': [{'file': f, 'weight': w} for f, w in faces],
|
|
118
130
|
},
|
|
119
131
|
},
|
|
@@ -121,13 +133,13 @@ def templates_from_server(repo: pathlib.Path) -> dict:
|
|
|
121
133
|
|
|
122
134
|
|
|
123
135
|
def main() -> None:
|
|
124
|
-
ap = argparse.ArgumentParser(
|
|
136
|
+
ap = argparse.ArgumentParser(
|
|
137
|
+
description='단일 HTML 렌더 — 서버 서식을 읽어 관보로 굽는다 (레포 렌더러 호출)')
|
|
125
138
|
ap.add_argument('config', help='report.json')
|
|
126
139
|
# 기본은 브랜드 서식이다 — 서버의 REPORT_TEMPLATE_DEFAULT 와 같은 값으로 맞춘다.
|
|
127
140
|
# 어긋나면 「스킬로 만든 문서」와 「MCP 로 올린 문서」의 서식이 갈린다.
|
|
128
141
|
ap.add_argument('--template', default='lim-report',
|
|
129
|
-
help='lim-report(
|
|
130
|
-
ap.add_argument('--palette', default=None, help='팔레트 이름(lim-report 는 기본 lim)')
|
|
142
|
+
help='lim-report(기본·관보 서식). 서버 목록에 있는 이름만 받는다')
|
|
131
143
|
args = ap.parse_args()
|
|
132
144
|
|
|
133
145
|
refuse_fork()
|
|
@@ -140,11 +152,10 @@ def main() -> None:
|
|
|
140
152
|
t = templates_from_server(repo)
|
|
141
153
|
if args.template not in t['names']:
|
|
142
154
|
sys.exit(f"모르는 서식입니다: {args.template} (있는 것: {'|'.join(t['names'])})")
|
|
155
|
+
# 서식이 render·css·폰트를 전부 정한다. 여기서 고를 것이 없다 —
|
|
156
|
+
# 관보는 팔레트 토큰을 안 쓴다(report_publish.php 가 palette.py 를 지문에서 뺐다).
|
|
143
157
|
if args.template == 'lim-report':
|
|
144
158
|
cfg.update(t['lim-report'])
|
|
145
|
-
cfg.setdefault('palette', 'lim')
|
|
146
|
-
if args.palette:
|
|
147
|
-
cfg['palette'] = args.palette
|
|
148
159
|
|
|
149
160
|
# 서식을 얹은 설정으로 새로 쓴다 — 원본 report.json 은 안 건드린다.
|
|
150
161
|
merged = cfg_path.parent / f'.render-{args.template}.json'
|
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
왜 이런 모양인가
|
|
8
8
|
· 산출물은 외부 리소스 0(단일 HTML)이 요건이라 웹폰트 CDN 을 쓸 수 없다.
|
|
9
9
|
그래서 base64 로 문서 안에 심는다 — 용량이 곧 파일 크기다.
|
|
10
|
-
· **가변 폰트를 wght
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
· **가변 폰트를 wght 300–700 으로 잘라** 한 벌로 쓴다. 관보 서식이 300 을 쓰기
|
|
11
|
+
때문이다 — 표지 대제목 둘째 행과 간지 로마숫자가 `--w-light: 300` 이다.
|
|
12
|
+
400–700 로 자르면 그 둘이 400 으로 눌린다.
|
|
13
|
+
가변 한 벌은 **409,044B**(`pretendard-gazette.woff2`). 굵기마다 정적 면을 심으면
|
|
14
|
+
그보다 커지고, CSS 의 중간값(`font-weight:650`)도 그 굵기로 안 나온다.
|
|
13
15
|
· 커버리지는 **KS X 1001 완성형 2,350자**. 문서에 지금 쓰인 글자만 넣으면 60KB 로
|
|
14
|
-
줄지만,
|
|
15
|
-
|
|
16
|
+
줄지만, 포털 편집기(`/edit`)에서 사람이 새로 타이핑한 글자가 다른 폰트로 떨어진다.
|
|
17
|
+
반대로 현대 한글 11,172자를 다 넣으면 1.19MB 로 커진다.
|
|
16
18
|
2,350자는 일반 한국어 산문을 사실상 전부 덮는다.
|
|
17
19
|
· `tnum` 피처를 반드시 남긴다. 지표·표에서 `font-variant-numeric:tabular-nums`
|
|
18
20
|
를 쓰는데, 레이아웃 피처를 통째로 버리면 자릿수 정렬이 조용히 죽는다.
|
|
@@ -25,8 +27,13 @@ packages/pretendard/dist/public/variable/PretendardVariable.ttf
|
|
|
25
27
|
|
|
26
28
|
사용
|
|
27
29
|
.fontenv/bin/python subset_font.py PretendardVariable.ttf \
|
|
28
|
-
~/project/FCG/report-portal/private/builder/fonts/pretendard-
|
|
30
|
+
~/project/FCG/report-portal/private/builder/fonts/pretendard-gazette.woff2
|
|
29
31
|
.fontenv/bin/python subset_font.py PretendardVariable.ttf out.woff2 --coverage=full
|
|
32
|
+
|
|
33
|
+
폰트를 고를 자리는 없다
|
|
34
|
+
어느 폰트를 심을지는 **서식이 정한다**(`report_publish.php` 의 REPORT_TEMPLATES).
|
|
35
|
+
발행에도 렌더에도 폰트 인자가 없다.
|
|
36
|
+
이 스크립트는 그 서식이 가리키는 파일을 다시 만들 때만 쓴다.
|
|
30
37
|
"""
|
|
31
38
|
import argparse, pathlib, subprocess, sys, tempfile
|
|
32
39
|
|
|
@@ -45,15 +52,24 @@ def ksx1001():
|
|
|
45
52
|
out.append(c)
|
|
46
53
|
return out
|
|
47
54
|
|
|
55
|
+
# ⚠ 여기 빠진 글자는 두부(□)로 깨진다. 특히 CSS `content:` 로 넣는 기호는
|
|
56
|
+
# 본문에 안 나타나므로 coverage=used 로도 안 걸린다 — 반드시 손으로 지킨다.
|
|
57
|
+
# gazette.css 가 요구하는 것(2026-08-24 실측):
|
|
58
|
+
# \2713 ✓ .notice[success] · .ask__check \203B ※ .note
|
|
59
|
+
# \25A0 ■ .tag--done \25A1 □ .tag--wait
|
|
60
|
+
# \25CF ● .tag--live \25D0 ◐ .tag--data
|
|
61
|
+
# \2014 — .tag--soon \2192 → .tally__go
|
|
48
62
|
EXTRA = (''.join(chr(c) for c in range(0x20, 0x7F))
|
|
49
|
-
+ '·–—“”‘’↑→↓↗←−…※○●◦「」『』€₩©®°±×÷≤≥≠№㎡'
|
|
63
|
+
+ '·–—“”‘’↑→↓↗←−…※○●◦「」『』€₩©®°±×÷≤≥≠№㎡'
|
|
64
|
+
+ '✓■□◐')
|
|
50
65
|
|
|
51
66
|
def main():
|
|
52
67
|
ap = argparse.ArgumentParser()
|
|
53
68
|
ap.add_argument('src'); ap.add_argument('dst')
|
|
54
69
|
ap.add_argument('--coverage', choices=['ksx1001', 'full', 'used'], default='ksx1001')
|
|
55
70
|
ap.add_argument('--used-from', help='coverage=used 일 때 글자를 긁어올 파일')
|
|
56
|
-
|
|
71
|
+
# 관보 서식이 300 을 쓴다(--w-light). 400 부터 자르면 표지 둘째 행과 간지 숫자가 눌린다.
|
|
72
|
+
ap.add_argument('--wght', default='300:700', help='가변 축 범위. 단일 굵기면 "400"')
|
|
57
73
|
ap.add_argument('--python', default=sys.executable)
|
|
58
74
|
a = ap.parse_args()
|
|
59
75
|
|