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,751 +0,0 @@
|
|
|
1
|
-
# 위지윅 편집기 계약 (report-editor)
|
|
2
|
-
|
|
3
|
-
산출물 HTML 안에 함께 실려 나가는 **인-플레이스 편집 레이어**의 사양이다.
|
|
4
|
-
문서를 브라우저에서 열어 문장을 고치고, 블록을 옮기고, 절을 지우고, 내부 전용 영역을
|
|
5
|
-
표시한 뒤, **원본 파일 자체에 덮어쓰거나** **고객용 사본을 내보내는** 것까지가 범위다.
|
|
6
|
-
|
|
7
|
-
이 문서를 읽어야 하는 때는 셋이다.
|
|
8
|
-
|
|
9
|
-
1. 이 스킬이 만드는 문서의 마크업을 바꿀 때 (`build.py` 의 `case_html`·`toc_items`·`vrow` 를 손댈 때)
|
|
10
|
-
2. 편집기를 **다른 문서 종류**에 붙일 때
|
|
11
|
-
3. "목차가 비었다 / 판정표가 날아갔다 / 고객본에 편집기가 남았다" 같은 증상을 만났을 때
|
|
12
|
-
|
|
13
|
-
관련 자산
|
|
14
|
-
|
|
15
|
-
| 파일 | 역할 |
|
|
16
|
-
|---|---|
|
|
17
|
-
| `assets/editor.js` | 편집 레이어 전체. IIFE 하나, 외부 의존성 0 |
|
|
18
|
-
| `assets/editor.css` | 편집 레이어 스타일 |
|
|
19
|
-
| `assets/build.py` | 문서 CSS · 편집기 CSS · 편집기 JS 를 한 HTML 로 조립 |
|
|
20
|
-
| `assets/report.css` | 문서 구조 스타일시트. 편집기가 만드는 클래스의 정의를 소유 |
|
|
21
|
-
| `assets/palette.py` | 색 토큰 유도기. 편집기가 참조하는 토큰도 여기서 나온다 |
|
|
22
|
-
| `assets/qa.py` | 산출물 정적 검수. **고객본 내보내기를 시뮬레이션해서 검사한다** |
|
|
23
|
-
|
|
24
|
-
이 문서에 나오는 `article.case`, `table.verdict`, `.method`, `.r-open .val` 같은 이름은
|
|
25
|
-
**"수정 내역 보고"라는 문서 종류의 도메인 어휘**다. 계약인 것과 예시인 것은 9장 치환표에서 갈랐다.
|
|
26
|
-
본문에 나오는 고객사·수행사 이름은 이 스킬을 처음 만든 실사례의 고유명사이며 예시일 뿐이다.
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## 1. 설계 원칙 — 코드 전체를 관통하는 셋
|
|
31
|
-
|
|
32
|
-
이 셋을 어기는 변경은 되돌려야 한다. 나머지 규칙은 전부 이 셋의 파생이다.
|
|
33
|
-
|
|
34
|
-
### 원칙 1 — 편집기는 문서의 손님이다
|
|
35
|
-
|
|
36
|
-
편집기는 문서에 **얹히는** 것이지 문서의 일부가 아니다. 세 겹으로 강제된다.
|
|
37
|
-
|
|
38
|
-
| 겹 | 수단 | 효과 |
|
|
39
|
-
|---|---|---|
|
|
40
|
-
| 실행 | `editor.js` 첫머리의 조기 반환 | `data-report-edit` 이 `"on"` 이 아니면 즉시 종료 |
|
|
41
|
-
| 스타일 | `editor.css` 의 모든 선택자가 `body[data-report-edit="on"]` 하위이거나 `[data-rte-ui]` 한정 | 속성이 없으면 어떤 요소에도 안 걸린다 |
|
|
42
|
-
| 격리 | IIFE + 의존성 0 + 외부 리소스 0 | 전역 오염 없음. 단일 HTML 원칙이 깨지지 않는다 |
|
|
43
|
-
|
|
44
|
-
```js
|
|
45
|
-
if (document.body.getAttribute('data-report-edit') !== 'on') return;
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
**근거** — 이 스킬의 산출물은 "고객에게 그대로 보내는 파일"이다. 편집기가 문서에 스며들면
|
|
49
|
-
고객본에서 무엇을 지워야 하는지 기계적으로 답할 수 없게 된다.
|
|
50
|
-
**어기면** — 고객본에 편집 UI 가 튀어나오거나, 편집기를 지웠더니 문서 레이아웃이 무너진다.
|
|
51
|
-
|
|
52
|
-
### 원칙 2 — 파생물은 손으로 고치지 않는다
|
|
53
|
-
|
|
54
|
-
절 번호·목차 항목·처리 현황표는 **저자가 쓰는 것이 아니라 문서에서 계산되는 것**이다.
|
|
55
|
-
구조가 바뀔 때마다 `renumber() → syncToc() → syncVerdict()` 가 셋을 다시 만든다.
|
|
56
|
-
`editor.js` 의 주석이 이유를 적어 두었다.
|
|
57
|
-
|
|
58
|
-
```js
|
|
59
|
-
// 절을 옮기거나 지우면 절 번호와 목차, 판정표가 어긋난다. 구조가 바뀔 때마다
|
|
60
|
-
// 문서 자신을 근거로 셋을 다시 만든다 — 손으로 맞추면 반드시 틀어진다.
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**어기면** — 파생 영역에 손으로 넣은 내용은 **다음 재생성에서 조용히 사라진다.** 4장 참조.
|
|
64
|
-
|
|
65
|
-
### 원칙 3 — 편집기 어휘와 문서 어휘를 분리한다
|
|
66
|
-
|
|
67
|
-
편집기가 DOM 에 남기는 이름은 전부 `data-rte-*` 접두사이거나 `[data-rte-ui]` 컨테이너 안이다.
|
|
68
|
-
문서 마크업에 편집기 전용 클래스를 심지 않는다. 인라인 서식조차 문서 자신의 의미 클래스만 쓴다 —
|
|
69
|
-
"굵게"는 `<strong>`, "강조"는 `<span class="ev">`.
|
|
70
|
-
|
|
71
|
-
`editor.js` 가 쓰는 `data-rte-*` 는 정확히 여섯 개다.
|
|
72
|
-
|
|
73
|
-
| 이름 | 소유자 | 성격 |
|
|
74
|
-
|---|---|---|
|
|
75
|
-
| `data-rte-ui` | 편집기 | 런타임 UI 표식. 내보내기에서 **요소째** 제거 |
|
|
76
|
-
| `data-rte-hot` | 편집기 | 호버 표식. 휘발성 (5장) |
|
|
77
|
-
| `data-rte-preview` | 편집기 | body 의 미리보기 상태 |
|
|
78
|
-
| `data-rte-src` | **조립기** | 편집기 소스 노드 표식. 고객본에서 요소째 제거 |
|
|
79
|
-
| `data-rte-standalone` | **문서** | 절에 속하지 않는 구조물 (3장) |
|
|
80
|
-
| `data-rte-derived` | **문서** | 자동 재생성되는 표 (4장) |
|
|
81
|
-
|
|
82
|
-
뒤의 셋은 **문서가 소유한다.** 고객본에도 그대로 남는다(남아야 한다 — 문서 구조의 일부다).
|
|
83
|
-
실제로 고객본을 시뮬레이션하면 `data-rte-derived`, `data-rte-standalone` 둘만 살아남는다.
|
|
84
|
-
|
|
85
|
-
> **왜 중요한가** — 이 분리 덕분에 `serialize()` 가 "무엇을 지워야 편집기 흔적이 사라지는가"를
|
|
86
|
-
> 선택자 몇 줄로 답할 수 있다. 접두사가 섞이면 지울 것과 남길 것을 구분할 수 없어진다.
|
|
87
|
-
|
|
88
|
-
---
|
|
89
|
-
|
|
90
|
-
## 2. 마크업 계약
|
|
91
|
-
|
|
92
|
-
편집기가 **읽는 것**(문서가 제공할 의무)과 **쓰는 것**(문서 CSS 가 받아 줄 의무)을 나눠 본다.
|
|
93
|
-
|
|
94
|
-
### 2-A. 문서가 반드시 제공해야 하는 것
|
|
95
|
-
|
|
96
|
-
| 선택자 / 속성 | 편집기 쪽 사용처 | 없거나 틀리면 나는 증상 |
|
|
97
|
-
|---|---|---|
|
|
98
|
-
| `body[data-report-edit="on"]` | 활성화 스위치 | `off` 이거나 없으면 **아무 일도 안 일어난다. 오류도 콘솔 로그도 없다.** 고객본은 이 속성을 제거해 무력화한다 |
|
|
99
|
-
| `main` (단수) | `container` | `if (!container) return` 으로 조용히 종료 — 하단 바조차 안 뜬다. 증상이 "속성은 켰는데 화면에 아무것도 없음"이라 원인 추적이 어렵다 |
|
|
100
|
-
| `nav.toc` | `toc` | 없으면 `syncToc()` 이 즉시 반환. 나머지는 정상 동작 |
|
|
101
|
-
| `nav.toc ol` — 절당 1개 | `syncToc()` | `h2[data-secno]` 와 **문서 순서로 index 짝짓기.** 개수·순서가 어긋나면 다른 절의 목록이 들어가거나 빈다. **예외 없이 조용히 틀어진다** |
|
|
102
|
-
| `h2.sec` | `group()`, `prevGroup()`, `nextGroup()` | `.sec` 가 없으면 절이 그룹으로 안 묶여, 제목만 혼자 움직이고 본문이 뒤에 남는다 |
|
|
103
|
-
| `h2[data-secno]` | `renumber()`, `secNumbers()` | 없는 `h2` 는 **번호를 0 으로 리셋한다**(아래 주의). 목차 `ol` 짝짓기 대상에서도 빠진다 |
|
|
104
|
-
| `article.case` | `renumber()`, `syncToc()`, `syncVerdict()` | `renumber()` 는 `classList.contains('case')` 만 보고, 나머지 둘은 `article.case` 를 본다. **`article` 이 아니면 번호는 매겨지는데 목차·판정표에서만 사라진다** |
|
|
105
|
-
| `article.case .num` | `renumber()` | `data-sec`·`id` 는 갱신되는데 **화면 숫자만 옛 값**으로 남는다 |
|
|
106
|
-
| `article.case h3` | `syncToc()`, `syncVerdict()` | 목차 항목명·판정표 건명 칸이 **빈칸으로 덮어써진다** |
|
|
107
|
-
| `article.case .method` | `syncVerdict()` — `outerHTML` 을 **복사** | 판정표 등급 칸이 빈다. 칩 색이 부모 문맥에 의존하면 복사본에서 색이 사라진다 → **칩은 자기 class 만으로 완결돼야 한다** |
|
|
108
|
-
| `article.case .r-open .val` | `syncVerdict()` — 텍스트 유무로 있음/없음 판정 | 선택자가 어긋나면 **전건이 "—"로 덮어써진다** |
|
|
109
|
-
| `table.verdict tbody` | `syncVerdict()` | 없으면 즉시 반환. **열 순서는 `syncVerdict()` 안에 하드코딩** — 마크업만 바꾸면 열이 어긋난다 |
|
|
110
|
-
| `table.verdict[data-rte-derived]` | `place()` | 없으면 자동 재생성되는 표에 행 편집 버튼이 붙는다 → 손으로 넣은 행이 다음 재생성에서 사라진다 |
|
|
111
|
-
| `[data-rte-standalone]` | `isStandalone()` | 3장 참조. 없으면 마지막 절이 꼬리말을 삼킨다 |
|
|
112
|
-
| `[data-rte-ui]` | `isBlock()` 에서 제외 | `main` 안에 자체 UI 를 넣을 때 이게 없으면 블록으로 오인돼 핸들이 붙고 **내보내기에 그 UI 가 그대로 실린다** |
|
|
113
|
-
| `[data-rte-src]` | `serialize(true)` | 안 붙이면 고객본에 편집기 CSS/JS 가 통째로 남는다. 조립기가 붙인다(7장) |
|
|
114
|
-
|
|
115
|
-
`syncVerdict()` 가 만드는 행의 실제 모양 — 열 순서가 여기 박혀 있다.
|
|
116
|
-
|
|
117
|
-
```js
|
|
118
|
-
return '<tr><td class="c-sec">' + esc(a.dataset.sec || '') + '</td>' +
|
|
119
|
-
'<td>' + esc(h ? h.textContent : '') + '</td>' +
|
|
120
|
-
'<td class="c-m">' + (m ? m.outerHTML : '') + '</td>' +
|
|
121
|
-
'<td class="c-o" data-open="' + (has ? 1 : 0) + '">' + (has ? '있음' : '—') + '</td></tr>';
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**재생성은 `innerHTML` 조립이므로 값을 반드시 이스케이프한다.** `build.py` 는 같은 값을 `e()`(파이썬
|
|
125
|
-
`html.escape`)로 감싸 넣는데, 편집기 쪽에서 안 하면 **같은 데이터가 두 경로에서 다르게 취급된다.**
|
|
126
|
-
제목에 `&` 나 `<` 를 한 글자만 쳐도 다음 재생성에서 목차 항목과 판정표 행의 마크업이 깨지거나
|
|
127
|
-
태그로 해석된다. 그래서 `esc()` 를 두고 `dataset.sec`·`textContent`·`id` 를 전부 통과시킨다.
|
|
128
|
-
|
|
129
|
-
실제로 확인한 방법: 건 제목을 `A&B <b>굵게</b> "따옴표" 시험` 로 바꾸고 목차·판정표를 본다.
|
|
130
|
-
문자 그대로 보이면 통과, `굵게` 가 굵어지면 이스케이프가 빠진 것이다.
|
|
131
|
-
|
|
132
|
-
`m.outerHTML` 만은 이스케이프하지 않는다 — 등급 칩 요소를 통째로 복제하는 자리라 의도된 예외다.
|
|
133
|
-
이 예외를 늘리지 마라. 문자열을 넣는 자리는 전부 `esc()` 를 거친다.
|
|
134
|
-
|
|
135
|
-
**여분 `<ol>` 도 비운다.** `syncToc()` 는 절 개수만큼만 덮어쓰므로, 절을 지우면 짝 잃은 `<ol>` 에
|
|
136
|
-
없어진 절의 항목이 그대로 남는다. 그래서 마지막에 남는 `<ol>` 들을 빈 문자열로 지운다.
|
|
137
|
-
|
|
138
|
-
**주의 — `data-secno` 없는 `h2` 는 번호 매김을 끊는다.** `renumber()` 는 이렇게 되어 있다.
|
|
139
|
-
|
|
140
|
-
```js
|
|
141
|
-
if (el.tagName === 'H2' && el.dataset.secno) { secNo = +el.dataset.secno; sub = 0; return; }
|
|
142
|
-
if (el.tagName === 'H2') { secNo = 0; return; }
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
즉 건이 없는 절(`요약`, `처리 현황`, `확인 요청`, 꼬리 안내)의 `h2` 는 **일부러 `data-secno` 를 안 붙여**
|
|
146
|
-
번호 매김의 벽으로 쓴다. `build.py` 도 `kind == 'cases'` 인 절에만 `data-secno` 를 찍는다.
|
|
147
|
-
실사례 문서의 `data-secno` 값은 `"3"`, `"4"` 둘뿐이다.
|
|
148
|
-
|
|
149
|
-
**주의 — `secNumbers()` 와 `renumber()` 의 탐색 범위가 다르다.**
|
|
150
|
-
`renumber()` 는 `container.children`(직계 자식만)을 훑고, `secNumbers()` 는
|
|
151
|
-
`container.querySelectorAll('h2[data-secno]')`(후손 전부)로 읽는다.
|
|
152
|
-
`h2[data-secno]` 를 래퍼 `div` 안에 넣으면 **목차 `ol` 은 짝지어지는데 그 절의 건에는 번호가 안 붙어**
|
|
153
|
-
목차가 빈다. 절 제목은 반드시 `main` 직계 자식이어야 한다.
|
|
154
|
-
|
|
155
|
-
### 2-B. 편집기가 써넣는 것 — 문서 CSS 가 받아야 하는 것
|
|
156
|
-
|
|
157
|
-
| 산출 마크업 | 만드는 함수 | 문서 CSS 가 준비해야 할 것 |
|
|
158
|
-
|---|---|---|
|
|
159
|
-
| `<li><a href="#{id}"><span class="i">번호</span><span class="t">제목</span></a></li>` | `syncToc()` | `.toc .i`, `.toc .t`. **`ol` 안의 다른 마크업은 첫 재생성에서 전부 소실된다** |
|
|
160
|
-
| `<tr><td class="c-sec">…<td>…<td class="c-m">…<td class="c-o" data-open="0\|1">` | `syncVerdict()` | `.c-sec`, `.c-m`, `.c-o`, `.c-o[data-open="1"]` |
|
|
161
|
-
| `<strong>` / `<span class="ev">` | `wrapSelection()` | **`.ev` 의 전역 정의 필수.** `report.css` 에 있다 — 주석까지 함께 옮길 것 |
|
|
162
|
-
| `id="s{n}-{m}"`, `data-sec="{n}.{m}"` | `renumber()` | 앵커 대상. 본문 상호참조 `href` 와 **같은 규칙**이어야 한다 |
|
|
163
|
-
| 행 추가 시 `<td class="…">` (기준 행의 className 복제) | `addRow()` | 열 클래스로 폭을 잡는 표라면 그대로 계승된다 |
|
|
164
|
-
| `[data-rte-hot]`, `body[data-rte-preview="customer"]` | `place()`, 하단 바 | `editor.css` 가 담당. **문서 CSS 는 건드리지 말 것** |
|
|
165
|
-
|
|
166
|
-
`report.css` 의 `.ev` 정의는 이렇게 되어 있다. 문맥별 재정의보다 **먼저** 전역 정의가 와야 한다.
|
|
167
|
-
|
|
168
|
-
```css
|
|
169
|
-
/* 편집기가 삽입할 수 있는 의미 클래스는 **문서 스타일시트에 전역 정의**가 있어야 한다.
|
|
170
|
-
.ev 는 편집기 "강조" 버튼이 만드는 클래스인데 전역 정의가 없으면 버튼이 먹통으로 보인다. */
|
|
171
|
-
.ev { background:var(--accent-bg); padding:.06em .22em; }
|
|
172
|
-
.ref .ev { background:none; color:var(--muted); padding:0; }
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
### 2-C. 편집 대상 태그와 편집 불가 영역
|
|
176
|
-
|
|
177
|
-
편집 대상은 상수 하나로 고정돼 있고, `wire()` 가 `container`(=`main`) 안에서만 부착한다.
|
|
178
|
-
|
|
179
|
-
```js
|
|
180
|
-
var EDITABLE = 'h1,h2,h3,h4,p,td,th,li,dd,dt';
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
**클릭해도 커서가 안 들어가는 것들** — 설계상 그렇다는 것을 알고 배치해야 한다.
|
|
184
|
-
|
|
185
|
-
| 영역 | 이유 | 실사례에서 해당하는 것 |
|
|
186
|
-
|---|---|---|
|
|
187
|
-
| `main` 밖 전부 | `wire()` 가 `container` 안만 훑는다 | 표지(`header.cover`)의 제목·부제·메타, 좌측 목차(`nav.toc`) 전체 |
|
|
188
|
-
| `EDITABLE` 목록 밖 태그 | 목록에 없다 | `figcaption`(도형 캡션), `blockquote`, `caption`, `h5`/`h6`, 순수 `span` |
|
|
189
|
-
| `span` 으로만 짜인 블록 | 위와 같음 | 지표 밴드 `.stat .v` / `.stat .k` — **숫자를 화면에서 못 고친다.** 지표를 바꾸려면 `report.json` 을 고쳐 다시 빌드한다 |
|
|
190
|
-
|
|
191
|
-
문서를 설계할 때 "내용을 담는 태그"를 이 목록에 맞추거나, 목록을 늘린다.
|
|
192
|
-
늘릴 때는 `serialize()` 가 `[contenteditable]` 전체에서 속성을 떼므로 별도 조치가 필요 없다.
|
|
193
|
-
|
|
194
|
-
### 2-D. 색 토큰 — 손으로 고르지 않는다
|
|
195
|
-
|
|
196
|
-
`editor.css` 는 문서 팔레트에서 **네 개 토큰만** 빌려 쓴다.
|
|
197
|
-
|
|
198
|
-
| 토큰 | 쓰이는 곳 |
|
|
199
|
-
|---|---|
|
|
200
|
-
| `--primary` | 편집 중인 요소의 포커스 아웃라인 |
|
|
201
|
-
| `--primary-hair` | 호버 표식(`[data-rte-hot]`)의 옅은 아웃라인 |
|
|
202
|
-
| `--accent-ink` | 내부 전용 표시(좌측 막대·라벨 배경), 수정됨 점, 내부 전용 개수, 경고 버튼 테두리 |
|
|
203
|
-
| `--accent-bg` | 내부 전용 블록의 사선 배경 |
|
|
204
|
-
|
|
205
|
-
**이 네 토큰은 문서가 손으로 선언하지 않는다.** `palette.py` 의 `derive()` 가 팔레트 6색
|
|
206
|
-
(Primary·Secondary·Background·Surface·Ink·Mid)에서 유도하고, `audit()` 이 17쌍의 대비를 되잰다.
|
|
207
|
-
`build.py` 는 유도 결과를 `:root` 로 만들어 `report.css` **앞에 붙인다**. `report.css` 는 색 토큰을
|
|
208
|
-
한 줄도 선언하지 않는다.
|
|
209
|
-
|
|
210
|
-
```python
|
|
211
|
-
tokens = P.derive(P.PALETTES[...])
|
|
212
|
-
bad = [r for r in P.audit(tokens) if not r[5]]
|
|
213
|
-
assert not bad, f'팔레트 대비 미달: ...'
|
|
214
|
-
root = ':root {\n' + '\n'.join(f' --{k}:{v};' for k, v in tokens.items())
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
**어기면** — 편집기 CSS 에 색을 직접 적으면 팔레트를 갈아끼웠을 때 편집 레이어만 옛 색으로 남고,
|
|
218
|
-
`palette.py` 의 대비 보장 밖으로 나간다. 새 토큰이 필요하면 `editor.css` 가 아니라 `palette.py`
|
|
219
|
-
의 `derive()` 에 추가하고 `audit()` 에 검사 한 줄을 같이 넣는다.
|
|
220
|
-
|
|
221
|
-
**예외 — 런타임 UI 크롬(핸들·인라인바·하단바·토스트)의 회색은 일부러 고정색이다.**
|
|
222
|
-
|
|
223
|
-
```css
|
|
224
|
-
/* 런타임 UI — 문서 활자와 섞이지 않게 계열을 분리 */
|
|
225
|
-
[data-rte-ui] { font-family: -apple-system, …; font-size: 12px; … }
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
버튼·바탕의 `#fff`·`#1b1f22`·`#c8ccd0` 류는 **문서 활자와 섞이지 않게 하려는 의도**다.
|
|
229
|
-
문서와 같은 팔레트로 칠하면 "이게 문서의 일부인가 도구인가"가 흐려진다.
|
|
230
|
-
그래서 `qa.py` 는 **편집기 CSS 구간만 색 검사에서 도려낸다.**
|
|
231
|
-
|
|
232
|
-
```python
|
|
233
|
-
EDITOR_CSS_MARK = '/* ── 편집 레이어'
|
|
234
|
-
i, j = doc.find(EDITOR_CSS_MARK), doc.find('</style>', doc.find(EDITOR_CSS_MARK))
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
마커를 못 찾으면 경고를 내고 문서 전체를 검사한다.
|
|
238
|
-
**`editor.css` 의 첫 줄 주석을 바꾸면 이 카브아웃이 어긋난다** — 바꿀 거면 `qa.py` 의
|
|
239
|
-
`EDITOR_CSS_MARK` 도 같이 바꾼다.
|
|
240
|
-
|
|
241
|
-
---
|
|
242
|
-
|
|
243
|
-
## 3. 블록 모델
|
|
244
|
-
|
|
245
|
-
조작 단위는 "블록"과 "그룹" 두 층이다. 이 정의를 모르면 ↑↓·삭제 버튼의 동작을 예측할 수 없다.
|
|
246
|
-
|
|
247
|
-
```js
|
|
248
|
-
function isBlock(el) {
|
|
249
|
-
return el && el.parentElement === container && !el.hasAttribute('data-rte-ui');
|
|
250
|
-
}
|
|
251
|
-
function isStandalone(el) {
|
|
252
|
-
return !!(el && el.nodeType === 1 && el.hasAttribute('data-rte-standalone'));
|
|
253
|
-
}
|
|
254
|
-
function group(el) {
|
|
255
|
-
if (!el.classList.contains('sec')) return [el];
|
|
256
|
-
var out = [el], n = el.nextElementSibling;
|
|
257
|
-
while (n && isBlock(n) && !n.classList.contains('sec') && !isStandalone(n)) {
|
|
258
|
-
out.push(n); n = n.nextElementSibling;
|
|
259
|
-
}
|
|
260
|
-
return out;
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
| 개념 | 규칙 |
|
|
265
|
-
|---|---|
|
|
266
|
-
| **블록** | `main` 의 **직계 자식**만. 중첩 요소는 `blockOf()` 가 조상으로 거슬러 올라가 블록으로 환원 |
|
|
267
|
-
| **그룹** | `.sec` 가 아니면 자기 자신 1개. `.sec` 이면 **다음 `.sec` 또는 standalone 직전까지** 전부 |
|
|
268
|
-
| **prevGroup** | standalone·컨테이너 시작에서 `null`. 일반 블록은 앞이 `.sec` 이면 `null`(절 경계를 넘지 않는다) |
|
|
269
|
-
| **nextGroup** | standalone 에서 `null`. 일반 블록은 뒤가 `.sec` 이면 `null` |
|
|
270
|
-
| **standalone** | 그룹의 벽이자 **핸들 자체가 안 붙는다**(`mouseover` 에서 `hide()`) |
|
|
271
|
-
|
|
272
|
-
> **"직계 자식만 블록"의 실무적 함의** — 편집 단위로 삼고 싶은 요소를 `div` 한 겹에 넣으면
|
|
273
|
-
> **그 `div` 가 통째로 하나의 블록**이 되어 개별 이동·삭제가 불가능해진다.
|
|
274
|
-
> 실사례에서 `<figure class="fig">`(도형)과 `<div class="tbl-wrap">`(가로 스크롤 표 래퍼)이
|
|
275
|
-
> 각각 블록 하나다 — 의도한 것이다. 반대로 건(`article.case`)은 절대 감싸면 안 된다.
|
|
276
|
-
> 레이아웃 때문에 래퍼가 필요하면 `main` 자체를 grid/flex 컨테이너로 만든다.
|
|
277
|
-
|
|
278
|
-
### `data-rte-standalone` 이 없으면 — 소스에 기록된 사고
|
|
279
|
-
|
|
280
|
-
`isStandalone()` 바로 위 주석이 사고를 그대로 적어 두었다.
|
|
281
|
-
|
|
282
|
-
```js
|
|
283
|
-
// 이게 없으면 마지막 절의 그룹이 컨테이너 끝까지 훑어 꼬리말까지 삼킨다.
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
`build.py` 는 꼬리말을 `main` **안** 마지막 직계 자식으로 넣으므로 표식을 함께 찍는다.
|
|
287
|
-
|
|
288
|
-
```python
|
|
289
|
-
body.append('<footer data-rte-standalone>' + …)
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
표식이 없으면 네 가지가 연쇄로 일어난다.
|
|
293
|
-
|
|
294
|
-
| # | 증상 | 메커니즘 |
|
|
295
|
-
|---|---|---|
|
|
296
|
-
| 1 | 꼬리말이 마지막 절의 일부가 된다 | `group()` 이 `nextElementSibling` 을 따라 컨테이너 끝까지 훑는다 |
|
|
297
|
-
| 2 | **절을 지우면 꼬리말이 함께 사라진다** | 확인 문구 `'이 절 전체(N개 블록)를 삭제합니다.'` 의 N 에 꼬리말이 포함되고 `g.forEach(n => n.remove())` 가 실행한다 |
|
|
298
|
-
| 3 | 절을 위로 옮기면 **꼬리말이 문서 중간으로 끌려 올라간다** | `move()` 가 그룹 전체를 fragment 에 담아 옮긴다 |
|
|
299
|
-
| 4 | 꼬리말에 마우스만 올려도 핸들이 붙는다 | `isStandalone()` 검사가 통과돼 개별 삭제·내부전용 지정이 가능해진다 |
|
|
300
|
-
|
|
301
|
-
> **일반화 규칙** — 절에 속하지 않는 구조물은 `main` **밖으로 빼거나**, 안에 둬야 한다면 반드시
|
|
302
|
-
> `data-rte-standalone` 을 붙인다. 후보: 꼬리말, 부록, 서명란, 배너 —
|
|
303
|
-
> **"어느 절에도 속하지 않는데 본문 흐름 안에 있는 것" 전부.**
|
|
304
|
-
|
|
305
|
-
---
|
|
306
|
-
|
|
307
|
-
## 4. 재생성 루프 — `renumber → syncToc → syncVerdict`
|
|
308
|
-
|
|
309
|
-
```
|
|
310
|
-
구조 변경
|
|
311
|
-
└─ renumber() 번호 재계산 (main 의 직계 자식을 문서 순서로 훑는다)
|
|
312
|
-
├─ syncToc() nav.toc 의 ol 을 절별로 재생성
|
|
313
|
-
└─ syncVerdict() table.verdict 의 tbody 재생성
|
|
314
|
-
└─ wire() 새로 생긴 td 에 contenteditable 재부착
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
진입점: `move()`(이동) · `remove()`(삭제) · `undo()`(되돌리기) · **최초 로드** ·
|
|
318
|
-
건의 `h3` 편집 종료(이때는 `syncToc()` + `syncVerdict()` 만).
|
|
319
|
-
|
|
320
|
-
> **최초 로드에서도 돈다는 점이 중요하다.** 편집 모드로 문서를 여는 순간, 조립기가 써넣은
|
|
321
|
-
> `data-sec`·`id`·`.num`·목차 `ol`·판정표 `tbody` 가 **전부 재계산 값으로 덮어써진다.**
|
|
322
|
-
> 즉 조립기가 만든 번호는 "고객본에서만 쓰이는 초기값"이고, 편집기가 계산하는 값이 진실이다.
|
|
323
|
-
> 조립기의 번호 규칙과 `renumber()` 의 규칙이 다르면 **파일을 열기만 해도 문서가 바뀐다.**
|
|
324
|
-
>
|
|
325
|
-
> `build.py` 는 `f"{int(g['no'])}.{i}"` 로 `sec` 를, `'s' + sec.replace('.', '-')` 로 `id` 를 만든다.
|
|
326
|
-
> `renumber()` 의 `secNo + '.' + sub` / `'s' + no.replace('.', '-')` 와 **의도적으로 같은 규칙**이다.
|
|
327
|
-
> 한쪽만 고치면 안 된다.
|
|
328
|
-
|
|
329
|
-
### 4-1. 절 번호를 코드에 적으면 왜 "조용히" 실패하는가
|
|
330
|
-
|
|
331
|
-
`editor.js` 의 주석이 이유를 적어 두었다.
|
|
332
|
-
|
|
333
|
-
```js
|
|
334
|
-
// 목차의 ol 은 건을 가진 절과 **문서 순서대로** 짝지어진다. 절 번호를 코드에
|
|
335
|
-
// 적어 두면 번호 체계가 바뀔 때 목차가 조용히 빈다 — 문서에서 읽어온다.
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
메커니즘은 세 줄이다.
|
|
339
|
-
|
|
340
|
-
```js
|
|
341
|
-
return (a.dataset.sec || '').indexOf(secno + '.') === 0; // casesIn — 문자열 prefix 매칭
|
|
342
|
-
return h.dataset.secno; // secNumbers — 문서에서 읽는다
|
|
343
|
-
ol.innerHTML = casesIn(secno).map(…).join(''); // 매칭 0건이면 빈 문자열
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
`casesIn()` 은 매칭이 0건일 때 예외가 아니라 `''` 를 반환하고, 그 `''` 가 `ol.innerHTML` 에
|
|
347
|
-
덮어써진다. 하드코딩한 번호가 문서와 어긋나는 순간 목차는 오류 없이 **깨끗하게 비워진다.**
|
|
348
|
-
사용자가 보는 것은 "목차가 사라짐"뿐이고 콘솔에는 아무것도 안 남는다.
|
|
349
|
-
|
|
350
|
-
이건 가설이 아니다. 실사례 문서는 작업 도중 절 번호 체계가 **1.x/2.x → 3.x/4.x** 로 갈아엎혔고,
|
|
351
|
-
그 흔적이 `report.json` 의 `text_fix` 항목들(`§4.3에 따로 정리했습니다` 같은 치환)에 남아 있다.
|
|
352
|
-
번호가 코드에 박혀 있었다면 그 순간 목차 두 개가 통째로 비었을 것이고, **아무 에러도 안 났을 것이다.**
|
|
353
|
-
|
|
354
|
-
### 4-2. `data-secno` 표기법
|
|
355
|
-
|
|
356
|
-
세 곳이 같은 값을 **다른 타입으로** 읽는다.
|
|
357
|
-
|
|
358
|
-
```js
|
|
359
|
-
secNo = +el.dataset.secno; // renumber — 숫자로 강제 변환
|
|
360
|
-
return h.dataset.secno; // secNumbers — 원문 문자열 그대로
|
|
361
|
-
(a.dataset.sec || '').indexOf(secno + '.') === 0 // casesIn — 문자열 비교
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
`data-secno="03"` 으로 쓰면 `renumber()` 는 `data-sec="3.1"` 을 만들고, `secNumbers()` 는 `"03"` 을
|
|
365
|
-
돌려주고, `casesIn("03")` 은 `"03."` prefix 를 찾아 **0건 → 목차가 빈다.**
|
|
366
|
-
|
|
367
|
-
> **규칙** — `data-secno` 는 **선행 0 없는 정수 문자열**. 화면에 보이는 라벨은 별도 `span` 으로 둔다.
|
|
368
|
-
> `build.py` 가 정확히 그렇게 한다 — 라벨은 `<span class="no">03</span>`, 키는 `data-secno="3"`
|
|
369
|
-
> (`int(s["no"])` 로 변환해서 찍는다).
|
|
370
|
-
|
|
371
|
-
### 4-3. 재생성되는 것 / 재생성되지 않는 것
|
|
372
|
-
|
|
373
|
-
이 경계는 **반드시 사용자에게 알려야 한다.** "편집기가 알아서 맞춰주겠지"라는 기대와 실제가 갈리는 곳이다.
|
|
374
|
-
|
|
375
|
-
| 재생성됨 — 손대지 말 것 | 재생성 안 됨 — 저자가 쓴 값 유지 |
|
|
376
|
-
|---|---|
|
|
377
|
-
| 건의 `data-sec`, `id`, `.num` 텍스트 | **`h2.sec` 자신의 번호** — `data-secno` 도 화면 라벨도 안 건드린다. 절을 재배열하면 04 가 03 앞에 온다 |
|
|
378
|
-
| `nav.toc` 안의 각 `ol` 의 `li` 전체 | `nav.toc` 의 그룹 헤더(`p.toc-g`) — **게다가 `main` 밖이라 편집도 안 된다.** 절 제목을 본문에서 고쳐도 좌측 목차의 절 이름은 안 따라온다 |
|
|
379
|
-
| `table.verdict tbody` 전체 | `thead` |
|
|
380
|
-
| | **본문 산문 안의 상호참조 링크**(`a.xref`) — href 도 표시 텍스트도 그대로 |
|
|
381
|
-
| | "해당 절 — 3.7 · 3.8" 류의 요약 문구(`.orefs`) |
|
|
382
|
-
|
|
383
|
-
> **가장 위험한 항목은 상호참조다.** `build.py` 의 `link()` 가 원고의 `§3.1` 을
|
|
384
|
-
> `<a class="xref" href="#s3-1">3.1</a>` 로 바꾼다. 건을 재배열하거나 삭제하면
|
|
385
|
-
> **`.num` 은 새 번호로 바뀌지만 `.xref` 는 옛 번호를 가리킨 채 남는다.** 편집기는 이를 고치지 않는다.
|
|
386
|
-
> 재배열·삭제를 했다면 상호참조를 육안으로 확인하거나, 차라리 `report.json` 을 고쳐 다시 빌드한다
|
|
387
|
-
> (다시 빌드하면 `qa.py` 의 "죽은 절 참조" 검사가 잡는다 — 편집기 경로에는 그 검사가 없다).
|
|
388
|
-
|
|
389
|
-
### 4-4. 자동 재생성되는 표에는 행 편집을 붙이지 않는다
|
|
390
|
-
|
|
391
|
-
`place()` 는 행 +/− 버튼을 붙일지 이렇게 판단한다.
|
|
392
|
-
|
|
393
|
-
```js
|
|
394
|
-
// 자동 재생성되는 표(data-rte-derived)에는 행 편집을 붙이지 않는다.
|
|
395
|
-
// 붙이면 손으로 넣은 행이 다음 syncVerdict 에서 조용히 사라진다.
|
|
396
|
-
var tbl = el.tagName === 'TABLE' ? el : el.querySelector('table');
|
|
397
|
-
var isTable = !!tbl && !tbl.hasAttribute('data-rte-derived');
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
`build.py` 는 처리 현황표를 `<table class="verdict" data-rte-derived>` 로 찍는다.
|
|
401
|
-
따라서 그 표 위에서는 `행+`/`행−` 버튼이 아예 뜨지 않고, 저자가 직접 쓴 표에서만 뜬다.
|
|
402
|
-
|
|
403
|
-
**새 표를 넣을 때의 판단 기준** — 그 표의 `tbody` 를 코드가 다시 만드는가?
|
|
404
|
-
그렇다면 `data-rte-derived` 를 붙인다. 아니면 붙이지 않는다.
|
|
405
|
-
|
|
406
|
-
행 추가의 폴백 순서도 알아 둘 것.
|
|
407
|
-
|
|
408
|
-
```js
|
|
409
|
-
addRow(hotRow || hot.querySelector('tbody tr:last-child') || hot.querySelector('tr:last-of-type'));
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
`hotRow` 는 `td`/`th` 에 포커스가 갔을 때만 채워진다. 셀을 클릭하지 않고 `행+` 를 누르면
|
|
413
|
-
`tbody` 의 마지막 행 뒤에 붙는다. `tbody` 가 없는 표를 위한 세 번째 폴백이 남아 있지만,
|
|
414
|
-
브라우저가 `tbody` 를 자동 삽입하므로 사실상 실행되지 않는다.
|
|
415
|
-
(과거에는 첫 폴백이 `tr:last-of-type` 이라 **머리글 행 뒤에 데이터 행이 생기는** 문제가 있었다.
|
|
416
|
-
지금은 `tbody tr:last-child` 가 먼저다.)
|
|
417
|
-
|
|
418
|
-
---
|
|
419
|
-
|
|
420
|
-
## 5. dirty 판정 — 휘발성 표식을 걷어내는 이유
|
|
421
|
-
|
|
422
|
-
```js
|
|
423
|
-
// data-rte-hot 은 마우스를 올릴 때마다 붙었다 떨어지는 **편집기 표식**이다.
|
|
424
|
-
// innerHTML 을 그대로 비교하면 호버만 해도 "수정됨" 이 되어, 저장하지 않았는데
|
|
425
|
-
// 이탈 경고가 뜨고 되돌리기 스택이 오염된다. 비교 전에 걷어낸다.
|
|
426
|
-
function contentOf() {
|
|
427
|
-
return container.innerHTML.replace(/\s*data-rte-hot=(""|'')/g, '');
|
|
428
|
-
}
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
`contentOf()` 는 두 곳에서 **유일한 진실**로 쓰인다.
|
|
432
|
-
|
|
433
|
-
| 용도 | 방식 |
|
|
434
|
-
|---|---|
|
|
435
|
-
| 되돌리기 스냅샷 | `snapshot()` 이 `undoStack.push(contentOf())` (상한 60) |
|
|
436
|
-
| 편집 여부 판정 | `focusin` 에서 `beforeEdit = contentOf()`, `focusout` 에서 값이 달라졌을 때만 push + `dirty = true` |
|
|
437
|
-
|
|
438
|
-
걷어내지 않으면 벌어지는 일 — 연쇄 4단계.
|
|
439
|
-
|
|
440
|
-
| # | 사건 | 근거 |
|
|
441
|
-
|---|---|---|
|
|
442
|
-
| 1 | 마우스를 다른 블록에 스치기만 해도 `container.innerHTML` 이 달라진다 | `place()` 가 호버할 때마다 `el.setAttribute('data-rte-hot','')` 를 옮겨 붙인다 |
|
|
443
|
-
| 2 | **글자 하나 안 고쳤는데 "수정됨"** 이 뜨고, 창을 닫을 때 이탈 경고가 뜬다 | `focusout` 비교가 참이 되고 `beforeunload` 가 반응 |
|
|
444
|
-
| 3 | 되돌리기 스택에 **내용은 같고 호버 위치만 다른 스냅샷**이 쌓인다 | 상한 60 을 쓰레기가 잡아먹어 **진짜 되돌릴 지점이 밀려나 사라진다** |
|
|
445
|
-
| 4 | 되돌리면 **옛 호버 표시가 문서에 되살아난다** | `undo()` 가 `container.innerHTML = undoStack.pop()` 으로 통째 복원한다 |
|
|
446
|
-
|
|
447
|
-
정규식이 `(""|'')` 두 형태만 다루는 것은, 표식을 붙이는 쪽이 항상 빈 문자열만 넣기 때문이다.
|
|
448
|
-
값 있는 표식으로 바꾸는 순간 이 필터는 무력해진다.
|
|
449
|
-
|
|
450
|
-
> **일반화 규칙** — 편집기가 DOM 에 심는 **휘발성 표식은 반드시 (a) 빈 값 속성이고,
|
|
451
|
-
> (b) `contentOf()` 에서 제거**돼야 한다. 표식을 늘릴 때마다 정규식을 같이 늘린다.
|
|
452
|
-
>
|
|
453
|
-
> "`data-rte-*` 를 전부 걷어내면 되지 않나"는 **틀린 일반화다.** `data-rte-standalone` 과
|
|
454
|
-
> `data-rte-derived` 는 **문서가 소유한 구조 정보**다. 스냅샷에서 지우면 되돌리기가 3장의 사고
|
|
455
|
-
> (꼬리말을 삼키는 그룹)와 4-4 의 사고(재생성 표에 행 편집이 붙음)를 그대로 재현한다.
|
|
456
|
-
|
|
457
|
-
`undo()` 가 복원하는 것은 **`main` 의 innerHTML 뿐이다.** 목차는 `main` 밖에 있는데도 되살아나는데,
|
|
458
|
-
`undo()` 가 곧이어 `renumber()` 를 부르고 그것이 `syncToc()` 으로 목차를 **다시 계산**하기 때문이다.
|
|
459
|
-
**`main` 밖의 상태는 "문서에서 재생성 가능"해야 undo 로 되살아난다** — 이것이 원칙 2 를 지켜야 하는
|
|
460
|
-
또 하나의 이유다.
|
|
461
|
-
|
|
462
|
-
---
|
|
463
|
-
|
|
464
|
-
## 6. 저장 / 내보내기
|
|
465
|
-
|
|
466
|
-
### 6-1. 두 갈래 산출물
|
|
467
|
-
|
|
468
|
-
| | **원본 저장** (`저장`, Cmd+S) | **고객본 내보내기** |
|
|
469
|
-
|---|---|---|
|
|
470
|
-
| 대상 | **열려 있는 그 파일 자체**를 덮어씀 | 새 파일 `{basename}_고객본.html` 내려받기 |
|
|
471
|
-
| 경로 | `showSaveFilePicker` → 핸들 재사용 | 항상 `<a download>` blob |
|
|
472
|
-
| `data-report-edit` | **강제로 `"on"`** | 속성 제거 |
|
|
473
|
-
| 확인 창 | 없음 | `confirm` 있음 |
|
|
474
|
-
|
|
475
|
-
소스 주석이 이유를 적어 두었다.
|
|
476
|
-
|
|
477
|
-
```js
|
|
478
|
-
// 저장은 **이 파일 자체**에 쓴다. 사본을 만들면 어느 파일이 최신인지 알 수 없다.
|
|
479
|
-
// 고객본은 성격이 다른 결과물이라 **별도 파일**로 내보낸다.
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
> **원본 저장이 항상 `on` 을 다시 박는다**는 점이 중요하다. "편집 끄기"로 화면에서 껐어도
|
|
483
|
-
> 저장하면 **다시 편집 가능한 원본**이 나온다. 버튼 툴팁이 그렇게 적혀 있다 —
|
|
484
|
-
> `"이 화면에서만 편집을 끕니다"`.
|
|
485
|
-
|
|
486
|
-
### 6-2. 파일 피커는 사용자 제스처 안에서 **동기적으로** 불러야 한다
|
|
487
|
-
|
|
488
|
-
이 제약이 저장 코드 전체의 모양을 결정했다.
|
|
489
|
-
|
|
490
|
-
```js
|
|
491
|
-
// ⚠️ 파일 선택 창(showSaveFilePicker)은 클릭 제스처가 살아 있는 동안에만 열린다.
|
|
492
|
-
// 그 앞에서 alert 를 띄우거나 IndexedDB 를 await 하면 제스처가 끊겨 SecurityError 가 난다.
|
|
493
|
-
// 그래서 핸들이 없을 때는 **클릭 핸들러 안에서 곧바로** 피커를 부른다.
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
```js
|
|
497
|
-
// 클릭 핸들러에서 **동기적으로** 호출된다. 여기서 await 를 앞세우면 안 된다.
|
|
498
|
-
function save() {
|
|
499
|
-
var html = serialize(false); // ① 동기 — 제스처 소모 없음
|
|
500
|
-
if (!window.showSaveFilePicker) { return saveFallback(html, …); }
|
|
501
|
-
if (fileHandle) { … } // ② 핸들 있으면 창 없이 바로 쓴다
|
|
502
|
-
var opening;
|
|
503
|
-
try { opening = window.showSaveFilePicker({ … }); } // ③ 클릭 스택 안에서 즉시 호출
|
|
504
|
-
catch (e) { return failed(e); }
|
|
505
|
-
opening.then(…).catch(…);
|
|
506
|
-
}
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
| 피커 앞에 오는 것 | 결과 |
|
|
510
|
-
|---|---|
|
|
511
|
-
| `alert()` / `confirm()` | 제스처 소모 → 피커가 `SecurityError` 로 거부. 코드는 `saveFallback` 으로 내려받기 전환하므로 **원본 덮어쓰기가 조용히 사라지고 사본만 남는다** |
|
|
512
|
-
| `await loadHandle()`(IndexedDB) | 매크로태스크 경계를 넘어 사용자 활성화 만료 → 같은 `SecurityError` |
|
|
513
|
-
| `await` 가 걸린 어떤 전처리든 | 동일 |
|
|
514
|
-
| `serialize()` 같은 **동기** 작업 | 안전 — 그래서 ①이 맨 앞에 있다 |
|
|
515
|
-
|
|
516
|
-
코드가 취한 회피책 셋.
|
|
517
|
-
|
|
518
|
-
1. **핸들 복원은 부팅 때 한 번**만 하고 변수에 보관 — `loadHandle().then(h => { if (h) fileHandle = h; })`.
|
|
519
|
-
주석: `문서를 다시 열었을 때 이전 핸들을 되살린다(권한 확인은 저장 시점에 한다).`
|
|
520
|
-
2. **핸들 보관은 저장 성공 뒤 fire-and-forget** — `storeHandle(h); // 실패해도 무시 — 이번 저장과 무관`.
|
|
521
|
-
3. **confirm 이 필요한 기능은 피커를 포기한다** — 고객본 내보내기 주석:
|
|
522
|
-
`confirm 으로 제스처가 소모됐을 수 있어 피커를 강요하지 않는다 — 내려받기로 간다.`
|
|
523
|
-
|
|
524
|
-
> **일반화 규칙** — **"사용자 확인이 필요한 동작"과 "파일 피커가 필요한 동작"은 같은 버튼에 담을 수 없다.**
|
|
525
|
-
> 확인이 필요하면 → 내려받기. 덮어쓰기를 원하면 → 확인 없이 바로 피커.
|
|
526
|
-
> 둘 다 원하면 → **확인을 UI(체크박스·별도 버튼)로 옮겨 클릭 자체가 확인이 되게 한다.**
|
|
527
|
-
|
|
528
|
-
### 6-3. IndexedDB 핸들 보존 — 편의 기능이지 전제가 아니다
|
|
529
|
-
|
|
530
|
-
```js
|
|
531
|
-
var DB = 'report-editor', STORE = 'handles', KEY = location.pathname;
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
| 항목 | 사실 | 함의 |
|
|
535
|
-
|---|---|---|
|
|
536
|
-
| 저장 키 | `location.pathname` | **파일 경로별**로 핸들이 따로 산다. 같은 폴더의 다른 보고서끼리 안 섞이고, **파일을 옮기면 다음 저장 때 피커가 한 번 더 뜬다** |
|
|
537
|
-
| 열기 실패 | `try { rq = indexedDB.open(DB, 1); } catch (e) { return rej(e); }` — 주석 `file:// 에서 막힐 수 있다` | `file://` 에서 IndexedDB 가 막혀도 **편집·저장은 정상**. 매 세션 피커가 한 번 뜰 뿐 |
|
|
538
|
-
| 실패 처리 | 모든 경로가 `res()` / `res(null)` / `.catch(() => {})` 로 삼켜짐 | 보존은 **편의 기능**이지 기능의 전제가 아니다 |
|
|
539
|
-
| 권한 | 부팅 때는 확인하지 않고 `ensurePermission()` 을 **저장 시점에** 실행 | 문서를 다시 연 첫 저장에서 권한 팝업이 뜰 수 있다. 거부하면 `toast('저장 권한이 없습니다')` 로 끝난다 |
|
|
540
|
-
| 폴백 | `showSaveFilePicker` 자체가 없으면 즉시 내려받기 | **원본 덮어쓰기는 File System Access API 를 지원하는 브라우저 전용**임을 사용자에게 알려야 한다 |
|
|
541
|
-
|
|
542
|
-
> **주의 — `saveFallback()` 은 내려받기 후 `dirty = false` 로 만든다.** 원본 파일은 아직 안 바뀌었는데
|
|
543
|
-
> 이탈 경고가 풀린다. 그래서 토스트가 지시문을 반드시 달고 있다 —
|
|
544
|
-
> `toast(why + ' — 내려받았습니다. 원본에 덮어써 주세요')`.
|
|
545
|
-
> **폴백 경로의 토스트 문구는 장식이 아니라 안전장치다. 지우지 말 것.**
|
|
546
|
-
|
|
547
|
-
### 6-4. `serialize()` — 무엇이 제거되는가
|
|
548
|
-
|
|
549
|
-
| 제거 대상 | 원본 저장 | 고객본 |
|
|
550
|
-
|---|:---:|:---:|
|
|
551
|
-
| `[data-rte-ui]` 요소 (핸들·인라인바·하단바·토스트) | 제거 | 제거 |
|
|
552
|
-
| `contenteditable`, `spellcheck` 속성 | 제거 | 제거 |
|
|
553
|
-
| `[data-rte-hot]` 속성 | 제거 | 제거 |
|
|
554
|
-
| body 의 `data-rte-preview` | 제거 | 제거 (미리보기 중 저장해도 그 상태가 굳지 않는다) |
|
|
555
|
-
| `[data-internal="true"]` 요소 | **유지** | 제거 |
|
|
556
|
-
| `[data-rte-src]` 요소 (편집기 CSS/JS) | **유지** | 제거 |
|
|
557
|
-
| body 의 `data-report-edit` | `"on"` 강제 | 제거 |
|
|
558
|
-
| `REPORT-EDITOR:BEGIN\|END` 주석 | 유지 | 제거 (TreeWalker) |
|
|
559
|
-
| `data-rte-standalone`, `data-rte-derived` | 유지 | **유지** (문서 소유) |
|
|
560
|
-
|
|
561
|
-
```js
|
|
562
|
-
// 편집기를 파일에서 들어낸다 — 꺼두는 것만으로는 흔적이 남는다
|
|
563
|
-
clone.querySelectorAll('[data-rte-src]').forEach(function (n) { n.remove(); });
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
**사소해 보이지만 치명적인 세부 둘**
|
|
567
|
-
|
|
568
|
-
| 세부 | 코드 | 빠뜨리면 |
|
|
569
|
-
|---|---|---|
|
|
570
|
-
| doctype 을 문자열로 직접 붙인다 | `return '<!doctype html>\n' + clone.outerHTML + '\n';` | `cloneNode` 는 doctype 을 안 가져온다. **고객본이 쿼크 모드로 렌더링돼 레이아웃이 무너진다** |
|
|
571
|
-
| objectURL 을 **4초 뒤** 해제한다 | `setTimeout(function () { URL.revokeObjectURL(a.href); }, 4000);` | 즉시 해제하면 브라우저가 저장을 마치기 전에 URL 이 죽는다 |
|
|
572
|
-
|
|
573
|
-
---
|
|
574
|
-
|
|
575
|
-
## 7. 조립 계약 — 편집기를 파일에 심는 방법
|
|
576
|
-
|
|
577
|
-
### 7-1. 실제 형태 (`build.py` 가 이렇게 출력한다)
|
|
578
|
-
|
|
579
|
-
```python
|
|
580
|
-
# 편집 레이어는 data-rte-src 와 REPORT-EDITOR 주석으로 감싼다.
|
|
581
|
-
# editor.js 의 고객본 내보내기가 **정확히 이 표식을 찾아 들어낸다.** 표식을 안 붙이면
|
|
582
|
-
# 내보내기 코드는 조용히 아무것도 못 지우고, 고객본에 편집기 30KB 가 그대로 실린다.
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
```html
|
|
586
|
-
<head>
|
|
587
|
-
<style>{:root 토큰 + 폰트 @font-face + report.css}</style>
|
|
588
|
-
<!-- REPORT-EDITOR:BEGIN -->
|
|
589
|
-
<style data-rte-src>{editor.css}</style>
|
|
590
|
-
<!-- REPORT-EDITOR:END -->
|
|
591
|
-
</head>
|
|
592
|
-
<body data-report-edit="on">
|
|
593
|
-
…
|
|
594
|
-
<!-- REPORT-EDITOR:BEGIN -->
|
|
595
|
-
<script data-rte-src>{editor.js}</script>
|
|
596
|
-
<!-- REPORT-EDITOR:END -->
|
|
597
|
-
</body>
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
| 규칙 | 근거 | 어기면 |
|
|
601
|
-
|---|---|---|
|
|
602
|
-
| 문서 CSS 와 편집기 CSS 를 **서로 다른 `<style>` 로 분리**하고 편집기 쪽에만 `data-rte-src` | `serialize(true)` 가 `[data-rte-src]` **요소 전체를 `remove()`** 한다 | 합친 태그에 속성을 붙이면 고객본에서 **문서 CSS 까지 통째로 삭제**되어 무스타일 HTML 이 나간다. 안 붙이면 편집기 소스가 고객본에 남는다. **분리가 유일한 정답이다** |
|
|
603
|
-
| 편집기 `<script>` 에 `data-rte-src` | 동상 | 고객본에 26KB 짜리 편집기 JS 가 그대로 실린다 |
|
|
604
|
-
| `REPORT-EDITOR:BEGIN/END` 주석으로 감싼다 | TreeWalker 가 지운다 | 없어도 동작엔 지장 없다. 있으면 흔적까지 깨끗해지고, **`qa.py` 의 내보내기 시뮬레이션이 이 주석을 앵커로 쓴다** |
|
|
605
|
-
| 편집기 `<script>` 앞에 다른 `<script>` 를 넣지 않는다 | `qa.py` 의 본문 슬라이싱이 `doc.split('<body',1)[1].split('<script',1)[0]` | 앞에 스크립트가 끼면 **검수 범위가 그 지점에서 잘려** 구조 검사가 통과한 것처럼 보인다 |
|
|
606
|
-
|
|
607
|
-
폰트 base64 는 문서 CSS 쪽 `<style>` 앞에 붙는다 — 고객본에도 남아야 하므로 `data-rte-src` 가
|
|
608
|
-
있어서는 **안 된다.** 임베드 자체의 근거와 옵션은 `references/font-embedding.md` 를 볼 것.
|
|
609
|
-
|
|
610
|
-
### 7-2. `qa.py` 가 내보내기를 시뮬레이션한다
|
|
611
|
-
|
|
612
|
-
고객본을 실제로 브라우저에서 뽑아 grep 하는 대신, 정적으로 같은 규칙을 적용해 검사한다.
|
|
613
|
-
|
|
614
|
-
```python
|
|
615
|
-
# 고객본 내보내기 시뮬레이션 — editor.js 의 serialize(customer=true) 와 같은 규칙
|
|
616
|
-
# 표식을 안 붙이면 내보내기 코드는 **조용히 아무것도 못 지운다**. 실제로 그랬다.
|
|
617
|
-
cust = re.sub(r'<!-- REPORT-EDITOR:BEGIN -->.*?<!-- REPORT-EDITOR:END -->', '', doc, flags=re.S)
|
|
618
|
-
cust = re.sub(r'<(style|script)\b[^>]*\bdata-rte-src\b.*?</\1>', '', cust, flags=re.S)
|
|
619
|
-
…
|
|
620
|
-
residue = [w for w in ('data-rte-ui', 'showSaveFilePicker', 'syncVerdict',
|
|
621
|
-
'data-report-edit', 'data-internal="true"') if w in cust]
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
두 가지를 본다.
|
|
625
|
-
|
|
626
|
-
- **잔존 문자열 0건** — 위 다섯 단어 중 하나라도 남으면 실패
|
|
627
|
-
- **크기가 원본의 98% 미만** — 아무것도 안 지워졌으면 크기가 그대로다. 크기 검사가 그것을 잡는다
|
|
628
|
-
|
|
629
|
-
실사례 18건 문서에서 원본 523,464B → 고객본 시뮬 491,765B(94%)로 통과한다.
|
|
630
|
-
**`serialize()` 의 규칙을 바꾸면 `qa.py` 의 이 블록도 같이 고쳐야 한다.** 두 곳이 같은 규칙의
|
|
631
|
-
두 구현이라는 것이 이 설계의 값이자 부채다.
|
|
632
|
-
|
|
633
|
-
---
|
|
634
|
-
|
|
635
|
-
## 8. 새 문서에 이 편집기를 붙일 때 — 체크리스트
|
|
636
|
-
|
|
637
|
-
### A. 문서 골격
|
|
638
|
-
- [ ] `<body data-report-edit="on">`
|
|
639
|
-
- [ ] 편집 대상 전부가 **`<main>` 직계 자식**인가. `main` 밖(표지·`nav.toc`)은 **편집 불가**임을 알고 배치했는가
|
|
640
|
-
- [ ] 편집 단위가 되어야 할 것이 **직계 자식**인가 (한 겹 더 감싼 `div` 는 통째로 하나의 블록이 된다)
|
|
641
|
-
- [ ] 절 제목은 **`h2.sec`**, 그리고 **`main` 직계 자식** — `group()` 은 `.sec` 만 보고 `renumber()` 는
|
|
642
|
-
`el.tagName === 'H2'` 만 보므로 `h3.sec` 같은 걸 만들면 두 함수의 판단이 갈린다
|
|
643
|
-
- [ ] 번호를 붙일 절에만 **`data-secno="3"`** — 선행 0 없는 정수 문자열. 화면 라벨은 별도 `span`
|
|
644
|
-
- [ ] 번호를 안 붙일 절(요약·현황·확인요청)의 `h2` 에는 `data-secno` 를 **일부러 뺐는가** (번호 매김의 벽)
|
|
645
|
-
- [ ] 건은 **`<article class="case">`** — `article` 태그 필수, 안에 `.num` 과 `h3` 필수
|
|
646
|
-
- [ ] 절에 속하지 않는 구조물(꼬리말·부록·서명란)에 **`data-rte-standalone`** — 또는 `main` 밖으로
|
|
647
|
-
|
|
648
|
-
### B. 파생 영역
|
|
649
|
-
- [ ] 목차 `<ol>` 의 개수·순서 = `h2[data-secno]` 의 개수·순서. **목차용이 아닌 `<ol>` 을 `nav.toc` 안에 두지 않았는가**
|
|
650
|
-
- [ ] 판정표 `thead` 가 `syncVerdict()` 가 만드는 **4열 구조**와 일치하는가. 다르면 `syncVerdict()` 를 같이 고친다
|
|
651
|
-
- [ ] 판정표에 쓸 값의 원본 선택자(`.method`, `.r-open .val`)가 본문 마크업과 일치하는가 —
|
|
652
|
-
소스 주석: `선택자는 본문 마크업과 반드시 같이 움직여야 한다. 어긋나면 표가 빈 값으로 덮어써진다.`
|
|
653
|
-
- [ ] 자동 재생성되는 표에 **`data-rte-derived`** 를 붙였는가
|
|
654
|
-
- [ ] **파생 영역에는 손으로 쓴 내용을 넣지 않는다.** 목차 `ol` 과 판정표 `tbody` 는 **파일을 여는 순간** 통째로 날아간다
|
|
655
|
-
- [ ] 조립기가 써넣는 번호 규칙이 `renumber()` 의 규칙(`s{n}-{m}` / `{n}.{m}`)과 **같은가**
|
|
656
|
-
- [ ] `main` 밖의 상태(목차 등)가 **문서에서 재생성 가능**한가 (undo 는 `main` 만 복원한다)
|
|
657
|
-
|
|
658
|
-
### C. 스타일
|
|
659
|
-
- [ ] 편집기가 참조하는 네 토큰(`--primary`, `--primary-hair`, `--accent-ink`, `--accent-bg`)이
|
|
660
|
-
`:root` 에 있는가 — `palette.py` 를 쓰면 자동이다. **손으로 색을 적지 말 것**
|
|
661
|
-
- [ ] **`.ev` 의 전역 정의**가 있는가 — 인라인 "강조" 버튼의 산출물. 없으면 눌러도 아무 변화가 없다
|
|
662
|
-
- [ ] 목차 `.i`/`.t`, 판정표 `.c-sec`/`.c-m`/`.c-o`/`.c-o[data-open="1"]` 스타일 존재
|
|
663
|
-
- [ ] **복사되는 요소**(판정표로 `outerHTML` 이 복사되는 등급 칩)가 **부모 문맥 없이 자기 class 만으로
|
|
664
|
-
완성**되는가 — `<span class="method m-live">` 처럼 상태를 자기 class 에 담는다
|
|
665
|
-
- [ ] `editor.css` 첫 줄 주석(`/* ── 편집 레이어`)을 바꿨다면 `qa.py` 의 `EDITOR_CSS_MARK` 도 바꿨는가
|
|
666
|
-
|
|
667
|
-
### D. 조립
|
|
668
|
-
- [ ] **문서 CSS 와 편집기 CSS 를 서로 다른 `<style>` 로 분리**하고 편집기 쪽에만 `data-rte-src`
|
|
669
|
-
- [ ] 편집기 `<script>` 에 `data-rte-src`, 그리고 그 앞에 다른 `<script>` 가 없는가
|
|
670
|
-
- [ ] `<!-- REPORT-EDITOR:BEGIN -->` / `<!-- REPORT-EDITOR:END -->` 로 감쌌는가
|
|
671
|
-
- [ ] 폰트 `@font-face` 는 **문서 CSS 쪽**에 있는가 (`data-rte-src` 가 붙으면 고객본에서 폰트가 사라진다)
|
|
672
|
-
|
|
673
|
-
### E. 검증
|
|
674
|
-
|
|
675
|
-
`python3 qa.py report.json` 이 자동으로 잡는 것 — **실패 0 이어야 한다.**
|
|
676
|
-
|
|
677
|
-
| 검사 | 무엇을 증명하는가 |
|
|
678
|
-
|---|---|
|
|
679
|
-
| 고객본 잔존 문자열 0건 + 크기 98% 미만 | 7장 조립 계약 이행 |
|
|
680
|
-
| 목차 항목 수 = 건 수, 판정표 행 수 = 건 수 | 조립기 산출 초기값과 본문의 일치 |
|
|
681
|
-
| 죽은 절 참조 없음 | `href="#s{n}-{m}"` 가 실재하는 `id` 를 가리킨다 |
|
|
682
|
-
|
|
683
|
-
브라우저에서 손으로 해야 하는 것 — **자동 검수가 대신하지 못한다.**
|
|
684
|
-
|
|
685
|
-
| # | 검사 | 무엇을 증명하는가 |
|
|
686
|
-
|---|---|---|
|
|
687
|
-
| 1 | 문서를 열자마자 목차·판정표·번호가 **그대로인지** (재계산으로 바뀌지 않는지) | 조립기 규칙 = `renumber()` 규칙 |
|
|
688
|
-
| 2 | 절 하나를 아래로 옮겼다 되돌린 뒤 목차·판정표·`.num` 이 **원상복구**되는지 | 재생성 루프 + undo |
|
|
689
|
-
| 3 | 마지막 절의 ↓ 버튼이 **비활성**인지 | standalone 벽이 작동 (3장) |
|
|
690
|
-
| 4 | 아무것도 안 고치고 여러 블록에 마우스만 굴린 뒤 **"수정됨"이 안 뜨는지** | hot 필터 작동 (5장) |
|
|
691
|
-
| 5 | 자동 재생성되는 표 위에서 `행+`/`행−` 버튼이 **안 보이는지** | `data-rte-derived` 작동 (4-4) |
|
|
692
|
-
| 6 | 본문에서 텍스트를 골라 "강조"를 눌렀을 때 **눈에 보이는 변화가 있는지** | `.ev` 전역 정의 존재 |
|
|
693
|
-
| 7 | 상호참조가 있는 문서라면, 건을 재배열한 뒤 **링크가 옛 번호를 가리키는지 육안 확인** | 4-3 의 미해결 경계 |
|
|
694
|
-
| 8 | 실제로 고객본을 내보내 브라우저에서 열어 보기 | 시뮬레이션이 아닌 실물 확인 |
|
|
695
|
-
|
|
696
|
-
---
|
|
697
|
-
|
|
698
|
-
## 9. 프로젝트 고유값 → 치환표
|
|
699
|
-
|
|
700
|
-
왼쪽 열은 **이 스킬이 기본으로 만드는 "수정 내역 보고" 문서의 값**이다. 계약이 아니라 예시라는 뜻이다.
|
|
701
|
-
|
|
702
|
-
| 기본 문서의 값 | 정체 | 다른 문서 종류에서 |
|
|
703
|
-
|---|---|---|
|
|
704
|
-
| `article.case`, `.num`, `.method`, `.r-open .val`, `table.verdict` | "수정 건" 도메인 어휘 | **문서 종류마다 다르다.** 바꾸려면 `editor.js` 의 `renumber`/`syncToc`/`syncVerdict` 와 `build.py` 의 `case_html`/`toc_items`/`vrow` 를 **함께** 고친다 |
|
|
705
|
-
| `h2.sec` + `data-secno`, `s{n}-{m}` id 규칙 | 2단 번호 체계(`3.1`) | 번호 체계가 다르면 `renumber()` 의 `secNo + '.' + sub` 와 `casesIn()` 의 prefix 규칙을 **반드시 같이** 바꾼다 |
|
|
706
|
-
| `'있음'` / `'—'`, `'되돌렸습니다'`, `'이 절 전체(N개 블록)를 삭제합니다.'` | 한국어 UI 문자열 | 전부 `editor.js` 에 하드코딩. 언어를 바꾸려면 소스 수정 (`report.json` 의 `labels` 는 **문서 쪽 라벨만** 바꾼다) |
|
|
707
|
-
| `{basename}_고객본.html` | 내보내기 파일명 규칙 | 배포 대상 명칭에 맞춰 치환 |
|
|
708
|
-
| `DB='report-editor'`, `STORE='handles'` | IndexedDB 이름 | 키가 `location.pathname` 이라 문서끼리 충돌하지 않는다. 그대로 둬도 무방 |
|
|
709
|
-
| `undoStack` 상한 60, 토스트 2200ms, objectURL 해제 4000ms | 경험적 상수 | 소스에 근거 주석이 없다. 그대로 계승해도 되고 조정해도 된다 |
|
|
710
|
-
| `h1,h2,h3,h4,p,td,th,li,dd,dt` | 편집 대상 태그 목록 | 문서가 쓰는 내용 태그에 맞춰 조정 |
|
|
711
|
-
| 고객사·수행사 이름 | 실사례 고유명사 | `report.json` 의 값일 뿐, 코드에는 없다 |
|
|
712
|
-
|
|
713
|
-
---
|
|
714
|
-
|
|
715
|
-
## 10. 알려진 한계 — 버그가 아니라 아직 안 그은 선
|
|
716
|
-
|
|
717
|
-
새 문서에 옮길 때 **그대로 따라온다.** 사용자에게 미리 알리거나, 필요하면 여기서부터 고친다.
|
|
718
|
-
|
|
719
|
-
| # | 한계 | 사실 | 실무 처방 |
|
|
720
|
-
|---|---|---|---|
|
|
721
|
-
| 1 | 상호참조(`a.xref`)가 재생성되지 않는다 | 건을 재배열·삭제해도 링크는 옛 번호를 가리킨다 | 재배열은 **편집기가 아니라 `report.json` 에서** 하고 다시 빌드한다. 편집기로 했다면 육안 확인 |
|
|
722
|
-
| 2 | 목차의 절 이름(`p.toc-g`)이 본문 절 제목을 안 따라온다 | `syncToc()` 은 `ol` 만 다시 만든다. `.toc-g` 는 `main` 밖이라 편집도 불가 | 절 이름 변경은 `report.json` 에서 |
|
|
723
|
-
| 3 | `h2.sec` 자신의 번호는 재계산되지 않는다 | `renumber()` 는 건에만 번호를 매긴다 | 절 자체를 재배열하지 않는다. 하려면 `report.json` 의 `sections` 순서를 바꾼다 |
|
|
724
|
-
| 4 | "편집 끄기"는 완전한 종료가 아니다 | UI 와 `contenteditable` 만 걷어낼 뿐 `document` 에 붙은 keydown 리스너는 남는다. 끈 상태에서도 Cmd+S 가 저장되고, Cmd+Z 가 `undo() → wire()` 를 돌려 편집을 되살린다 | 툴팁의 `"이 화면에서만 편집을 끕니다"` 가 정확한 표현이다. 그대로 둘 것 |
|
|
725
|
-
| 5 | "고객본 미리보기"에서도 글자가 고쳐진다 | 미리보기는 `[data-internal]` 을 숨기고 핸들·인라인바를 억제할 뿐 `contenteditable` 은 그대로 | 미리보기를 "읽기 전용"이라고 안내하면 안 된다 |
|
|
726
|
-
| 6 | 내부 전용 개수가 실제와 다를 수 있다 | `internalCount()` 는 `container` 안만 세고, `serialize()` 의 제거는 **문서 전체**를 훑는다 | `main` 밖에 내부 전용을 표시하지 않는다 |
|
|
727
|
-
|
|
728
|
-
---
|
|
729
|
-
|
|
730
|
-
## 11. 미확정 — 소스만으로 확정할 수 없었던 것
|
|
731
|
-
|
|
732
|
-
추측하지 않고 남겨 둔 것이다. 이식하는 사람이 실측하거나 원저자에게 확인해야 한다.
|
|
733
|
-
|
|
734
|
-
1. **`prevGroup()` 의 후진 탐색이 standalone 을 건너뛴다.**
|
|
735
|
-
`while (cur && isBlock(cur) && !cur.classList.contains('sec')) cur = cur.previousElementSibling;`
|
|
736
|
-
에 standalone 검사가 없다. 바로 앞 형제가 standalone 인 경우는 함수 첫 줄이 막지만,
|
|
737
|
-
**절과 절 사이에 standalone 이 끼어 있는 배치**에서 어떻게 동작하는지는 미검증이다.
|
|
738
|
-
실사례의 standalone 은 문서 끝 꼬리말 하나뿐이라 이 경로가 실행된 적이 없다.
|
|
739
|
-
2. **브라우저에서 실제로 내보낸 `_고객본.html` 을 열어 본 기록이 없다.**
|
|
740
|
-
`qa.py` 의 시뮬레이션은 통과하지만, 그것은 정규식으로 같은 규칙을 흉내 낸 것이지
|
|
741
|
-
`serialize(true)` 를 실행한 결과가 아니다. 8-E 의 8번을 한 번은 사람이 해야 한다.
|
|
742
|
-
3. **`showSaveFilePicker` 미지원 브라우저에서의 실동작 로그가 없다.**
|
|
743
|
-
"지원 브라우저 전용"은 `if (!window.showSaveFilePicker)` 폴백 분기의 존재로부터 읽은 것이지,
|
|
744
|
-
Safari/Firefox 실측 기록이 아니다.
|
|
745
|
-
4. **미리보기 중 `contenteditable` 이 유지되는 것이 의도인지 누락인지 판별 불가.** 주석이 없다.
|
|
746
|
-
5. **`EDITABLE` 목록에서 `figcaption`·`blockquote`·`caption` 이 빠진 것이 의도인지 누락인지 판별 불가.**
|
|
747
|
-
실사례 문서에는 실제로 편집 불가한 `figcaption.fig-cap`(도형 캡션)이 있다.
|
|
748
|
-
도형 캡션은 도형과 함께 바뀌어야 하므로 "안 되는 편이 낫다"고 볼 여지도 있으나, 근거는 없다.
|
|
749
|
-
6. **`undoStack` 상한 60 · 토스트 2200ms · objectURL 4000ms 의 근거.**
|
|
750
|
-
주석이 없어 실험값인지 임의값인지 알 수 없다. (다만 objectURL 해제를 **지연시킨다**는 결정 자체는
|
|
751
|
-
즉시 해제 시 저장이 깨지는 것을 피하려는 것으로 코드 형태상 분명하다.)
|