@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.
- package/claude/references/sd-requirement-source-handling.md +20 -20
- package/claude/references/sd-simplysm14/README.md +13 -13
- package/claude/references/sd-simplysm14/manuals/client-component.md +92 -92
- package/claude/references/sd-simplysm14/manuals/client-crud.md +11 -11
- package/claude/references/sd-simplysm14/manuals/client-demo.md +28 -28
- package/claude/references/sd-simplysm14/manuals/client-rules.md +1 -1
- package/claude/references/sd-simplysm14/manuals/client-setup.md +21 -21
- package/claude/references/sd-simplysm14/manuals/client-tab.md +3 -3
- package/claude/references/sd-simplysm14/manuals/logging.md +15 -15
- package/claude/references/sd-simplysm14/manuals/orm-union.md +6 -6
- package/claude/references/sd-simplysm14/manuals/orm.md +19 -19
- package/claude/references/sd-simplysm14/manuals/test.md +33 -33
- package/claude/rules/sd-design-rules.md +18 -18
- package/claude/sd-system-prompt.md +369 -0
- package/claude/skills/sd-commit/SKILL.md +10 -10
- package/claude/skills/sd-config/SKILL.md +2 -2
- package/claude/skills/sd-demo/SKILL.md +45 -45
- package/claude/skills/sd-dev/SKILL.md +15 -15
- package/claude/skills/sd-docs/SKILL.md +7 -7
- package/claude/skills/sd-docs/references/subagent-prompt.md +33 -33
- package/claude/skills/sd-impl/SKILL.md +60 -60
- package/claude/skills/sd-review/SKILL.md +9 -9
- package/claude/skills/sd-skill/SKILL.md +74 -74
- package/claude/skills/sd-skill/evals/fixtures/existing-skill/.claude/skills/todo-format/SKILL.md +1 -1
- package/claude/skills/sd-spec/SKILL.md +354 -319
- package/claude/skills/sd-spec/references/example-spec.md +104 -104
- package/claude/skills/sd-unpack/SKILL.md +34 -34
- package/claude/skills/sd-unpack/scripts/handlers/__pycache__/office_com.cpython-314.pyc +0 -0
- package/claude/skills/sd-unpack/scripts/handlers/office_com.py +234 -159
- package/claude/skills/sd-use/SKILL.md +4 -4
- package/package.json +1 -1
- package/claude/references/sd-simplysm14/apis/angular/README.md +0 -37
- package/claude/references/sd-simplysm14/apis/angular/app-structure.md +0 -92
- package/claude/references/sd-simplysm14/apis/angular/buttons.md +0 -88
- package/claude/references/sd-simplysm14/apis/angular/crud.md +0 -100
- package/claude/references/sd-simplysm14/apis/angular/forms.md +0 -200
- package/claude/references/sd-simplysm14/apis/angular/infrastructure.md +0 -231
- package/claude/references/sd-simplysm14/apis/angular/kanban.md +0 -80
- package/claude/references/sd-simplysm14/apis/angular/layout.md +0 -92
- package/claude/references/sd-simplysm14/apis/angular/modal.md +0 -115
- package/claude/references/sd-simplysm14/apis/angular/routing.md +0 -107
- package/claude/references/sd-simplysm14/apis/angular/select-dropdown.md +0 -35
- package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +0 -82
- package/claude/references/sd-simplysm14/apis/angular/shared-data.md +0 -134
- package/claude/references/sd-simplysm14/apis/angular/sheet.md +0 -127
- package/claude/references/sd-simplysm14/apis/angular/toast.md +0 -97
- package/claude/references/sd-simplysm14/apis/angular/visual.md +0 -167
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +0 -79
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-file-system/README.md +0 -83
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +0 -91
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +0 -49
- package/claude/references/sd-simplysm14/apis/core-browser/README.md +0 -143
- package/claude/references/sd-simplysm14/apis/core-common/README.md +0 -58
- package/claude/references/sd-simplysm14/apis/core-common/extensions.md +0 -88
- package/claude/references/sd-simplysm14/apis/core-common/features.md +0 -51
- package/claude/references/sd-simplysm14/apis/core-common/types.md +0 -88
- package/claude/references/sd-simplysm14/apis/core-common/utils.md +0 -189
- package/claude/references/sd-simplysm14/apis/core-node/README.md +0 -12
- package/claude/references/sd-simplysm14/apis/core-node/consola.md +0 -59
- package/claude/references/sd-simplysm14/apis/core-node/cpx.md +0 -44
- package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +0 -42
- package/claude/references/sd-simplysm14/apis/core-node/fsx.md +0 -53
- package/claude/references/sd-simplysm14/apis/core-node/pathx.md +0 -24
- package/claude/references/sd-simplysm14/apis/core-node/worker.md +0 -65
- package/claude/references/sd-simplysm14/apis/excel/README.md +0 -193
- package/claude/references/sd-simplysm14/apis/lint/README.md +0 -94
- package/claude/references/sd-simplysm14/apis/orm-common/README.md +0 -58
- package/claude/references/sd-simplysm14/apis/orm-common/db-context.md +0 -77
- package/claude/references/sd-simplysm14/apis/orm-common/executable.md +0 -20
- package/claude/references/sd-simplysm14/apis/orm-common/expr.md +0 -92
- package/claude/references/sd-simplysm14/apis/orm-common/queryable.md +0 -98
- package/claude/references/sd-simplysm14/apis/orm-common/schema-builders.md +0 -128
- package/claude/references/sd-simplysm14/apis/orm-node/README.md +0 -69
- package/claude/references/sd-simplysm14/apis/sd-claude/README.md +0 -32
- package/claude/references/sd-simplysm14/apis/sd-cli/README.md +0 -80
- package/claude/references/sd-simplysm14/apis/sd-cli/sd-config.md +0 -155
- package/claude/references/sd-simplysm14/apis/service-client/README.md +0 -131
- package/claude/references/sd-simplysm14/apis/service-common/README.md +0 -29
- package/claude/references/sd-simplysm14/apis/service-common/app-structure.md +0 -63
- package/claude/references/sd-simplysm14/apis/service-common/messages.md +0 -56
- package/claude/references/sd-simplysm14/apis/service-common/protocol.md +0 -64
- package/claude/references/sd-simplysm14/apis/service-common/service-types.md +0 -43
- package/claude/references/sd-simplysm14/apis/service-server/README.md +0 -13
- package/claude/references/sd-simplysm14/apis/service-server/auth.md +0 -39
- package/claude/references/sd-simplysm14/apis/service-server/builtin-services.md +0 -71
- package/claude/references/sd-simplysm14/apis/service-server/define-service.md +0 -55
- package/claude/references/sd-simplysm14/apis/service-server/internals.md +0 -82
- package/claude/references/sd-simplysm14/apis/service-server/server.md +0 -57
- package/claude/references/sd-simplysm14/apis/storage/README.md +0 -71
- 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>/` 폴더가 있으면 호출
|
|
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 생성까지
|
|
15
|
+
스크립트가 형식 분기·재귀 풀이·README 생성까지 처리. stdout 으로 결과 폴더 절대경로 출력.
|
|
16
16
|
|
|
17
|
-
여러 파일을 풀어달라는 요청이 오면 단일 파일 단위로 반복
|
|
17
|
+
여러 파일을 풀어달라는 요청이 오면 단일 파일 단위로 반복 호출.
|
|
18
18
|
|
|
19
19
|
## 환경
|
|
20
20
|
|
|
21
|
-
Windows + MS Office 필요 (docx/pptx/xlsx
|
|
21
|
+
Windows + MS Office 필요 (docx/pptx/xlsx 변환용). Python 패키지 의존은 `ensure_pip` 가 자동 처리. COM(Component Object Model) 의존 미충족 시 해당 형식 핸들러는 throw.
|
|
22
22
|
|
|
23
23
|
## 결과 폴더
|
|
24
24
|
|
|
25
|
-
입력 파일 옆에 `<basename>_<ext>/`
|
|
25
|
+
입력 파일 옆에 `<basename>_<ext>/` 폴더 생성. 컨테이너 첨부는 같은 패턴으로 재귀적으로 풀림. 폴더 안 `_source.<ext>` + `README.md` 가 풀린 폴더의 식별 마커.
|
|
26
26
|
|
|
27
|
-
동일 입력 재호출 시 기존 결과 폴더는 사전 삭제 후 재생성 (이전 산출
|
|
27
|
+
동일 입력 재호출 시 기존 결과 폴더는 사전 삭제 후 재생성 (이전 산출 잔존물이 섞이지 않게).
|
|
28
28
|
|
|
29
|
-
시각은 PNG,
|
|
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/` 안 컨테이너 첨부는 같은
|
|
68
|
+
`attachments/` 안 컨테이너 첨부는 같은 패턴의 `<basename>_<ext>/` 폴더로 재귀 풀이.
|
|
69
69
|
|
|
70
70
|
## JSONL 공통 규약
|
|
71
71
|
|
|
72
72
|
모든 jsonl 출력은 **한 줄 = 한 노드 (또는 한 행/셀)**. 빈 키 생략. JSON 네이티브 타입 보존. datetime → ISO 8601 문자열.
|
|
73
73
|
|
|
74
74
|
조회 패턴:
|
|
75
|
-
-
|
|
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)
|
|
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
|
|
103
|
-
- 일반 시트·Chartsheet
|
|
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
|
|
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
|
|
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 스타일 기반만 (휴리스틱 추정
|
|
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
|
|
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% 가정 시 정보 손실
|
|
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→평문
|
|
182
|
+
- `images.rels.json`: `{"<cid>":"attachments/image001.png", ...}` (HTML 본문 안 cid → 첨부 파일명 매핑).
|
|
183
|
+
- HTML→평문 변환본의 원래 `<img>` 위치에 `` 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
|
|
190
|
+
Outlook RTF 메일이 첨부를 `winmail.dat` 단일 binary (TNEF, Transport Neutral Encapsulation Format) 로 패키징한 경우, `tnefparse` 로 내부 첨부를 추출하여 `attachments/` 에 풀어 둠. 원본 `winmail.dat` 도 유지 (원본 보존).
|
|
191
191
|
|
|
192
|
-
내부
|
|
192
|
+
내부 첨부가 컨테이너 (xlsx·pptx 등) 면 재귀 풀이 (다른 첨부와 동일).
|
|
193
193
|
|
|
194
194
|
## eml/msg 본문 규약
|
|
195
195
|
|
|
196
|
-
본문 흐름 정확성(text/plain 우선)
|
|
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→평문
|
|
203
|
-
- body.md 가 plain 이라
|
|
202
|
+
- HTML→평문 변환본 (이미지 위치 placeholder 포함).
|
|
203
|
+
- `body.md` 가 plain 이라 누락된 위치 정보를 보완.
|
|
204
204
|
- `body.html`: 원본 HTML (있을 때).
|
|
205
|
-
- `headers.json`: 모든 메일
|
|
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` 저장 (변형
|
|
212
|
-
- VBA 시트
|
|
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
|
|
216
|
+
후속 스킬 (sd-spec 등) 은 결과 폴더의 `README.md` 를 한 번 Read 하여 본문 위치·헤더·첨부 목록·손실 영역을 모두 파악 가능. 컨테이너 첨부는 자체 `README.md` 를 가지므로 같은 방식으로 재귀 진입.
|
|
217
217
|
|
|
218
218
|
각 형식별 jsonl 의 grep 패턴:
|
|
219
219
|
- xlsx: `"r":<행>` · `"_f"` (수식 행) · 열문자 키.
|
|
Binary file
|
|
@@ -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
|
-
|
|
726
|
-
|
|
789
|
+
# 비표준 셀값(NaN/Infinity) 사전 정제 + openpyxl 로드. 정제본 임시파일은 마지막에 unlink.
|
|
790
|
+
_xlsx_cleanups: list[Path] = []
|
|
727
791
|
try:
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
- 시스템
|
|
8
|
-
- 추천 후보에서 sd-use 자신은 제외 (
|
|
9
|
-
-
|
|
6
|
+
`/sd-use <요청>` 의 `<요청>` 에 적합한 sd-* 스킬을 추천만 출력하고 종료. 추천 대상 스킬의 자동 실행·산출물 생성 금지.
|
|
7
|
+
- 시스템 프롬프트 내 sd-* 스킬의 프론트매터(name·description)를 매칭 근거로 사용.
|
|
8
|
+
- 추천 후보에서 sd-use 자신은 제외 (자기 추천 방지).
|
|
9
|
+
- 적합한 스킬이 여러 개면, 적합도 높은 순으로 나열하고 각 스킬마다 추천 이유를 함께 출력.
|