- 숫자/통화/전화번호/사업자번호 등 특수 입력 컴포넌트 추가 - MobileCard 컴포넌트 통합 (ListMobileCard 제거) - IntegratedListTemplateV2 페이지네이션 버그 수정 (NaN 이슈) - IntegratedDetailTemplate 타이틀 중복 수정 - 문서 시스템 컴포넌트 추가 - 헤더 벨 아이콘 포커스 스타일 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
247 lines
7.1 KiB
TypeScript
247 lines
7.1 KiB
TypeScript
/**
|
|
* formatters.ts - 입력값 포맷팅 유틸리티 함수
|
|
*
|
|
* 전화번호, 사업자번호, 주민번호, 숫자 포맷팅 함수 모음
|
|
*/
|
|
|
|
/**
|
|
* 전화번호 포맷팅
|
|
* - 휴대폰 (11자리): 010-1234-5678
|
|
* - 서울 (9~10자리): 02-123-4567 / 02-1234-5678
|
|
* - 지역 (10~11자리): 031-123-4567 / 031-1234-5678
|
|
*/
|
|
export function formatPhoneNumber(value: string): string {
|
|
const numbers = value.replace(/\D/g, '');
|
|
|
|
if (numbers.length === 0) return '';
|
|
|
|
// 서울 (02로 시작)
|
|
if (numbers.startsWith('02')) {
|
|
if (numbers.length <= 2) return numbers;
|
|
if (numbers.length <= 5) return `${numbers.slice(0, 2)}-${numbers.slice(2)}`;
|
|
if (numbers.length <= 9) return `${numbers.slice(0, 2)}-${numbers.slice(2, 5)}-${numbers.slice(5)}`;
|
|
return `${numbers.slice(0, 2)}-${numbers.slice(2, 6)}-${numbers.slice(6, 10)}`;
|
|
}
|
|
|
|
// 휴대폰 및 지역번호 (3자리 지역번호)
|
|
if (numbers.length <= 3) return numbers;
|
|
if (numbers.length <= 7) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`;
|
|
if (numbers.length <= 10) return `${numbers.slice(0, 3)}-${numbers.slice(3, 6)}-${numbers.slice(6)}`;
|
|
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7, 11)}`;
|
|
}
|
|
|
|
/**
|
|
* 전화번호에서 숫자만 추출
|
|
*/
|
|
export function parsePhoneNumber(formatted: string): string {
|
|
return formatted.replace(/\D/g, '');
|
|
}
|
|
|
|
/**
|
|
* 사업자번호 포맷팅 (000-00-00000)
|
|
*/
|
|
export function formatBusinessNumber(value: string): string {
|
|
const numbers = value.replace(/\D/g, '').slice(0, 10);
|
|
|
|
if (numbers.length === 0) return '';
|
|
if (numbers.length <= 3) return numbers;
|
|
if (numbers.length <= 5) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`;
|
|
return `${numbers.slice(0, 3)}-${numbers.slice(3, 5)}-${numbers.slice(5)}`;
|
|
}
|
|
|
|
/**
|
|
* 사업자번호에서 숫자만 추출
|
|
*/
|
|
export function parseBusinessNumber(formatted: string): string {
|
|
return formatted.replace(/\D/g, '').slice(0, 10);
|
|
}
|
|
|
|
/**
|
|
* 사업자번호 유효성 검사 (체크섬 검증)
|
|
* @see https://www.law.go.kr/법령/법인세법시행령/(20230101,33262,20221231)/제43조
|
|
*/
|
|
export function validateBusinessNumber(value: string): boolean {
|
|
const numbers = value.replace(/\D/g, '');
|
|
|
|
if (numbers.length !== 10) return false;
|
|
|
|
const weights = [1, 3, 7, 1, 3, 7, 1, 3, 5];
|
|
let sum = 0;
|
|
|
|
for (let i = 0; i < 9; i++) {
|
|
sum += parseInt(numbers[i]) * weights[i];
|
|
}
|
|
|
|
sum += Math.floor((parseInt(numbers[8]) * 5) / 10);
|
|
const checkDigit = (10 - (sum % 10)) % 10;
|
|
|
|
return checkDigit === parseInt(numbers[9]);
|
|
}
|
|
|
|
/**
|
|
* 주민번호 포맷팅 (000000-0000000)
|
|
*/
|
|
export function formatPersonalNumber(value: string): string {
|
|
const numbers = value.replace(/\D/g, '').slice(0, 13);
|
|
|
|
if (numbers.length === 0) return '';
|
|
if (numbers.length <= 6) return numbers;
|
|
return `${numbers.slice(0, 6)}-${numbers.slice(6)}`;
|
|
}
|
|
|
|
/**
|
|
* 주민번호 마스킹 (000000-*******)
|
|
*/
|
|
export function formatPersonalNumberMasked(value: string): string {
|
|
const numbers = value.replace(/\D/g, '').slice(0, 13);
|
|
|
|
if (numbers.length === 0) return '';
|
|
if (numbers.length <= 6) return numbers;
|
|
return `${numbers.slice(0, 6)}-${'*'.repeat(Math.min(7, numbers.length - 6))}`;
|
|
}
|
|
|
|
/**
|
|
* 주민번호에서 숫자만 추출
|
|
*/
|
|
export function parsePersonalNumber(formatted: string): string {
|
|
return formatted.replace(/\D/g, '').slice(0, 13);
|
|
}
|
|
|
|
/**
|
|
* 카드번호 포맷팅 (0000-0000-0000-0000)
|
|
*/
|
|
export function formatCardNumber(value: string): string {
|
|
const numbers = value.replace(/\D/g, '').slice(0, 16);
|
|
|
|
if (numbers.length === 0) return '';
|
|
if (numbers.length <= 4) return numbers;
|
|
if (numbers.length <= 8) return `${numbers.slice(0, 4)}-${numbers.slice(4)}`;
|
|
if (numbers.length <= 12) return `${numbers.slice(0, 4)}-${numbers.slice(4, 8)}-${numbers.slice(8)}`;
|
|
return `${numbers.slice(0, 4)}-${numbers.slice(4, 8)}-${numbers.slice(8, 12)}-${numbers.slice(12)}`;
|
|
}
|
|
|
|
/**
|
|
* 카드번호에서 숫자만 추출
|
|
*/
|
|
export function parseCardNumber(formatted: string): string {
|
|
return formatted.replace(/\D/g, '').slice(0, 16);
|
|
}
|
|
|
|
/**
|
|
* 계좌번호 포맷팅 (4자리마다 하이픈)
|
|
* 예: 1234-5678-9012-34
|
|
*/
|
|
export function formatAccountNumber(value: string): string {
|
|
const numbers = value.replace(/\D/g, '').slice(0, 16);
|
|
|
|
if (numbers.length === 0) return '';
|
|
|
|
// 4자리씩 나눠서 하이픈으로 연결
|
|
const groups = [];
|
|
for (let i = 0; i < numbers.length; i += 4) {
|
|
groups.push(numbers.slice(i, i + 4));
|
|
}
|
|
return groups.join('-');
|
|
}
|
|
|
|
/**
|
|
* 계좌번호에서 숫자만 추출
|
|
*/
|
|
export function parseAccountNumber(formatted: string): string {
|
|
return formatted.replace(/\D/g, '').slice(0, 16);
|
|
}
|
|
|
|
/**
|
|
* 숫자 천단위 콤마 포맷팅
|
|
*/
|
|
export function formatNumber(value: number | string, options?: {
|
|
useComma?: boolean;
|
|
decimalPlaces?: number;
|
|
suffix?: string;
|
|
}): string {
|
|
const { useComma = true, decimalPlaces, suffix = '' } = options || {};
|
|
|
|
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
|
|
if (isNaN(num)) return '';
|
|
|
|
let formatted: string;
|
|
|
|
if (decimalPlaces !== undefined) {
|
|
formatted = num.toFixed(decimalPlaces);
|
|
} else {
|
|
formatted = String(num);
|
|
}
|
|
|
|
if (useComma) {
|
|
const parts = formatted.split('.');
|
|
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
formatted = parts.join('.');
|
|
}
|
|
|
|
return suffix ? `${formatted}${suffix}` : formatted;
|
|
}
|
|
|
|
/**
|
|
* 포맷된 숫자 문자열에서 숫자 추출
|
|
*/
|
|
export function parseNumber(formatted: string): number {
|
|
const cleaned = formatted.replace(/[^\d.-]/g, '');
|
|
const num = parseFloat(cleaned);
|
|
return isNaN(num) ? 0 : num;
|
|
}
|
|
|
|
/**
|
|
* Leading zero 제거 (01 → 1)
|
|
*/
|
|
export function removeLeadingZeros(value: string): string {
|
|
// 빈 값 또는 '0' 하나만 있는 경우
|
|
if (!value || value === '0') return value;
|
|
|
|
// 소수점이 있는 경우 (0.5 등)
|
|
if (value.includes('.')) {
|
|
const parts = value.split('.');
|
|
parts[0] = parts[0].replace(/^0+/, '') || '0';
|
|
return parts.join('.');
|
|
}
|
|
|
|
// 음수인 경우
|
|
if (value.startsWith('-')) {
|
|
return '-' + (value.slice(1).replace(/^0+/, '') || '0');
|
|
}
|
|
|
|
// 일반 정수
|
|
return value.replace(/^0+/, '') || '0';
|
|
}
|
|
|
|
/**
|
|
* 숫자만 추출 (음수, 소수점 허용 옵션)
|
|
*/
|
|
export function extractNumbers(value: string, options?: {
|
|
allowNegative?: boolean;
|
|
allowDecimal?: boolean;
|
|
}): string {
|
|
const { allowNegative = false, allowDecimal = false } = options || {};
|
|
|
|
let pattern = '\\d';
|
|
if (allowNegative) pattern = '-?' + pattern;
|
|
if (allowDecimal) pattern = pattern + '|\\.';
|
|
|
|
const regex = new RegExp(`[^${allowNegative ? '-' : ''}${allowDecimal ? '.' : ''}\\d]`, 'g');
|
|
let result = value.replace(regex, '');
|
|
|
|
// 중복 마이너스 제거 (첫 번째만 유지)
|
|
if (allowNegative && result.includes('-')) {
|
|
const isNegative = result.startsWith('-');
|
|
result = result.replace(/-/g, '');
|
|
if (isNegative) result = '-' + result;
|
|
}
|
|
|
|
// 중복 소수점 제거 (첫 번째만 유지)
|
|
if (allowDecimal && result.includes('.')) {
|
|
const parts = result.split('.');
|
|
result = parts[0] + (parts.length > 1 ? '.' + parts.slice(1).join('') : '');
|
|
}
|
|
|
|
return result;
|
|
} |