diff --git a/src/components/templates/IntegratedDetailTemplate/index.tsx b/src/components/templates/IntegratedDetailTemplate/index.tsx index 590fce5e..0577c206 100644 --- a/src/components/templates/IntegratedDetailTemplate/index.tsx +++ b/src/components/templates/IntegratedDetailTemplate/index.tsx @@ -94,7 +94,9 @@ function IntegratedDetailTemplateInner>( } else { const defaultData: Record = {}; config.fields.forEach((field) => { - if (field.type === 'checkbox') { + if (field.defaultValue !== undefined) { + defaultData[field.key] = field.defaultValue; + } else if (field.type === 'checkbox') { defaultData[field.key] = false; } else { defaultData[field.key] = ''; @@ -135,10 +137,12 @@ function IntegratedDetailTemplateInner>( : (initialData as Record); setFormData(transformed); } else { - // 기본값 설정 + // 기본값 설정 (field.defaultValue 우선 적용) const defaultData: Record = {}; config.fields.forEach((field) => { - if (field.type === 'checkbox') { + if (field.defaultValue !== undefined) { + defaultData[field.key] = field.defaultValue; + } else if (field.type === 'checkbox') { defaultData[field.key] = false; } else { defaultData[field.key] = ''; diff --git a/src/components/templates/UniversalListPage/index.tsx b/src/components/templates/UniversalListPage/index.tsx index 9d30a7cc..dff79ee4 100644 --- a/src/components/templates/UniversalListPage/index.tsx +++ b/src/components/templates/UniversalListPage/index.tsx @@ -246,8 +246,8 @@ export function UniversalListPage({ // 삭제 등으로 현재 페이지가 빈 페이지가 되면 마지막 유효 페이지로 이동 useEffect(() => { - if (totalPages > 0 && currentPage > totalPages) { - setCurrentPage(totalPages); + if (currentPage > 1 && (totalPages === 0 || currentPage > totalPages)) { + setCurrentPage(Math.max(1, totalPages)); } }, [totalPages, currentPage]); @@ -328,8 +328,9 @@ export function UniversalListPage({ }, []); // initialData prop 변경 감지 (부모 컴포넌트에서 데이터 로드 후 전달하는 경우) + // 삭제 후 빈 배열도 동기화해야 빈 페이지가 올바르게 표시됨 useEffect(() => { - if (initialData && initialData.length > 0) { + if (initialData) { setRawData(initialData); } }, [initialData]); diff --git a/src/components/ui/currency-input.tsx b/src/components/ui/currency-input.tsx index 28f6a802..1f95fb04 100644 --- a/src/components/ui/currency-input.tsx +++ b/src/components/ui/currency-input.tsx @@ -23,10 +23,11 @@ export interface CurrencyInputProps * CurrencyInput - 금액 입력 전용 컴포넌트 * * 특징: - * - 항상 천단위 콤마 표시 + * - 입력 중에도 실시간 천단위 콤마 표시 * - 정수만 허용 (소수점 없음) * - 통화 기호 표시 (₩, $, ¥, €) * - Leading zero 자동 제거 + * - 커서 위치 자동 보정 * * @example * // 기본 (원화) @@ -57,6 +58,38 @@ const CurrencyInput = React.forwardRef( ) => { const [isFocused, setIsFocused] = React.useState(false); const [displayValue, setDisplayValue] = React.useState(""); + const innerRef = React.useRef(null); + const cursorPosRef = React.useRef(null); + + // 외부 ref와 내부 ref 결합 + const combinedRef = React.useCallback( + (node: HTMLInputElement | null) => { + innerRef.current = node; + if (typeof ref === 'function') ref(node); + else if (ref) (ref as React.MutableRefObject).current = node; + }, + [ref] + ); + + // 렌더 후 커서 위치 복원 + React.useLayoutEffect(() => { + if (cursorPosRef.current !== null && innerRef.current && isFocused) { + innerRef.current.setSelectionRange(cursorPosRef.current, cursorPosRef.current); + cursorPosRef.current = null; + } + }); + + // 숫자를 콤마 포맷 문자열로 변환 + const formatWithCommas = (sanitized: string): string => { + if (!sanitized || sanitized === "-") return sanitized || ""; + const isNeg = sanitized.startsWith("-"); + const digits = isNeg ? sanitized.slice(1) : sanitized; + if (!digits) return sanitized; + const num = parseInt(digits, 10); + if (isNaN(num)) return ""; + const formatted = formatNumber(num, { useComma: true }); + return isNeg ? `-${formatted}` : formatted; + }; // 외부 value 변경 시 displayValue 동기화 React.useEffect(() => { @@ -111,37 +144,53 @@ const CurrencyInput = React.forwardRef( const handleChange = (e: React.ChangeEvent) => { const rawValue = e.target.value; - // 콤마 제거 + const cursorPos = e.target.selectionStart || 0; + + // 커서 앞 숫자 개수 계산 (콤마 무시) + const digitsBeforeCursor = rawValue.slice(0, cursorPos).replace(/[^\d]/g, "").length; + + // 콤마 제거 후 정제 const withoutComma = rawValue.replace(/,/g, ""); const sanitized = sanitizeInput(withoutComma); - setDisplayValue(sanitized); - - // 입력 중 "-"만 있으면 onChange 호출 안함 - if (sanitized === "-") return; + if (sanitized === "-") { + setDisplayValue(sanitized); + cursorPosRef.current = 1; + return; + } const numValue = parseValue(sanitized); onChange(numValue); + + // 실시간 콤마 포맷 + const formatted = formatWithCommas(sanitized); + setDisplayValue(formatted); + + // 새 커서 위치 계산: 숫자 개수 기준으로 위치 복원 + let digitCount = 0; + let newPos = formatted.length; + for (let i = 0; i < formatted.length; i++) { + if (/\d/.test(formatted[i])) { + digitCount++; + } + if (digitCount === digitsBeforeCursor) { + newPos = i + 1; + break; + } + } + cursorPosRef.current = newPos; }; const handleFocus = (e: React.FocusEvent) => { setIsFocused(true); - - // 포커스 시 콤마 제거된 순수 숫자로 표시 - if (value !== undefined && value !== null && value !== "") { - const numValue = typeof value === "string" ? parseFloat(value) : value; - if (!isNaN(numValue)) { - setDisplayValue(String(Math.floor(numValue))); - } - } - + // 포커스 시에도 콤마 유지 (실시간 포맷) onFocus?.(e); }; const handleBlur = (e: React.FocusEvent) => { setIsFocused(false); - const sanitized = sanitizeInput(displayValue); + const sanitized = sanitizeInput(displayValue.replace(/,/g, "")); const numValue = parseValue(sanitized); onChange(numValue); @@ -193,7 +242,7 @@ const CurrencyInput = React.forwardRef(