@simplysm/sd-claude 14.0.78 → 14.0.80

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 (42) hide show
  1. package/claude/references/sd-simplysm14/manuals/logging.md +1 -1
  2. package/claude/rules/sd-base-rules.md +293 -377
  3. package/claude/rules/sd-design-rules.md +44 -0
  4. package/claude/{sd-check-forbidden-files.py → sd-check-edit.py} +2 -1
  5. package/claude/{sd-check-bash.py → sd-check-shell.py} +2 -2
  6. package/claude/settings.json +3 -4
  7. package/claude/skills/sd-demo/SKILL.md +6 -0
  8. package/claude/skills/sd-dev/SKILL.md +1 -1
  9. package/claude/skills/sd-impl/SKILL.md +15 -14
  10. package/claude/skills/sd-impl/references/spec-cross-check.md +2 -2
  11. package/claude/skills/sd-skill/SKILL.md +209 -29
  12. package/claude/skills/sd-spec/SKILL.md +820 -192
  13. package/claude/skills/sd-spec/references/example-spec.md +129 -80
  14. package/claude/skills/sd-unpack/SKILL.md +39 -14
  15. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/_common.cpython-314.pyc +0 -0
  16. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/eml_handler.cpython-314.pyc +0 -0
  17. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/office_com.cpython-314.pyc +0 -0
  18. package/claude/skills/sd-unpack/scripts/handlers/__pycache__/pdf_handler.cpython-314.pyc +0 -0
  19. package/claude/skills/sd-unpack/scripts/handlers/_common.py +59 -0
  20. package/claude/skills/sd-unpack/scripts/handlers/eml_handler.py +7 -0
  21. package/claude/skills/sd-unpack/scripts/handlers/msg_handler.py +11 -0
  22. package/claude/skills/sd-unpack/scripts/handlers/office_com.py +288 -79
  23. package/claude/skills/sd-unpack/scripts/handlers/office_worker.py +3 -2
  24. package/claude/skills/sd-unpack/scripts/handlers/pdf_handler.py +78 -10
  25. package/package.json +1 -1
  26. package/claude/output-styles/sd-tone.md +0 -128
  27. package/claude/skills/sd-skill/evals/fixtures/empty/.gitkeep +0 -0
  28. package/claude/skills/sd-skill/evals/fixtures/with-existing-review/.claude/skills/review/SKILL.md +0 -14
  29. package/claude/skills/sd-skill/evals/golden.jsonl +0 -5
  30. package/claude/skills/sd-skill/references/eval-authoring.md +0 -81
  31. package/claude/skills/sd-skill/references/eval-run.md +0 -32
  32. package/claude/skills/sd-skill/references/skill-authoring.md +0 -70
  33. package/claude/skills/sd-spec/references/spec-authoring.md +0 -298
  34. package/claude/skills/sd-spec/references/spec-md-template.md +0 -29
  35. package/claude/skills/sd-wip/SKILL.md +0 -38
  36. package/claude/skills/sd-wip/evals/fixtures/empty/.gitkeep +0 -0
  37. package/claude/skills/sd-wip/evals/fixtures/with-artifact/projects/acct/_wip.md +0 -3
  38. package/claude/skills/sd-wip/evals/fixtures/with-artifact/projects/acct/spec.md +0 -15
  39. package/claude/skills/sd-wip/evals/fixtures/with-existing-wip/.wips/260101120000_acct.md +0 -6
  40. package/claude/skills/sd-wip/evals/fixtures/with-existing-wip-for-compact/.wips/260101120000_acct.md +0 -14
  41. package/claude/skills/sd-wip/evals/golden.jsonl +0 -4
  42. package/claude/skills/sd-wip/references/compact.md +0 -79
@@ -75,8 +75,7 @@ flowchart LR
75
75
 
76
76
  ### 2.2 출고 [OPEN: 2026-04-01]
77
77
 
78
- - 자료 위치: 회의록.md L40-50
79
- - 대화 발췌: 사용자 — "출고 부분은 입고 끝나고 다음 단계에서 보자"
78
+ 분석 방법: 회의록.md L40-50 재독 (입고 분석 끝난 후). 사용자 보류 발언 — "출고 부분은 입고 끝나고 다음 단계에서 보자".
80
79
 
81
80
  ## 3. 기타 요구사항
82
81
 
@@ -86,6 +85,12 @@ flowchart LR
86
85
 
87
86
  관련 섹션: [화면.재고 확인], [자동 처리.재고 스냅샷]
88
87
 
88
+ ### 3.2 마스터 데이터 변경 이력 [확정: 2026-04-01]
89
+
90
+ 모든 마스터 데이터의 변경 (등록·편집·활성/비활성 전환) 이력을 자동 기록해야 함. 변경 추적·감사 목적.
91
+
92
+ 관련 섹션: [횡단 처리.마스터 데이터 변경 이력]
93
+
89
94
  ## 4. 화면
90
95
 
91
96
  | § | 분류 | 화면 | 유형 | 장치 |
@@ -106,7 +111,7 @@ Actor: 창고 관리자
106
111
 
107
112
  - 품목 마스터 CRUD (등록 / 편집 / 삭제·복구 / 엑셀 업·다운로드)
108
113
 
109
- 와이어프레임:
114
+ #### 와이어프레임
110
115
 
111
116
  ```
112
117
  ┌──────────────────────────────────────────────────────────────────────────┐
@@ -120,7 +125,7 @@ Actor: 창고 관리자
120
125
  └──────────────────────────────────────────────────────────────────────────┘
121
126
  ```
122
127
 
123
- 항목:
128
+ #### 항목
124
129
 
125
130
  **필터**
126
131
 
@@ -137,7 +142,7 @@ Actor: 창고 관리자
137
142
  | 코드 | 문자 | - | [모델.품목.코드] | |
138
143
  | 명칭 | 문자 | - | [모델.품목.명칭] | |
139
144
 
140
- 동작:
145
+ #### 동작
141
146
 
142
147
  - `[조회]` 클릭: 검색 필터 조건으로 목록 갱신
143
148
  - `[등록]` 클릭: [화면.품목 등록·편집] 을 빈 폼으로 모달 띄움
@@ -146,15 +151,15 @@ Actor: 창고 관리자
146
151
  - `[선택 복구]` 클릭: 선택된 행들의 활성 여부 true 전환
147
152
  - `[엑셀 업로드]` 클릭: 엑셀 파일 선택 → 코드 기준 일괄 등록/수정
148
153
  - `[엑셀 다운로드]` 클릭: 현재 검색 필터 적용된 목록을 엑셀로 내보내기
149
- - `[화주 자료 업로드]` 클릭: [공통 정의.화주 품목 자료] 파일 선택 → 아래 양식 매핑 적용 후 코드 기준 일괄 등록/수정
154
+ - `[화주 자료 업로드]` 클릭: [공통 정의.화주 품목 자료] 파일 선택 → 아래 업로드 양식 적용 후 코드 기준 일괄 등록/수정
150
155
 
151
- 시각 규칙:
156
+ #### 시각 규칙
152
157
 
153
158
  - 비활성 행: 전체 취소선 + 회색
154
159
 
155
- 양식 매핑:
160
+ #### 업로드 양식
156
161
 
157
- **업로드 양식** ([공통 정의.화주 품목 자료] → [모델.품목])
162
+ [공통 정의.화주 품목 자료] → [모델.품목]
158
163
 
159
164
  | 파일 컬럼 | 도메인 모델 | 변환·규칙 |
160
165
  | ------------------ | ---------------- | -------------------------------- |
@@ -170,7 +175,7 @@ Actor: 창고 관리자
170
175
 
171
176
  - 품목 신규 등록 또는 기존 품목 편집 (호출하는 쪽에서 빈 폼/기존 데이터로 모달 띄움)
172
177
 
173
- 와이어프레임:
178
+ #### 와이어프레임
174
179
 
175
180
  ```
176
181
  ┌──────────────────────────────────────┐
@@ -182,7 +187,7 @@ Actor: 창고 관리자
182
187
  └──────────────────────────────────────┘
183
188
  ```
184
189
 
185
- 항목:
190
+ #### 항목
186
191
 
187
192
  **입력 폼**
188
193
 
@@ -191,7 +196,7 @@ Actor: 창고 관리자
191
196
  | 코드 | 텍스트 입력 | O | [모델.품목.코드] | |
192
197
  | 명칭 | 텍스트 입력 | O | [모델.품목.명칭] | |
193
198
 
194
- 동작:
199
+ #### 동작
195
200
 
196
201
  - `[저장]` 클릭: 활성 품목 내 코드·명칭 유일성 검증 → 저장 → 모달 닫음
197
202
  - `[취소]` 클릭: 변경 사항 폐기 → 모달 닫음
@@ -206,7 +211,7 @@ Actor: 창고 관리자
206
211
  - Location 마스터 CRUD (등록 / 편집 / 삭제·복구 / 엑셀 업·다운로드)
207
212
  - 두 가지 바코드 출력: A4 다중 + 라벨프린터 1매씩
208
213
 
209
- 와이어프레임:
214
+ #### 와이어프레임
210
215
 
211
216
  ```
212
217
  ┌────────────────────────────────────────────────────────────────────────────────────┐
@@ -219,7 +224,7 @@ Actor: 창고 관리자
219
224
  └────────────────────────────────────────────────────────────────────────────────────┘
220
225
  ```
221
226
 
222
- 항목:
227
+ #### 항목
223
228
 
224
229
  **필터**
225
230
 
@@ -235,7 +240,7 @@ Actor: 창고 관리자
235
240
  | ID | 숫자 + 편집 버튼 | - | [모델.Location.ID] | 클릭 시 편집 모달 |
236
241
  | 코드 | 문자 | - | [모델.Location.코드] | |
237
242
 
238
- 동작:
243
+ #### 동작
239
244
 
240
245
  - `[조회]` 클릭: 검색 필터 조건으로 목록 갱신
241
246
  - `[등록]` 클릭: [화면.Location 등록·편집] 을 빈 폼으로 모달 띄움
@@ -255,7 +260,7 @@ Actor: 창고 관리자
255
260
  1. 선택된 Location 을 [공통 정의.Location 라벨] 규격으로 라벨프린터 1매씩 출력
256
261
  2. 다중 선택 시 선택 수만큼 연속 출력
257
262
 
258
- 시각 규칙:
263
+ #### 시각 규칙
259
264
 
260
265
  - 비활성 행: 전체 취소선 + 회색
261
266
 
@@ -268,7 +273,7 @@ Actor: 창고 관리자
268
273
 
269
274
  - Location 신규 등록 또는 기존 Location 편집 (호출하는 쪽에서 빈 폼/기존 데이터로 모달 띄움)
270
275
 
271
- 와이어프레임:
276
+ #### 와이어프레임
272
277
 
273
278
  ```
274
279
  ┌──────────────────────────────────────┐
@@ -280,7 +285,7 @@ Actor: 창고 관리자
280
285
  └──────────────────────────────────────┘
281
286
  ```
282
287
 
283
- 항목:
288
+ #### 항목
284
289
 
285
290
  **입력 폼**
286
291
 
@@ -288,7 +293,7 @@ Actor: 창고 관리자
288
293
  | ---- | ----------- | ---- | -------------------- | ----------------------- |
289
294
  | 코드 | 텍스트 입력 | O | [모델.Location.코드] | 영문 대문자/숫자/하이픈 |
290
295
 
291
- 동작:
296
+ #### 동작
292
297
 
293
298
  - `[저장]` 클릭: 활성 Location 내 코드 유일성 검증 → 저장 → 모달 닫음
294
299
  - `[취소]` 클릭: 변경 사항 폐기 → 모달 닫음
@@ -302,7 +307,7 @@ Actor: 창고 작업자
302
307
 
303
308
  - 박스 → Location 순서로 스캔하여 재고 등록
304
309
 
305
- 와이어프레임:
310
+ #### 와이어프레임
306
311
 
307
312
  ```
308
313
  ┌────────────────────────┐
@@ -315,7 +320,7 @@ Actor: 창고 작업자
315
320
  (360 × 568)
316
321
  ```
317
322
 
318
- 항목:
323
+ #### 항목
319
324
 
320
325
  **스캔 입력 폼**
321
326
 
@@ -324,7 +329,7 @@ Actor: 창고 작업자
324
329
  | 박스 바코드 | 텍스트 입력 | O | - | PDA 스캐너 입력. 파싱 = 품목코드 + 수량 + 박스번호 |
325
330
  | Location | 텍스트 입력 | O | [모델.Location.코드] | PDA 스캐너 입력 |
326
331
 
327
- 동작:
332
+ #### 동작
328
333
 
329
334
  - `[등록]` 클릭:
330
335
  - 절차:
@@ -343,7 +348,7 @@ Actor: 창고 관리자
343
348
  - 현재 재고: Location 별 적치된 박스 + 품목·수량 조회
344
349
  - 과거 시점 재고: 특정 날짜의 스냅샷 조회
345
350
 
346
- 와이어프레임:
351
+ #### 와이어프레임
347
352
 
348
353
  ```
349
354
  ┌──────────────────────────────────────────────────────────────────────────┐
@@ -356,7 +361,7 @@ Actor: 창고 관리자
356
361
  └──────────────────────────────────────────────────────────────────────────┘
357
362
  ```
358
363
 
359
- 항목:
364
+ #### 항목
360
365
 
361
366
  **필터**
362
367
 
@@ -375,7 +380,7 @@ Actor: 창고 관리자
375
380
  | 품목 | 문자 | - | [모델.재고.박스.품목.코드] | 품목 코드 표시 |
376
381
  | 수량 | 숫자 | - | [모델.재고.박스.수량] | |
377
382
 
378
- 동작:
383
+ #### 동작
379
384
 
380
385
  - `[조회]` 클릭:
381
386
  - 기준일 비어있음: 현재 [모델.재고] 조회
@@ -387,49 +392,73 @@ Actor: 창고 관리자
387
392
 
388
393
  ### 5.1 재고 스냅샷 [확정: 2026-04-01]
389
394
 
390
- ```mermaid
391
- flowchart LR
392
- S([매일 0시]) --> T1[현재 재고 전체 조회]
393
- T1 --> T2[스냅샷 테이블에 날짜별 저장]
394
- T2 --> E1([완료])
395
- ```
396
-
397
- 목적:
398
-
399
- - 과거 시점 재고 조회 지원
395
+ 목적: 과거 시점 재고 조회 지원
400
396
 
401
- 트리거:
397
+ 트리거: 매일 0시 정각
402
398
 
403
- - 매일 0시 정각
399
+ 관련 섹션: [기타.과거 재고 조회], [화면.재고 확인]
404
400
 
405
- 처리:
401
+ #### 처리
406
402
 
407
403
  - [모델.재고] 전체를 [모델.재고 스냅샷] 에 날짜별로 저장
408
404
 
409
- 예외 처리:
405
+ #### 예외 처리
410
406
 
411
407
  - 스냅샷 생성 실패 시:
412
408
  - 위험: 당일 분 누락 시 해당 날짜의 과거 시점 조회 불가
413
409
  - 대처: 관리자 알림. 동일 일자 재실행 시 (날짜, 박스) 유일성 검증으로 중복 생성 방지
414
410
  - 재시도 한계: 별도 재시도 없음 (다음 날 0시 정상 동작)
415
411
 
416
- 관련 섹션: [기타.과거 재고 조회], [화면.재고 확인]
412
+ ## 6. 횡단 처리
413
+
414
+ ### 6.1 마스터 데이터 변경 이력 [확정: 2026-04-01]
415
+
416
+ 목적: 마스터 변경 추적·감사 (누가 언제 무엇을 어떻게 바꿨는지)
417
+
418
+ 트리거 (부수효과 발동 조건):
419
+ - 마스터 엔티티의 insert / update / delete 트랜잭션 commit 시
420
+ - 적용 범위: [모델.품목], [모델.Location]
421
+ - 적용 제외: 트랜잭션 모델 ([모델.재고], [모델.재고 스냅샷])
422
+
423
+ 관련 섹션: [기타.마스터 데이터 변경 이력], [모델.품목], [모델.Location], [모델.DataLog]
424
+
425
+ #### 처리
426
+
427
+ - 변경 전 값과 변경 후 값 캡처 (insert 시 변경 전 = null, delete 시 변경 후 = null)
428
+ - [모델.DataLog] 에 한 행 적재 (테이블·키·전·후·작업자·시각)
429
+
430
+ #### 예외 처리
431
+
432
+ - DataLog 적재 실패 시:
433
+ - 위험: 변경 추적 누락
434
+ - 대처: 본 트랜잭션은 정상 commit 유지, DataLog 적재 실패만 별도 큐로 적재 후 관리자 알림
435
+ - 재시도 한계: 큐에서 5회 지수 백오프 후 운영팀 알림
436
+
437
+ #### 모델 매핑
438
+
439
+ | 필드 ([모델.DataLog]) | 용도 |
440
+ | --------------------- | ------------------------------- |
441
+ | [모델.DataLog.테이블명] | 변경 대상 모델명 (예: `품목`) |
442
+ | [모델.DataLog.키] | 변경 대상 ID |
443
+ | [모델.DataLog.변경 전] | 컬럼별 변경 전 값 (JSON) |
444
+ | [모델.DataLog.변경 후] | 컬럼별 변경 후 값 (JSON) |
445
+ | [모델.DataLog.작업자] | 세션 사용자 |
446
+ | [모델.DataLog.시각] | 변경 commit 시각 |
417
447
 
418
- ## 6. 공통 정의
448
+ ## 7. 공통 정의
419
449
 
420
- ### 6.1 용어 사전 [확정: 2026-04-01]
450
+ ### 7.1 용어 사전 [확정: 2026-04-01]
421
451
 
422
452
  - 박스: 외부에서 이미 바코드(품목+수량+박스번호)가 부착된 입고 단위
423
453
  - Location: 창고 내 적치 위치 ([공통 정의.Location 라벨])
424
454
  - ERP 통보: 입출고 발생 시 WMS → ERP 단방향 알림
425
455
  - 활성 여부: 마스터 데이터의 소프트 삭제 플래그 (true = 정상, false = 삭제 처리됨). UI 의 "삭제/복구" 액션과 매핑. 마스터에 완전 삭제(DB 레코드 제거) 없음.
426
456
 
427
- ### 6.2 박스 바코드 형식 [OPEN: 2026-04-01]
457
+ ### 7.2 박스 바코드 형식 [OPEN: 2026-04-01]
428
458
 
429
- - 자료 위치: 첨부B.pdf p.5 "박스 코드 체계" 외부 송장 시스템 규격 명세
430
- - 자료 연결 메모: 박스 코드는 외부 규격이지만 [모델.박스] 식별 키 구조와 직결 — §7 박스 확정과 함께 검토
459
+ 분석 방법: 첨부B.pdf p.5 "박스 코드 체계" (외부 송장 시스템 규격) 분석. [모델.박스] 식별 키 구조와 직결 — §8 박스 확정과 함께 검토.
431
460
 
432
- ### 6.3 Location 라벨 [확정: 2026-04-01]
461
+ ### 7.3 Location 라벨 [확정: 2026-04-01]
433
462
 
434
463
  ```
435
464
  ┌────────────────────────────────┐
@@ -446,7 +475,7 @@ flowchart LR
446
475
  - 라벨 크기: 100 × 50 mm
447
476
  - 라벨 재질: 유포지, 흰색 무지
448
477
 
449
- ### 6.4 화주 품목 자료 [확정: 2026-04-01]
478
+ ### 7.4 화주 품목 자료 [확정: 2026-04-01]
450
479
 
451
480
  화주(품목을 본 WMS 에 보관 위탁한 거래처)가 자기 ERP 에서 수시로 다운로드해 화주별 별도 파일로 전달한다. 신규 화주 등록 또는 화주 측 품목 마스터 변경 시점에 본 WMS 에 1회 일괄 업로드한다. 식별 키는 `화주코드 + SEQ` 2컬럼 합성.
452
481
 
@@ -457,18 +486,18 @@ flowchart LR
457
486
  | 품목명 | O | 화주 측 명칭 |
458
487
  | 단위 | X | `EA` / `BOX` 등 |
459
488
 
460
- ## 7. 도메인 모델
489
+ ## 8. 도메인 모델
461
490
 
462
- ### 7.1 품목 [확정: 2026-04-01]
491
+ ### 8.1 품목 [확정: 2026-04-01]
463
492
 
464
493
  필드:
465
494
 
466
- | 필드 | 타입 | 필수 | 비고 |
467
- | --------- | ------- | ---- | ---------------- |
468
- | ID | 숫자 | O | 자동 부여 |
495
+ | 필드 | 타입 | 필수 | 비고 |
496
+ | --------- | ------- | ---- | -------------------- |
497
+ | ID | 숫자 | O | 자동 부여 |
469
498
  | 코드 | 문자 | O | 자유 형식, 수정 가능 |
470
- | 명칭 | 문자 | O | |
471
- | 활성 여부 | boolean | O | |
499
+ | 명칭 | 문자 | O | |
500
+ | 활성 여부 | boolean | O | |
472
501
 
473
502
  키/제약:
474
503
 
@@ -476,7 +505,7 @@ flowchart LR
476
505
  - 비즈니스 키: 코드 (사용자 부여, 수정 가능)
477
506
  - 유일성: 활성 품목 내 코드, 명칭 각각 유일
478
507
 
479
- ### 7.2 박스 [확정: 2026-04-01]
508
+ ### 8.2 박스 [확정: 2026-04-01]
480
509
 
481
510
  필드:
482
511
 
@@ -494,7 +523,7 @@ flowchart LR
494
523
  - 박스 1개 = 단일 품목
495
524
  - 유일성: 박스번호 유일
496
525
 
497
- ### 7.3 Location [확정: 2026-04-01]
526
+ ### 8.3 Location [확정: 2026-04-01]
498
527
 
499
528
  필드:
500
529
 
@@ -510,7 +539,7 @@ flowchart LR
510
539
  - 비즈니스 키: 코드 (사용자 부여, 수정 가능)
511
540
  - 유일성: 활성 Location 내 코드 유일
512
541
 
513
- ### 7.4 재고 [확정: 2026-04-01]
542
+ ### 8.4 재고 [확정: 2026-04-01]
514
543
 
515
544
  필드:
516
545
 
@@ -526,31 +555,52 @@ flowchart LR
526
555
  - 박스 1개는 동시에 1개 Location 에만 존재 (= 박스-재고 1:1)
527
556
  - Location 1개에 박스 다중 가능
528
557
 
529
- ### 7.5 재고 스냅샷 [확정: 2026-04-01]
558
+ ### 8.5 재고 스냅샷 [확정: 2026-04-01]
530
559
 
531
560
  필드:
532
561
 
533
- | 필드 | 타입 | 필수 | 비고 |
534
- | -------- | --------------- | ---- | --------- |
535
- | ID | 숫자 | O | 자동 부여 |
562
+ | 필드 | 타입 | 필수 | 비고 |
563
+ | -------- | --------------- | ---- | ----------- |
564
+ | ID | 숫자 | O | 자동 부여 |
536
565
  | 날짜 | 날짜 | O | 스냅샷 일자 |
537
- | 박스 | 참조 - 박스 | O | |
538
- | Location | 참조 - Location | O | |
566
+ | 박스 | 참조 - 박스 | O | |
567
+ | Location | 참조 - Location | O | |
539
568
 
540
569
  키/제약:
541
570
 
542
571
  - 식별 키: ID (자동 부여)
543
572
  - 비즈니스 키: (날짜, 박스) 조합 유일
544
573
 
545
- ## 8. 외부 인터페이스
574
+ ### 8.6 DataLog [확정: 2026-04-01]
546
575
 
547
- ### 8.1 ERP 입고 통보 [확정: 2026-04-01]
576
+ 필드:
577
+
578
+ | 필드 | 타입 | 필수 | 비고 |
579
+ | -------- | ---- | ---- | ------------------------------------- |
580
+ | ID | 숫자 | O | 자동 부여 |
581
+ | 테이블명 | 문자 | O | 변경 대상 모델명 (예: `품목`) |
582
+ | 키 | 숫자 | O | 변경 대상 ID |
583
+ | 변경 전 | JSON | X | 컬럼별 이전 값. insert 시 null |
584
+ | 변경 후 | JSON | X | 컬럼별 변경 후 값. delete 시 null |
585
+ | 작업자 | 문자 | O | 세션 사용자 |
586
+ | 시각 | 일시 | O | 변경 commit 시각 |
587
+
588
+ 키/제약:
589
+
590
+ - 식별 키: ID (자동 부여)
591
+ - 인덱스: (테이블명, 키, 시각) — 특정 엔티티 변경 이력 조회용
592
+
593
+ ## 9. 외부 인터페이스
594
+
595
+ ### 9.1 ERP 입고 통보 [확정: 2026-04-01]
548
596
 
549
597
  - 상대 시스템: ERP
550
598
  - 방향: WMS → ERP
551
599
  - 전송 방식: REST API (POST)
552
600
 
553
- 자료 매핑:
601
+ 관련 섹션: [화면.입고 스캔]
602
+
603
+ #### 자료 매핑
554
604
 
555
605
  | ERP 필드 | WMS 출처 |
556
606
  | -------- | --------------------- |
@@ -559,7 +609,7 @@ flowchart LR
559
609
  | 박스번호 | [모델.박스.박스번호] |
560
610
  | 발생시각 | 입고 등록 시각 |
561
611
 
562
- 예외 처리:
612
+ #### 예외 처리
563
613
 
564
614
  - 호출 실패 (네트워크 오류, ERP 서버 일시 다운 등):
565
615
  - 위험: 단순 재시도 시 ERP 에 같은 박스가 2번 입고 등록될 수 있음 (첫 호출이 실제로는 ERP 에 도달했는데 응답만 못 받은 경우 등)
@@ -567,23 +617,22 @@ flowchart LR
567
617
  - 대처: 위 전제 하에 지수 백오프로 재시도, 최대 5회
568
618
  - 재시도 한계 초과: 관리자 알림 (이후 수동 확인 필요)
569
619
 
570
- 관련 섹션: [화면.입고 스캔]
571
-
572
- ## 9. 본문 외 결정사항
620
+ ## 10. 본문 외 결정사항
573
621
 
574
- ### 9.1 분석 제외 항목 [확정: 2026-04-01]
575
-
576
- - 직원 관리 사원번호 컬럼 추가
622
+ - 2026-04-01 [제외]: 직원 관리 사원번호 컬럼 추가
623
+ - 근거: 본 spec 범위 밖 — 인사 마스터 영역
577
624
  - 후속 처리: sd-dev
578
625
  - 자료 위치: 회의록.md L42
579
- - 재고 확인 화면 정렬 버그
626
+
627
+ - 2026-04-01 [제외]: 재고 확인 화면 정렬 버그
628
+ - 근거: 본 spec 범위 밖 — 운영 잔손
580
629
  - 후속 처리: 운영 잔손
581
630
  - 자료 위치: 이슈 #128
582
631
 
583
- ## 10. 변경 이력
632
+ - 2026-04-01: 출고 분석은 입고 끝나고 다음 단계
633
+ - 근거: 사용자 결정 — 입고 검증 후 출고 진행이 안전
634
+ - 자료 위치: 회의록.md L40-50
635
+ - 영향: §2.2 출고 OPEN 상태 유지
584
636
 
585
- - 2026-04-01: 운영 잔손 분리 직원 관리 사원번호 컬럼 추가, 재고 확인 화면 정렬 버그. 분석 제외 항목으로 기록.
586
- - 2026-04-01: 입고 분석 완료. 출고·박스 바코드 형식 보류.
587
- - 2026-04-01: 과거 재고 조회 요구 식별 → 재고 스냅샷 모델·자동 처리 추가, 재고 확인 화면에 기준일 필터 반영.
588
- - 2026-04-01: ERP 입고 통보 식별 → 입고 스캔 등록 후속 동작에서 호출.
589
- - 2026-04-05: 품목 관리·품목 등록·편집 sd-impl 완료 (구현 마커 부착).
637
+ - 2026-04-05: 품목 마스터 sd-impl 완료 (구현 마커 부착)
638
+ - 근거: 시연 검증 통과 (§4.1·§4.2)
@@ -49,10 +49,10 @@ meeting_eml/
49
49
  embedded_xlsx/
50
50
  README.md
51
51
  _source.xlsx
52
- workbook.meta.json ← defined names (있을 때)
52
+ workbook.meta.json ← defined names·sheet_code_map (있을 때)
53
53
  sheets/
54
- 01_Sheet1.png
55
- 01_Sheet1.jsonl
54
+ 01_Sheet1.png ← 시각 (서식·바탕색·border 모두)
55
+ 01_Sheet1.jsonl ← 분석 데이터 (값·number_format·수식·merges 등)
56
56
  ```
57
57
 
58
58
  형식별 산출물 매트릭스:
@@ -61,7 +61,7 @@ meeting_eml/
61
61
  |---|---|---|---|
62
62
  | pptx/ppt | `slides/<idx>_<title>.png` | `slides/<idx>_<title>.jsonl` (슬라이드별 노드) | `charts/*.data.json`, `images/`, `attachments/`, `macros/` |
63
63
  | docx/doc | `pages/<NNN>.png` (시각 검증용) | `content.jsonl` (단일 시퀀스), `pages.meta.json` (PNG↔노드 매핑) | `images/`, `attachments/`, `macros/` |
64
- | xlsx/xlsb/xls | `sheets/<idx>_<name>.png` | `sheets/<idx>_<name>.jsonl` (값·수식·시트 메타 통합), `workbook.meta.json` | `charts/*.data.json`, `images/<sheet>_<cell>`, `attachments/`, `macros/` |
64
+ | xlsx/xlsb/xls | `sheets/<idx>_<name>.png` (일반 시트만) | `sheets/<idx>_<name>.jsonl` (값·수식·시트 메타), `workbook.meta.json` | `charts/sheet<idx>_chart*.data.json` (일반 시트 안 차트 + Chartsheet 의 차트), `images/<sheet>_<cell>`, `attachments/`, `macros/` |
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
 
@@ -78,11 +78,14 @@ meeting_eml/
78
78
 
79
79
  ## xlsx jsonl 규약
80
80
 
81
- 시트별 `.jsonl`. 좌표 명시로 위치 오차 차단.
81
+ 시트별 `.jsonl` 분석 핵심 (값·number_format·수식·merges·hyperlinks·comments). 시각 표시 (바탕색·border·폰트)·frozen·dims 는 미보존 (PNG 가 시각 보조, 필요 시 `_source.xlsx` 직접 추출).
82
82
 
83
- - 첫 줄: `{"_meta":{"dims":[행수,열수], "merges":["A1:C1",...], "frozen":"A4", "hyperlinks":{"D5":"http://..."}, "comments":{"E3":"메모"}, "number_formats":{"H4":"#,##0", "E1":"yyyy-mm-dd"}}}`
84
- - 비어있는 메타 키는 생략
85
- - `number_formats`: General(기본) 외 셀의 표시 형식 (통화·날짜·%)
83
+ - 첫 줄: `{"_meta":{"merges":["A1:C1",...], "number_formats":{"E1":"yyyy-mm-dd",...}, "hyperlinks":{"D5":"http://..."}, "comments":{"E3":"메모"}}}`
84
+ - `merges`: 머지된 영역 (셀 좌표 해석에 필수 — 머지 영역 안 빈 셀 오해 차단)
85
+ - `number_formats`: General(기본) 외 표시 형식 Date·통화·% 셀 값 의미 단서
86
+ - `hyperlinks`: 셀 URL (URL 자체가 셀 정보)
87
+ - `comments`: 셀 메모
88
+ - 비어있는 메타 키는 생략 (모두 비면 `{"_meta":{}}`)
86
89
  - 데이터 줄: `{"r":11, "A":"P001", "I":7800, "J":12.5, "_f":{"I":"=SUM(...)", "J":"=I11*1.5"}}`
87
90
  - `r`: 1-based 행번호 (Excel 동일)
88
91
  - 열문자 키 (`A`·`B`·...·`AA`·...): 셀 값. 빈 셀은 키 생략
@@ -90,8 +93,22 @@ meeting_eml/
90
93
  - 빈 행도 `{"r":N}` 한 줄 유지 → Read offset = 행번호 (오프바이원 차단)
91
94
  - 값 타입: JSON 네이티브 (`int`·`float`·`bool`·`str`), datetime 은 ISO 8601 문자열
92
95
 
93
- 워크북 단위 메타 (시트 ) 는 `workbook.meta.json`:
96
+ ### Chartsheet (시트 자체가 차트)
97
+
98
+ xlsx 안 시트는 일반 Worksheet 외에 **Chartsheet** (셀 없이 차트 1개) 도 있을 수 있음.
99
+
100
+ - Chartsheet 는 `sheets/<idx>_<name>.jsonl` 미생성 (셀 없음)
101
+ - Chartsheet 의 차트 데이터: `charts/sheet<idx>_chart.data.json`
102
+ - README sheet_summaries 에 `(chart sheet — "...")` 명시
103
+ - 일반 시트·Chartsheet 통합 시트 순서 (idx) 대로 보존
104
+
105
+ ### 워크북 단위 `workbook.meta.json`
106
+
107
+ 시트 외 워크북 공통 정보 (있을 때만 생성):
108
+
94
109
  - `defined_names`: `{"이름":["'Sheet1'!$A$1:$C$10", ...]}` (다중 destination 시 list 다수 항목)
110
+ - `sheet_code_map`: `{"Sheet1":"BOA", ...}` (VBA codeName → raw 시트명. 매크로 모듈 파일명과 매칭용)
111
+ - `pivots`: pivot table 정의 list. 각 항목 `{name, source, location, rowFields, colFields, pageFields, dataFields}`. 결과 셀은 시트별 jsonl 에 일반 셀로 들어감
95
112
 
96
113
  ## pptx jsonl 규약
97
114
 
@@ -146,15 +163,17 @@ paragraph 안 hyperlink 가 있으면 `hyperlinks`: `[{"text":"...", "url":"..."
146
163
 
147
164
  페이지별 `pages/<NNN>.jsonl`. PDF 페이지는 원본 단위.
148
165
 
149
- - 첫 줄: `{"_meta":{"page":N, "size":[w,h], "blocks":B, "tables":T, "table_cells":C}}`
166
+ - 첫 줄: `{"_meta":{"page":N, "size":[w,h], "blocks":B, "tables":T, "table_cells":C, "form_fields":F, "annotations":A}}`
150
167
  - 노드 줄:
151
168
  - `text_block`: `{"page":N, "block":B, "type":"text_block", "bbox":[x0,y0,x1,y1], "text":"..."}`
152
169
  - `image_block`: `{"page":N, "block":B, "type":"image_block", "bbox":[...], "ref":"images/p001_b03.png"}`
153
170
  - `table_cell`: `{"page":N, "type":"table_cell", "table_idx":T, "table_bbox":[...], "row":R, "col":C, "text":"..."}`
171
+ - `form_field`: `{"page":N, "type":"form_field", "name":"...", "field_type":"text", "value":"...", "bbox":[...]}` (PDF 양식 입력란)
172
+ - `annotation`: `{"page":N, "type":"annotation", "subtype":"Highlight", "bbox":[...], "content":"...", "author":"..."}` (주석·highlight·sticky note)
154
173
  - 모든 블록 보존 (표 영역과 겹쳐도 skip 안 함) — find_tables 정확도 100% 가정 시 정보 손실 위험 회피. text_block·image_block·table_cell 노드가 동일 영역에 중복 출력될 수 있음. Claude 가 양쪽 비교 판단
155
- - bbox 는 PDF 기준 좌표 (left-top, pt 단위, 소수점 2자리)
174
+ - bbox 는 PDF 기준 좌표 (left-top, pt 단위, raw float)
156
175
 
157
- heading 추출은 미적용 (PDF 는 style 정보 없음). 필요 본문 grep 으로 패턴 검출.
176
+ heading 추출은 미적용 (PDF 는 style 정보 없음). OCR 미적용 (스캔 PDF image_block 만 추출).
158
177
 
159
178
  ## 인라인 이미지 매핑 (eml/msg)
160
179
 
@@ -166,6 +185,12 @@ heading 추출은 미적용 (PDF 는 style 정보 없음). 필요 시 본문 gre
166
185
  - text/plain·HTML 둘 다 있을 때 → `body.md` 는 plain (placeholder 없음), `body.from_html.md` 가 변환본 (placeholder 포함)
167
186
  - 인라인 이미지 없으면 `images.rels.json` 미생성
168
187
 
188
+ ## TNEF (winmail.dat) 풀이
189
+
190
+ Outlook RTF 메일이 첨부를 `winmail.dat` 단일 binary (TNEF 형식) 로 패키징한 경우, `tnefparse` 로 내부 첨부 추출하여 `attachments/` 에 같이 풀어 둠. 원본 `winmail.dat` 도 유지 (원본 보존).
191
+
192
+ 내부 첨부도 컨테이너 (xlsx·pptx 등) 면 재귀 풀이 (다른 첨부와 동일).
193
+
169
194
  ## eml/msg 본문 규약
170
195
 
171
196
  본문 흐름 정확성(text/plain 우선) + 인라인 이미지 위치 단서(HTML→평문 변환본) 둘 다 보존:
@@ -183,8 +208,8 @@ heading 추출은 미적용 (PDF 는 style 정보 없음). 필요 시 본문 gre
183
208
  ## xlsb 클린업
184
209
 
185
210
  - legacy → xlsx 변환 시 `_converted.xlsx` 는 임시 폴더에서만 처리 (산출 폴더에 미잔존)
186
- - VBA 매크로 파일 줄에 시트 객체명↔raw 시트명 매핑 코멘트 추가
187
- - 예: `Sheet1.vba` `' (object: Sheet1, sheet: "BOA")`
211
+ - VBA 매크로는 원본 코드 그대로 `macros/<모듈명>.vba` 저장 (변형 X)
212
+ - VBA 시트 객체명↔raw 시트명 매핑은 `workbook.meta.json` `sheet_code_map` (예: `{"Sheet1":"BOA","Sheet3":"Mapping"}`)
188
213
 
189
214
  ## 산출물 사용
190
215
 
@@ -160,6 +160,65 @@ def temp_workdir():
160
160
  shutil.rmtree(d, ignore_errors=True)
161
161
 
162
162
 
163
+ def is_tnef(path: Path) -> bool:
164
+ """TNEF (winmail.dat) 형식인지 검사. filename 또는 magic bytes."""
165
+ if path.name.lower() in ("winmail.dat", "win.dat"):
166
+ return True
167
+ try:
168
+ with open(long_str(path), "rb") as f:
169
+ magic = f.read(4)
170
+ return magic == b"\x78\x9f\x3e\x22"
171
+ except Exception:
172
+ return False
173
+
174
+
175
+ def unpack_tnef(path: Path, attachments_dir: Path) -> list[Path]:
176
+ """TNEF (winmail.dat) 내부 첨부 추출. 추출된 path list 반환.
177
+
178
+ TNEF 아니거나 실패 시 빈 list. 원본 winmail.dat 은 유지 (원본 보존).
179
+ """
180
+ if not is_tnef(path):
181
+ return []
182
+ ensure_pip("tnefparse")
183
+ try:
184
+ from tnefparse import TNEF
185
+ with open(long_str(path), "rb") as f:
186
+ t = TNEF(f.read())
187
+ except Exception:
188
+ return []
189
+
190
+ saved: list[Path] = []
191
+ for att in getattr(t, "attachments", []):
192
+ try:
193
+ name = None
194
+ lf = getattr(att, "long_filename", None)
195
+ if callable(lf):
196
+ try:
197
+ name = lf()
198
+ except Exception:
199
+ name = None
200
+ if not name:
201
+ name = getattr(att, "name", None)
202
+ if isinstance(name, bytes):
203
+ try:
204
+ name = name.decode("utf-8")
205
+ except UnicodeDecodeError:
206
+ name = name.decode("cp949", errors="replace")
207
+ if not name:
208
+ name = "tnef_attachment.bin"
209
+ data = att.data
210
+ if not data:
211
+ continue
212
+ except Exception:
213
+ continue
214
+ if not isinstance(name, str):
215
+ name = "tnef_attachment.bin"
216
+ dst = unique_path(attachments_dir, name)
217
+ write_bytes(dst, data)
218
+ saved.append(dst)
219
+ return saved
220
+
221
+
163
222
  def save_source(input_path: Path, out_dir: Path) -> None:
164
223
  ext = input_path.suffix.lstrip(".")
165
224
  dst = out_dir / f"_source.{ext}"
@@ -136,6 +136,13 @@ def run(input_path: Path, out_dir: Path) -> None:
136
136
  json.dumps(rels, ensure_ascii=False, indent=2),
137
137
  )
138
138
 
139
+ # TNEF (winmail.dat) 풀이 — Outlook RTF 메일의 첨부 패키지 안 내부 첨부 추출
140
+ tnef_saved: list[Path] = []
141
+ for ap in saved_attachments:
142
+ extra = _common.unpack_tnef(ap, attachments_dir)
143
+ tnef_saved.extend(extra)
144
+ saved_attachments.extend(tnef_saved)
145
+
139
146
  attachment_links: list[str] = []
140
147
  for ap in saved_attachments:
141
148
  size = ap.stat().st_size