2026-02-12 11:17:57 +09:00
|
|
|
/**
|
|
|
|
|
* 통화 금액 입력 필드
|
|
|
|
|
* 천단위 콤마 포맷, 통화 기호 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 {
|
2026-03-11 10:27:10 +09:00
|
|
|
const cleaned = str.replace(/[^0-9.-]/g, '');
|
2026-02-12 11:17:57 +09:00
|
|
|
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;
|
|
|
|
|
// 숫자, 점, 마이너스만 허용
|
2026-03-11 10:27:10 +09:00
|
|
|
const pattern = allowNegative ? /[^0-9.-]/g : /[^0-9.]/g;
|
2026-02-12 11:17:57 +09:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|