korean-bank-detect 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LESANF
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,320 @@
1
+ # korean-bank-detect
2
+
3
+ > Detect the Korean deposit institution behind a CMS account number — from the account number alone.
4
+
5
+ [![npm](https://img.shields.io/badge/npm-korean--bank--detect-cb3837?style=flat-square)](https://www.npmjs.com/package/korean-bank-detect)
6
+ [![types](https://img.shields.io/badge/TypeScript-ready-3178c6?style=flat-square)](#api)
7
+ [![deps](https://img.shields.io/badge/dependencies-0-success?style=flat-square)](#why)
8
+ [![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](#license)
9
+
10
+ A zero-dependency TypeScript library that maps a Korean bank/securities account
11
+ number to its **candidate deposit institutions**, sourced entirely from the
12
+ [금융결제원 CMS 계좌번호체계](data/source/) reference (금융결제원 = Korea Financial
13
+ Telecommunications & Clearings Institute), rev. **2026.06.01**.
14
+
15
+ 한국 계좌번호를 입력하면 **입금 기관(은행·증권사)** 후보를 판별합니다. 금융결제원 CMS 계좌번호체계 문서를 단일 출처로 삼습니다.
16
+
17
+ ```ts
18
+ import { detect } from 'korean-bank-detect';
19
+
20
+ detect('212-123456-789').candidates[0].institutionName;
21
+ // → '수협중앙회' (resolved by the PDF's branch rule)
22
+ ```
23
+
24
+ ## Why
25
+
26
+ - **Data, not opinion.** Pure JSON in, pure JSON out. No UI, no `score`, no
27
+ `confidence` — just the institution and the document-sourced evidence for it.
28
+ - **Faithful to the source.** Every value traces to the 금결원 PDF. It does **not**
29
+ compute or verify check digits (those algorithms aren't public) — it only
30
+ reports the static flag the PDF itself declares.
31
+ - **Ranks the right bank first.** Uses the PDF's 계정과목코드 (account subject codes)
32
+ as evidence: for a modern 차세대 account, the front 3-digit code identifies the
33
+ bank uniquely, so it surfaces as candidate #1. Show your users the top 1–3.
34
+ - **Recall-first.** The true institution is never dropped from the candidate set,
35
+ even for partial input. Candidates only shrink as you type more digits.
36
+ - **Real branch routing.** Where the document routes one code to another by digit
37
+ value (수협 007 ⇄ 030), that rule is executed, not just described.
38
+ - **Zero runtime dependencies.** Works in Node, browsers, and React Native.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ npm install korean-bank-detect
44
+ # pnpm add korean-bank-detect · yarn add korean-bank-detect · bun add korean-bank-detect
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ### Quick start — bundled registry
50
+
51
+ `detect()` returns a **short, pickable candidate list** (~8 options, ranked) —
52
+ never the whole 30–50 institution registry.
53
+
54
+ ```ts
55
+ import { detect } from 'korean-bank-detect';
56
+
57
+ detect('611-234567-890').candidates[0]; // 하나은행 (unique front 과목코드 611)
58
+ detect('212-123456-789').candidates[0]; // 수협중앙회 (수협 branch rule fired)
59
+ detect('3333-05-1234567').candidates[0]; // 카카오뱅크 (실무 프리픽스 3333 — see below)
60
+
61
+ const [best, ...rest] = detect('611-234567-890').candidates;
62
+ best.institutionName; // '하나은행'
63
+ ```
64
+
65
+ The narrowing model is **evidence-based** — every candidate falls into one of
66
+ three grades:
67
+
68
+ | Grade | Meaning | Treatment |
69
+ |---|---|---|
70
+ | **matched** | its 과목코드/프리픽스/분기규칙 fits these digits | always included, ranked first |
71
+ | **unknown** | no signal capability at this length (length-only institutions) | no information — fills the remaining `limit` budget by rank (only while NOT determined) |
72
+ | **contradicted** | it HAS codes at this exact length, none matched | demoted last — reachable via `limit: Infinity` / `matchAccount`, never silently unreachable (code sets can't be proven exhaustive) |
73
+
74
+ Every candidate exposes its `grade`, so your UI can render the tiers explicitly.
75
+
76
+ The display rules:
77
+
78
+ - **Suggestions gate at 7 digits** (`minDigits`, default 7 — the shortest
79
+ documented account length; below it nothing can be complete, so real apps
80
+ like Toss show nothing either).
81
+ - **DETERMINED — complete input with an exact-length strong signal → matched
82
+ only.** A fully-typed 카카오 account is `[카카오뱅크]`, not 8 rows. While typing
83
+ (before the pattern length is reached) suggestions stay padded.
84
+ - **Only exact-length strong signals determine.** A 14-digit pattern's prefix
85
+ firing while you're 13 digits in boosts ranking but does not suppress
86
+ alternatives — a complete 13-digit account and a 14-digit account in progress
87
+ are indistinguishable.
88
+ - **Strong codes aren't always unique** (`100`(12d) → 우체국·K뱅크·토스). All
89
+ matching institutions stay listed; priority orders them.
90
+
91
+ ### 실무 프리픽스 — internet banks (progressive, like the real apps)
92
+
93
+ The PDF carries no 계정과목코드 for internet banks, so a **practical prefix layer**
94
+ (clearly separated from official data, evidence token `프리픽스`) identifies them —
95
+ and it fires **progressively while typing**, exactly like the KakaoBank app:
96
+
97
+ ```ts
98
+ detect('3333056').candidates[0].institutionName; // '카카오뱅크' — from the 7th digit
99
+ detect('3333', { minDigits: 4 }).candidates[0]; // 게이트를 낮추면 4자리부터도 가능
100
+ detect('3333051234567').candidates[0].matched; // ['기관코드','자릿수','프리픽스']
101
+ detect('100012345678').candidates[0].institutionName; // '토스뱅크'
102
+ ```
103
+
104
+ | Bank | Prefix | Source |
105
+ |---|---|---|
106
+ | 카카오뱅크 (13d) | `3333` 입출금 · `7979`/`7942` 모임통장 | [KakaoBank official blog](https://blog.kakaobank.com/posts/behind-account-number) |
107
+ | 토스뱅크 (12d) | `1000` 입출금 · `1060` 모으기 · `3000` 적금 | community-observed ([나무위키](https://namu.wiki/w/%EA%B3%84%EC%A2%8C%EB%B2%88%ED%98%B8)) |
108
+ | 케이뱅크 (12d) | `100-XXX-XXXXXX` | community-observed — genuinely shared with 토스's official 과목 `100`, so both stay listed |
109
+
110
+ Official 과목코드 front codes fire progressively too (typing `611` already floats
111
+ 하나은행). These prefixes are evidence-only — they never remove other candidates.
112
+
113
+ `detect()` shows **top 3 by default** — the density real apps (Toss) use.
114
+ `detect(account, { limit })` widens the list. For the full unfiltered
115
+ ranked list, call `matchAccount(account, registry)`.
116
+
117
+ ### Bring your own registry — update DATA without a library release
118
+
119
+ Institution data changes on its own clock (금결원 PDF 개정, new prefixes, fixes).
120
+ Host the registry JSON anywhere (DB, S3, config service) and pass it in — the
121
+ bundled copy is just the fallback:
122
+
123
+ ```ts
124
+ const remote = await fetch(REGISTRY_URL).then((r) => r.json()); // cache it
125
+ detect(acc, { registry: remote });
126
+ ```
127
+
128
+ Replace the stored JSON → every consumer picks it up. No npm release, no
129
+ redeploy. (`registry.generatedFrom` / `sourceSha256` identify which PDF revision
130
+ a blob was built from.)
131
+
132
+ ### Hard filter — banks your service supports
133
+
134
+ The strongest narrowing lever is the one only you have: the list of banks your
135
+ product actually handles.
136
+
137
+ ```ts
138
+ detect(acc, { allowedCodes: ['004', '088', '090', '092'] }); // 국민·신한·카카오·토스만
139
+ ```
140
+
141
+ ### Category filter — 전체 / 은행 / 비은행 / 증권사 / 결제기관
142
+
143
+ Every candidate carries a `category` (`'bank' | 'non-bank' | 'securities' |
144
+ 'clearing' | 'foreign-branch'`), and `detect()` can restrict to any subset:
145
+
146
+ ```ts
147
+ detect(acc); // 전체 (default)
148
+ detect(acc, { categories: ['bank'] }); // 은행만
149
+ detect(acc, { categories: ['securities'] }); // 증권사만
150
+ detect(acc, { categories: ['bank', 'non-bank'] });// 은행 + 비은행
151
+ ```
152
+
153
+ 'non-bank' covers 중앙회·금고·우체국·저축은행 (수협중앙회, 농협중앙회, 새마을금고중앙회,
154
+ 신협중앙회, 상호저축은행, 산림조합중앙회, 우체국); 'clearing' is 금융결제원;
155
+ 'foreign-branch' is the 5 foreign bank branches with no retail accounts for
156
+ individuals (HSBC·도이치·JP모간체이스·BOA·BNP파리바) — so `categories: ['bank']`
157
+ gives the 19 retail banks a transfer UI actually wants. (SC제일·한국씨티 are
158
+ foreign-owned but licensed Korean retail banks — they stay 'bank'.) A branch-routed
159
+ candidate carries the **target**'s category — 수협중앙회 is `non-bank`, so it appears
160
+ under the non-bank filter, not the bank filter. The structural guarantee and
161
+ padding operate within the filtered set.
162
+
163
+ **Two UX modes.** The default `detect()` is a *suggestion* list (~`limit` items).
164
+ For a *picker* UI with 전체/은행/증권사 tabs — where 전체 must show everything (the
165
+ union of every tab) — pass `limit: Infinity` (or use `matchAccount` directly) and
166
+ filter by `category`; the ranking still puts the likeliest institution on top.
167
+
168
+ ```ts
169
+ detect(acc, { limit: Infinity }); // 전체 — full ranked list
170
+ detect(acc, { limit: Infinity, categories: ['bank'] }); // 은행 탭
171
+ ```
172
+
173
+ ### Progressive input (as the user types)
174
+
175
+ ```ts
176
+ detect('2').candidates.length; // many candidates
177
+ detect('21243').candidates.length; // fewer — narrowing
178
+ detect('21243345678').candidates[0].institutionName; // '수협중앙회'
179
+ ```
180
+
181
+ ### Bring your own registry
182
+
183
+ Pin a specific revision instead of the bundled one:
184
+
185
+ ```ts
186
+ import { matchAccount } from 'korean-bank-detect';
187
+ import registry from 'korean-bank-detect/registry' with { type: 'json' };
188
+
189
+ matchAccount('110-436-387740', registry);
190
+ ```
191
+
192
+ ## Result shape
193
+
194
+ `detect(account)` and `matchAccount(account, registry)` both return:
195
+
196
+ ```ts
197
+ interface AccountInstitutionMatchResult {
198
+ input: string; // the original string, verbatim
199
+ isComplete: boolean; // input length equals a candidate's documented full length
200
+ candidates: CandidateMatch[];
201
+ }
202
+ ```
203
+
204
+ Each candidate (real output for `detect('212123456789')[0]`):
205
+
206
+ ```jsonc
207
+ {
208
+ "institutionCode": "030", // 대표코드 (the routed target, if a branch rule fired)
209
+ "institutionName": "수협중앙회",
210
+ "accountLengths": [11, 12, 14], // documented full lengths for this institution
211
+ "matched": ["기관코드", "자릿수", "분기규칙"], // evidence tokens satisfied
212
+ "checkDigitDeclared": false, // static flag from the PDF — NEVER computed
213
+ "routedByBranchRule": true, // a document-declared rule selected this institution
214
+ "logo": "" // reserved; empty for now (see Caveats)
215
+ }
216
+ ```
217
+
218
+ Candidates are **deterministically ordered**: by `matched` length (desc), then by
219
+ `institutionName` (Korean collation). A candidate whose subject code (`과목코드`) or
220
+ branch rule (`분기규칙`) matched carries an extra evidence token and therefore sorts
221
+ to the top — so the actual bank is candidate #0 for accounts that encode it.
222
+
223
+ ```ts
224
+ detect('611234567890').candidates[0].institutionName; // '하나은행' (611 = 하나 차세대 보통예금)
225
+ detect('1011234567890').candidates[0].institutionName; // '부산은행' (101 = 부산 신계좌 보통예금)
226
+ ```
227
+
228
+ For a real "which bank?" UX, take `candidates[0]` (or show the top 2–3 and let the
229
+ user confirm — the true bank is always present).
230
+
231
+ ## Behavior
232
+
233
+ | Input | Result |
234
+ |---|---|
235
+ | Hyphens / whitespace | stripped before matching (`"212-123456-789"` ≡ `"212123456789"`) |
236
+ | Empty / non-digit / over-length string | `{ candidates: [], isComplete: false }` — **never throws** |
237
+ | Non-string (`null`, `number`, `object`, …) | same as above, never throws |
238
+ | Partial input | all reachable candidates; the set only shrinks as digits grow |
239
+
240
+ ### 수협 branch routing
241
+
242
+ The PDF splits 수협 accounts between **007 수협은행** and **030 수협중앙회** by digit
243
+ value. This library executes those rules verbatim:
244
+
245
+ | Length | Deciding digits | Routes to 030 수협중앙회 when the value is |
246
+ |---|---|---|
247
+ | 11 | 4th–5th | 43–45, 47, 49, 59, 61–64, 66–68, 74, 75, 78, 81–85, 93 |
248
+ | 12 | 1st | 2, 7, 9 |
249
+ | 14 | 1st–3rd | 493, 481–489 |
250
+
251
+ Before the deciding digits are present, the account stays a 수협은행 (007)
252
+ candidate; once they arrive, it is definitively routed.
253
+
254
+ ## API
255
+
256
+ | Export | Signature | Notes |
257
+ |---|---|---|
258
+ | `detect` | `(account: unknown) => AccountInstitutionMatchResult` | uses the bundled registry |
259
+ | `matchAccount` | `(account: unknown, registry: Registry) => AccountInstitutionMatchResult` | pure; pass any registry |
260
+ | `registry` | `Registry` | the bundled 금결원 CMS registry (26.06.01) |
261
+
262
+ Types exported: `AccountInstitutionMatchResult`, `CandidateMatch`, `Registry`,
263
+ `RegistryInstitution`, `BranchRule`.
264
+
265
+ ## Caveats
266
+
267
+ - **A single guaranteed answer is mathematically impossible from the account
268
+ number alone.** Korean account numbers don't uniquely encode the bank: a modern
269
+ 차세대 account (front 3-digit 과목코드) usually pins one bank, but an old 구계좌
270
+ (shared 2-digit code like `01` = 보통예금) is used by dozens of banks. Internet
271
+ banks are covered by the 실무 프리픽스 layer above; old shared-code accounts are
272
+ why Toss shows several suggestions and confirms with a live account-holder
273
+ inquiry. `detect()` gives you the short candidate list; for a definitive pick,
274
+ add a 예금주조회 (account-holder inquiry) API on top — out of scope here.
275
+ The structural floor, by bank count per length: 10d → 11 · 11d → 14 ·
276
+ 12d → 20 · 13d → 13 · 14d → 16 banks. Without a subject-code/prefix signal,
277
+ no offline method can narrow below this.
278
+ - **Subject-code coverage.** All institutions that carry a 계정과목코드 in the PDF
279
+ (~37) are encoded, cross-referenced between this repo's own PDF extraction and
280
+ the MIT-licensed [korean-account](https://github.com/dydals3440/korean-account)
281
+ dataset (a faithful transcription of the same KFTC document; spot-checks match
282
+ exactly). Institutions with no subject code in the PDF are covered by the 실무
283
+ 프리픽스 layer where a stable real-world prefix exists (internet banks); a few
284
+ pure-serial securities accounts remain structurally indistinguishable — an
285
+ inherent limit of the document.
286
+ - **Priority is a display heuristic, not data.** There is **no official
287
+ popularity/usage reference for Korean banks** — the bundled `priority` values
288
+ are an experience-based ordering inherited from the korean-account dataset
289
+ (no sample, no formula, no observation window). It only breaks ties and picks
290
+ which `unknown` candidates fill the display budget. For production ranking,
291
+ prefer, in order: `allowedCodes` (banks your service supports) → structural
292
+ evidence (`grade`/`exactStrength`) → your own per-user selection & verification
293
+ stats → static priority last.
294
+ - **Never auto-trust `candidates[0]` for money movement.** Use
295
+ `exactStrength >= 3` as the minimum bar for even *suggesting* auto-selection,
296
+ let the user confirm, and verify with a 예금주조회 (account-holder inquiry) API
297
+ before transferring.
298
+ - **Length data.** Core institutions (수협, 상호저축은행, …) were verified against the
299
+ PDF; some institutions' `accountLengths` may be slightly broad (more candidates,
300
+ never fewer — recall stays safe). File an issue with a misdetection and that
301
+ institution can be tightened.
302
+ - **Logos.** The `logo` field exists in the schema but is `""` — bank logos are
303
+ trademarks with no official redistributable source; supply your own URLs.
304
+ - **Snapshot.** Data is a snapshot of the 2026.06.01 revision. Tracking future
305
+ revisions of the PDF is out of scope.
306
+
307
+ ## Development
308
+
309
+ ```bash
310
+ node scripts/build-registry.mjs # regenerate data/registry.json (byte-identical)
311
+ npm test # data fidelity · branch routing · input contract
312
+ npm run build # emit dist/
313
+ ```
314
+
315
+ The registry is generated deterministically from a reviewed source
316
+ (`scripts/registry-source.mjs`) whose every fact is traceable to the 금결원 PDF.
317
+
318
+ ## License
319
+
320
+ [MIT](LICENSE)