@simplysm/sd-claude 14.0.82 → 14.0.84

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 (90) hide show
  1. package/claude/references/sd-requirement-source-handling.md +20 -20
  2. package/claude/references/sd-simplysm14/README.md +13 -13
  3. package/claude/references/sd-simplysm14/manuals/client-component.md +92 -92
  4. package/claude/references/sd-simplysm14/manuals/client-crud.md +11 -11
  5. package/claude/references/sd-simplysm14/manuals/client-demo.md +28 -28
  6. package/claude/references/sd-simplysm14/manuals/client-rules.md +1 -1
  7. package/claude/references/sd-simplysm14/manuals/client-setup.md +21 -21
  8. package/claude/references/sd-simplysm14/manuals/client-tab.md +3 -3
  9. package/claude/references/sd-simplysm14/manuals/logging.md +15 -15
  10. package/claude/references/sd-simplysm14/manuals/orm-union.md +6 -6
  11. package/claude/references/sd-simplysm14/manuals/orm.md +19 -19
  12. package/claude/references/sd-simplysm14/manuals/test.md +33 -33
  13. package/claude/rules/sd-design-rules.md +18 -18
  14. package/claude/sd-system-prompt.md +369 -0
  15. package/claude/skills/sd-commit/SKILL.md +10 -10
  16. package/claude/skills/sd-config/SKILL.md +2 -2
  17. package/claude/skills/sd-demo/SKILL.md +45 -45
  18. package/claude/skills/sd-dev/SKILL.md +15 -15
  19. package/claude/skills/sd-docs/SKILL.md +7 -7
  20. package/claude/skills/sd-docs/references/subagent-prompt.md +33 -33
  21. package/claude/skills/sd-impl/SKILL.md +60 -60
  22. package/claude/skills/sd-review/SKILL.md +9 -9
  23. package/claude/skills/sd-skill/SKILL.md +74 -74
  24. package/claude/skills/sd-skill/evals/fixtures/existing-skill/.claude/skills/todo-format/SKILL.md +1 -1
  25. package/claude/skills/sd-spec/SKILL.md +354 -319
  26. package/claude/skills/sd-spec/references/example-spec.md +104 -104
  27. package/claude/skills/sd-unpack/SKILL.md +34 -34
  28. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/office_com.cpython-314.pyc +0 -0
  29. package/claude/skills/sd-unpack/scripts/handlers/office_com.py +234 -159
  30. package/claude/skills/sd-use/SKILL.md +4 -4
  31. package/package.json +1 -1
  32. package/claude/references/sd-simplysm14/apis/angular/README.md +0 -37
  33. package/claude/references/sd-simplysm14/apis/angular/app-structure.md +0 -92
  34. package/claude/references/sd-simplysm14/apis/angular/buttons.md +0 -88
  35. package/claude/references/sd-simplysm14/apis/angular/crud.md +0 -100
  36. package/claude/references/sd-simplysm14/apis/angular/forms.md +0 -200
  37. package/claude/references/sd-simplysm14/apis/angular/infrastructure.md +0 -231
  38. package/claude/references/sd-simplysm14/apis/angular/kanban.md +0 -80
  39. package/claude/references/sd-simplysm14/apis/angular/layout.md +0 -92
  40. package/claude/references/sd-simplysm14/apis/angular/modal.md +0 -115
  41. package/claude/references/sd-simplysm14/apis/angular/routing.md +0 -107
  42. package/claude/references/sd-simplysm14/apis/angular/select-dropdown.md +0 -35
  43. package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +0 -82
  44. package/claude/references/sd-simplysm14/apis/angular/shared-data.md +0 -134
  45. package/claude/references/sd-simplysm14/apis/angular/sheet.md +0 -127
  46. package/claude/references/sd-simplysm14/apis/angular/toast.md +0 -97
  47. package/claude/references/sd-simplysm14/apis/angular/visual.md +0 -167
  48. package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +0 -79
  49. package/claude/references/sd-simplysm14/apis/capacitor-plugin-file-system/README.md +0 -83
  50. package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +0 -91
  51. package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +0 -49
  52. package/claude/references/sd-simplysm14/apis/core-browser/README.md +0 -143
  53. package/claude/references/sd-simplysm14/apis/core-common/README.md +0 -58
  54. package/claude/references/sd-simplysm14/apis/core-common/extensions.md +0 -88
  55. package/claude/references/sd-simplysm14/apis/core-common/features.md +0 -51
  56. package/claude/references/sd-simplysm14/apis/core-common/types.md +0 -88
  57. package/claude/references/sd-simplysm14/apis/core-common/utils.md +0 -189
  58. package/claude/references/sd-simplysm14/apis/core-node/README.md +0 -12
  59. package/claude/references/sd-simplysm14/apis/core-node/consola.md +0 -59
  60. package/claude/references/sd-simplysm14/apis/core-node/cpx.md +0 -44
  61. package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +0 -42
  62. package/claude/references/sd-simplysm14/apis/core-node/fsx.md +0 -53
  63. package/claude/references/sd-simplysm14/apis/core-node/pathx.md +0 -24
  64. package/claude/references/sd-simplysm14/apis/core-node/worker.md +0 -65
  65. package/claude/references/sd-simplysm14/apis/excel/README.md +0 -193
  66. package/claude/references/sd-simplysm14/apis/lint/README.md +0 -94
  67. package/claude/references/sd-simplysm14/apis/orm-common/README.md +0 -58
  68. package/claude/references/sd-simplysm14/apis/orm-common/db-context.md +0 -77
  69. package/claude/references/sd-simplysm14/apis/orm-common/executable.md +0 -20
  70. package/claude/references/sd-simplysm14/apis/orm-common/expr.md +0 -92
  71. package/claude/references/sd-simplysm14/apis/orm-common/queryable.md +0 -98
  72. package/claude/references/sd-simplysm14/apis/orm-common/schema-builders.md +0 -128
  73. package/claude/references/sd-simplysm14/apis/orm-node/README.md +0 -69
  74. package/claude/references/sd-simplysm14/apis/sd-claude/README.md +0 -32
  75. package/claude/references/sd-simplysm14/apis/sd-cli/README.md +0 -80
  76. package/claude/references/sd-simplysm14/apis/sd-cli/sd-config.md +0 -155
  77. package/claude/references/sd-simplysm14/apis/service-client/README.md +0 -131
  78. package/claude/references/sd-simplysm14/apis/service-common/README.md +0 -29
  79. package/claude/references/sd-simplysm14/apis/service-common/app-structure.md +0 -63
  80. package/claude/references/sd-simplysm14/apis/service-common/messages.md +0 -56
  81. package/claude/references/sd-simplysm14/apis/service-common/protocol.md +0 -64
  82. package/claude/references/sd-simplysm14/apis/service-common/service-types.md +0 -43
  83. package/claude/references/sd-simplysm14/apis/service-server/README.md +0 -13
  84. package/claude/references/sd-simplysm14/apis/service-server/auth.md +0 -39
  85. package/claude/references/sd-simplysm14/apis/service-server/builtin-services.md +0 -71
  86. package/claude/references/sd-simplysm14/apis/service-server/define-service.md +0 -55
  87. package/claude/references/sd-simplysm14/apis/service-server/internals.md +0 -82
  88. package/claude/references/sd-simplysm14/apis/service-server/server.md +0 -57
  89. package/claude/references/sd-simplysm14/apis/storage/README.md +0 -71
  90. package/claude/rules/sd-base-rules.md +0 -306
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sd-unpack
3
- description: 메일·문서(eml/msg/pdf/docx/pptx/xlsx/xlsb, 레거시 doc/ppt/xls)를 첨부 포함 재귀적으로 풀어 평문 트리로 펼치기. Use when 위 형식 파일의 본문·첨부 전반을 훑어야 할 때 (분석·요약·정리·검토 등). 단순 단답 조회(특정 값/셀 확인)나 옆에 이미 펼친 `<basename>_<ext>/` 폴더가 있으면 호출 X.
3
+ description: 메일·문서(eml/msg/pdf/docx/pptx/xlsx/xlsb, 레거시 doc/ppt/xls)를 첨부 포함 재귀적으로 풀어 평문 트리로 펼치기. Use when 위 형식 파일의 본문·첨부 전반을 훑어야 할 때 (분석·요약·정리·검토 등). 단순 단답 조회(특정 값/셀 확인)나 옆에 이미 펼친 `<basename>_<ext>/` 폴더가 있으면 호출 금지.
4
4
 
5
5
  ---
6
6
 
@@ -12,21 +12,21 @@ description: 메일·문서(eml/msg/pdf/docx/pptx/xlsx/xlsb, 레거시 doc/ppt/x
12
12
  python .claude/skills/sd-unpack/scripts/unpack.py <입력파일 절대경로>
13
13
  ```
14
14
 
15
- 스크립트가 형식 분기·재귀 풀이·README 생성까지 알아서 처리함. stdout 으로 결과 폴더 절대경로가 출력됨.
15
+ 스크립트가 형식 분기·재귀 풀이·README 생성까지 처리. stdout 으로 결과 폴더 절대경로 출력.
16
16
 
17
- 여러 파일을 풀어달라는 요청이 오면 단일 파일 단위로 반복 호출함.
17
+ 여러 파일을 풀어달라는 요청이 오면 단일 파일 단위로 반복 호출.
18
18
 
19
19
  ## 환경
20
20
 
21
- Windows + MS Office 필요 (docx/pptx/xlsx 변환). Python 패키지 의존은 `ensure_pip` 가 자동 처리. COM 의존 미충족 시 해당 형식 핸들러는 throw.
21
+ Windows + MS Office 필요 (docx/pptx/xlsx 변환용). Python 패키지 의존은 `ensure_pip` 가 자동 처리. COM(Component Object Model) 의존 미충족 시 해당 형식 핸들러는 throw.
22
22
 
23
23
  ## 결과 폴더
24
24
 
25
- 입력 파일 옆에 `<basename>_<ext>/` 생김. 컨테이너 첨부는 같은 패턴으로 재귀적으로 풀림. 폴더 안 `_source.<ext>` + `README.md` 가 풀린 폴더의 식별 마커.
25
+ 입력 파일 옆에 `<basename>_<ext>/` 폴더 생성. 컨테이너 첨부는 같은 패턴으로 재귀적으로 풀림. 폴더 안 `_source.<ext>` + `README.md` 가 풀린 폴더의 식별 마커.
26
26
 
27
- 동일 입력 재호출 시 기존 결과 폴더는 사전 삭제 후 재생성 (이전 산출 잔존물 섞이지 않게).
27
+ 동일 입력 재호출 시 기존 결과 폴더는 사전 삭제 후 재생성 (이전 산출 잔존물이 섞이지 않게).
28
28
 
29
- 시각은 PNG, 텍스트/구조는 JSONL/JSON/MD 로 분리해 출력함.
29
+ 시각은 PNG, 텍스트·구조는 JSONL/JSON/MD 로 분리 출력.
30
30
 
31
31
  ```
32
32
  meeting_eml/
@@ -65,23 +65,23 @@ meeting_eml/
65
65
  | pdf | `pages/<NNN>.png` | `pages/<NNN>.jsonl` (블록 bbox + 표 셀 단위) | `images/p<NNN>_b<bid>.<ext>`, `attachments/` (PDF 임베드) |
66
66
  | eml/msg | — | `body.md` (평문 본문), `headers.json`, `images.rels.json` | `body.html` (원본), `attachments/` (컨테이너면 재귀) |
67
67
 
68
- `attachments/` 안 컨테이너 첨부는 같은 패턴으로 `<basename>_<ext>/` 폴더로 재귀 풀이.
68
+ `attachments/` 안 컨테이너 첨부는 같은 패턴의 `<basename>_<ext>/` 폴더로 재귀 풀이.
69
69
 
70
70
  ## JSONL 공통 규약
71
71
 
72
72
  모든 jsonl 출력은 **한 줄 = 한 노드 (또는 한 행/셀)**. 빈 키 생략. JSON 네이티브 타입 보존. datetime → ISO 8601 문자열.
73
73
 
74
74
  조회 패턴:
75
- - 좌표·인덱스 직격 grep (`"r":11`·`"slide":5`·`"node":42`).
75
+ - 좌표·인덱스로 직접 grep (`"r":11`·`"slide":5`·`"node":42`).
76
76
  - 키 grep (`"type":"heading"`·`"_f"` 수식 행만).
77
77
  - Read offset = 행/노드 인덱스 1:1 (빈 노드도 한 줄 유지).
78
78
 
79
79
  ## xlsx jsonl 규약
80
80
 
81
- 시트별 `.jsonl` — 분석 핵심 (값·number_format·수식·merges·hyperlinks·comments). 시각 표시 (바탕색·border·폰트)·frozen·dims 는 미보존 (PNG 가 시각 보조, 필요 시 `_source.xlsx` 직접 추출).
81
+ 시트별 `.jsonl` — 분석 핵심 (값·number_format·수식·merges·hyperlinks·comments) 보존. 시각 표시 (바탕색·border·폰트)·frozen·dims 는 미보존 (PNG 가 시각 보조 역할, 필요 시 `_source.xlsx` 직접 추출).
82
82
 
83
83
  - 첫 줄: `{"_meta":{"merges":["A1:C1",...], "number_formats":{"E1":"yyyy-mm-dd",...}, "hyperlinks":{"D5":"http://..."}, "comments":{"E3":"메모"}}}`.
84
- - `merges`: 머지된 셀 영역 (셀 좌표 해석에 필수 — 머지 영역 안 빈 셀 오해 차단).
84
+ - `merges`: 머지된 셀 영역 (셀 좌표 해석에 필수 — 머지 영역 안 빈 셀 오해 방지).
85
85
  - `number_formats`: General(기본) 외 셀 표시 형식 — Date·통화·% 등 셀 값 의미 단서.
86
86
  - `hyperlinks`: 셀 URL (URL 자체가 셀 정보).
87
87
  - `comments`: 셀 메모.
@@ -90,17 +90,17 @@ meeting_eml/
90
90
  - `r`: 1-based 행번호 (Excel 동일).
91
91
  - 열문자 키 (`A`·`B`·...·`AA`·...): 셀 값. 빈 셀은 키 생략.
92
92
  - `_f`: 같은 행 수식 맵 `{열문자: 수식문자열}`. 수식 없는 행은 키 생략.
93
- - 빈 행도 `{"r":N}` 한 줄 유지 → Read offset = 행번호 (오프바이원 차단).
93
+ - 빈 행도 `{"r":N}` 한 줄 유지 → Read offset = 행번호 (오프바이원 방지).
94
94
  - 값 타입: JSON 네이티브 (`int`·`float`·`bool`·`str`), datetime 은 ISO 8601 문자열.
95
95
 
96
96
  ### Chartsheet (시트 자체가 차트)
97
97
 
98
- xlsx 안 시트는 일반 Worksheet 외에 **Chartsheet** (셀 없이 차트 1) 도 있을 수 있음.
98
+ xlsx 안 시트는 일반 Worksheet 외에 **Chartsheet** (셀 없이 차트 1개만 있는 시트) 도 있을 수 있음.
99
99
 
100
100
  - Chartsheet 는 `sheets/<idx>_<name>.jsonl` 미생성 (셀 없음).
101
101
  - Chartsheet 의 차트 데이터: `charts/sheet<idx>_chart.data.json`.
102
- - README sheet_summaries `(chart sheet — "...")` 명시.
103
- - 일반 시트·Chartsheet 통합 시트 순서 (idx) 대로 보존.
102
+ - README sheet_summaries 항목에 `(chart sheet — "...")` 명시.
103
+ - 일반 시트·Chartsheet 통합한 시트 순서 (idx) 대로 보존.
104
104
 
105
105
  ### 워크북 단위 `workbook.meta.json`
106
106
 
@@ -112,7 +112,7 @@ xlsx 안 시트는 일반 Worksheet 외에 **Chartsheet** (셀 없이 차트 1
112
112
 
113
113
  ## pptx jsonl 규약
114
114
 
115
- 슬라이드별 `slides/<idx>_<title>.jsonl`. 원본 XML 순서 (shape_idx 순) 그대로. 시각 순서는 `pos` 좌표 기반으로 Claude 가 필요시 정렬.
115
+ 슬라이드별 `slides/<idx>_<title>.jsonl` 생성. 원본 XML 순서 (shape_idx 순) 그대로 유지. 시각 순서는 `pos` 좌표 기반으로 Claude 가 필요시 정렬.
116
116
 
117
117
  - 첫 줄: `{"_meta":{"slide":N, "title":"슬라이드 제목 또는 빈 문자열", "size":[w,h], "shapes":S}}`.
118
118
  - `size`: 슬라이드 폭/높이 (EMU 단위, python-pptx 원본).
@@ -139,14 +139,14 @@ paragraph 안 hyperlink 가 있으면 `hyperlinks`: `[{"text":"...", "url":"..."
139
139
 
140
140
  ## docx jsonl 규약
141
141
 
142
- 문서 단일 시퀀스 `content.jsonl`. 페이지 단위 폐기 (Word 렌더 산물). 원본 = python-docx 의 문단/표/이미지 시퀀스.
142
+ 문서 단일 시퀀스 `content.jsonl` 생성. 페이지 단위는 폐기 (Word 렌더 산물이라 원본 구조와 무관). 원본 시퀀스는 python-docx 의 문단·표·이미지 시퀀스를 따름.
143
143
 
144
144
  - 첫 줄: `{"_meta":{"paragraphs":P, "tables":T, "images":I}}`.
145
145
  - 노드 줄: `{"node":N, "type":"<type>", ...추가 키}`.
146
146
  - `node`: 0-based 시퀀스 인덱스 (Read offset = node).
147
147
 
148
148
  노드 type:
149
- - `heading`: 키 `text`·`level` (1·2·3·...) — docx Heading 스타일 기반만 (휴리스틱 추정 X).
149
+ - `heading`: 키 `text`·`level` (1·2·3·...) — docx Heading 스타일 기반만 (휴리스틱 추정 금지).
150
150
  - `para`: 키 `text` (빈 paragraph 도 노드로 보존, text="").
151
151
  - `bullet`: 키 `text`·`level` (0-based ilvl).
152
152
  - `table_cell`: 키 `table_idx`·`row`·`col` (1-based)·`text`. 머지 시 `colspan` 추가 (gridSpan>1 일 때만). vMerge='continue' cell 은 skip (origin 만).
@@ -157,11 +157,11 @@ paragraph 안 hyperlink 가 있으면 `hyperlinks`: `[{"text":"...", "url":"..."
157
157
  페이지 매핑 별도 `pages.meta.json`:
158
158
  - `{"001":{"text":"<페이지 평문>"}, "002":{...}, ...}` (PNG 페이지 ↔ fitz 추출 raw text).
159
159
  - PNG 는 fitz 페이지 분할 그대로 (시각 검증용).
160
- - 노드 인덱스 자동 매핑은 미적용 (fitz·python-docx 텍스트 분할 차이로 오매핑 위험) — Claude 가 페이지 text 와 content.jsonl 노드 text 를 직접 grep 비교.
160
+ - 노드 인덱스 자동 매핑은 미적용 (fitz·python-docx 텍스트 분할 차이로 오매핑 위험) — Claude 가 페이지 text 와 content.jsonl 노드 text 를 직접 grep 비교.
161
161
 
162
162
  ## pdf jsonl 규약
163
163
 
164
- 페이지별 `pages/<NNN>.jsonl`. PDF 페이지는 원본 단위.
164
+ 페이지별 `pages/<NNN>.jsonl` 생성. PDF 페이지는 원본 단위.
165
165
 
166
166
  - 첫 줄: `{"_meta":{"page":N, "size":[w,h], "blocks":B, "tables":T, "table_cells":C, "form_fields":F, "annotations":A}}`.
167
167
  - 노드 줄:
@@ -170,7 +170,7 @@ paragraph 안 hyperlink 가 있으면 `hyperlinks`: `[{"text":"...", "url":"..."
170
170
  - `table_cell`: `{"page":N, "type":"table_cell", "table_idx":T, "table_bbox":[...], "row":R, "col":C, "text":"..."}`.
171
171
  - `form_field`: `{"page":N, "type":"form_field", "name":"...", "field_type":"text", "value":"...", "bbox":[...]}` (PDF 양식 입력란).
172
172
  - `annotation`: `{"page":N, "type":"annotation", "subtype":"Highlight", "bbox":[...], "content":"...", "author":"..."}` (주석·highlight·sticky note).
173
- - 모든 블록 보존 (표 영역과 겹쳐도 skip 안 함) — find_tables 정확도 100% 가정 시 정보 손실 위험 회피. text_block·image_block·table_cell 노드가 동일 영역에 중복 출력될 수 있음. Claude 가 양쪽 비교 판단.
173
+ - 모든 블록 보존 (표 영역과 겹쳐도 skip 안 함) — find_tables 정확도 100% 가정 시 정보 손실 위험을 회피. text_block·image_block·table_cell 노드가 동일 영역에 중복 출력될 수 있음. Claude 가 양쪽을 비교하여 판단.
174
174
  - bbox 는 PDF 기준 좌표 (left-top, pt 단위, raw float).
175
175
 
176
176
  heading 추출은 미적용 (PDF 는 style 정보 없음). OCR 미적용 (스캔 PDF 는 image_block 만 추출).
@@ -179,41 +179,41 @@ heading 추출은 미적용 (PDF 는 style 정보 없음). OCR 미적용 (스캔
179
179
 
180
180
  본문 안 `<img cid:...>` 가 첨부의 어느 파일인지 추적.
181
181
 
182
- - `images.rels.json`: `{"<cid>":"attachments/image001.png", ...}` (HTML 본문 안 cid → 첨부 파일명).
183
- - HTML→평문 변환본 원래 `<img>` 위치에 `![image001.png](attachments/image001.png)` placeholder 삽입.
182
+ - `images.rels.json`: `{"<cid>":"attachments/image001.png", ...}` (HTML 본문 안 cid → 첨부 파일명 매핑).
183
+ - HTML→평문 변환본의 원래 `<img>` 위치에 `![image001.png](attachments/image001.png)` placeholder 삽입.
184
184
  - text/plain 만 있을 때 → `body.md` 자체가 변환본 → placeholder 포함.
185
185
  - text/plain·HTML 둘 다 있을 때 → `body.md` 는 plain (placeholder 없음), `body.from_html.md` 가 변환본 (placeholder 포함).
186
186
  - 인라인 이미지 없으면 `images.rels.json` 미생성.
187
187
 
188
188
  ## TNEF (winmail.dat) 풀이
189
189
 
190
- Outlook RTF 메일이 첨부를 `winmail.dat` 단일 binary (TNEF 형식) 로 패키징한 경우, `tnefparse` 로 내부 첨부 추출하여 `attachments/` 에 같이 풀어 둠. 원본 `winmail.dat` 도 유지 (원본 보존).
190
+ Outlook RTF 메일이 첨부를 `winmail.dat` 단일 binary (TNEF, Transport Neutral Encapsulation Format) 로 패키징한 경우, `tnefparse` 로 내부 첨부를 추출하여 `attachments/` 에 풀어 둠. 원본 `winmail.dat` 도 유지 (원본 보존).
191
191
 
192
- 내부 첨부도 컨테이너 (xlsx·pptx 등) 면 재귀 풀이 (다른 첨부와 동일).
192
+ 내부 첨부가 컨테이너 (xlsx·pptx 등) 면 재귀 풀이 (다른 첨부와 동일).
193
193
 
194
194
  ## eml/msg 본문 규약
195
195
 
196
- 본문 흐름 정확성(text/plain 우선) + 인라인 이미지 위치 단서(HTML→평문 변환본) 둘 다 보존:
196
+ 본문 흐름 정확성 (text/plain 우선) 인라인 이미지 위치 단서 (HTML→평문 변환본) 둘 다 보존:
197
197
 
198
198
  - `body.md`: 항상 별도 파일 (인라인 cutoff 폐기).
199
199
  - text/plain 있으면 우선 — 발신자가 의도한 평문, 변환 잡음 없음.
200
200
  - 없으면 text/html → 평문 추출.
201
201
  - `body.from_html.md`: text/plain·HTML 둘 다 있을 때만 별도 생성.
202
- - HTML→평문 변환 (이미지 위치 placeholder 포함).
203
- - body.md 가 plain 이라 잃은 위치 정보를 보완.
202
+ - HTML→평문 변환본 (이미지 위치 placeholder 포함).
203
+ - `body.md` 가 plain 이라 누락된 위치 정보를 보완.
204
204
  - `body.html`: 원본 HTML (있을 때).
205
- - `headers.json`: 모든 메일 헤더 원본 보존 (envelope + `X-Mailer`·`Authentication-Results` 등 모두). 동일 키 다수면 list 누적.
206
- - README 헤더 섹션에는 표준 envelope 키만 표기 (전체는 headers.json 직접 조회).
205
+ - `headers.json`: 모든 메일 헤더를 원본 그대로 보존 (envelope + `X-Mailer`·`Authentication-Results` 등 모두). 동일 키 다수면 list 누적.
206
+ - `README.md` 헤더 섹션에는 표준 envelope 키만 표기 (전체는 `headers.json` 직접 조회).
207
207
 
208
208
  ## xlsb 클린업
209
209
 
210
- - legacy → xlsx 변환 시 `_converted.xlsx` 는 임시 폴더에서만 처리 (산출 폴더에 미잔존).
211
- - VBA 매크로는 원본 코드 그대로 `macros/<모듈명>.vba` 저장 (변형 X).
212
- - VBA 시트 객체명↔raw 시트명 매핑은 `workbook.meta.json` 의 `sheet_code_map` (예: `{"Sheet1":"BOA","Sheet3":"Mapping"}`).
210
+ - legacy → xlsx 변환 시 `_converted.xlsx` 는 임시 폴더에서만 처리 (산출 폴더에 잔존시키지 않음).
211
+ - VBA(Visual Basic for Applications) 매크로는 원본 코드 그대로 `macros/<모듈명>.vba` 저장 (변형 금지).
212
+ - VBA 시트 객체명 ↔ raw 시트명 매핑은 `workbook.meta.json` 의 `sheet_code_map` 키에 저장 (예: `{"Sheet1":"BOA","Sheet3":"Mapping"}`).
213
213
 
214
214
  ## 산출물 사용
215
215
 
216
- 후속 스킬(sd-spec 등)은 결과 폴더의 `README.md` 한 번 Read 본문 위치·헤더·첨부 목록·손실 영역을 모두 파악할 있음. 컨테이너 첨부는 자체 `README.md` 를 가지므로 재귀적으로 같은 방식으로 들어감.
216
+ 후속 스킬 (sd-spec 등) 은 결과 폴더의 `README.md` 한 번 Read 하여 본문 위치·헤더·첨부 목록·손실 영역을 모두 파악 가능. 컨테이너 첨부는 자체 `README.md` 를 가지므로 같은 방식으로 재귀 진입.
217
217
 
218
218
  각 형식별 jsonl 의 grep 패턴:
219
219
  - xlsx: `"r":<행>` · `"_f"` (수식 행) · 열문자 키.
@@ -702,6 +702,70 @@ def _pptx_save_picture(
702
702
  # XLSX
703
703
  # ====================================================================
704
704
 
705
+ def _xlsx_clean_nonfinite(src: Path, dst: Path) -> None:
706
+ """xlsx 시트 XML 안 `<v>NaN</v>`/`<v>Infinity</v>`/`<v>-Infinity</v>` 를 제거한 사본을 dst 에 생성.
707
+
708
+ 원인: 일부 third-party 라이브러리가 만든 xlsx 가 비유한 부동소수점(NaN/Inf) 을 numeric 셀에
709
+ 문자열 그대로 기록 → openpyxl 의 `_cast_number → int('NaN')` 에서 ValueError.
710
+ 대응: 시트 XML 의 해당 `<v>` 요소만 제거(해당 셀은 빈 셀 처리). 다른 part(images·drawings·
711
+ styles·shared strings) 는 그대로 복사.
712
+ """
713
+ pat = re.compile(rb"<v>(?:NaN|Infinity|-Infinity|INF|-INF)</v>")
714
+ with zipfile.ZipFile(_common.long_str(src), "r") as zin, \
715
+ zipfile.ZipFile(_common.long_str(dst), "w", zipfile.ZIP_DEFLATED) as zout:
716
+ for item in zin.infolist():
717
+ data = zin.read(item.filename)
718
+ if item.filename.startswith("xl/worksheets/") and item.filename.endswith(".xml"):
719
+ data = pat.sub(b"", data)
720
+ zout.writestr(item, data)
721
+
722
+
723
+ def _safe_load_xlsx_workbooks(
724
+ input_path: Path,
725
+ cleanup_paths: list[Path],
726
+ ) -> tuple[Any, Any, Path]:
727
+ """openpyxl 로 wb_values(data_only=True) + wb_formulas(data_only=False) 둘 다 로드.
728
+
729
+ 비표준 셀값(NaN/Infinity 문자열을 numeric 셀에 담은 xlsx) 은 openpyxl 의 strict int cast 로
730
+ ValueError throw. 이 경우 시트 XML 의 비유한값만 제거한 정제본을 임시 파일로 만들어 재시도.
731
+ 정제 발생시 임시 파일을 `cleanup_paths` 에 등록(호출자가 finally 에서 unlink).
732
+
733
+ 반환: (wb_values, wb_formulas, openpyxl_input_path). openpyxl_input_path 는 후속 openpyxl
734
+ 호출(이미지 추출 등) 이 같은 정제본을 재사용하도록 path 노출. 정제 불필요시 input_path 그대로.
735
+ """
736
+ import tempfile
737
+ from openpyxl import load_workbook
738
+
739
+ def _is_nonfinite_error(e: BaseException) -> bool:
740
+ cur: Optional[BaseException] = e
741
+ while cur is not None:
742
+ msg = str(cur)
743
+ if "NaN" in msg or "Infinity" in msg:
744
+ return True
745
+ cur = cur.__cause__
746
+ return False
747
+
748
+ src_str = _common.long_str(input_path)
749
+ try:
750
+ wb_values = load_workbook(src_str, data_only=True)
751
+ wb_formulas = load_workbook(src_str, data_only=False)
752
+ return wb_values, wb_formulas, input_path
753
+ except ValueError as e:
754
+ if not _is_nonfinite_error(e):
755
+ raise
756
+
757
+ base = _common._ensure_tmp_base()
758
+ fd, tmp_str = tempfile.mkstemp(prefix="sd-unpack-xlsx-clean-", suffix=".xlsx", dir=str(base))
759
+ os.close(fd)
760
+ cleaned = Path(tmp_str)
761
+ # 등록을 정제·로드 전에 수행 → 도중 throw 해도 호출자 finally 가 unlink.
762
+ cleanup_paths.append(cleaned)
763
+ _xlsx_clean_nonfinite(input_path, cleaned)
764
+ wb_values = load_workbook(_common.long_str(cleaned), data_only=True)
765
+ wb_formulas = load_workbook(_common.long_str(cleaned), data_only=False)
766
+ return wb_values, wb_formulas, cleaned
767
+
768
+
705
769
  def _run_xlsx(
706
770
  input_path: Path,
707
771
  out_dir: Path,
@@ -722,174 +786,185 @@ def _run_xlsx(
722
786
  sheet_formula_count: dict[str, int] = {}
723
787
  sheet_dims: dict[str, tuple[int, int]] = {}
724
788
 
725
- wb_values = load_workbook(_common.long_str(input_path), data_only=True)
726
- wb_formulas = load_workbook(_common.long_str(input_path), data_only=False)
789
+ # 비표준 셀값(NaN/Infinity) 사전 정제 + openpyxl 로드. 정제본 임시파일은 마지막에 unlink.
790
+ _xlsx_cleanups: list[Path] = []
727
791
  try:
728
- _common.mkdir(sheets_dir)
729
- # openpyxl 의 sheetnames 는 일반 Worksheet 와 Chartsheet 둘 다 포함.
730
- # 시트 순서 그대로 idx 통합 부여 (사용자 워크북 순서 보존).
731
- # 일반 Worksheet COM Excel PNG export 대상, Chartsheet 차트 데이터만 추출.
732
- idx_counter = 0
733
- for name in wb_values.sheetnames:
734
- obj = wb_values[name]
735
- idx_counter += 1
736
- idx = f"{idx_counter:02d}"
737
- safe_name = _common.slugify_filename(name, max_len=40)
738
- if isinstance(obj, Worksheet):
739
- sheet_names.append((idx, safe_name, name))
740
- else:
741
- # Chartsheet 등 비-worksheet
742
- chart_sheet_names.append((idx, safe_name, name))
743
-
744
- # COM Excel 호출: 데이터 영역 → ChartObject + Range.CopyPicture 시트별 PNG.
745
- # 시트별 (last_row, last_col) 도 같이 반환되어 .jsonl 이 같은 데이터 영역으로 통일됨.
746
- # PNG export 실패한 시트는 sheet_png_skipped 사유 (silent skip 금지).
747
- with _common.com_lock():
748
- sheet_ranges, sheet_png_skipped = _excel_export_sheet_pngs(input_path, sheets_dir, sheet_names)
749
-
750
- for idx, safe_name, raw_name in sheet_names:
751
- ws_v = wb_values[raw_name]
752
- ws_f = wb_formulas[raw_name]
753
-
754
- # COM Find 결과가 있으면 그 범위, 없으면 openpyxl max_row/max_column fallback.
755
- last_row, last_col = sheet_ranges.get(raw_name, (ws_v.max_row, ws_v.max_column))
756
- sheet_dims[idx] = (last_row, last_col)
757
-
758
- jsonl_lines, formula_n = _sheet_to_jsonl(ws_v, ws_f, last_row, last_col)
759
- _common.write_text(sheets_dir / f"{idx}_{safe_name}.jsonl", "\n".join(jsonl_lines))
760
- sheet_formula_count[idx] = formula_n
761
-
762
- for chart_idx, chart in enumerate(getattr(ws_f, "_charts", []), start=1):
763
- data = _extract_openpyxl_chart_data(chart)
764
- _common.mkdir(charts_dir)
765
- chart_filename = f"sheet{idx}_chart{chart_idx:02d}.data.json"
766
- _common.write_text(
767
- charts_dir / chart_filename,
768
- json.dumps(data, ensure_ascii=False, indent=2),
769
- )
770
- sheet_charts.setdefault(idx, []).append(chart_filename)
771
-
772
- # Chartsheet 처리: 차트 데이터를 charts/sheet<idx>_chart.data.json 으로 저장
773
- chart_sheet_chart_files: dict[str, str] = {} # idx -> chart filename
774
- for idx, safe_name, raw_name in chart_sheet_names:
775
- cs = wb_formulas[raw_name]
776
- chart = None
777
- # Chartsheet.charts 또는 _charts 속성 (openpyxl 버전 따라 다름)
778
- for attr in ("charts", "_charts"):
779
- v = getattr(cs, attr, None)
780
- if v:
781
- if hasattr(v, "__iter__"):
782
- try:
783
- chart = next(iter(v), None)
784
- except Exception:
785
- chart = None
786
- else:
787
- chart = v
788
- if chart is not None:
789
- break
790
- if chart is None:
791
- # 단일 chart 속성 fallback
792
- chart = getattr(cs, "chart", None)
793
- if chart is not None:
794
- try:
792
+ wb_values, wb_formulas, openpyxl_input = _safe_load_xlsx_workbooks(input_path, _xlsx_cleanups)
793
+ try:
794
+ _common.mkdir(sheets_dir)
795
+ # openpyxl sheetnames 일반 Worksheet Chartsheet 포함.
796
+ # 시트 순서 그대로 idx 통합 부여 (사용자 워크북 순서 보존).
797
+ # 일반 Worksheet 만 COM Excel PNG export 대상, Chartsheet 는 차트 데이터만 추출.
798
+ idx_counter = 0
799
+ for name in wb_values.sheetnames:
800
+ obj = wb_values[name]
801
+ idx_counter += 1
802
+ idx = f"{idx_counter:02d}"
803
+ safe_name = _common.slugify_filename(name, max_len=40)
804
+ if isinstance(obj, Worksheet):
805
+ sheet_names.append((idx, safe_name, name))
806
+ else:
807
+ # Chartsheet 등 비-worksheet
808
+ chart_sheet_names.append((idx, safe_name, name))
809
+
810
+ # COM Excel 호출: 데이터 영역 ChartObject + Range.CopyPicture → 시트별 PNG.
811
+ # 시트별 (last_row, last_col) 도 같이 반환되어 .jsonl 이 같은 데이터 영역으로 통일됨.
812
+ # PNG export 실패한 시트는 sheet_png_skipped 사유 (silent skip 금지).
813
+ with _common.com_lock():
814
+ # openpyxl_input 사용: 정제본(NaN 제거) 이 있으면 COM Excel 도 정제본을 열어야 함
815
+ # (Excel 역시 `<v>NaN</v>` 가 있는 xlsx 의 Open 에 실패).
816
+ sheet_ranges, sheet_png_skipped = _excel_export_sheet_pngs(openpyxl_input, sheets_dir, sheet_names)
817
+
818
+ for idx, safe_name, raw_name in sheet_names:
819
+ ws_v = wb_values[raw_name]
820
+ ws_f = wb_formulas[raw_name]
821
+
822
+ # COM Find 결과가 있으면 범위, 없으면 openpyxl max_row/max_column fallback.
823
+ last_row, last_col = sheet_ranges.get(raw_name, (ws_v.max_row, ws_v.max_column))
824
+ sheet_dims[idx] = (last_row, last_col)
825
+
826
+ jsonl_lines, formula_n = _sheet_to_jsonl(ws_v, ws_f, last_row, last_col)
827
+ _common.write_text(sheets_dir / f"{idx}_{safe_name}.jsonl", "\n".join(jsonl_lines))
828
+ sheet_formula_count[idx] = formula_n
829
+
830
+ for chart_idx, chart in enumerate(getattr(ws_f, "_charts", []), start=1):
795
831
  data = _extract_openpyxl_chart_data(chart)
796
- except Exception:
797
- data = None
798
- if data is not None:
799
832
  _common.mkdir(charts_dir)
800
- chart_filename = f"sheet{idx}_chart.data.json"
833
+ chart_filename = f"sheet{idx}_chart{chart_idx:02d}.data.json"
801
834
  _common.write_text(
802
835
  charts_dir / chart_filename,
803
836
  json.dumps(data, ensure_ascii=False, indent=2),
804
837
  )
805
- chart_sheet_chart_files[idx] = chart_filename
806
-
807
- # 워크북 단위 메타 (defined names·pivots·sheet codeName 등) — 시트 jsonl 외부 분리.
808
- wb_meta = _workbook_meta(wb_formulas, input_path)
809
- # VBA 시트 객체명 raw 시트명 매핑 (시트 codeName 기반)
810
- sheet_code_map: dict[str, str] = {}
811
- for ws in wb_formulas.worksheets:
812
- code = getattr(ws.sheet_properties, "codeName", None)
813
- if code:
814
- sheet_code_map[code] = ws.title
815
- if sheet_code_map:
816
- wb_meta["sheet_code_map"] = sheet_code_map
817
- if wb_meta:
818
- _common.write_text(
819
- out_dir / "workbook.meta.json",
820
- json.dumps(wb_meta, ensure_ascii=False, indent=2),
821
- )
822
- finally:
823
- wb_values.close()
824
- wb_formulas.close()
825
-
826
- # 시트 PNG 데이터 영역(Find 범위) 만 캡처 → 데이터 영역 밖 이미지는 누락될 수 있음 →
827
- # raw 이미지를 시트+셀 위치 정보 포함해서 별도 보존.
828
- sheet_images = _extract_xlsx_images_with_position(input_path, out_dir, sheet_names)
829
- attachment_links = _extract_zip_media(
830
- input_path,
831
- out_dir,
832
- media_zip_prefix="xl/media/",
833
- embed_zip_prefix="xl/embeddings/",
834
- )
835
-
836
- # 시트별 산출물 풀목록 — 일반 시트 + chart sheet 통합, 시트 순서 (idx) 대로
837
- sheet_summary_map: dict[str, str] = {}
838
- for idx, safe_name, raw_name in sheet_names:
839
- last_row, last_col = sheet_dims.get(idx, (0, 0))
840
- formula_n = sheet_formula_count.get(idx, 0)
841
- png_path = sheets_dir / f"{idx}_{safe_name}.png"
842
- if png_path.exists():
843
- parts = [f"`sheets/{idx}_{safe_name}.png`", "`.jsonl`"]
844
- else:
845
- # PNG 미생성 worker 가 사유 전달 (16-bit cap / COM 실패 등)
846
- reason = sheet_png_skipped.get(raw_name, "사유 미상")
847
- parts = [f"`sheets/{idx}_{safe_name}.jsonl`", f"(PNG 미생성 — {reason})"]
848
- chart_refs = sheet_charts.get(idx, [])
849
- if chart_refs:
850
- parts.append("(차트: " + ", ".join(f"`charts/{c}`" for c in chart_refs) + ")")
851
- img_refs = sheet_images.get(raw_name, [])
852
- if img_refs:
853
- parts.append("(이미지: " + ", ".join(f"`images/{n}`" for n in img_refs) + ")")
854
- meta = f"({last_row}행×{last_col}열"
855
- if formula_n:
856
- meta += f", 수식 {formula_n}개"
857
- meta += ")"
858
- sheet_summary_map[idx] = " ".join(parts) + " " + meta
859
-
860
- for idx, safe_name, raw_name in chart_sheet_names:
861
- chart_filename = chart_sheet_chart_files.get(idx)
862
- if chart_filename:
863
- sheet_summary_map[idx] = f"`charts/{chart_filename}` (chart sheet \"{raw_name}\")"
864
- else:
865
- sheet_summary_map[idx] = f"(chart sheet — \"{raw_name}\", 차트 데이터 추출 실패)"
866
-
867
- # idx 순서대로 통합
868
- for idx in sorted(sheet_summary_map.keys()):
869
- sheet_summaries.append(sheet_summary_map[idx])
870
-
871
- source_name, source_size = _source_meta(input_path, out_dir, source_name_override)
872
- macro_modules = _extract_macros(_source_path(out_dir, source_name), out_dir)
838
+ sheet_charts.setdefault(idx, []).append(chart_filename)
839
+
840
+ # Chartsheet 처리: 차트 데이터를 charts/sheet<idx>_chart.data.json 으로 저장
841
+ chart_sheet_chart_files: dict[str, str] = {} # idx -> chart filename
842
+ for idx, safe_name, raw_name in chart_sheet_names:
843
+ cs = wb_formulas[raw_name]
844
+ chart = None
845
+ # Chartsheet.charts 또는 _charts 속성 (openpyxl 버전 따라 다름)
846
+ for attr in ("charts", "_charts"):
847
+ v = getattr(cs, attr, None)
848
+ if v:
849
+ if hasattr(v, "__iter__"):
850
+ try:
851
+ chart = next(iter(v), None)
852
+ except Exception:
853
+ chart = None
854
+ else:
855
+ chart = v
856
+ if chart is not None:
857
+ break
858
+ if chart is None:
859
+ # 단일 chart 속성 fallback
860
+ chart = getattr(cs, "chart", None)
861
+ if chart is not None:
862
+ try:
863
+ data = _extract_openpyxl_chart_data(chart)
864
+ except Exception:
865
+ data = None
866
+ if data is not None:
867
+ _common.mkdir(charts_dir)
868
+ chart_filename = f"sheet{idx}_chart.data.json"
869
+ _common.write_text(
870
+ charts_dir / chart_filename,
871
+ json.dumps(data, ensure_ascii=False, indent=2),
872
+ )
873
+ chart_sheet_chart_files[idx] = chart_filename
874
+
875
+ # 워크북 단위 메타 (defined names·pivots·sheet codeName 등) — 시트 jsonl 외부 분리.
876
+ wb_meta = _workbook_meta(wb_formulas, input_path)
877
+ # VBA 시트 객체명 ↔ raw 시트명 매핑 (시트 codeName 기반)
878
+ sheet_code_map: dict[str, str] = {}
879
+ for ws in wb_formulas.worksheets:
880
+ code = getattr(ws.sheet_properties, "codeName", None)
881
+ if code:
882
+ sheet_code_map[code] = ws.title
883
+ if sheet_code_map:
884
+ wb_meta["sheet_code_map"] = sheet_code_map
885
+ if wb_meta:
886
+ _common.write_text(
887
+ out_dir / "workbook.meta.json",
888
+ json.dumps(wb_meta, ensure_ascii=False, indent=2),
889
+ )
890
+ finally:
891
+ wb_values.close()
892
+ wb_formulas.close()
893
+
894
+ # 시트 PNG 는 데이터 영역(Find 범위) 만 캡처 → 데이터 영역 밖 이미지는 누락될 수 있음 →
895
+ # raw 이미지를 시트+셀 위치 정보 포함해서 별도 보존.
896
+ # openpyxl_input 사용: 정제본이 있으면 같은 정제본으로 로드(원본은 openpyxl 못 읽음).
897
+ sheet_images = _extract_xlsx_images_with_position(openpyxl_input, out_dir, sheet_names)
898
+ attachment_links = _extract_zip_media(
899
+ input_path,
900
+ out_dir,
901
+ media_zip_prefix="xl/media/",
902
+ embed_zip_prefix="xl/embeddings/",
903
+ )
873
904
 
874
- sections: dict[str, list[str]] = {}
875
- if sheet_summaries:
876
- sections[f"시트 (총 {len(sheet_summaries)}개)"] = sheet_summaries
877
- if macro_modules:
878
- sections[f"VBA 매크로 ( {len(macro_modules)}개)"] = [f"`macros/{m}`" for m in macro_modules]
905
+ # 시트별 산출물 풀목록 — 일반 시트 + chart sheet 통합, 시트 순서 (idx) 대로
906
+ sheet_summary_map: dict[str, str] = {}
907
+ for idx, safe_name, raw_name in sheet_names:
908
+ last_row, last_col = sheet_dims.get(idx, (0, 0))
909
+ formula_n = sheet_formula_count.get(idx, 0)
910
+ png_path = sheets_dir / f"{idx}_{safe_name}.png"
911
+ if png_path.exists():
912
+ parts = [f"`sheets/{idx}_{safe_name}.png`", "`.jsonl`"]
913
+ else:
914
+ # PNG 미생성 — worker 가 사유 전달 (16-bit cap / COM 실패 등)
915
+ reason = sheet_png_skipped.get(raw_name, "사유 미상")
916
+ parts = [f"`sheets/{idx}_{safe_name}.jsonl`", f"(PNG 미생성 — {reason})"]
917
+ chart_refs = sheet_charts.get(idx, [])
918
+ if chart_refs:
919
+ parts.append("(차트: " + ", ".join(f"`charts/{c}`" for c in chart_refs) + ")")
920
+ img_refs = sheet_images.get(raw_name, [])
921
+ if img_refs:
922
+ parts.append("(이미지: " + ", ".join(f"`images/{n}`" for n in img_refs) + ")")
923
+ meta = f"({last_row}행×{last_col}열"
924
+ if formula_n:
925
+ meta += f", 수식 {formula_n}개"
926
+ meta += ")"
927
+ sheet_summary_map[idx] = " ".join(parts) + " " + meta
879
928
 
880
- _common.write_readme(
881
- out_dir,
882
- source_name=source_name,
883
- source_size=source_size,
884
- tool=("openpyxl + COM Excel + ZIP " + tool_extra).strip(),
885
- loss_notes=(
886
- "셀 서식(바탕색·border·폰트)·frozen·dims 미보존 (필요 시 _source.xlsx 직접 추출). "
887
- "시각은 시트별 PNG, 분석 데이터(셀값·number_format·수식·merges·hyperlinks·comments) 는 "
888
- "시트별 .jsonl 줄=한 행(좌표 명시), 워크북 단위 메타(defined names 등) 는 workbook.meta.json."
889
- ),
890
- sections=sections or None,
891
- attachments=attachment_links,
892
- )
929
+ for idx, safe_name, raw_name in chart_sheet_names:
930
+ chart_filename = chart_sheet_chart_files.get(idx)
931
+ if chart_filename:
932
+ sheet_summary_map[idx] = f"`charts/{chart_filename}` (chart sheet — \"{raw_name}\")"
933
+ else:
934
+ sheet_summary_map[idx] = f"(chart sheet — \"{raw_name}\", 차트 데이터 추출 실패)"
935
+
936
+ # idx 순서대로 통합
937
+ for idx in sorted(sheet_summary_map.keys()):
938
+ sheet_summaries.append(sheet_summary_map[idx])
939
+
940
+ source_name, source_size = _source_meta(input_path, out_dir, source_name_override)
941
+ macro_modules = _extract_macros(_source_path(out_dir, source_name), out_dir)
942
+
943
+ sections: dict[str, list[str]] = {}
944
+ if sheet_summaries:
945
+ sections[f"시트 (총 {len(sheet_summaries)}개)"] = sheet_summaries
946
+ if macro_modules:
947
+ sections[f"VBA 매크로 (총 {len(macro_modules)}개)"] = [f"`macros/{m}`" for m in macro_modules]
948
+
949
+ _common.write_readme(
950
+ out_dir,
951
+ source_name=source_name,
952
+ source_size=source_size,
953
+ tool=("openpyxl + COM Excel + ZIP " + tool_extra).strip(),
954
+ loss_notes=(
955
+ "셀 서식(바탕색·border·폰트)·frozen·dims 미보존 (필요 시 _source.xlsx 직접 추출). "
956
+ "시각은 시트별 PNG, 분석 데이터(셀값·number_format·수식·merges·hyperlinks·comments) 는 "
957
+ "시트별 .jsonl 한 줄=한 행(좌표 명시), 워크북 단위 메타(defined names 등) 는 workbook.meta.json."
958
+ ),
959
+ sections=sections or None,
960
+ attachments=attachment_links,
961
+ )
962
+ finally:
963
+ for _p in _xlsx_cleanups:
964
+ try:
965
+ _p.unlink()
966
+ except Exception:
967
+ pass
893
968
 
894
969
 
895
970
  # ====================================================================
@@ -3,7 +3,7 @@ name: sd-use
3
3
  description: 사용자 요청을 분석해 워크스페이스의 sd-* 스킬 중 가장 적합한 스킬을 추천하는 스킬. Use when 어떤 sd-* 스킬을 써야 할지 모를 때.
4
4
  ---
5
5
 
6
- `/sd-use <요청>` 의 `<요청>` 에 적합한 sd-* 스킬을 추천만 하고 종료. 자동 실행·산출물 생성 금지.
7
- - 시스템 프롬프트내 sd-* 스킬의 프론트매터를 활용함.
8
- - 추천 후보에서 sd-use 자신은 제외 (회귀 방지).
9
- - 추천할 여러 스킬이 있다면, 추천순대로 나열하고 각각의 추천 이유를 출력함.
6
+ `/sd-use <요청>` 의 `<요청>` 에 적합한 sd-* 스킬을 추천만 출력하고 종료. 추천 대상 스킬의 자동 실행·산출물 생성 금지.
7
+ - 시스템 프롬프트 sd-* 스킬의 프론트매터(name·description)를 매칭 근거로 사용.
8
+ - 추천 후보에서 sd-use 자신은 제외 (자기 추천 방지).
9
+ - 적합한 스킬이 여러 개면, 적합도 높은 순으로 나열하고 스킬마다 추천 이유를 함께 출력.