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 +21 -0
- package/README.md +320 -0
- package/data/registry.json +2347 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +92 -0
- package/dist/match.d.ts +161 -0
- package/dist/match.js +172 -0
- package/package.json +57 -0
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
|
+
[](https://www.npmjs.com/package/korean-bank-detect)
|
|
6
|
+
[](#api)
|
|
7
|
+
[](#why)
|
|
8
|
+
[](#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)
|