Skip to content

Latest commit

 

History

History
279 lines (210 loc) · 12 KB

File metadata and controls

279 lines (210 loc) · 12 KB

numkey

English | 한국어 · 📺 라이브 데모

바로 실행해보기: ⚡ StackBlitz — Vanilla · Vue · React | 📦 CodeSandbox

numkey 데모 — 실시간 천 단위 콤마, 한글 금액 병기 (150만 원), 붙여넣기 정제

npm CI license

"문자열인데 숫자야." 업무 시스템엔 이런 필드가 꼭 있습니다 — 금액, 수량, 단가. 그리고 매번 같은 걸 손으로 다시 만들죠: 실시간 천 단위 콤마(10,000), 앞자리 0 제거, 오른쪽 정렬, 모바일 숫자 키패드, 그리고 콤마가 생기고 사라져도 튀지 않는 커서까지.

numkey는 그 인풋을 한 번에 끝냅니다:

  • 커서 안전 실시간 포맷팅1,234,567 중간에 타이핑해도 그룹이 재배치되는 동안 커서가 기대한 자리에 그대로
  • 문자열 우선 값 모델 — 정식 값은 순수 숫자 문자열("1234567.89"). IEEE 754 부동소수점을 거치지 않아 금액에 안전
  • 앞자리 0 정리(0077), 전각 숫자 정규화(123123 — 한글/일본어 IME), 붙여넣기 정제(₩ 1,234원1234)
  • 오른쪽 정렬 + inputmode 자동 설정 (옵트아웃 가능)
  • IME 안전 — 조합 중에는 값을 건드리지 않음
  • 의존성 0, TypeScript 우선, ESM/CJS 듀얼 + CDN 글로벌 빌드
  • 순수 <script> (JSP/PHP 등 서버 렌더링), Vue 3, React 모두 지원

같은 폼에서 한/영 키 오타도 처리해야 한다면? numkey의 형제 kokey를 보세요.

빌드 도구 없이 (JSP, PHP, 정적 페이지)

스크립트 태그 한 줄, 나머지는 마크업입니다. 나중에 DOM에 추가되는 인풋까지 자동으로 바인딩됩니다.

<script src="https://cdn.jsdelivr.net/npm/@devslab/numkey"></script>

<input data-numkey>                            <!-- 정수: 1,234,567 -->
<input data-numkey="2">                        <!-- 소수 2자리: 1,234.56 -->
<input data-numkey data-numkey-negative>       <!-- 음수 허용 -->
<input data-numkey data-numkey-align="left">   <!-- 왼쪽 정렬 유지 -->

서버가 렌더링한 값(<input data-numkey value="1234567">)은 로드 시 포맷됩니다. 제출 전에 원본 값을 읽으려면:

<script>
  const raw = numkey.getValue(document.querySelector('#amount')) // "1234567"
</script>

(또는 서버에서 콤마를 제거해도 됩니다 — POST되는 값은 표시 값입니다.)

속성이 동작하는 방식

data-numkey가 스위치입니다. 인풋을 바인딩시키는 것이 바로 이 속성이고 (자동 초기화가 input[data-numkey]를 감시합니다), 값은 최대 소수 자릿수를 겸합니다 — 빈 값이면 정수 전용. 나머지 data-numkey-* 속성들은 전부 data-numkey가 있는 인풋에서만 읽히는 옵션이라, 단독으로는 아무것도 하지 않습니다:

<input data-numkey>                        <!-- ON, 정수: 1,234,567 -->
<input data-numkey="2">                    <!-- ON, 소수 2자리: 1,234.56 -->
<input data-numkey="2" data-numkey-negative>  <!-- 옵션은 겹쳐 씀 -->
<input data-numkey-locale="auto">          <!-- ✗ 아무것도 안 함 — data-numkey 없음 -->
<input>                                    <!-- 평범한 인풋, 건드리지 않음 -->
속성 의미
data-numkey 스위치 — 인풋을 바인딩; 값은 최대 소수 자릿수 (빈 값 = 정수)
data-numkey-negative 앞자리 마이너스 허용
data-numkey-align="left" 자동 오른쪽 정렬 옵트아웃
data-numkey-group="4" 그룹 크기 (기본 3, 만 단위는 4)
data-numkey-separator=" " 그룹 구분자 (기본 ,)
data-numkey-point="," 필드에 표시되는 소수점 (기본 .)
data-numkey-locale 로케일에서 구분자 유도 — 아래 참조
data-numkey-korean 실시간 한글 금액 병기 ("150만") — 아래 참조
data-numkey-korean-entry 만/억 축약 입력 허용 ("3만5천" → blur → "35,000") — 아래 참조
data-numkey-name="amount" 정식 값을 전송하는 hidden 인풋 — 아래 참조
data-numkey-min / data-numkey-max 최소/최대 — blur 시에만 적용 (입력 중 간섭 없음)

로케일 표시 (옵트인)

기본 동작은 고정 표시입니다: 방문자 브라우저 설정이 무엇이든 모두가 1,234,567.89를 봅니다 — 업무 폼이 보통 원하는 동작이죠. data-numkey-locale을 붙인 필드만 로케일 구분자를 따릅니다:

<!-- 모두가 1,234,567.89 — 기본값, 로케일 무관 -->
<input data-numkey="2">

<!-- 방문자의 브라우저 언어를 따름:
     독일어 브라우저 → 1.234.567,89
     한국어 브라우저 → 1,234,567.89 -->
<input data-numkey="2" data-numkey-locale="auto">

<!-- 모든 방문자에게 독일식으로 고정 -->
<input data-numkey="2" data-numkey-locale="de-DE">

로케일이 바꾸는 건 그리는 방식뿐입니다. 정식 값은 항상 "1234567.89" — 세 경우 모두 numkey.getValue(el)이 같은 문자열을 돌려줍니다. 일반 폼 POST는 표시 값을 전송하므로, 로케일을 쓰는 폼은 data-numkey-name(아래)으로 전송하거나 서버에서 정규화하세요.

한글 금액 병기

은행·핀테크 UI가 금액 필드 옆에 그리는 "150만" 힌트 — 프로젝트마다 손으로 다시 만드는 바로 그것:

<input data-numkey data-numkey-korean>
<!-- 1500000 입력 시:
     <input value="1,500,000"> <span class="numkey-korean">150만</span> -->

<input data-numkey data-numkey-korean="#my-hint">  <!-- 기존 요소 사용 -->

속성이 빈 값이면 인풋 바로 뒤에 <span class="numkey-korean">을 생성합니다 (무스타일 — "원"은 CSS로: .numkey-korean::after { content: " 원" }). 값을 주면 기존 요소의 CSS 셀렉터로 해석합니다. 같은 엔진을 순수 함수로도 쓸 수 있습니다:

import { toKorean } from '@devslab/numkey'

toKorean('1500000')      // "150만"
toKorean('927483041001') // "9,274억 8,304만 1,001"
toKorean('100000001')    // "1억 1" — 0인 그룹은 생략

역방향도 됩니다 — 부동산·주식 앱에서 실제로 입력하는 축약형을 순수 함수로, 또는 필드에서 직접:

import { fromKorean } from '@devslab/numkey'

fromKorean('3만5천')   // "35000"
fromKorean('1.5억')    // "150000000"
fromKorean('삼십오만')  // "350000"
<input data-numkey data-numkey-korean-entry>
<!-- 3만5천 입력 중에는 건드리지 않고 (IME 조합과 안 싸움),
     blur에 35,000으로 변환 -->

getValuedata-numkey-name hidden 동기화(아래)는 초안이 화면에 있는 동안에도 파싱된 값을 봅니다.

정식 값으로 전송하기 (data-numkey-name)

일반 폼 POST는 필드에 보이는 그대로 — 1,234,567 — 전송하고, 서버는 매번 콤마를 제거해야 합니다. data-numkey-name은 정리된 정식 값을 항상 담고 있는 hidden 인풋을 생성합니다:

<form method="post">
  <!-- 보이는 인풋에는 name이 없고, hidden이 amount=1234567을 전송 -->
  <input data-numkey data-numkey-name="amount" value="1234567">
</form>

hidden 값은 입력 중간 상태에서도 정리된 상태를 유지하므로 (1,234.1234로 전송), 어느 순간 제출되든 서버는 깨끗한 숫자를 받습니다.

type="text" 인풋을 쓰세요. numkey가 inputmode를 설정해 모바일에서 숫자 키패드가 뜹니다. type="number"는 커서 API가 없어 포맷팅과 충돌합니다.

npm

npm install @devslab/numkey
import { format, parse, bind, observe } from '@devslab/numkey'

format('1234567.5', { decimals: 2 })   // "1,234,567.5"
parse('₩ 1,234,567원')                  // "1234567"

bind(document.querySelector('#amount'), { decimals: 2 }) // 요소 하나
observe()                                                // 모든 [data-numkey]

Vue 3

<script setup>
import { ref } from 'vue'
import { NumkeyInput, vNumkey } from '@devslab/numkey/vue'

const amount = ref('') // 항상 정식 값: "1234567"
</script>

<template>
  <!-- v-model은 정식 값을 받고, 필드에는 1,234,567이 보입니다 -->
  <NumkeyInput v-model="amount" :decimals="2" negative />

  <!-- 일반 인풋에는 디렉티브 -->
  <input v-numkey="2">
</template>

React

import { NumkeyInput, useNumkey } from '@devslab/numkey/react'

// Controlled: value/onValueChange는 정식 값 문자열로 통신
const [amount, setAmount] = useState('')
<NumkeyInput value={amount} onValueChange={setAmount} decimals={2} negative />

// Uncontrolled: ref 콜백 훅
<input ref={useNumkey({ decimals: 2 })} defaultValue="1234567" />

API

옵션

옵션 기본값
decimals 0 최대 소수 자릿수 (0 = 정수만)
negative false 앞자리 마이너스 허용
group 3 그룹당 자릿수 (만 단위 그룹핑은 4)
separator "," 표시용 그룹 구분자
decimalPoint "." 표시용 소수점 (정식 값은 항상 .)
locale 옵트인: Intlseparator/decimalPoint 유도 — "auto"(브라우저 언어) 또는 BCP 47 태그. 지정하지 않으면 방문자 브라우저와 무관하게 표시가 고정됩니다 (업무 폼의 기본 요구). 명시한 separator/decimalPoint가 우선.
min / max 최소/최대 — blur 시에만 적용. 입력 도중 클램핑하면 min이 10인 필드에 50을 칠 수 없게 되니까요

Core (순수 함수)

parse(display, opts?) 표시 값/붙여넣기 → 정식 값 "1234567.89"
format(canonical, opts?) 정식 값 → 표시 값 "1,234,567.89"
finalize(canonical) 입력 중간 상태 정리 ("1234.""1234")
toKorean(canonical, opts?) 한글 금액 병기 ("1500000""150만")
fromKorean(text) 만/억 축약 → 정식 값 ("3만5천""35000")

DOM

bind(el, opts?) 실시간 포맷팅 연결; 해제 함수 반환
observe(root?) 현재/미래의 모든 [data-numkey] 바인딩
getValue(el, opts?) 바인딩된 인풋의 정식 값
setValue(el, canonical, opts?) 정식 값을 포맷된 표시 값으로 기록
applyToInput(el, opts?) 커서 보존 1회 재포맷 (빌딩 블록)
createRefBinder(opts?) 어떤 프레임워크에서든 쓰는 ref 콜백 팩토리

참고

  • 유럽식 포맷도 옵션으로 지원: { separator: '.', decimalPoint: ',' }이면 1.234.567,89로 표시되고 정식 값은 "1234567.89"로 유지됩니다.
  • 구분자 옆 백스페이스/Delete는 한 번에 인접한 숫자를 지웁니다 (구분자는 건너뛰고, 재포맷이 나머지를 처리).
  • 로드맵: 인도식 lakh 그룹핑 (12,34,567 — 비균일 그룹 크기).

기여하기

이슈·PR 환영합니다 — 개발 환경 셋업과 규칙(문자열 우선 값 모델, IME 안전, 한 글자씩 타이핑하는 regression 테스트)은 CONTRIBUTING.md를 보세요.

License

MIT © devslab