Skill: mupengi-bot/korean-invoice (version 1.0.0)

--- name: korean-invoice description: 한국형 견적서/세금계산서 자동 생성 (사업자등록번호, 부가세 자동 계산) version: 1.0.0 author: 무펭이 🐧 --- # korean-invoice **한국형 견적서/세금계산서 생성기** — 한국 표준 양식으로 견적서와 세금계산서를 자동 생성. 사업자등록번호, 공급가액, 부가세 자동 계산 지원. ## 언제 사용하나요? - 고객에게 견적서 발송 - 세금계산서 발행 - 거래처 정보 관리 - 자주 쓰는 품목 DB 관리 - PDF/HTML 형식으로 출력 ## 빠른 시작 ```bash # 견적서 생성 (대화형) korean-invoice quote # 거래처 저장된 경우 korean-invoice quote --client "무펭이즘" # 세금계산서 생성 korean-invoice tax --client "무펭이즘" # 거래처 추가 korean-invoice client add "무펭이즘" --business-number "123-45-67890" --ceo "김형님" # 품목 추가 korean-invoice item add "포토부스 대여" --price 500000 --unit "일" # 거래처 목록 korean-invoice client list # 품목 목록 korean-invoice item list ``` ## 견적서 필드 | 필드 | 설명 | 필수 | |------|------|------| | client | 거래처명 (저장된 거래처 ID) | ✅ | | items | 품목 리스트 (품목명, 수량, 단가) | ✅ | | issueDate | 작성일자 (기본: 오늘) | ❌ | | validUntil | 유효기간 (기본: +30일) | ❌ | | notes | 비고 | ❌ | | includeVAT | 부가세 포함 여부 (기본: true) | ❌ | ## 세금계산서 필드 | 필드 | 설명 | 필수 | |------|------|------| | client | 공급받는자 (거래처 ID) | ✅ | | items | 품목 리스트 | ✅ | | issueDate | 작성일자 | ✅ | | type | 영수/청구 (기본: 영수) | ❌ | | notes | 비고 | ❌ | ## 자동 계산 ``` 공급가액: 500,000 원 부가세(10%): 50,000 원 합계: 550,000 원 ``` - 공급가액 입력 시 부가세 10% 자동 계산 - 총액 입력 시 역산 (총액 ÷ 1.1) - 품목별 소계 자동 합산 ## 거래처 관리 ```bash # 거래처 추가 korean-invoice client add "무펭이즘" \ --business-number "123-45-67890" \ --ceo "김형님" \ --address "서울시 강남구..." \ --phone "010-1234-5678" \ --email "[REDACTED_SECRET]" # 거래처 수정 korean-invoice client edit "무펭이즘" --phone "010-9999-9999" # 거래처 삭제 korean-invoice client remove "무펭이즘" # 거래처 상세 조회 korean-invoice client view "무펭이즘" ``` 거래처 데이터는 `data/clients.json`에 저장됩니다. ## 품목 관리 ```bash # 품목 추가 korean-invoice item add "포토부스 대여" --price 500000 --unit "일" # 품목 수정 korean-invoice item edit "포토부스 대여" --price 600000 # 품목 삭제 korean-invoice item remove "포토부스 대여" # 품목 목록 korean-invoice item list ``` 품목 데이터는 `data/items.json`에 저장됩니다. ## 템플릿 ### 견적서 템플릿 - 공급자 정보 (사업자등록번호, 상호, 대표자, 주소, 연락처) - 공급받는자 정보 - 작성일자, 유효기간 - 품목 테이블 (품명, 규격, 수량, 단가, 공급가액) - 합계 (공급가액, 부가세, 총액) - 비고 ### 세금계산서 템플릿 - 승인번호 (임의 생성) - 공급자/공급받는자 정보 (사업자등록번호, 상호, 성명, 주소) - 작성일자 - 품목 테이블 - 합계 (공급가액, 세액) - 영수/청구 구분 ## 출력 형식 - **HTML**: 브라우저에서 바로 확인 - **PDF**: HTML → PDF 변환 (puppeteer 사용) - 저장 경로: `output/YYYY-MM-DD-{type}-{client}.pdf` ## 내 정보 설정 내 사업자 정보는 `data/my-info.json`에 저장: ```json { "businessNumber": "123-45-67890", "companyName": "무펭이즘", "ceo": "김무펭", "address": "서울시 강남구...", "phone": "010-1234-5678", "email": "[REDACTED_SECRET]", "bankAccount": "우리은행 1002-123-456789" } ``` ## 사용 예시 ### 1. 간단한 견적서 생성 ```bash korean-invoice quote \ --client "무펭이즘" \ --items "포토부스 대여,2,500000" \ --notes "부가세 별도" ``` ### 2. 저장된 품목 사용 ```bash # 품목 미리 저장 korean-invoice item add "포토부스 대여" --price 500000 --unit "일" korean-invoice item add "출장비" --price 100000 --unit "회" # 품목 ID로 견적서 생성 korean-invoice quote --client "무펭이즘" --item-ids "포토부스 대여,출장비" ``` ### 3. 세금계산서 발행 ```bash korean-invoice tax --client "무펭이즘" --items "포토부스 대여,1,500000" --type 영수 ``` ## 통합 - `message` 스킬과 연동하여 이메일/Discord로 전송 - `daily-report`에서 작업 시간 → 견적서 변환 - `calendar` 스킬로 유효기간 알림 설정 ## 주의사항 - 사업자등록번호는 `123-45-67890` 형식으로 입력 - 금액은 원 단위로 입력 (콤마 없이 숫자만) - PDF 생성은 OpenClaw 브라우저가 실행중이어야 함

path: README.md
content: # korean-invoice

한국형 견적서/세금계산서 자동 생성 스킬

## 디렉토리 구조

```
korean-invoice/
├── SKILL.md              # 스킬 설명서
├── README.md             # 개발자 문서 (이 파일)
├── package.json          # 의존성
├── scripts/
│   ├── generate.js       # 견적서/세금계산서 생성 (메인)
│   ├── manage-clients.js # 거래처 관리
│   └── manage-items.js   # 품목 관리
├── templates/
│   ├── quote.html        # 견적서 템플릿
│   └── tax-invoice.html  # 세금계산서 템플릿
├── data/
│   ├── my-info.json      # 내 사업자 정보
│   ├── clients.json      # 거래처 DB
│   └── items.json        # 품목 DB
└── output/               # 생성된 파일 저장 (HTML, PDF)
```

## 설치

```bash
cd /Users/mupeng/.openclaw/workspace/skills/korean-invoice
npm install
```

## 사용법

### 1. 내 정보 설정

`data/my-info.json` 파일을 편집하여 내 사업자 정보 입력:

```json
{
  "businessNumber": "123-45-67890",
  "companyName": "무펭이즘",
  "ceo": "김무펭",
  "address": "서울특별시 강남구 테헤란로 123",
  "phone": "010-1234-5678",
  "email": "[REDACTED_SECRET]",
  "bankAccount": "우리은행 1002-123-456789"
}
```

### 2. 거래처 관리

```bash
# 거래처 추가
node scripts/manage-clients.js add "무펭이즘" \
  --business-number "123-45-67890" \
  --ceo "김형님" \
  --address "서울시..." \
  --phone "010-1234-5678" \
  --email "[REDACTED_SECRET]"

# 거래처 목록
node scripts/manage-clients.js list

# 거래처 조회
node scripts/manage-clients.js view "무펭이즘"

# 거래처 수정
node scripts/manage-clients.js edit "무펭이즘" --phone "010-9999-9999"

# 거래처 삭제
node scripts/manage-clients.js remove "무펭이즘"
```

### 3. 품목 관리

```bash
# 품목 추가
node scripts/manage-items.js add "포토부스 대여" \
  --price 500000 \
  --unit "일" \
  --description "이동형 포토부스 대여 (1일 기준)"

# 품목 목록
node scripts/manage-items.js list

# 품목 조회
node scripts/manage-items.js view "포토부스 대여"

# 품목 수정
node scripts/manage-items.js edit "포토부스 대여" --price 600000

# 품목 삭제
node scripts/manage-items.js remove "포토부스 대여"
```

### 4. 견적서 생성

```bash
# 품목 직접 입력 (품목명,수량,단가;...)
node scripts/generate.js quote \
  --client "무펭이즘" \
  --items "포토부스 대여,2,500000;출장비,1,100000" \
  --notes "부가세 별도"

# 저장된 품목 사용
node scripts/generate.js quote \
  --client "무펭이즘" \
  --item-ids "포토부스 대여,출장비"

# HTML만 생성 (PDF 변환 안 함)
node scripts/generate.js quote \
  --client "무펭이즘" \
  --items "포토부스 대여,1,500000" \
  --no-pdf
```

### 5. 세금계산서 생성

```bash
# 세금계산서 생성 (영수)
node scripts/generate.js tax \
  --client "무펭이즘" \
  --items "포토부스 대여,1,500000" \
  --type 영수

# 세금계산서 생성 (청구)
node scripts/generate.js tax \
  --client "무펭이즘" \
  --items "포토부스 대여,1,500000" \
  --type 청구 \
  --issue-date "2026-02-17"
```

## 출력 파일

생성된 파일은 `output/` 디렉토리에 저장됩니다:

- `YYYY-MM-DD-견적서-{거래처명}.html`
- `YYYY-MM-DD-견적서-{거래처명}.pdf`
- `YYYY-MM-DD-세금계산서-{거래처명}.html`
- `YYYY-MM-DD-세금계산서-{거래처명}.pdf`

## PDF 변환

PDF 변환은 puppeteer를 사용하여 OpenClaw 브라우저(포트 18800)에 연결합니다.

**주의:**
- OpenClaw 브라우저가 실행중이어야 함
- `browser start` 명령으로 브라우저 시작

## 템플릿 커스터마이징

`templates/` 디렉토리의 HTML 파일을 편집하여 양식을 커스터마이징할 수 있습니다.

### 변수

견적서 (`quote.html`):
- `{{myCompanyName}}`, `{{myBusinessNumber}}`, `{{myCEO}}`, `{{myAddress}}`, `{{myPhone}}`, `{{myEmail}}`
- `{{clientName}}`, `{{clientBusinessNumber}}`, `{{clientCEO}}`, `{{clientAddress}}`, `{{clientPhone}}`
- `{{issueDate}}`, `{{validUntil}}`
- `{{itemRows}}` - 품목 테이블 행
- `{{subtotal}}`, `{{vat}}`, `{{total}}`
- `{{notes}}`

세금계산서 (`tax-invoice.html`):
- `{{approvalNumber}}`, `{{type}}`, `{{issueDate}}`
- 공급자/공급받는자 정보는 견적서와 동일
- `{{itemRows}}` - 품목 테이블 행
- `{{subtotal}}`, `{{vat}}`, `{{total}}`

## OpenClaw 통합

OpenClaw에서 자연어로 호출:

```
"무펭이즘 거래처에 포토부스 대여 2일 견적서 작성해줘"
"예시거래처에 세금계산서 발행해줘 (포토부스 대여 1일)"
```

Agent가 자동으로 스크립트를 실행하고 결과를 보고합니다.

## 트러블슈팅

### PDF 생성 실패

```
Error: connect ECONNREFUSED 127.0.0.1:18800
```

→ OpenClaw 브라우저가 실행중인지 확인: `openclaw browser status`

### 거래처/품목 없음

→ `manage-clients.js` / `manage-items.js`로 먼저 등록

### 템플릿 변수 미치환

→ `data/my-info.json`에 모든 필드가 입력되었는지 확인

## 라이선스

MIT

## 작성자

무펭이 🐧 (2026-02-17)


path: data/clients.json
content: name: 예시거래처
businessNumber: 987-65-43210
ceo: 홍길동
address: 서울특별시 서초구 강남대로 456
phone: 02-1234-5678
email: [REDACTED_SECRET]
createdAt: 2026-02-17T00:00:00.000Z

path: data/items.json
content: name: 포토부스 대여
price: 500000
unit: 일
description: 이동형 포토부스 대여 (1일 기준)
createdAt: 2026-02-17T00:00:00.000Z

name: 출장비
price: 100000
unit: 회
description: 지방 출장 시 추가 비용
createdAt: 2026-02-17T00:00:00.000Z

path: data/my-info.json
content: businessNumber: 123-45-67890
companyName: 무펭이즘
ceo: 김무펭
address: 서울특별시 강남구 테헤란로 123
phone: 010-1234-5678
email: [REDACTED_SECRET]
bankAccount: 우리은행 1002-123-456789

path: package-lock.json
content: {
  "name": "korean-invoice",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "korean-invoice",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "puppeteer-core": "^23.0.0"
      }
    },
    "node_modules/@puppeteer/browsers": {
      "version": "2.6.1",
      "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "debug": "^4.4.0",
        "extract-zip": "^2.0.1",
        "progress": "^2.0.3",
        "proxy-agent": "^6.5.0",
        "semver": "^7.6.3",
        "tar-fs": "^3.0.6",
        "unbzip2-stream": "^1.4.3",
        "yargs": "^17.7.2"
      },
      "bin": {
        "browsers": "lib/cjs/main-cli.js"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@tootallnate/quickjs-emscripten": {
      "version": "0.23.0",
      "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/@types/node": {
      "version": "25.2.3",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "optional": true,
      "dependencies": {
        "undici-types": "~7.16.0"
      }
    },
    "node_modules/@types/yauzl": {
      "version": "2.10.3",
      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "optional": true,
      "dependencies": {
        "@types/node": "*"
      }
    },
    "node_modules/agent-base": {
      "version": "7.1.4",
      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/ansi-regex": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/ast-types": {
      "version": "0.13.4",
      "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "tslib": "^2.0.1"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/b4a": {
      "version": "1.7.4",
      "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "peerDependencies": {
        "react-native-b4a": "*"
      },
      "peerDependenciesMeta": {
        "react-native-b4a": {
          "optional": true
        }
      }
    },
    "node_modules/bare-events": {
      "version": "2.8.2",
      "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "peerDependencies": {
        "bare-abort-controller": "*"
      },
      "peerDependenciesMeta": {
        "bare-abort-controller": {
          "optional": true
        }
      }
    },
    "node_modules/bare-fs": {
      "version": "4.5.4",
      "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "optional": true,
      "dependencies": {
        "bare-events": "^2.5.4",
        "bare-path": "^3.0.0",
        "bare-stream": "^2.6.4",
        "bare-url": "^2.2.2",
        "fast-fifo": "^1.3.2"
      },
      "engines": {
        "bare": ">=1.16.0"
      },
      "peerDependencies": {
        "bare-buffer": "*"
      },
      "peerDependenciesMeta": {
        "bare-buffer": {
          "optional": true
        }
      }
    },
    "node_modules/bare-os": {
      "version": "3.6.2",
      "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "optional": true,
      "engines": {
        "bare": ">=1.14.0"
      }
    },
    "node_modules/bare-path": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "optional": true,
      "dependencies": {
        "bare-os": "^3.0.1"
      }
    },
    "node_modules/bare-stream": {
      "version": "2.7.0",
      "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "optional": true,
      "dependencies": {
        "streamx": "^2.21.0"
      },
      "peerDependencies": {
        "bare-buffer": "*",
        "bare-events": "*"
      },
      "peerDependenciesMeta": {
        "bare-buffer": {
          "optional": true
        },
        "bare-events": {
          "optional": true
        }
      }
    },
    "node_modules/bare-url": {
      "version": "2.3.2",
      "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "optional": true,
      "dependencies": {
        "bare-path": "^3.0.0"
      }
    },
    "node_modules/base64-js": {
      "version": "1.5.1",
      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/basic-ftp": {
      "version": "5.1.0",
      "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      }
    },
    "node_modules/buffer": {
      "version": "5.7.1",
      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "base64-js": "^1.3.1",
        "ieee754": "^1.1.13"
      }
    },
    "node_modules/buffer-crc32": {
      "version": "0.2.13",
      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": "*"
      }
    },
    "node_modules/chromium-bidi": {
      "version": "0.11.0",
      "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "mitt": "3.0.1",
        "zod": "3.23.8"
      },
      "peerDependencies": {
        "devtools-protocol": "*"
      }
    },
    "node_modules/cliui": {
      "version": "8.0.1",
      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "string-width": "^4.2.0",
        "strip-ansi": "^6.0.1",
        "wrap-ansi": "^7.0.0"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/data-uri-to-buffer": {
      "version": "6.0.2",
      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/degenerator": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ast-types": "^0.13.4",
        "escodegen": "^2.1.0",
        "esprima": "^4.0.1"
      },
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/devtools-protocol": {
      "version": "0.0.1367902",
      "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause"
    },
    "node_modules/emoji-regex": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/end-of-stream": {
      "version": "1.4.5",
      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "once": "^1.4.0"
      }
    },
    "node_modules/escalade": {
      "version": "3.2.0",
      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/escodegen": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "esprima": "^4.0.1",
        "estraverse": "^5.2.0",
        "esutils": "^2.0.2"
      },
      "bin": {
        "escodegen": "bin/escodegen.js",
        "esgenerate": "bin/esgenerate.js"
      },
      "engines": {
        "node": ">=6.0"
      },
      "optionalDependencies": {
        "source-map": "~0.6.1"
      }
    },
    "node_modules/esprima": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "bin": {
        "esparse": "bin/esparse.js",
        "esvalidate": "bin/esvalidate.js"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/estraverse": {
      "version": "5.3.0",
      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=4.0"
      }
    },
    "node_modules/esutils": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/events-universal": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "bare-events": "^2.7.0"
      }
    },
    "node_modules/extract-zip": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "dependencies": {
        "debug": "^4.1.1",
        "get-stream": "^5.1.0",
        "yauzl": "^2.10.0"
      },
      "bin": {
        "extract-zip": "cli.js"
      },
      "engines": {
        "node": ">= 10.17.0"
      },
      "optionalDependencies": {
        "@types/yauzl": "^2.9.1"
      }
    },
    "node_modules/fast-fifo": {
      "version": "1.3.2",
      "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/fd-slicer": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "pend": "~1.2.0"
      }
    },
    "node_modules/get-caller-file": {
      "version": "2.0.5",
      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "engines": {
        "node": "6.* || 8.* || >= 10.*"
      }
    },
    "node_modules/get-stream": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "pump": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/get-uri": {
      "version": "6.0.5",
      "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "basic-ftp": "^5.0.2",
        "data-uri-to-buffer": "^6.0.2",
        "debug": "^4.3.4"
      },
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/http-proxy-agent": {
      "version": "7.0.2",
      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "agent-base": "^7.1.0",
        "debug": "^4.3.4"
      },
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/https-proxy-agent": {
      "version": "7.0.6",
      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "agent-base": "^7.1.2",
        "debug": "4"
      },
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/ieee754": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "BSD-3-Clause"
    },
    "node_modules/ip-address": {
      "version": "10.1.0",
      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/is-fullwidth-code-point": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/lru-cache": {
      "version": "7.18.3",
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/mitt": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/netmask": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4.0"
      }
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/pac-proxy-agent": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@tootallnate/quickjs-emscripten": "^0.23.0",
        "agent-base": "^7.1.2",
        "debug": "^4.3.4",
        "get-uri": "^6.0.1",
        "http-proxy-agent": "^7.0.0",
        "https-proxy-agent": "^7.0.6",
        "pac-resolver": "^7.0.1",
        "socks-proxy-agent": "^8.0.5"
      },
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/pac-resolver": {
      "version": "7.0.1",
      "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "degenerator": "^5.0.0",
        "netmask": "^2.0.2"
      },
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/pend": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/progress": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/proxy-agent": {
      "version": "6.5.0",
      "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "agent-base": "^7.1.2",
        "debug": "^4.3.4",
        "http-proxy-agent": "^7.0.1",
        "https-proxy-agent": "^7.0.6",
        "lru-cache": "^7.14.1",
        "pac-proxy-agent": "^7.1.0",
        "proxy-from-env": "^1.1.0",
        "socks-proxy-agent": "^8.0.5"
      },
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/proxy-from-env": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/pump": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "end-of-stream": "^1.1.0",
        "once": "^1.3.1"
      }
    },
    "node_modules/puppeteer-core": {
      "version": "23.11.1",
      "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@puppeteer/browsers": "2.6.1",
        "chromium-bidi": "0.11.0",
        "debug": "^4.4.0",
        "devtools-protocol": "0.0.1367902",
        "typed-query-selector": "^2.12.0",
        "ws": "^8.18.0"
      },
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/require-directory": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/semver": {
      "version": "7.7.4",
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "bin": {
        "semver": "bin/semver.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/smart-buffer": {
      "version": "4.2.0",
      "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 6.0.0",
        "npm": ">= 3.0.0"
      }
    },
    "node_modules/socks": {
      "version": "2.8.7",
      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ip-address": "^10.0.1",
        "smart-buffer": "^4.2.0"
      },
      "engines": {
        "node": ">= 10.0.0",
        "npm": ">= 3.0.0"
      }
    },
    "node_modules/socks-proxy-agent": {
      "version": "8.0.5",
      "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "agent-base": "^7.1.2",
        "debug": "^4.3.4",
        "socks": "^2.8.3"
      },
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/source-map": {
      "version": "0.6.1",
      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "optional": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/streamx": {
      "version": "2.23.0",
      "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "events-universal": "^1.0.0",
        "fast-fifo": "^1.3.2",
        "text-decoder": "^1.1.0"
      }
    },
    "node_modules/string-width": {
      "version": "4.2.3",
      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "emoji-regex": "^8.0.0",
        "is-fullwidth-code-point": "^3.0.0",
        "strip-ansi": "^6.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/strip-ansi": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ansi-regex": "^5.0.1"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/tar-fs": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "pump": "^3.0.0",
        "tar-stream": "^3.1.5"
      },
      "optionalDependencies": {
        "bare-fs": "^4.0.1",
        "bare-path": "^3.0.0"
      }
    },
    "node_modules/tar-stream": {
      "version": "3.1.7",
      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "b4a": "^1.6.4",
        "fast-fifo": "^1.2.0",
        "streamx": "^2.15.0"
      }
    },
    "node_modules/text-decoder": {
      "version": "1.2.7",
      "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "b4a": "^1.6.4"
      }
    },
    "node_modules/through": {
      "version": "2.3.8",
      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/tslib": {
      "version": "2.8.1",
      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "0BSD"
    },
    "node_modules/typed-query-selector": {
      "version": "2.12.0",
      "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/unbzip2-stream": {
      "version": "1.4.3",
      "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "buffer": "^5.2.1",
        "through": "^2.3.8"
      }
    },
    "node_modules/undici-types": {
      "version": "7.16.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "optional": true
    },
    "node_modules/wrap-ansi": {
      "version": "7.0.0",
      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ansi-styles": "^4.0.0",
        "string-width": "^4.1.0",
        "strip-ansi": "^6.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
      }
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ws": {
      "version": "8.19.0",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    },
    "node_modules/y18n": {
      "version": "5.0.8",
      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/yargs": {
      "version": "17.7.2",
      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "cliui": "^8.0.1",
        "escalade": "^3.1.1",
        "get-caller-file": "^2.0.5",
        "require-directory": "^2.1.1",
        "string-width": "^4.2.3",
        "y18n": "^5.0.5",
        "yargs-parser": "^21.1.1"
      },
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/yargs-parser": {
      "version": "21.1.1",
      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "engines": {
        "node": ">=12"
      }
    },
    "node_modules/yauzl": {
      "version": "2.10.0",
      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "buffer-crc32": "~0.2.3",
        "fd-slicer": "~1.1.0"
      }
    },
    "node_modules/zod": {
      "version": "3.23.8",
      "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/colinhacks"
      }
    }
  }
}


path: package.json
content: {
  "name": "korean-invoice",
  "version": "1.0.0",
  "description": "한국형 견적서/세금계산서 자동 생성",
  "main": "scripts/generate.js",
  "scripts": {
    "test": "node scripts/generate.js --help"
  },
  "keywords": ["invoice", "quote", "tax", "korea"],
  "author": "무펭이",
  "license": "MIT",
  "dependencies": {
    "puppeteer-core": "^23.0.0"
  }
}


path: scripts/generate.js
content: #!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const puppeteer = require('puppeteer-core');

const SKILL_DIR = path.resolve(__dirname, '..');
const DATA_DIR = path.join(SKILL_DIR, 'data');
const TEMPLATE_DIR = path.join(SKILL_DIR, 'templates');
const OUTPUT_DIR = path.join(SKILL_DIR, 'output');

// 데이터 로드 함수
function loadJSON(filename, defaultValue = {}) {
  const filepath = path.join(DATA_DIR, filename);
  if (!fs.existsSync(filepath)) {
    return defaultValue;
  }
  return JSON.parse(fs.readFileSync(filepath, 'utf-8'));
}

function saveJSON(filename, data) {
  const filepath = path.join(DATA_DIR, filename);
  fs.mkdirSync(path.dirname(filepath), { recursive: true });
  fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf-8');
}

// 금액 포맷 (천 단위 콤마)
function formatCurrency(amount) {
  return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

// 날짜 포맷 (YYYY-MM-DD)
function formatDate(date) {
  if (typeof date === 'string') return date;
  const d = date || new Date();
  const year = d.getFullYear();
  const month = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

// 견적서 생성
async function generateQuote(options) {
  const myInfo = loadJSON('my-info.json');
  const clients = loadJSON('clients.json', []);
  const items = loadJSON('items.json', []);

  // 거래처 찾기
  const client = clients.find(c => c.name === options.client);
  if (!client) {
    throw new Error(`거래처를 찾을 수 없습니다: ${options.client}`);
  }

  // 품목 파싱
  let itemList = [];
  if (options.items) {
    // "품목명,수량,단가" 형식
    const itemStrings = options.items.split(';');
    itemList = itemStrings.map(str => {
      const [name, quantity, price] = str.split(',').map(s => s.trim());
      return {
        name,
        quantity: parseInt(quantity) || 1,
        price: parseInt(price) || 0,
        unit: '개'
      };
    });
  } else if (options.itemIds) {
    // 저장된 품목 ID 사용
    const ids = options.itemIds.split(',');
    itemList = ids.map(id => {
      const item = items.find(i => i.name === id.trim());
      if (!item) throw new Error(`품목을 찾을 수 없습니다: ${id}`);
      return {
        name: item.name,
        quantity: 1,
        price: item.price,
        unit: item.unit || '개'
      };
    });
  }

  // 금액 계산
  const subtotal = itemList.reduce((sum, item) => sum + (item.quantity * item.price), 0);
  const vat = options.includeVAT !== false ? Math.round(subtotal * 0.1) : 0;
  const total = subtotal + vat;

  // 템플릿 로드
  const template = fs.readFileSync(path.join(TEMPLATE_DIR, 'quote.html'), 'utf-8');

  // 템플릿 변수 치환
  const issueDate = formatDate(options.issueDate);
  const validUntil = formatDate(options.validUntil || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000));

  let itemRows = '';
  itemList.forEach((item, idx) => {
    const amount = item.quantity * item.price;
    itemRows += `
      <tr>
        <td>${idx + 1}</td>
        <td>${item.name}</td>
        <td>${item.unit}</td>
        <td>${formatCurrency(item.quantity)}</td>
        <td>${formatCurrency(item.price)}</td>
        <td>${formatCurrency(amount)}</td>
      </tr>
    `;
  });

  const html = template
    .replace(/{{myCompanyName}}/g, myInfo.companyName || '')
    .replace(/{{myBusinessNumber}}/g, myInfo.businessNumber || '')
    .replace(/{{myCEO}}/g, myInfo.ceo || '')
    .replace(/{{myAddress}}/g, myInfo.address || '')
    .replace(/{{myPhone}}/g, myInfo.phone || '')
    .replace(/{{myEmail}}/g, myInfo.email || '')
    .replace(/{{clientName}}/g, client.name || '')
    .replace(/{{clientBusinessNumber}}/g, client.businessNumber || '')
    .replace(/{{clientCEO}}/g, client.ceo || '')
    .replace(/{{clientAddress}}/g, client.address || '')
    .replace(/{{clientPhone}}/g, client.phone || '')
    .replace(/{{issueDate}}/g, issueDate)
    .replace(/{{validUntil}}/g, validUntil)
    .replace(/{{itemRows}}/g, itemRows)
    .replace(/{{subtotal}}/g, formatCurrency(subtotal))
    .replace(/{{vat}}/g, formatCurrency(vat))
    .replace(/{{total}}/g, formatCurrency(total))
    .replace(/{{notes}}/g, options.notes || '');

  // HTML 저장
  const filename = `${issueDate}-견적서-${client.name}`;
  const htmlPath = path.join(OUTPUT_DIR, `${filename}.html`);
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  fs.writeFileSync(htmlPath, html, 'utf-8');

  console.log(`✅ 견적서 HTML 생성: ${htmlPath}`);

  // PDF 변환
  if (options.pdf !== false) {
    const pdfPath = path.join(OUTPUT_DIR, `${filename}.pdf`);
    await convertToPDF(htmlPath, pdfPath);
    console.log(`✅ PDF 생성: ${pdfPath}`);
    return pdfPath;
  }

  return htmlPath;
}

// 세금계산서 생성
async function generateTaxInvoice(options) {
  const myInfo = loadJSON('my-info.json');
  const clients = loadJSON('clients.json', []);
  const items = loadJSON('items.json', []);

  const client = clients.find(c => c.name === options.client);
  if (!client) {
    throw new Error(`거래처를 찾을 수 없습니다: ${options.client}`);
  }

  // 품목 파싱 (견적서와 동일)
  let itemList = [];
  if (options.items) {
    const itemStrings = options.items.split(';');
    itemList = itemStrings.map(str => {
      const [name, quantity, price] = str.split(',').map(s => s.trim());
      return {
        name,
        quantity: parseInt(quantity) || 1,
        price: parseInt(price) || 0
      };
    });
  } else if (options.itemIds) {
    const ids = options.itemIds.split(',');
    itemList = ids.map(id => {
      const item = items.find(i => i.name === id.trim());
      if (!item) throw new Error(`품목을 찾을 수 없습니다: ${id}`);
      return {
        name: item.name,
        quantity: 1,
        price: item.price
      };
    });
  }

  const subtotal = itemList.reduce((sum, item) => sum + (item.quantity * item.price), 0);
  const vat = Math.round(subtotal * 0.1);
  const total = subtotal + vat;

  const template = fs.readFileSync(path.join(TEMPLATE_DIR, 'tax-invoice.html'), 'utf-8');

  const issueDate = formatDate(options.issueDate);
  const approvalNumber = `T${Date.now().toString().slice(-10)}`; // 임의 승인번호

  let itemRows = '';
  itemList.forEach((item, idx) => {
    const amount = item.quantity * item.price;
    itemRows += `
      <tr>
        <td>${issueDate}</td>
        <td>${item.name}</td>
        <td>${formatCurrency(item.quantity)}</td>
        <td>${formatCurrency(item.price)}</td>
        <td>${formatCurrency(amount)}</td>
        <td>${options.notes || ''}</td>
      </tr>
    `;
  });

  const html = template
    .replace(/{{approvalNumber}}/g, approvalNumber)
    .replace(/{{type}}/g, options.type || '영수')
    .replace(/{{issueDate}}/g, issueDate)
    .replace(/{{myCompanyName}}/g, myInfo.companyName || '')
    .replace(/{{myBusinessNumber}}/g, myInfo.businessNumber || '')
    .replace(/{{myCEO}}/g, myInfo.ceo || '')
    .replace(/{{myAddress}}/g, myInfo.address || '')
    .replace(/{{clientName}}/g, client.name || '')
    .replace(/{{clientBusinessNumber}}/g, client.businessNumber || '')
    .replace(/{{clientCEO}}/g, client.ceo || '')
    .replace(/{{clientAddress}}/g, client.address || '')
    .replace(/{{itemRows}}/g, itemRows)
    .replace(/{{subtotal}}/g, formatCurrency(subtotal))
    .replace(/{{vat}}/g, formatCurrency(vat))
    .replace(/{{total}}/g, formatCurrency(total));

  const filename = `${issueDate}-세금계산서-${client.name}`;
  const htmlPath = path.join(OUTPUT_DIR, `${filename}.html`);
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  fs.writeFileSync(htmlPath, html, 'utf-8');

  console.log(`✅ 세금계산서 HTML 생성: ${htmlPath}`);

  if (options.pdf !== false) {
    const pdfPath = path.join(OUTPUT_DIR, `${filename}.pdf`);
    await convertToPDF(htmlPath, pdfPath);
    console.log(`✅ PDF 생성: ${pdfPath}`);
    return pdfPath;
  }

  return htmlPath;
}

// HTML → PDF 변환 (puppeteer)
async function convertToPDF(htmlPath, pdfPath) {
  const browser = await puppeteer.connect({
    browserURL: 'http://localhost:18800'
  });

  const page = await browser.newPage();
  await page.goto(`file://${htmlPath}`, { waitUntil: 'networkidle0' });
  await page.pdf({
    path: pdfPath,
    format: 'A4',
    printBackground: true,
    margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' }
  });

  await page.close();
  // browser.disconnect() - 세션 유지
}

// CLI 인터페이스
const args = process.argv.slice(2);
const command = args[0];

async function main() {
  if (command === 'quote') {
    const options = {};
    for (let i = 1; i < args.length; i++) {
      if (args[i] === '--client') options.client = args[++i];
      else if (args[i] === '--items') options.items = args[++i];
      else if (args[i] === '--item-ids') options.itemIds = args[++i];
      else if (args[i] === '--notes') options.notes = args[++i];
      else if (args[i] === '--issue-date') options.issueDate = args[++i];
      else if (args[i] === '--valid-until') options.validUntil = args[++i];
      else if (args[i] === '--no-pdf') options.pdf = false;
    }

    if (!options.client || (!options.items && !options.itemIds)) {
      console.error('사용법: node generate.js quote --client "거래처명" --items "품목,수량,단가;..."');
      process.exit(1);
    }

    await generateQuote(options);
  } else if (command === 'tax') {
    const options = {};
    for (let i = 1; i < args.length; i++) {
      if (args[i] === '--client') options.client = args[++i];
      else if (args[i] === '--items') options.items = args[++i];
      else if (args[i] === '--item-ids') options.itemIds = args[++i];
      else if (args[i] === '--notes') options.notes = args[++i];
      else if (args[i] === '--issue-date') options.issueDate = args[++i];
      else if (args[i] === '--type') options.type = args[++i];
      else if (args[i] === '--no-pdf') options.pdf = false;
    }

    if (!options.client || (!options.items && !options.itemIds)) {
      console.error('사용법: node generate.js tax --client "거래처명" --items "품목,수량,단가;..."');
      process.exit(1);
    }

    await generateTaxInvoice(options);
  } else {
    console.log(`사용법:
  node generate.js quote --client "거래처명" --items "품목,수량,단가;..."
  node generate.js tax --client "거래처명" --items "품목,수량,단가;..."
    `);
  }
}

main().catch(err => {
  console.error('❌ 오류:', err.message);
  process.exit(1);
});


path: scripts/manage-clients.js
content: #!/usr/bin/env node
const fs = require('fs');
const path = require('path');

const DATA_DIR = path.resolve(__dirname, '..', 'data');
const CLIENTS_FILE = path.join(DATA_DIR, 'clients.json');

function loadClients() {
  if (!fs.existsSync(CLIENTS_FILE)) {
    return [];
  }
  return JSON.parse(fs.readFileSync(CLIENTS_FILE, 'utf-8'));
}

function saveClients(clients) {
  fs.mkdirSync(DATA_DIR, { recursive: true });
  fs.writeFileSync(CLIENTS_FILE, JSON.stringify(clients, null, 2), 'utf-8');
}

function addClient(name, options) {
  const clients = loadClients();
  
  // 중복 체크
  if (clients.some(c => c.name === name)) {
    console.error(`❌ 이미 존재하는 거래처입니다: ${name}`);
    process.exit(1);
  }

  const client = {
    name,
    businessNumber: options.businessNumber || '',
    ceo: options.ceo || '',
    address: options.address || '',
    phone: options.phone || '',
    email: options.email || '',
    createdAt: new Date().toISOString()
  };

  clients.push(client);
  saveClients(clients);
  console.log(`✅ 거래처 추가: ${name}`);
}

function editClient(name, options) {
  const clients = loadClients();
  const client = clients.find(c => c.name === name);

  if (!client) {
    console.error(`❌ 거래처를 찾을 수 없습니다: ${name}`);
    process.exit(1);
  }

  if (options.businessNumber) client.businessNumber = options.businessNumber;
  if (options.ceo) client.ceo = options.ceo;
  if (options.address) client.address = options.address;
  if (options.phone) client.phone = options.phone;
  if (options.email) client.email = options.email;

  saveClients(clients);
  console.log(`✅ 거래처 수정: ${name}`);
}

function removeClient(name) {
  const clients = loadClients();
  const filtered = clients.filter(c => c.name !== name);

  if (filtered.length === clients.length) {
    console.error(`❌ 거래처를 찾을 수 없습니다: ${name}`);
    process.exit(1);
  }

  saveClients(filtered);
  console.log(`✅ 거래처 삭제: ${name}`);
}

function listClients() {
  const clients = loadClients();

  if (clients.length === 0) {
    console.log('등록된 거래처가 없습니다.');
    return;
  }

  console.log(`총 ${clients.length}개 거래처:\n`);
  clients.forEach((client, idx) => {
    console.log(`${idx + 1}. ${client.name}`);
    console.log(`   사업자번호: ${client.businessNumber || '(없음)'}`);
    console.log(`   대표자: ${client.ceo || '(없음)'}`);
    console.log(`   연락처: ${client.phone || '(없음)'}`);
    console.log('');
  });
}

function viewClient(name) {
  const clients = loadClients();
  const client = clients.find(c => c.name === name);

  if (!client) {
    console.error(`❌ 거래처를 찾을 수 없습니다: ${name}`);
    process.exit(1);
  }

  console.log(`거래처 정보: ${client.name}\n`);
  console.log(`사업자등록번호: ${client.businessNumber || '(없음)'}`);
  console.log(`대표자: ${client.ceo || '(없음)'}`);
  console.log(`주소: ${client.address || '(없음)'}`);
  console.log(`전화: ${client.phone || '(없음)'}`);
  console.log(`이메일: ${client.email || '(없음)'}`);
  console.log(`등록일: ${client.createdAt}`);
}

// CLI
const args = process.argv.slice(2);
const command = args[0];

if (command === 'add') {
  const name = args[1];
  const options = {};
  for (let i = 2; i < args.length; i++) {
    if (args[i] === '--business-number') options.businessNumber = args[++i];
    else if (args[i] === '--ceo') options.ceo = args[++i];
    else if (args[i] === '--address') options.address = args[++i];
    else if (args[i] === '--phone') options.phone = args[++i];
    else if (args[i] === '--email') options.email = args[++i];
  }
  addClient(name, options);
} else if (command === 'edit') {
  const name = args[1];
  const options = {};
  for (let i = 2; i < args.length; i++) {
    if (args[i] === '--business-number') options.businessNumber = args[++i];
    else if (args[i] === '--ceo') options.ceo = args[++i];
    else if (args[i] === '--address') options.address = args[++i];
    else if (args[i] === '--phone') options.phone = args[++i];
    else if (args[i] === '--email') options.email = args[++i];
  }
  editClient(name, options);
} else if (command === 'remove') {
  removeClient(args[1]);
} else if (command === 'list') {
  listClients();
} else if (command === 'view') {
  viewClient(args[1]);
} else {
  console.log(`사용법:
  node manage-clients.js add "거래처명" --business-number "123-45-67890" --ceo "홍길동" --address "..." --phone "..." --email "..."
  node manage-clients.js edit "거래처명" --phone "..."
  node manage-clients.js remove "거래처명"
  node manage-clients.js list
  node manage-clients.js view "거래처명"
  `);
}


path: scripts/manage-items.js
content: #!/usr/bin/env node
const fs = require('fs');
const path = require('path');

const DATA_DIR = path.resolve(__dirname, '..', 'data');
const ITEMS_FILE = path.join(DATA_DIR, 'items.json');

function loadItems() {
  if (!fs.existsSync(ITEMS_FILE)) {
    return [];
  }
  return JSON.parse(fs.readFileSync(ITEMS_FILE, 'utf-8'));
}

function saveItems(items) {
  fs.mkdirSync(DATA_DIR, { recursive: true });
  fs.writeFileSync(ITEMS_FILE, JSON.stringify(items, null, 2), 'utf-8');
}

function addItem(name, options) {
  const items = loadItems();
  
  // 중복 체크
  if (items.some(i => i.name === name)) {
    console.error(`❌ 이미 존재하는 품목입니다: ${name}`);
    process.exit(1);
  }

  const item = {
    name,
    price: parseInt(options.price) || 0,
    unit: options.unit || '개',
    description: options.description || '',
    createdAt: new Date().toISOString()
  };

  items.push(item);
  saveItems(items);
  console.log(`✅ 품목 추가: ${name} (${item.price.toLocaleString()}원/${item.unit})`);
}

function editItem(name, options) {
  const items = loadItems();
  const item = items.find(i => i.name === name);

  if (!item) {
    console.error(`❌ 품목을 찾을 수 없습니다: ${name}`);
    process.exit(1);
  }

  if (options.price) item.price = parseInt(options.price);
  if (options.unit) item.unit = options.unit;
  if (options.description) item.description = options.description;

  saveItems(items);
  console.log(`✅ 품목 수정: ${name}`);
}

function removeItem(name) {
  const items = loadItems();
  const filtered = items.filter(i => i.name !== name);

  if (filtered.length === items.length) {
    console.error(`❌ 품목을 찾을 수 없습니다: ${name}`);
    process.exit(1);
  }

  saveItems(filtered);
  console.log(`✅ 품목 삭제: ${name}`);
}

function listItems() {
  const items = loadItems();

  if (items.length === 0) {
    console.log('등록된 품목이 없습니다.');
    return;
  }

  console.log(`총 ${items.length}개 품목:\n`);
  items.forEach((item, idx) => {
    console.log(`${idx + 1}. ${item.name}`);
    console.log(`   단가: ${item.price.toLocaleString()}원 / ${item.unit}`);
    if (item.description) {
      console.log(`   설명: ${item.description}`);
    }
    console.log('');
  });
}

function viewItem(name) {
  const items = loadItems();
  const item = items.find(i => i.name === name);

  if (!item) {
    console.error(`❌ 품목을 찾을 수 없습니다: ${name}`);
    process.exit(1);
  }

  console.log(`품목 정보: ${item.name}\n`);
  console.log(`단가: ${item.price.toLocaleString()}원`);
  console.log(`단위: ${item.unit}`);
  console.log(`설명: ${item.description || '(없음)'}`);
  console.log(`등록일: ${item.createdAt}`);
}

// CLI
const args = process.argv.slice(2);
const command = args[0];

if (command === 'add') {
  const name = args[1];
  const options = {};
  for (let i = 2; i < args.length; i++) {
    if (args[i] === '--price') options.price = args[++i];
    else if (args[i] === '--unit') options.unit = args[++i];
    else if (args[i] === '--description') options.description = args[++i];
  }
  addItem(name, options);
} else if (command === 'edit') {
  const name = args[1];
  const options = {};
  for (let i = 2; i < args.length; i++) {
    if (args[i] === '--price') options.price = args[++i];
    else if (args[i] === '--unit') options.unit = args[++i];
    else if (args[i] === '--description') options.description = args[++i];
  }
  editItem(name, options);
} else if (command === 'remove') {
  removeItem(args[1]);
} else if (command === 'list') {
  listItems();
} else if (command === 'view') {
  viewItem(args[1]);
} else {
  console.log(`사용법:
  node manage-items.js add "품목명" --price 500000 --unit "일" --description "..."
  node manage-items.js edit "품목명" --price 600000
  node manage-items.js remove "품목명"
  node manage-items.js list
  node manage-items.js view "품목명"
  `);
}


path: templates/quote.html
content: <!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>견적서</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      font-family: 'Malgun Gothic', '맑은 고딕', sans-serif;
      padding: 40px;
      font-size: 12pt;
      line-height: 1.6;
    }
    .header {
      text-align: center;
      margin-bottom: 30px;
    }
    .header h1 {
      font-size: 28pt;
      font-weight: bold;
      margin-bottom: 10px;
    }
    .info-section {
      margin-bottom: 20px;
      border: 1px solid #333;
      padding: 15px;
    }
    .info-section h2 {
      font-size: 14pt;
      font-weight: bold;
      margin-bottom: 10px;
      padding-bottom: 5px;
      border-bottom: 2px solid #333;
    }
    .info-row {
      display: flex;
      margin-bottom: 5px;
    }
    .info-label {
      width: 120px;
      font-weight: bold;
    }
    .info-value {
      flex: 1;
    }
    table {
      width: 100%;
      border-collapse: collapse;
      margin: 20px 0;
    }
    table th, table td {
      border: 1px solid #333;
      padding: 8px;
      text-align: center;
    }
    table th {
      background-color: #f0f0f0;
      font-weight: bold;
    }
    table td.left {
      text-align: left;
    }
    table td.right {
      text-align: right;
    }
    .summary {
      margin-top: 20px;
      text-align: right;
    }
    .summary table {
      width: 400px;
      margin-left: auto;
    }
    .summary td {
      padding: 10px;
      font-size: 14pt;
    }
    .summary .total {
      font-weight: bold;
      font-size: 16pt;
      background-color: #f0f0f0;
    }
    .notes {
      margin-top: 30px;
      padding: 15px;
      border: 1px solid #ccc;
      background-color: #f9f9f9;
    }
    .footer {
      margin-top: 40px;
      text-align: center;
      font-size: 10pt;
      color: #666;
    }
  </style>
</head>
<body>
  <div class="header">
    <h1>견 적 서</h1>
    <p>Quotation</p>
  </div>

  <div class="info-section">
    <h2>공급자 (Supplier)</h2>
    <div class="info-row">
      <div class="info-label">상호 (Company):</div>
      <div class="info-value">{{myCompanyName}}</div>
    </div>
    <div class="info-row">
      <div class="info-label">사업자등록번호:</div>
      <div class="info-value">{{myBusinessNumber}}</div>
    </div>
    <div class="info-row">
      <div class="info-label">대표자 (CEO):</div>
      <div class="info-value">{{myCEO}}</div>
    </div>
    <div class="info-row">
      <div class="info-label">주소 (Address):</div>
      <div class="info-value">{{myAddress}}</div>
    </div>
    <div class="info-row">
      <div class="info-label">전화 (Phone):</div>
      <div class="info-value">{{myPhone}}</div>
    </div>
    <div class="info-row">
      <div class="info-label">이메일 (Email):</div>
      <div class="info-value">{{myEmail}}</div>
    </div>
  </div>

  <div class="info-section">
    <h2>공급받는자 (Recipient)</h2>
    <div class="info-row">
      <div class="info-label">상호 (Company):</div>
      <div class="info-value">{{clientName}}</div>
    </div>
    <div class="info-row">
      <div class="info-label">사업자등록번호:</div>
      <div class="info-value">{{clientBusinessNumber}}</div>
    </div>
    <div class="info-row">
      <div class="info-label">대표자 (CEO):</div>
      <div class="info-value">{{clientCEO}}</div>
    </div>
    <div class="info-row">
      <div class="info-label">주소 (Address):</div>
      <div class="info-value">{{clientAddress}}</div>
    </div>
    <div class="info-row">
      <div class="info-label">전화 (Phone):</div>
      <div class="info-value">{{clientPhone}}</div>
    </div>
  </div>

  <div class="info-section">
    <div class="info-row">
      <div class="info-label">작성일자:</div>
      <div class="info-value">{{issueDate}}</div>
    </div>
    <div class="info-row">
      <div class="info-label">유효기간:</div>
      <div class="info-value">{{validUntil}}</div>
    </div>
  </div>

  <table>
    <thead>
      <tr>
        <th style="width: 50px;">No.</th>
        <th style="width: 250px;">품명 (Description)</th>
        <th style="width: 80px;">단위 (Unit)</th>
        <th style="width: 80px;">수량 (Qty)</th>
        <th style="width: 120px;">단가 (Unit Price)</th>
        <th style="width: 120px;">공급가액 (Amount)</th>
      </tr>
    </thead>
    <tbody>
      {{itemRows}}
    </tbody>
  </table>

  <div class="summary">
    <table>
      <tr>
        <td>공급가액 (Subtotal)</td>
        <td class="right">{{subtotal}} 원</td>
      </tr>
      <tr>
        <td>부가세 (VAT 10%)</td>
        <td class="right">{{vat}} 원</td>
      </tr>
      <tr class="total">
        <td>합계 (Total)</td>
        <td class="right">{{total}} 원</td>
      </tr>
    </table>
  </div>

  <div class="notes">
    <strong>비고 (Notes):</strong><br>
    {{notes}}
  </div>

  <div class="footer">
    <p>위 금액을 청구합니다.</p>
    <p>{{myCompanyName}} | {{myPhone}} | {{myEmail}}</p>
  </div>
</body>
</html>


path: templates/tax-invoice.html
content: <!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>세금계산서</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      font-family: 'Malgun Gothic', '맑은 고딕', sans-serif;
      padding: 30px;
      font-size: 11pt;
      line-height: 1.5;
    }
    .header {
      text-align: center;
      margin-bottom: 20px;
      border-bottom: 3px double #000;
      padding-bottom: 15px;
    }
    .header h1 {
      font-size: 26pt;
      font-weight: bold;
      margin-bottom: 5px;
    }
    .header .subtitle {
      font-size: 10pt;
      color: #666;
    }
    .meta {
      display: flex;
      justify-content: space-between;
      margin-bottom: 20px;
      padding: 10px;
      border: 1px solid #333;
      background-color: #f5f5f5;
    }
    .meta div {
      display: flex;
      align-items: center;
    }
    .meta strong {
      margin-right: 10px;
    }
    .party-section {
      display: flex;
      gap: 20px;
      margin-bottom: 20px;
    }
    .party {
      flex: 1;
      border: 2px solid #333;
      padding: 15px;
    }
    .party h2 {
      font-size: 14pt;
      font-weight: bold;
      text-align: center;
      margin-bottom: 10px;
      padding-bottom: 8px;
      border-bottom: 2px solid #333;
    }
    .party-row {
      display: flex;
      margin-bottom: 6px;
      font-size: 10pt;
    }
    .party-label {
      width: 120px;
      font-weight: bold;
    }
    .party-value {
      flex: 1;
    }
    table {
      width: 100%;
      border-collapse: collapse;
      margin: 20px 0;
    }
    table th, table td {
      border: 1px solid #333;
      padding: 8px;
      text-align: center;
      font-size: 10pt;
    }
    table th {
      background-color: #e0e0e0;
      font-weight: bold;
    }
    table td.left {
      text-align: left;
    }
    table td.right {
      text-align: right;
    }
    .summary {
      margin-top: 20px;
      border: 2px solid #333;
      padding: 15px;
      background-color: #f9f9f9;
    }
    .summary table {
      width: 100%;
      border: none;
      margin: 0;
    }
    .summary td {
      border: none;
      padding: 8px;
      font-size: 13pt;
    }
    .summary .total {
      font-weight: bold;
      font-size: 16pt;
      background-color: #333;
      color: white;
    }
    .footer {
      margin-top: 30px;
      text-align: center;
      font-size: 9pt;
      color: #666;
      border-top: 1px solid #ccc;
      padding-top: 15px;
    }
  </style>
</head>
<body>
  <div class="header">
    <h1>세 금 계 산 서</h1>
    <div class="subtitle">Tax Invoice</div>
  </div>

  <div class="meta">
    <div>
      <strong>승인번호:</strong> {{approvalNumber}}
    </div>
    <div>
      <strong>구분:</strong> {{type}}
    </div>
    <div>
      <strong>작성일자:</strong> {{issueDate}}
    </div>
  </div>

  <div class="party-section">
    <div class="party">
      <h2>공급자</h2>
      <div class="party-row">
        <div class="party-label">사업자등록번호</div>
        <div class="party-value">{{myBusinessNumber}}</div>
      </div>
      <div class="party-row">
        <div class="party-label">상호 (법인명)</div>
        <div class="party-value">{{myCompanyName}}</div>
      </div>
      <div class="party-row">
        <div class="party-label">성명 (대표자)</div>
        <div class="party-value">{{myCEO}}</div>
      </div>
      <div class="party-row">
        <div class="party-label">사업장 주소</div>
        <div class="party-value">{{myAddress}}</div>
      </div>
    </div>

    <div class="party">
      <h2>공급받는자</h2>
      <div class="party-row">
        <div class="party-label">사업자등록번호</div>
        <div class="party-value">{{clientBusinessNumber}}</div>
      </div>
      <div class="party-row">
        <div class="party-label">상호 (법인명)</div>
        <div class="party-value">{{clientName}}</div>
      </div>
      <div class="party-row">
        <div class="party-label">성명 (대표자)</div>
        <div class="party-value">{{clientCEO}}</div>
      </div>
      <div class="party-row">
        <div class="party-label">사업장 주소</div>
        <div class="party-value">{{clientAddress}}</div>
      </div>
    </div>
  </div>

  <table>
    <thead>
      <tr>
        <th style="width: 100px;">작성일자</th>
        <th style="width: 200px;">품목</th>
        <th style="width: 80px;">수량</th>
        <th style="width: 120px;">단가</th>
        <th style="width: 120px;">공급가액</th>
        <th style="width: 150px;">비고</th>
      </tr>
    </thead>
    <tbody>
      {{itemRows}}
    </tbody>
  </table>

  <div class="summary">
    <table>
      <tr>
        <td style="width: 200px;">공급가액 (Supply Amount)</td>
        <td class="right">{{subtotal}} 원</td>
      </tr>
      <tr>
        <td>세액 (Tax Amount 10%)</td>
        <td class="right">{{vat}} 원</td>
      </tr>
      <tr class="total">
        <td>합계 (Total)</td>
        <td class="right">{{total}} 원</td>
      </tr>
    </table>
  </div>

  <div class="footer">
    <p>이 세금계산서는 전자세금계산서가 아닌 일반 세금계산서입니다.</p>
    <p>{{myCompanyName}} | 사업자등록번호: {{myBusinessNumber}}</p>
  </div>
</body>
</html>


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)