# 82excel

**엑셀 파일 검수, 직접 만들지 마세요. 저희가 대신 할게요.**
**Stop building Excel file validation yourself. We'll handle it.**

82excel은 브라우저와 Node에서 xlsx·xls·csv 파일을 읽고, 하나의 JSON 규칙으로 머리글과 칸 내용을 검사하고 값을 정리하는 무료 라이브러리입니다. MIT 라이선스, 런타임 의존성은 `fflate` 하나입니다.
82excel reads xlsx, xls and csv files in the browser or Node, checks headers and cell contents against one JSON rules file, and cleans the values. MIT, one runtime dependency (`fflate`).

## 0. 1차 검수와 2차 검수 / First pass and second pass

```
 사용자 파일 (xlsx·xls·csv)
        │
        ▼
 ┌──────────────────────────┐      정리된 행 + 리포트       ┌──────────────────────────────┐
 │ 브라우저: 82excel (1차)   │ ───────────────────────────▶ │ 서버: validate(rows, rules)   │
 │ check(file, rules.json)  │  { schema, columns, rows,    │ (2차, 같은 rules.json)         │
 │ 암호·손상·머리글·칸 검사   │    report } (+ 원본 파일)     │ 해시 비교 → 재검사 → 저장/422   │
 └──────────────────────────┘                              └──────────────────────────────┘
```

- **1차(브라우저).** 파일을 열 수 있는지(암호, 손상, 형식), 머리글, 칸 내용(형식, 필수, 범위, 한국형 번호), 행 간 규칙, 수식 인젝션, 개인정보를 검사하고 값을 정리합니다. 사용자는 업로드 전에 어느 칸이 왜 틀렸는지 봅니다.
- **2차(서버).** 서버는 파일을 다시 열지 않고, 브라우저가 보낸 **정리된 행**을 **같은 `rules.json`** 으로 `validate()` 에 다시 넣습니다.
- **브라우저 리포트는 힌트입니다. 서버는 반드시 다시 검사해야 합니다.** 브라우저에서 온 것은 무엇이든 바뀔 수 있습니다. 서버 예제는 [82excel.com/docs/server/](https://82excel.com/docs/server/) 에 있습니다.

- First pass (browser): file problems, headers, cell contents, row rules, formula injection and personal data are checked and values are cleaned before upload.
- Second pass (server): the server does not reopen the file; it runs `validate()` on the cleaned rows with the same `rules.json`.
- **The browser report is a hint. The server must validate again.** See [82excel.com/en/docs/server/](https://82excel.com/en/docs/server/).

## 1. 특징 / Features

- **규칙 하나로 끝.** 열 형식(`number`, `date`, `kr.phone`, `kr.brn` …), 필수·빈칸, 중복, 목록, 정규식, 범위, 행 간 규칙을 순수 JSON으로 적습니다. 같은 파일을 서버와 다른 언어에서도 씁니다.
  One pure-JSON rules file for types, required/empty, unique, enum, pattern, ranges and row rules, shared with the server.
- **값을 정리해서 넘깁니다.** `2024.1.5` → `2024-01-05`, `010 1234 5678` → `010-1234-5678`, `₩12,000` → `12000`. 엑셀 날짜 숫자, 1904 날짜 체계, 앞자리 0이 사라진 전화번호·우편번호도 알아챕니다.
  Values are cleaned to canonical forms; Excel date serials, the 1904 date system and lost leading zeros are handled.
- **한국형 검증기.** 사업자등록번호, 법인등록번호, 주민등록번호, 전화번호, 우편번호, 금액. 정상 번호를 거부하지 않도록 만들었습니다(주민번호 체크섬은 경고, 법인번호 체크섬은 기본으로 끔).
  Korean validators tuned not to reject real numbers.
- **개인정보 마스킹.** 메시지·리포트·오류 목록에는 `900101-1******` 처럼 가려서 넣습니다.
  Personal data is masked in messages and reports.
- **암호·손상 감지, EUC-KR CSV, NFC 머리글.** 0.1의 파일 검사는 그대로입니다.
  The 0.1 file checks are unchanged.
- **가볍습니다.** CDN 파일 gzip 54,588 B(약 53 KB), 함수만 42,059 B(약 41 KB). SheetJS의 약 1/6입니다. [12. 크기](#12-크기--size)
  Light: 54,588 B gzipped for the CDN file, 42,059 B for the functions.
- **파일과 그 내용은 밖으로 나가지 않습니다.** 개발자가 `upload` 를 켰을 때만, 개발자의 서버로 보냅니다.
  Files never leave the browser unless you turn on `upload`, and then only to your server.

## 사용 방식 두 가지 / Two ways to use it

1. **함수만 쓰기.** `check`, `validate`, `readRows`, `makeTemplate` 등을 직접 부릅니다. CDN 전역 `Excel82` 나 `import { … } from '82excel'` 로 쓰고 Node에서도 됩니다.
   Functions only, from the CDN global `Excel82`, from `import … from '82excel'`, or in Node.
2. **끌어다 놓는 폼 컴포넌트 `<excel-82>`.** `<excel-82 rules="/rules.json" upload="/api/orders">` 한 줄로 검사, 요약, 오류 칸 미리보기, 오류 목록 내려받기, 업로드까지 합니다.
   The drop-in form component `<excel-82>`.

## 2. CDN으로 시작하기 / Quick start (CDN)

```html
<script src="https://cdn.jsdelivr.net/npm/82excel@0.2.0/dist/cdn/82excel.min.js"></script>

<form action="/orders" method="post">
  <excel-82 name="excel" required rules="/rules.json" upload="/api/orders"
            error-report="xlsx" template-download="xlsx" locale="ko"></excel-82>
  <button>제출</button>
</form>
```

운영에서는 버전을 고정하세요. 대안 주소: `https://82excel.com/cdn/v0.2.0/82excel.min.js`. ESM 빌드는 `dist/cdn/82excel.esm.js`, 오류 표시 xlsx 청크는 `dist/cdn/82excel.report-xlsx.min.js`(필요할 때 컴포넌트가 불러옴)입니다.
Pin the version in production. Alternative URL: `https://82excel.com/cdn/v0.2.0/82excel.min.js`. The xlsx error-report chunk is loaded by the component when needed.

```js
// 함수만 / functions only
const rules = await (await fetch('/rules.json')).json();
const r = await Excel82.check(file, { ...rules, readRows: true });
console.log(r.ok, r.summary, r.rows);
```

## 3. npm으로 시작하기 / Quick start (npm)

```bash
npm i 82excel
```

```js
import { check, validate, defineRules } from '82excel'; // 함수만. DOM을 쓰지 않아 Node에서도 됩니다
import '82excel/element';                                // <excel-82> 등록(브라우저)
import '82excel/locales/ko';                             // 한국어 메시지
```

| 진입점 / entry | 내용 / contents |
|---|---|
| `82excel` | `inspect`, `check`, `readRows`, `rulesFromTemplate`, `makeTemplate`, `makeErrorReport`, `validate`, `defineRules`, `rulesHash`, `sha256`, `registerLocale`, `VERSION`. Node 사용 가능 / works in Node |
| `82excel/element` | `<excel-82>`. import하면 등록됩니다(여러 번 import해도 한 번). 다른 태그는 `defineExcelElement('my-excel')`. 한국어 메시지 포함 |
| `82excel/kr` | 한국형 검증기 `isBrn`, `isCrn`, `isRrn`, `isPhone`, `isZip`, `isAmount` 와 `normalizeKr*` |
| `82excel/schema` | `toJSONSchema(rules)` (Node·빌드 도구용 / for Node and tooling) |
| `82excel/report-xlsx` | `makeErrorReportXlsx(result, rows, opts)` — 오류 칸을 표시한 xlsx (별도 청크 / separate chunk) |
| `82excel/rules-1.schema.json` | 규칙 파일의 JSON Schema(`https://82excel.com/schema/rules-1.json` 과 같음) |
| `82excel/locales/ko` | 한국어 메시지 / Korean messages |
| `82excel/cdn/*` | CDN 빌드 / CDN builds |

ESM 전용입니다. 영어가 기본 메시지이고, throw하는 오류는 `82excel: ` 로 시작합니다. CDN 전역 `Excel82` 에는 위 함수와 `Excel82.kr`, `defineExcelElement` 가 들어 있습니다.
ESM only. English messages by default; thrown errors start with `82excel: `. The CDN global `Excel82` has the functions above plus `Excel82.kr` and `defineExcelElement`.

## 4. 규칙 형식 / Rules format

규칙은 **순수 JSON**입니다. 정규식은 문자열로 쓰고 함수는 넣지 않습니다(JS 훅은 따로, [4.8](#48-행-간-규칙--row-rules)). 그래서 해시를 계산하고, 서버와 공유하고, 다른 언어로 옮길 수 있습니다. `columns` 가 **배열**이면 0.2 규칙이고, 없거나 숫자면 0.1 규칙(`headers`, 열 수)입니다. `headers` 와 `columns[]` 를 함께 주면 `82excel: use either headers or columns[]` 로 throw합니다.
Rules are pure JSON (patterns as strings, no functions). A `columns` **array** makes them 0.2 rules; without it the 0.1 rules (`headers`, a column count) apply. Giving both `headers` and `columns[]` throws.

```jsonc
{
  "$schema": "https://82excel.com/schema/rules-1.json",
  "id": "orders-upload", "version": "3",
  "columns": [
    { "key": "orderNo", "header": "주문번호", "aliases": ["주문 번호", "OrderNo"],
      "nullable": false, "unique": true, "pattern": "^[A-Z]{2}\\d{6}$",
      "message": "주문번호는 AB123456 형식입니다" },
    { "key": "kind",   "header": "구분",       "enum": ["개인", "법인"] },
    { "key": "crn",    "header": "법인등록번호", "type": "kr.crn", "required": false },
    { "key": "phone",  "header": "전화번호",    "type": "kr.phone", "phone": { "kind": "mobile" } },
    { "key": "amount", "header": "금액",        "type": "kr.amount", "min": 0 },
    { "key": "start",  "header": "시작일",      "type": "date" },
    { "key": "end",    "header": "종료일",      "type": "date" }
  ],
  "extraColumns": "warn",
  "rows": { "min": 1, "max": 10000, "blank": "skip", "totals": "warn" },
  "rowRules": [
    { "when": { "col": "kind", "eq": "법인" }, "require": ["crn"] },
    { "compare": ["start", "<=", "end"] },
    { "uniqueWith": ["start", "orderNo"] }
  ],
  "formulaInjection": "warn",
  "pii": { "detect": ["rrn", "card", "phone", "email"], "mask": true }
}
```

`defineRules(json)` 가 형식을 검사하고 기본값을 채운 뒤 얼린(frozen) 객체를 돌려줍니다. 모르는 키, 잘못된 type, 컴파일되지 않는 정규식, `min > max` 같은 모순, `rowRules` 가 없는 열을 가리키는 경우는 `82excel: …` 로 throw합니다. `check`, `readRows`, `validate`, `makeTemplate` 은 JSON을 그대로 받아도 되고 `defineRules()` 결과를 받아도 됩니다.
`defineRules(json)` validates the rules, fills defaults and returns a frozen object; unknown keys, bad types, invalid patterns, contradictions and row rules naming unknown columns throw. Every function accepts the JSON or the `defineRules()` result.

### 4.1 최상위 키와 기본값 / Top-level keys and defaults

| 키 / key | 기본값 / default | 설명 / meaning |
|---|---|---|
| `$schema`, `id`, `version` | — | 규칙 이름표. `id`·`version` 은 리포트와 업로드 본문에 들어갑니다 / labels, copied into the report |
| `columns` | (필수 / required) | 열 규칙 배열 [4.2](#42-열-규칙--column-rules) |
| `columnsOrder` | `"any"` | `"strict"` 면 찾은 열이 규칙 순서여야 함(`HEADER_ORDER`) / found columns must be in rule order |
| `extraColumns` | `"ignore"` | 규칙에 없는 열: `ignore` / `warn` / `error` (`COLUMN_EXTRA`) |
| `rows.min`, `rows.max` | — | 결과 행 수(빈 행·제외한 합계 행 빼고). `check()` 와 `validate()` 모두 검사(`ROWS_TOO_FEW`/`ROWS_TOO_MANY`) / result rows, after blank and ignored totals rows; checked by `check()` and `validate()` alike |
| `rows.blank` | `"skip"` | 중간 빈 행: `skip` / `warn` / `error` / `stop` [4.9](#49-빈-행과-합계-행--blank-and-totals-rows) |
| `rows.totals` | `"warn"` | 마지막 합계 행: `warn` / `ignore` / `error` |
| `rowRules` | `[]` | 행 간 규칙 [4.8](#48-행-간-규칙--row-rules) |
| `formulaInjection` | `"warn"` | `warn` / `error` / `escape` / `off` [4.10](#410-수식-인젝션--formula-injection) |
| `pii.detect` | `["rrn"]` | 자유 텍스트에서 찾을 개인정보: `rrn`, `card`, `phone`, `email` |
| `pii.mask` | `true` | 메시지·리포트의 개인정보를 가림 / mask personal data in messages and reports |
| `normalize.trim` / `.invisible` / `.fullwidthDigits` | `true` | `text` 열의 정리 스위치 [4.4](#44-값-정리--cleanup) |
| `messages` | — | 코드별 문구 덮어쓰기 `{ "CELL_TYPE": "…" }` [4.12](#412-메시지--messages) |
| `sheet`, `headerRow` | 첫 시트, `1` | 0.1과 같음 / as in 0.1 |
| `accept`, `maxSize`, `maxUncompressedSize`, `maxCells`, `delimiter` | 0.1과 같음 | 파일 단위 옵션 / file options ([5.3](#53-공통-옵션--shared-options)) |
| `failOn` | `[]` | 오류로 올릴 경고 코드. `check()` 와 `validate()` 가 같게 적용 / warning codes promoted to errors, by `check()` and `validate()` alike |
| `readRows`, `previewRows`, `locale`, `telemetry` | — | 한 번의 실행만 바꾸는 키. **해시에서 빠집니다** / runtime-only, left out of the hash |

### 4.2 열 규칙 / Column rules

| 키 / key | 기본값 / default | 설명 / meaning |
|---|---|---|
| `key` | (필수) | 결과 행 객체의 키. `$` 로 시작할 수 없고(`$row` 예약), `__proto__`·`constructor`·`prototype` 금지 / row key; no `$` prefix, no prototype names |
| `header` | `key` | 머리글. 공백·대소문자·NFKC 차이를 무시하고 비교 / compared loosely |
| `aliases` | `[]` | 같은 열로 볼 다른 머리글 / alternative headers |
| `required` | `true` | 열이 파일에 있어야 함(`COLUMN_MISSING`). `false` 면 없을 때 값은 `null` / the column must exist |
| `nullable` | `true` | 빈칸 허용. `false` 면 빈칸이 `CELL_REQUIRED` / empty cells allowed |
| `type` | `"text"` | [4.3](#43-형식과-정규형--types-and-canonical-forms) |
| `unique` | `false` | 열 안에서 중복 금지(`CELL_UNIQUE`, 두 번째부터) / no duplicates; repeats are flagged |
| `enum` | — | 허용 값 목록. 정리된 값과 **엄격 비교**(`1` ≠ `"1"`) / strict equality on the cleaned value |
| `pattern` | — | ECMAScript 정규식 문자열, **`u` 플래그**로 컴파일. 정리된 문자열 값에 `test()`(고정하려면 `^…$`) / compiled with the `u` flag, tested anywhere unless anchored |
| `min`, `max` | — | 숫자 값의 범위(`CELL_RANGE`) / numeric values only |
| `minLength`, `maxLength` | —, `32767` | 문자열 길이(UTF-16 단위). `maxLength` 를 넘는 값에는 정규식을 돌리지 않음(ReDoS 완화) / longer values skip the pattern |
| `date` | `{ formats: 기본 목록, pivot: 50 }` | [4.5](#45-날짜--dates) |
| `phone.kind` | `"any"` | `mobile` / `landline` / `any` |
| `boolean` | `{ true: ["Y","O","예","TRUE"], false: ["N","X","아니오","FALSE"] }` | 대소문자 무시. 양쪽에 있으면 true / case-insensitive |
| `rrn.checksum` | `"warn"` | `off` 면 `RRN_CHECKSUM` 경고를 끔 |
| `crn.checksum` | `"off"` | `warn` 이면 `CRN_CHECKSUM` 경고를 켬 |
| `severity` | `"error"` | 이 열의 규칙 위반 등급: `error` / `warning` |
| `pii` | — | `allow`: 이 열에서 개인정보 감지 안 함 / `forbid`: 감지되면 오류(`PII_FORBIDDEN`) |
| `message` | — | 이 열의 칸 오류 문구 / message for this column's cell issues |

한 칸은 **빈칸 → 형식 → enum → pattern → 범위 → 길이 → 중복** 순서로 검사합니다. 형식이 틀리면 그 칸의 나머지 검사는 하지 않습니다. pattern·길이는 문자열 값에, min·max는 숫자 값에만 적용합니다.
Per cell: empty → type → enum → pattern → range → length → unique. A type failure stops the rest. Pattern and length apply to strings, min/max to numbers.

### 4.3 형식과 정규형 / Types and canonical forms

정리된 값(정규형)은 다시 넣어도 항상 통과하고, 정리는 여러 번 해도 같습니다(멱등). 서버는 정리된 값을 받습니다.
The cleaned (canonical) value always passes again; cleaning is idempotent.

| type | 받는 값 / accepts | 정규형 / canonical | 오류 코드 |
|---|---|---|---|
| `text` | 아무 문자열. 숫자 칸은 문자열로(`00000` 서식이면 앞자리 0 포함), 불리언은 `TRUE`/`FALSE` | 문자열 | `CELL_TYPE` |
| `number` | 숫자, `1,234.5`, `1.`, `1e3`, 전각 숫자. `0,123`·`1천` 은 거부 | JS 숫자 | `CELL_TYPE` |
| `integer` | `number` 중 정수, ±2^53−1 이내 | JS 숫자 | `CELL_TYPE` |
| `date` | `YYYY-MM-DD`, 기본 형식 목록, 엑셀 날짜 칸 | `YYYY-MM-DD` | `CELL_DATE` |
| `boolean` | `boolean` 표의 값, 불리언 칸 | `true` / `false` | `CELL_TYPE` |
| `email` | `a@b.c` 모양(대소문자 유지) | 문자열 | `CELL_TYPE` |
| `kr.phone` | `01012345678`, `010 1234 5678`, `+82 10-1234-5678`, `(02)123-4567` | `010-1234-5678`, `02-123-4567`, `1588-1234` | `KR_PHONE`, `LEADING_ZERO_LOSS` |
| `kr.brn` | 10자리(`-`·공백·`.` 구분 가능), 검증 숫자 일치 | `123-45-67890` | `KR_BRN` |
| `kr.crn` | 13자리 | `110111-1234567` | `KR_CRN`, `CRN_CHECKSUM`(경고) |
| `kr.rrn` | 13자리, 실제 생년월일, 7번째 자리 0–9 | `YYMMDD-NNNNNNN` | `KR_RRN`, `RRN_CHECKSUM`(경고), `LEADING_ZERO_LOSS` |
| `kr.zip` | 5자리 | `01234`(5자리 문자열) | `KR_ZIP`, `LEADING_ZERO_LOSS` |
| `kr.amount` | `12,000`, `12000원`, `₩12,000`, `KRW 12000`, `(1,000)`·`△1,000`·`▲1,000`·`-1,000`(음수), `+1000` | JS 숫자 | `KR_AMOUNT` |

- 빈칸은 `null`, 빈 문자열, 정리 후 빈 문자열입니다. 숫자 `0` 과 `false` 는 빈칸이 아닙니다. 결과 행에서 빈칸은 `null` 입니다.
  Empty = `null`, missing, or `''` after cleanup; `0` and `false` are not empty. Empty cells are `null` in result rows.
- 형식이 틀린 칸은 결과 행에 **원래 값**을, 형식은 맞고 규칙(enum 등)이 틀린 칸은 **정리된 값**을 둡니다.
  A cell failing its type keeps its raw input in the row; one failing a later rule keeps its cleaned value.
- `integer`·`kr.amount` 는 ±2^53−1(Number.MAX_SAFE_INTEGER)을 넘으면 거부합니다. Values beyond the safe-integer range are rejected.

### 4.4 값 정리 / Cleanup

- **형식이 있는 열**(`number`, `integer`, `date`, `boolean`, `email`, `kr.*`)은 `normalize` 설정과 관계없이 **항상** 앞뒤 공백, 보이지 않는 문자(제로폭 문자, BOM, NBSP), 전각 숫자·기호(`０-９ ， ． － ＋ （ ）`)를 처리한 뒤 해석합니다.
  Typed columns always tolerate surrounding whitespace, invisible characters and full-width digits, whatever `normalize` says.
- **`normalize` 스위치는 `text` 열의 결과에만 적용됩니다.** `trim`(앞뒤 공백, U+3000 포함), `invisible`(제로폭 문자·BOM 제거, NBSP → 공백).
  The `normalize` switches govern `text` output only.
- **`text` 열은 전각 숫자를 그대로 둡니다.** 사용자가 입력한 글(`１호점`)을 바꾸지 않기 위해서입니다. 그래서 `fullwidthDigits` 스위치는 현재 결과를 바꾸지 않습니다(규칙 호환을 위해 받기만 합니다). 한글에는 NFKC를 쓰지 않습니다.
  `text` keeps full-width characters as typed, so `fullwidthDigits` currently changes nothing. No NFKC on text.

### 4.5 날짜 / Dates

- **결과는 항상 `YYYY-MM-DD`.** 정규형 `YYYY-MM-DD` 는 `formats` 와 관계없이 항상 받습니다. 연도는 0001–9999, 윤년은 그레고리력 규칙입니다.
  Output is always `YYYY-MM-DD`; the canonical form is always accepted. Years 0001–9999.
- **토큰 문법.** `YYYY`(4자리), `YY`(2자리, `pivot` 미만이면 20YY, 아니면 19YY; 기본 50), `MM`·`DD`(정확히 2자리), `M`·`D`(1–2자리). 그 밖에 쓸 수 있는 글자는 `.` `-` `/` 공백 `년` `월` `일` 입니다. 연·월·일 토큰이 하나씩 있어야 합니다.
  Tokens: `YYYY`, `YY` (+ `pivot`, default 50), `MM`/`DD` exactly 2 digits, `M`/`D` 1–2 digits; literals `. - / space 년 월 일`; one year, month and day token each.
- **기본 형식** / default formats: `YYYY-M-D`, `YYYY.M.D`, `YYYY. M. D.`, `YYYY.M.D.`, `YYYY/M/D`, `YY-M-D`, `YYYYMMDD`, `YYYY년 M월 D일`, `YYYY년M월D일`.
- **엑셀 날짜 칸.** 날짜 서식이 있는 숫자 칸은 엑셀 시리얼로 바꿉니다(1899-12-30 기준, 1900-02-29 버그 처리, `date1904` 반영). 시각은 버립니다. 이 변환은 파일을 읽을 때 하므로 서버는 시리얼을 받지 않습니다.
  Date-formatted number cells are Excel serials (1900 leap-year bug and the 1904 system handled), converted when the file is read.
- **날짜 서식이 없는 숫자**(그리고 JSON 숫자)는 숫자 글자로 읽습니다. `20240105` → `2024-01-05`.
  A number without a date format (and any JSON number) is read as its digits.

### 4.6 한국형 검증기 / Korean validators

| type | 규칙 / rule | 주의 / note |
|---|---|---|
| `kr.brn` 사업자등록번호 | 10자리, 가중치 1,3,7,1,3,7,1,3,5 검증 숫자 | 검증 숫자가 맞아도 실제 사업자인지는 국세청 API로 서버에서 확인해야 합니다 / a valid check digit is not proof the business exists |
| `kr.crn` 법인등록번호 | 13자리 형식만(기본) | 검증 공식에 공식 출처가 없어 **체크섬은 기본으로 끕니다**. `crn.checksum: "warn"` 으로 켜도 경고일 뿐입니다 / checksum off by default, never an error |
| `kr.rrn` 주민·외국인등록번호 | 13자리, 7번째 자리로 정한 세기의 실제 생년월일 | **체크섬 불일치는 오류가 아니라 경고 `RRN_CHECKSUM`** 입니다. 2020년 10월부터 새로 받은 번호는 뒷자리가 임의 번호라 예전 검증 공식이 맞지 않기 때문입니다. 미래 날짜의 생년월일도 형식이 맞으면 받습니다 / checksum mismatch is only a warning: numbers issued since October 2020 have random digits. Future birth dates are accepted |
| `kr.phone` 전화번호 | 휴대폰 `010`+4+4(`010` 은 가운데 4자리만), `011/016/017/018/019`+3–4+4; 지역번호 `02`, `031–033`, `041–044`, `051–055`, `061–064`+3–4+4, `070`+4+4, `0502–0508`; 대표번호 `15xx/16xx/18xx`+4(`any` 만) | `+82`·`82` 는 0으로 바꿉니다. `1012345678` 처럼 0만 붙이면 맞는 숫자는 `LEADING_ZERO_LOSS`(셀 서식을 텍스트로 바꾸라는 안내) |
| `kr.zip` 우편번호 | 5자리(2015-08부터) | 6자리 옛 번호(`123-456`)는 전용 메시지, 4자리는 `LEADING_ZERO_LOSS` |
| `kr.amount` 금액 | 위 표 / see table | `1천원`, `-` 하나만, 부호 두 개는 거부 |

`82excel/kr`(CDN은 `Excel82.kr`)로 함수만 쓸 수도 있습니다: `isBrn(s)`, `isCrn(s)`, `isRrn(s)`(형식·생년월일만, 체크섬 무시), `isPhone(s, kind?)`, `isZip(s)`, `isAmount(s)`, 그리고 정리된 값과 코드를 돌려주는 `normalizeKrBrn` 등.
The validators are also exported from `82excel/kr` (`Excel82.kr` in the CDN build).

### 4.7 열 찾기 / Column mapping

- 머리글 행에서 `header` 와 `aliases` 를 공백·대소문자·NFKC 차이 없이 비교합니다. 결과 행의 키는 머리글 원문이 아니라 **`key`** 입니다.
  Headers and aliases are compared loosely; result rows are keyed by `key`.
- `required: true` 인 열이 없으면 `COLUMN_MISSING`(오류), 같은 열이 두 번 있으면 `COLUMN_DUPLICATE`(첫 번째를 씀), 규칙에 없는 열은 `extraColumns` 에 따라 `COLUMN_EXTRA` 입니다.
- `check()` 결과의 `columns` 는 `[{ key, source, col }]`(찾은 머리글 원문과 열 글자)입니다.

### 4.8 행 간 규칙 / Row rules

| 규칙 / rule | 뜻 / meaning | 코드 / code (key) |
|---|---|---|
| `{ "when": { "col": "kind", "eq": "법인" }, "require": ["crn"] }` | 조건이 맞으면 필수 / conditional required | `ROW_REQUIRE` (필수 열) |
| `{ "when": { … }, "forbid": ["x"] }` | 조건이 맞으면 비워야 함 / conditional forbidden | `ROW_FORBID` (금지 열) |
| `{ "compare": ["start", "<=", "end"] }` | 두 열 비교 `<` `<=` `=` `!=` `>=` `>` (날짜·숫자) | `ROW_COMPARE` (왼쪽 열) |
| `{ "uniqueWith": ["date", "orderNo"] }` | 열 조합 중복 금지 / unique combination | `ROW_UNIQUE_WITH` (첫 열) |

- `when` 조건은 `eq`, `neq`, `in`, `empty: true`, `notEmpty: true` 중 하나입니다. 정리된 값과 엄격 비교하며, `empty` 말고는 빈칸과 맞지 않습니다.
- `compare` 는 두 칸 중 하나라도 비었거나 형식이 틀리면 건너뜁니다. `uniqueWith` 는 빈칸이 있는 행을 건너뜁니다.
- 각 규칙에 `severity`, `message` 를 줄 수 있습니다. 규칙이 가리키는 열은 `columns` 에 있어야 합니다(오타 방지).
- **JS 훅**: `validate(rows, rules, { rowRules: [(row) => [...issues]] })`. 규칙 JSON에 들어가지 않으므로 **서버에서도 같은 훅을 직접 구현해야 합니다.**
  `when` takes one of `eq`/`neq`/`in`/`empty`/`notEmpty` (strict equality; empty cells only match `empty`). `compare` skips empty or invalid cells. JS hooks live outside the JSON — the server must implement the same hook itself.

### 4.9 빈 행과 합계 행 / Blank and totals rows

- `rows.blank`: `skip`(기본, 결과에서 뺌), `warn`·`error`(`BLANK_ROW`), `stop`(첫 빈 행에서 멈추고 그 뒤 행은 읽지 않음). 어느 정책이든 빈 행은 결과 행이 아니며, 마지막 데이터 행 뒤의 빈 행은 조용히 버립니다.
  Blank rows are never result rows; trailing blank rows are dropped silently.
- `rows.totals`: 마지막 데이터 행의 첫 값이 `합계`·`소계`·`총계`·`Total` 로 시작하거나 모든 칸이 수식이면 합계 행입니다. `warn`(기본) / `error`(`TOTALS_ROW`) / `ignore`(결과에서 뺌). **파일이 있어야 알 수 있으므로 브라우저(`check`, `readRows`)에서만** 검사합니다.
  Totals rows are detected from the file only (browser side).

### 4.10 수식 인젝션 / Formula injection

- `text`·`email` 열에서 `=` `+` `-` `@`, 탭, CR로 시작하는 문자열입니다. xlsx의 텍스트 열에 있는 실제 수식 칸도 대상입니다(브라우저만).
- **오탐 방지.** 숫자·전화번호·금액으로 읽히는 값(`-1000`, `+82 10-1234-5678`, `-₩5,000`)과 이미 `'` 로 시작하는 값은 제외합니다.
- `warn`(기본, 경고) / `error`(오류) / `escape`(결과 값 앞에 `'` 를 붙임. `'=…` 는 정규형이라 서버에서도 통과하고, 중복·행 규칙은 `'` 없이 비교) / `off`.
- **`HYPERLINK(`, `DDE(`, `cmd|`, `IMPORTXML(`, `IMPORTDATA(` 가 들어 있으면 설정과 관계없이(`off` 라도) 오류입니다.**

Strings starting with `= + - @`, tab or CR in `text`/`email` columns, except values that read as numbers, phones or amounts. Dangerous payloads are always errors, even under `off`.

### 4.11 개인정보 / Personal data

- **감지**(`pii.detect`, `text` 열만): `rrn`(형식+생년월일, 체크섬 없이 — 2020년 이후 번호도 잡음), `card`(13–19자리 Luhn), `phone`(휴대폰), `email`. 기본은 `["rrn"]` 입니다. 열마다 `PERSONAL_DATA` 경고 하나(첫 행에, 개수와 행 목록 포함).
- 열 설정: `pii: "allow"` 는 그 열에서 감지하지 않고, `pii: "forbid"` 는 감지되면 오류 `PII_FORBIDDEN` 입니다. `kr.rrn` 처럼 그 정보를 받기로 한 형식의 열은 감지 대상이 아닙니다.
- **마스킹**(`pii.mask: true` 기본): 주민번호 `900101-1******`, 카드 `1234-****-****-5678`, 전화 `010-****-5678`, 이메일 `a***@b.com`.
  - 대상 값: `kr.rrn`·`kr.phone`·`email` 열의 값은 항상, 다른 열은 감지된 부분.
  - `kr.phone`·`email` 열은 형식 마스킹 뒤에 감지 마스킹을 한 번 더 합니다. 전화번호 열에 주민번호를 잘못 넣어도 `900101-1******` 로 가려집니다.
  - 가리는 곳: 오류 메시지, issue의 `raw`·`value`, `summary` 예시, `result.report`, CSV·xlsx 오류 리포트, 컴포넌트의 오류 칸 미리보기와 행 미리보기.
  - **`result.rows` 는 가리지 않습니다.** 서버로 보낼 데이터이기 때문입니다.
- 16자리 이상인 숫자 칸(카드번호가 숫자로 저장된 경우 등)은 `PRECISION_LOSS` 경고가 납니다(파일 검사).
- 감지는 칸 길이에 비례하는 시간(선형)에 끝납니다. `maxLength` 를 넘는 값은 이미 `CELL_LENGTH` 이므로 감지하지 않습니다(보고하는 `raw` 는 그래도 가립니다).

Detection runs on `text` columns for the kinds in `pii.detect` (default `["rrn"]`), in linear time; values over `maxLength` are not scanned. `kr.phone`/`email` values get the type mask and then the scan (an RRN typed into a phone column is masked as an RRN). Masked: messages, `raw`/`value`, summary examples, the report, CSV/xlsx error reports and the component's previews. **`result.rows` is never masked** — it is the data you upload.

### 4.12 메시지 / Messages

- 칸 오류 코드는 고정된 대문자 상수입니다([9. 오류 코드표](#9-오류-코드표--error-codes)). 영어가 기본이고 `locale: 'ko'` 로 한국어가 됩니다.
- 덮어쓰기: 열의 `message` 는 그 열의 칸 오류에만, `messages: { CELL_TYPE: '…' }`(규칙 또는 옵션)는 모든 0.2 코드에 적용됩니다. 열의 `message` 가 우선합니다. 문구 안의 `{row}`, `{col}`, `{header}`, `{raw}`, `{min}` 등은 값으로 바뀝니다.
- 다른 언어는 `registerLocale(name, messages)` 로 더합니다.

Column `message` applies to that column's cell issues; `messages` applies to every 0.2 code and is overridden by a column `message`. `{row}`, `{header}`, `{raw}` … are filled in.

### 4.13 해시 / Hash

`rulesHash(rules)` 는 키를 정렬한 정규화 JSON(공백 없음)의 SHA-256, `sha256:<hex>` 입니다. `defineRules()` 결과는 기본값을 채우기 전의 원래 JSON으로 계산합니다. **`readRows`, `previewRows`, `locale`, `telemetry` 는 해시에서 뺍니다**(한 번의 실행 옵션일 뿐이라서). 그래서 브라우저 리포트의 `schemaHash` 와 서버가 자기 `rules.json` 으로 계산한 값이 같습니다. 브라우저는 `crypto.subtle`, 없으면(http 페이지) 내장 JS 구현을 씁니다.
`rulesHash(rules)` is the SHA-256 of canonical JSON (sorted keys). The runtime-only keys `readRows`, `previewRows`, `locale`, `telemetry` are left out, so the browser's `report.schemaHash` equals the server's hash of its own `rules.json`.

## 5. 함수 / Functions

입력은 `File | Blob | ArrayBuffer | Uint8Array`(Node의 `Buffer` 포함)입니다. 파일 경로 문자열은 받지 않습니다.
Input is `File | Blob | ArrayBuffer | Uint8Array` (Node `Buffer` included); no file paths.

### 5.1 0.2 함수 / 0.2 functions

- **`check(file, rules)`** — 0.2 규칙이면 `{ ok, errors, warnings, info, summary, report, columns, rows? }` 입니다.
  - `errors`/`warnings`: 파일 문제, 머리글 문제, 칸 문제. 칸 문제는 `{ code, severity, message, row, col, key, header, rule, raw, value? }`(`header` 는 규칙의 머리글)이고 `row` 는 **엑셀에서 보이는 행 번호**(머리글 포함, 1부터)입니다.
  - `summary`: `[{ key, col, header, code, severity, count, examples: [{ row, raw }] }]`(칸 문제만, 예시 최대 3개). 예: "C열(금액) 숫자 아님 120건 — 3행 '1천원', 7행 '-'".
  - `report`: 서버에 보낼 리포트 `{ schemaId, schemaVersion, schemaHash, lib, file: { name, size, sha256, sheet, format }, header: { row, columns }, stats: { rows, errors, warnings }, issues, checkedAt }`. **힌트일 뿐 증명이 아닙니다.**
  - `rows`(`readRows: true` 일 때): 정리된 행 `{ $row, key: 값… }`. 마스킹하지 않습니다.
  - 파일 단위 검사(H): 텍스트 계열 열의 숫자 칸 `LEADING_ZERO_LOSS`, 16자리 이상 숫자 `PRECISION_LOSS`, CSV의 `1.2E+10` 모양 `SCIENTIFIC_NOTATION`, 합계 행, 실제 수식 칸. 0.1 경고(숨김·매크로·병합 등)도 그대로입니다.
- **`validate(rows, rules, opts?)`** — `{ ok, errors, warnings, summary, rows }`. 행은 키가 `key` 인 JSON 객체 배열이고 원래 값도 정리된 값도 받습니다. 동기·순수 함수이고 브라우저와 Node에서 같은 결과입니다(`check()` 가 칸 검사에 쓰는 코드 그대로). 행 번호는 `$row`(1 이상 정수)이고, 없으면 `index + headerRow + 1` 입니다. 규칙에 없는 키는 무시합니다. `opts`: `locale`, `messages`, `columnLetters`(`{ key: 'C' }` → issue의 `col`), `rowRules`(JS 훅). `rows.min/max`(결과 행 수, `ROWS_TOO_FEW`/`ROWS_TOO_MANY` 는 머리글 행 번호에 key 없이)와 `failOn` 은 `check()` 와 똑같이 적용합니다. 엑셀 오류 값과 같은 문자열(`#REF!`, `#N/A` …)은 어느 형식 열에서든 `CELL_TYPE` 입니다(브라우저에서 오류 칸이었던 값). 파일이 있어야 하는 검사(H, 합계 행, 수식 칸, 열 찾기)는 하지 않습니다. 걸리는 시간은 입력 크기에 비례합니다(선형).
- **`readRows(file, { rules, raw?, messages?, … })`** — `messages` 는 규칙의 `messages` 에 코드별로 덧씌웁니다. 0.2 규칙을 주면 `{ rows, columns, issues }` 의 `rows` 는 `key` 기준의 정리된 행이고 `columns` 는 찾은 열의 key입니다. `raw: true` 면 정리 전 값(`check()` 가 `validate()` 에 넘기는 값, 날짜 칸은 이미 ISO)을 돌려줍니다.
- **`makeTemplate(rules, 'xlsx' | 'csv', { example?, sheetName?, locale?, now? })`** — 규칙으로 양식을 만듭니다. xlsx: 머리글 메모(예: "필수 · 숫자 · 0–100"), enum 열 드롭다운(1,000행), 텍스트 계열 열은 텍스트 서식(`@`), 날짜 열은 날짜 서식. 긴 목록(255자 초과나 값에 쉼표)은 두 번째 시트(`Lists`/`목록`, 보이는 시트)에 둡니다. `example: true` 면 2행에 규칙을 통과하는 가짜 예시를 넣습니다. **예시는 열 규칙만 따르고 `rowRules` 는 보지 않으며, `enum` 없이 `pattern` 만 있는 열과 선택(nullable) 주민번호 열은 비워 둡니다.** CSV는 머리글과 예시만. 0.1 시그니처 `makeTemplate(headers[], fmt, sheetName?)` 도 그대로입니다.
- **`makeErrorReport(result, 'csv', { locale? })`** — UTF-8 BOM이 붙은 CSV `Blob`. 열: 행, 열, 머리글, 원래 값, 메시지, 코드, 등급. 오류 다음 경고 순서이고, 수식으로 실행될 수 있는 글자는 `'` 를 붙입니다. 값은 이미 마스킹되어 있습니다.
- **`makeErrorReportXlsx(result, rows, { rules?, locale?, sheetName? })`**(`82excel/report-xlsx`) — 데이터 시트에서 오류 칸은 빨간색, 경고 칸은 노란색에 메모를 달고, "오류" 시트를 덧붙인 xlsx `Blob`(Promise). `rules` 를 주면 데이터 값도 같은 규칙으로 마스킹합니다(주지 않으면 주민번호로 보이는 값만). 원본 서식은 보존하지 않습니다. CDN에서는 `82excel.report-xlsx.min.js`(전역 `Excel82ReportXlsx`)이며, 이 파일의 시트·열 이름은 영어와 한국어만 있습니다.
- **`toJSONSchema(rules)`**(`82excel/schema`) — **정리된 행** 하나의 JSON Schema(draft 2020-12). `kr.amount` 는 number, 날짜는 `format: "date"`, `kr.*` 는 `format: "kr-brn"` 등과 정규형 `pattern`, 사용자 `pattern` 은 `allOf` 로 함께. `nullable` 열은 `type: [t, "null"]`, `nullable: false` 열만 `required`. `minimum`/`maximum` 은 숫자 형식에만. `title` 은 `id`.
- **Standard Schema** — `defineRules(json)['~standard'].validate(row)` 는 [Standard Schema v1](https://standard-schema.dev)(행 하나, 동기)입니다. 성공이면 `{ value: 정리된 행 }`, 실패면 `{ issues: [{ message, path: [key] }] }`.
- **`rulesHash(rules)`** → `Promise<'sha256:…'>`, **`sha256(bytes)`** → `Promise<hex>` — 서버가 `report.schemaHash` 와 `report.file.sha256` 을 비교할 때 씁니다.
- **`defineRules(json)`** — [4](#4-규칙-형식--rules-format).

- `check(file, rules)` with 0.2 rules adds `summary`, `report` and `columns`; issues carry `row` (as Excel shows it), `col`, `key`, `header` (from the rules), `rule`, `raw`, `value`. `report` is a hint for the server, not proof. `rows` (with `readRows: true`) are cleaned and never masked.
- `validate(rows, rules, opts?)` is the server-side engine: key-based JSON rows, raw or cleaned, synchronous, linear-time, the same results as the browser, including `rows.min/max` and `failOn`. Excel error literals (`#REF!` …) are `CELL_TYPE` in every column. It skips the checks that need the file.
- `readRows(file, { rules })` returns cleaned key rows; `raw: true` returns the values before cleanup.
- `makeTemplate(rules, fmt, { example })` builds a rule-aware template; examples ignore `rowRules`, and pattern-only columns and optional RRN columns stay empty.
- `makeErrorReport(result, 'csv')`, `makeErrorReportXlsx(result, rows, { rules })` (masked), `toJSONSchema(rules)` (describes cleaned rows; nullable → `[t, "null"]`), Standard Schema `~standard.validate`, `rulesHash`, `sha256`.

```js
import { check, validate, makeTemplate, makeErrorReport } from '82excel';

const rules = { id: 'orders', columns: [
  { key: 'date', header: '주문일', type: 'date', nullable: false },
  { key: 'phone', header: '전화번호', type: 'kr.phone' },
  { key: 'amount', header: '금액', type: 'kr.amount', min: 0 },
] };

const v = validate([{ date: '2024.1.5', phone: '010 1234 5678', amount: '₩12,000' }], rules);
console.log(v.ok, v.rows);
// true [ { '$row': 2, date: '2024-01-05', phone: '010-1234-5678', amount: 12000 } ]

const bad = validate([{ date: '2024-02-30', phone: '1012345678', amount: '1천원' }], rules);
console.log(bad.errors.map((e) => `${e.code}: ${e.message}`));
// [ 'CELL_DATE: Row 2 (주문일): Date does not exist',
//   'LEADING_ZERO_LOSS: Row 2 (전화번호): Leading zero seems to be lost. Set the cell format to Text and enter it again',
//   'KR_AMOUNT: Row 2 (금액): Invalid amount' ]

const template = makeTemplate(rules, 'xlsx', { example: true, locale: 'ko' });
const csv = makeErrorReport(bad, 'csv');
```

### 5.2 0.1 함수 / 0.1 functions (unchanged)

- **`inspect(file, { headerRow?, previewRows?, locale?, telemetry?, delimiter?, maxUncompressedSize? })`** — 모든 시트를 훑어 `{ file, meta?, csv?, sheets[], definedNames, issues }`. 암호·손상 파일에서도 throw하지 않습니다.
- **`check(file, { headers, columns, rows, … })`** — 0.1 규칙은 0.1과 똑같이 `{ ok, errors, warnings, info, rows? }` 입니다. 머리글은 NFC로 정규화해 순서까지 비교합니다.
- **`readRows(file, { sheet?, headerRow?, … })`** — 규칙 없이 부르면 `{머리글: 값}` 객체(프로토타입 없음)입니다. 같은 머리글은 `이름_2` 처럼 번호를 붙입니다.
- **`rulesFromTemplate(file)`** → `{ headers, columns }`, **`makeTemplate(headers, 'csv' | 'xlsx', sheetName?)`**, `registerLocale(name, messages)`, `VERSION`.

### 5.3 공통 옵션 / Shared options

- `locale` — 메시지 언어(`en` 기본, `ko`). Message language.
- `messages` — 코드별 문구 덮어쓰기(0.2). Per-code message templates.
- `delimiter` — CSV 구분자 `','`, `';'`, `'\t'`. 주지 않으면 자동. CSV delimiter; detected when omitted.
- `maxSize`(`check`, 기본 50 MB) — 파일 크기 상한. `File`/`Blob` 은 읽기 전에 봅니다. File size cap.
- `maxUncompressedSize`(기본 200 MB) — xlsx 압축을 푼 크기 합 상한(`FILE_TOO_LARGE`, 압축 폭탄 방지). Zip-bomb guard.
- `maxCells`(기본 2,000,000) — 데이터 행 수 × 열 너비 상한. 행을 만들기 전에 `FILE_TOO_LARGE` 를 냅니다. Cap on rows × width, checked before rows are built.
- 엑셀 격자 밖의 셀은 무시합니다. Cells outside Excel's grid are ignored.

## 6. 웹 컴포넌트 `<excel-82>` / Web component

### 속성 / Attributes

| 속성 | 설명 |
|---|---|
| `rules` | **0.2.** 규칙 JSON 문자열(`{` 로 시작) 또는 규칙 JSON의 URL. JS로는 `.rules = {...}`(JSON 또는 `defineRules()` 결과). 불러오지 못하면 알림(`RULES_LOAD_FAILED`)을 보이고 검사도 업로드도 하지 않습니다. `.rules` 속성만 대신할 수 있고, `template`·0.1 규칙 속성으로 바꿔 검사하지는 않습니다. `rules` 를 쓰면 0.1 규칙 속성(`headers` 등)은 쓰지 않고 `locale`·`telemetry` 만 합칩니다 |
| `error-report` | **0.2.** `csv` 또는 `xlsx`. "오류 목록 내려받기" 버튼. `xlsx` 는 0.2 규칙 결과에서 오류 칸을 표시한 파일이며 코드를 처음 누를 때 불러옵니다 |
| `report-xlsx-src` | **0.2.** CDN 빌드에서 `82excel.report-xlsx.min.js` 의 주소(직접 올린 경우). 없으면 CDN 파일 옆, 그다음 jsDelivr |
| `upload-report` | **0.2.** `off` 면 업로드에 리포트를 넣지 않음(기본은 넣음) |
| `upload-file` | **0.2.** `include` 면 0.2 업로드에 원본 파일도 함께(multipart) |
| `upload-as` | `json` 또는 `file`. **0.2 규칙이면 기본 `json`**, 0.1 규칙이면 기본 `file` |
| `headers`, `columns`, `min-rows`, `max-rows`, `sheet`, `header-row`, `accept`, `max-size`, `strict-warnings`, `template`, `locale`, `preview-rows`, `template-download`, `name`, `required`, `disabled`, `upload`, `upload-field`, `upload-auto`, `telemetry` | 0.1과 같습니다. `template-download` 는 0.2 규칙이면 규칙으로 만든 양식(드롭다운·서식·메모)을 내려받습니다 |

### 화면 / What it shows

- 0.2 규칙: 열·코드별 **요약 목록**(예: "C열(금액) 금액이 올바르지 않습니다 120건 — 3행 '1천원', 7행 '-'"), 그리고 처음 50개 칸 문제가 있는 행의 **오류 칸 미리보기**(읽기 전용 표, 문제 칸 강조, 메시지는 툴팁). 미리보기의 개인정보는 리포트처럼 가립니다.
- 0.1 규칙: 0.1과 같은 문제 목록과 미리보기.

0.2 rules show a summary list grouped by column and code, and an error preview of the rows with the first 50 cell issues (masked like the reports).

### JS 속성·메서드 / Properties and methods

`.rules`, `.file`, `.result`(check 결과), `.info`, `.state`, `.uploadResponse`, `.check(file)`, `.upload()`, `.reset()`, `.form`, `.validity`, `.validationMessage`, `.checkValidity()`, `.reportValidity()`.

상태: `idle → checking → invalid | valid → uploading → uploaded | uploadfailed`. 0.1과 같이 `required` 인데 파일이 없거나 `invalid`·`uploading`·`uploadfailed` 이면 폼 제출이 막힙니다.

### 이벤트 / Events

`check`(detail: check 결과), `beforeupload`(`{ file, init }`, `init.headers` 에 인증 헤더, `preventDefault()` 로 취소), `uploadprogress`(`{ loaded, total }`), `uploaded`(`{ status, response }`), `uploaderror`(`{ status?, error }`). 모두 bubbles·composed.

### CSS 변수와 `::part()` / CSS variables and parts

`--e82-accent`, `--e82-error`, `--e82-warn`, `--e82-bg`, `--e82-fg`, `--e82-border`, `--e82-radius`(다크 모드 지원).

`::part()`: `drop`, `label`, `choose`, `input`, `summary`, `status`, `issues`, `preview`, `template`, `progress`, `retry`, `notice`, 그리고 0.2의 `summary-list`, `error-preview`, `error-report`.

```css
excel-82::part(error-preview) { max-height: 320px; overflow: auto; }
excel-82::part(error-report) { font-weight: 600; }
```

## 7. 업로드와 폼 / Upload and forms

`upload` 속성을 주면 검사를 통과한 파일을 XHR로 보냅니다(진행률). 쿠키는 같은 출처일 때만 가고, 인증 헤더는 `beforeupload` 에서 넣습니다. 업로드에 성공하면 폼 값은 서버 응답 문자열입니다.

### 0.2 규칙의 업로드 본문 / 0.2 upload payload

**기본(`upload-as="json"`)**, `Content-Type: application/json`:

```json
{
  "fileName": "orders.xlsx",
  "sheet": "Sheet1",
  "schema": { "id": "orders-upload", "version": "3", "hash": "sha256:…" },
  "columns": ["orderNo", "date", "phone", "amount"],
  "rows": [{ "$row": 2, "orderNo": "AB000001", "date": "2024-01-05", "phone": "010-1234-5678", "amount": 12000 }],
  "report": { "schemaHash": "sha256:…", "lib": "82excel@0.2.0", "file": { "sha256": "…" }, "…": "…" }
}
```

- `rows` 는 정리된 값이고 마스킹하지 않습니다. `$row` 는 엑셀 행 번호입니다. `upload-report="off"` 면 `report` 키가 없습니다.
- **`upload-file="include"`**: multipart/form-data, 필드 `file`(이름은 `upload-field`, 기본 `file`), `data`(위 JSON에서 `report` 를 뺀 문자열), `report`(리포트 JSON 문자열). **`data` 에는 `report` 가 들어 있지 않습니다**(따로 보냄).
- **`upload-as="file"`**: multipart, `file` 과 `report` 필드.
- 서버는 `validate(body.rows, rules)` 로 다시 검사합니다. 파일을 함께 받았다면 `sha256(file)` 과 `report.file.sha256` 을 비교할 수 있습니다. [82excel.com/docs/server/](https://82excel.com/docs/server/)

Default for 0.2 rules is JSON `{ fileName, sheet, schema: { id, version, hash }, columns, rows, report }` (rows cleaned, unmasked). `upload-file="include"` sends multipart `file`, `data` (that JSON **without** `report`) and `report`; `upload-as="file"` sends `file` and `report`; `upload-report="off"` drops the report.

### 0.1 규칙 / 0.1 rules (unchanged)

- `upload-as="file"`(기본): multipart, 필드 `upload-field`.
- `upload-as="json"`: `{ fileName, sheet, columns, rows }`. `columns` 는 행 객체의 키와 같은 이름(중복 머리글은 `이름_2`), 머리글 순서입니다.
- `upload-auto="false"`: 폼 제출을 가로채 업로드한 뒤 다시 제출합니다.
- 업로드한 파일이 규칙 변경으로 다시 검사되어도 같은 파일을 두 번 올리지 않습니다(통과면 `uploaded` 유지, 실패면 `invalid`).

## 8. Node 예 / Node example

```js
import { readFile } from 'node:fs/promises';
import { check, readRows } from '82excel';

const bytes = await readFile('./orders.xlsx');                 // Buffer 는 Uint8Array 입니다
const rules = JSON.parse(await readFile('./rules.json', 'utf8'));
const r = await check(bytes, { ...rules, readRows: true });
console.log(r.ok, r.summary, r.rows?.[0]);

const { rows } = await readRows(bytes, { rules });             // 정리된 행 / cleaned rows
```

서버 2차 검수 전체 예(Express)는 [82excel.com/docs/server/](https://82excel.com/docs/server/) 에 있습니다. Node에서는 사용 통계를 보내지 않습니다.
A full Express second-pass example is at [82excel.com/en/docs/server/](https://82excel.com/en/docs/server/). Statistics are never sent from Node.

## 9. 오류 코드표 / Error codes

`{ code, severity, message, …값 }` 입니다. 기본 메시지는 영어이고 `locale: 'ko'` 로 한국어가 됩니다. 칸 문제의 메시지는 "Row 3, column C (금액): …" 처럼 위치가 앞에 붙습니다.
Cell-issue messages are prefixed with the location, e.g. "Row 3, column C (금액): …".

### 파일·머리글 / File and header (0.1)

| 코드 | 종류 | 기본 메시지(en) |
|---|---|---|
| `PASSWORD_PROTECTED` | error | File is password protected |
| `ENCRYPTED_DEFAULT_PASSWORD` | error | File is protected by Excel's built-in password; re-save the file without protection |
| `CORRUPTED` | error | File is corrupted or cannot be read |
| `UNSUPPORTED_FORMAT` | error | Unsupported file format |
| `FILE_TOO_LARGE` | error | File is larger than {max} / File has more than {maxCells} cells |
| `EMPTY_FILE` | error | File has no data |
| `FORMAT_NOT_ALLOWED` | error | {format} files are not allowed |
| `SHEET_NOT_FOUND` | error | Sheet "{name}" not found |
| `COLUMN_COUNT` | error | Expected {expected} columns, found {actual} |
| `HEADER_MISMATCH` | error | Column {col}: expected "{expected}", found "{actual}" |
| `HEADER_ORDER` | error | "{name}" should be column {expected}, found at column {actual} |
| `ROWS_TOO_FEW` / `ROWS_TOO_MANY` | error | Expected at least / at most {n} rows, found {actual} |
| `TEMPLATE_LOAD_FAILED` | error | Could not load template file |
| `RULES_LOAD_FAILED` | 알림 / notice | Could not load the rules — `<excel-82 rules>` 를 불러오지 못함. 검사·업로드하지 않음 / the component neither checks nor uploads |
| `CELL_ERRORS` | warning | {count} cells contain errors (first: {first}) |
| `PERSONAL_DATA` | warning | Possible resident registration numbers in {count} cells (0.2: Possible personal data ({kinds}) in {count} cells) |
| `HAS_MACROS`, `HIDDEN_CONTENT`, `MERGED_HEADER`, `EXTENSION_MISMATCH`, `RAGGED_ROWS`, `DUPLICATE_HEADER` | warning | 0.1과 같음 / as in 0.1 |

### 0.2 열·칸·행 / 0.2 columns, cells and rows

| 코드 | 기본 등급 | 기본 메시지(en) |
|---|---|---|
| `COLUMN_MISSING` | error | Required column "{header}" not found |
| `COLUMN_DUPLICATE` | error | Column "{header}" appears more than once ({cols}); the first one is used |
| `COLUMN_EXTRA` | `extraColumns` | Unexpected column {col} "{header}" |
| `CELL_REQUIRED` | 열 severity | Required value is empty |
| `CELL_TYPE` | 열 severity | Not a number / Not a whole number / Not a yes/no value / Not an email address … |
| `CELL_DATE` | 열 severity | Date does not exist / Date format not recognized (use YYYY-MM-DD) / Number cannot be read as a date / 1900-02-29 never existed |
| `CELL_ENUM` | 열 severity | Not one of the allowed values |
| `CELL_PATTERN` | 열 severity | Does not match the required pattern |
| `CELL_RANGE` | 열 severity | Must be at least {min} / Must be at most {max} |
| `CELL_LENGTH` | 열 severity | Must be at least {minLength} characters (now {length}) / at most {maxLength} |
| `CELL_UNIQUE` | 열 severity | Duplicate of row {firstRow} |
| `KR_BRN` | 열 severity | Invalid business registration number |
| `KR_CRN` | 열 severity | Invalid corporate registration number |
| `CRN_CHECKSUM` | warning | Corporate registration number check digit does not match |
| `KR_RRN` | 열 severity | Invalid resident registration number |
| `RRN_CHECKSUM` | warning | Resident registration number check digit does not match (numbers issued since October 2020 may be valid) |
| `KR_PHONE` | 열 severity | Invalid phone number / Invalid mobile phone number / Invalid landline phone number |
| `KR_ZIP` | 열 severity | Invalid postal code (5 digits) / Old 6-digit postal code; use the 5-digit code |
| `KR_AMOUNT` | 열 severity | Invalid amount |
| `LEADING_ZERO_LOSS` | 열 severity(형식 검사) / warning(파일 검사) | Leading zero seems to be lost. Set the cell format to Text and enter it again |
| `PRECISION_LOSS` | warning | Number has 16 or more digits; Excel keeps only 15, so the end may be lost |
| `SCIENTIFIC_NOTATION` | warning | Converted to scientific notation (e.g. 1.2E+10); the original digits are lost |
| `FORMULA_INJECTION` | `formulaInjection`(위험 패턴은 error) | Value could run as a formula |
| `PII_FORBIDDEN` | error | Contains personal data ({kinds}) |
| `ROW_REQUIRE` | 규칙 severity | Required because of the value in "{when}" |
| `ROW_FORBID` | 규칙 severity | Must be empty because of the value in "{when}" |
| `ROW_COMPARE` | 규칙 severity | Must satisfy {left} {op} {right} |
| `ROW_UNIQUE_WITH` | 규칙 severity | Duplicate combination of {keys} (first at row {firstRow}) |
| `BLANK_ROW` | `rows.blank` | Blank row |
| `TOTALS_ROW` | `rows.totals` | Looks like a totals row |

`failOn: ['PERSONAL_DATA', …]` 로 경고를 오류로 올릴 수 있습니다(0.1과 같음, 0.2 규칙이면 `validate()` 도 같게). `failOn` promotes warnings to errors, in `validate()` too.

## 10. 서버 2차 검수 / Server second pass

```js
import { validate, makeErrorReport, rulesHash } from '82excel';

app.post('/api/orders', express.json({ limit: '20mb' }), async (req, res) => {
  if (req.body.schema?.hash !== await rulesHash(rules)) return res.status(409).end(); // 규칙이 바뀜
  const result = validate(req.body.rows, rules);                                    // 다시 검사
  if (!result.ok) return res.status(422).type('text/csv').send(Buffer.from(await makeErrorReport(result, 'csv').arrayBuffer()));
  await save(result.rows);                                                          // 정리된 행
  res.json({ saved: result.rows.length });
});
```

전체 예제와 실행 결과, 파일을 함께 받을 때의 sha256 비교는 [82excel.com/docs/server/](https://82excel.com/docs/server/) 를 보세요. **브라우저 리포트는 힌트입니다.**
See [82excel.com/en/docs/server/](https://82excel.com/en/docs/server/) for the full example, its output, and the sha256 check when the file is included. **The browser report is a hint.**

## 11. 적합성 테스트와 언어별 SDK / Conformance suite and SDKs

- [`dist/conformance/`](https://cdn.jsdelivr.net/npm/82excel@0.2.0/dist/conformance/) (`npm i 82excel` 후 `node_modules/82excel/dist/conformance/`) 에 `validate(rows, rules)` 의 언어 중립 테스트 476개가 있습니다(규칙, 입력 행, 기대 오류·경고·정리된 행). Node 구현은 100% 통과합니다. 다른 언어로 옮길 때는 이 파일들을 그대로 100% 통과해야 합니다. 비교 규칙과 정해 둔 세부 동작은 그 폴더의 README에 있습니다. 서버에서 돌리는 코드이므로 개인정보 감지와 형식 검사는 반드시 입력 크기에 비례하는 시간(선형)이어야 합니다.
- **서버 SDK: Java, PHP, Python, C#, Go — 준비 중입니다.** 그때까지는 Node `validate()` 를 쓰거나 적합성 테스트를 기준으로 직접 옮기세요.
- `node_modules/82excel/dist/conformance/` (after `npm i 82excel`; browse at https://cdn.jsdelivr.net/npm/82excel@0.2.0/dist/conformance/) holds 476 language-neutral cases for `validate(rows, rules)`; the Node implementation passes all of them, and any port must too. Ports must keep detection and parsing linear-time (they run on untrusted server input).
- **Server SDKs for Java, PHP, Python, C#, Go — 준비 중 (in preparation).**

## 12. 크기 / Size

`pnpm --filter 82excel size` 로 잰 gzip(레벨 9) 크기입니다. Gzipped (level 9), measured with `pnpm --filter 82excel size`.

| 파일 / bundle | gzip | 예산 / budget |
|---|---|---|
| CDN `82excel.min.js`(전역 `Excel82` + `<excel-82>` + 영어·한국어 + `kr`) | 54,588 B | 57,344 B |
| 함수만 `82excel`(+ `fflate`, kr 포함, 압축) | 42,059 B | 45,056 B |
| `82excel.report-xlsx.min.js`(필요할 때만) | 10,865 B | 12,288 B |

## 13. 사용 통계 / Usage statistics

- **보내는 것:** `{ version, url, framework }` 뿐입니다. `url` 은 `origin + pathname`(쿼리·해시 제외), `framework` 는 `vanilla`·`element`·`cdn`. **파일 이름, 크기, 내용, 검사 결과 등 파일에 관한 정보는 보내지 않습니다.**
- 브라우저마다 하루 한 번, 3초 뒤 `https://82excel.com/api/usage` 로 `keepalive` POST(`credentials: 'omit'`). localStorage 키 `e82:usage`.
- **보내지 않는 곳:** Node, localhost와 사설 도메인(`.local`, `.internal`, `.corp` 등), 점 없는 인트라넷 이름, IP 주소, 82excel.com.
- **끄는 법:** `<html data-82excel-telemetry="off">`, `check(file, { telemetry: false })`, `<excel-82 telemetry="off">`.

Sent: only `{ version, url (origin + pathname), framework }`, once a day per browser; nothing about any file. Turn off with `<html data-82excel-telemetry="off">`, `telemetry: false`, or `telemetry="off"`.

## 다음 버전 / Next version

셀 직접 편집 그리드, 열 맞추기 화면, DB 조회 검증, 계좌번호 은행별 규칙, 병합 셀 자동 채우기, Web Worker, 나눠 올리기는 0.2에 없습니다. 언어별 서버 SDK는 별도로 준비합니다.
Not in 0.2: an editable grid, a column-mapping screen, database lookups, bank-specific account rules, merged-cell fill, Web Workers, chunked upload. Server SDKs come separately.

## 링크 / Links

https://82excel.com · [CHANGELOG](https://82excel.com/changelog/) · MIT © secondkorea
