Files
sam-react-prod/src/components/items/DynamicItemForm/fields/CurrencyField.tsx
유병철 81affdc441 feat: ESLint 정리 및 전체 코드 품질 개선
- eslint.config.mjs 규칙 강화 및 정리
- 전역 unused import/변수 제거 (312개 파일)
- next.config.ts, middleware, proxy route 개선
- CopyableCell molecule 추가
- 회계/결재/HR/생산/건설/품질/영업 등 전 도메인 lint 정리
- IntegratedListTemplateV2, DataTable, MobileCard 등 공통 컴포넌트 개선
- execute-server-action 에러 핸들링 보강
2026-03-11 10:27:10 +09:00

127 lines
3.7 KiB
TypeScript

/**
* 통화 금액 입력 필드
* 천단위 콤마 포맷, 통화 기호 prefix 지원
*
* properties: { currency, precision, showSymbol, allowNegative }
* 저장값: number (포맷 없이)
*/
'use client';
import { useState, useCallback } from 'react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import type { DynamicFieldRendererProps, CurrencyConfig } from '../types';
const CURRENCY_SYMBOLS: Record<string, string> = {
KRW: '\u20A9',
USD: '$',
EUR: '\u20AC',
JPY: '\u00A5',
CNY: '\u00A5',
GBP: '\u00A3',
};
function formatCurrency(num: number, precision: number): string {
const fixed = num.toFixed(precision);
const [intPart, decPart] = fixed.split('.');
const formatted = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return decPart !== undefined ? `${formatted}.${decPart}` : formatted;
}
function parseCurrency(str: string): number {
const cleaned = str.replace(/[^0-9.-]/g, '');
const num = parseFloat(cleaned);
return isNaN(num) ? 0 : num;
}
export function CurrencyField({
field,
value,
onChange,
error,
disabled,
}: DynamicFieldRendererProps) {
const fieldKey = field.field_key || `field_${field.id}`;
const config = (field.properties || {}) as CurrencyConfig;
const currency = config.currency || 'KRW';
const precision = config.precision ?? 0;
const showSymbol = config.showSymbol !== false;
const allowNegative = config.allowNegative === true;
const symbol = CURRENCY_SYMBOLS[currency] || currency;
const numericValue = value !== null && value !== undefined ? Number(value) : null;
const [isFocused, setIsFocused] = useState(false);
const [inputValue, setInputValue] = useState(
numericValue !== null ? String(numericValue) : ''
);
const handleFocus = useCallback(() => {
setIsFocused(true);
setInputValue(numericValue !== null ? String(numericValue) : '');
}, [numericValue]);
const handleBlur = useCallback(() => {
setIsFocused(false);
if (inputValue === '' || inputValue === '-') {
onChange(null);
return;
}
const parsed = parseCurrency(inputValue);
const final = allowNegative ? parsed : Math.abs(parsed);
onChange(final);
}, [inputValue, onChange, allowNegative]);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value;
// 숫자, 점, 마이너스만 허용
const pattern = allowNegative ? /[^0-9.-]/g : /[^0-9.]/g;
const cleaned = raw.replace(pattern, '');
setInputValue(cleaned);
}, [allowNegative]);
const displayValue = isFocused
? inputValue
: numericValue !== null
? formatCurrency(numericValue, precision)
: '';
return (
<div>
<Label htmlFor={fieldKey}>
{field.field_name}
{field.is_required && <span className="text-red-500"> *</span>}
</Label>
<div className="relative">
{showSymbol && (
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none">
{symbol}
</span>
)}
<Input
id={fieldKey}
type="text"
inputMode="decimal"
placeholder={field.placeholder || '0'}
value={displayValue}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
disabled={disabled}
className={`${showSymbol ? 'pl-8' : ''} text-right ${error ? 'border-red-500' : ''}`}
/>
</div>
{error && (
<p className="text-xs text-red-500 mt-1">{error}</p>
)}
{!error && field.description && (
<p className="text-xs text-muted-foreground mt-1">
* {field.description}
</p>
)}
</div>
);
}