"use client" import * as React from "react" import { useCallback } from "react" import { Edit2 } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Checkbox } from "@/components/ui/checkbox" import { cn } from "@/lib/utils" export type EditableCellType = | "text" | "textarea" | "select" | "checkbox" | "number" export interface EditableCellProps { value: T type: EditableCellType onSave: (newValue: T) => void // 일괄 저장이므로 Promise 제거 onCancel?: () => void onChange?: (newValue: T) => void // 값 변경 시 실시간 콜백 className?: string options?: { label: string; value: T }[] // select 타입용 placeholder?: string disabled?: boolean maxLength?: number validation?: (value: T) => string | null // 유효성 검증 함수 autoSave?: boolean // 자동 저장 여부 (기본값: true) initialEditMode?: boolean // 초기 편집 모드 여부 (기본값: false) isModified?: boolean // 수정되었는지 여부 } export function EditableCell({ value, type, onSave, onCancel, onChange, className, options = [], placeholder, disabled = false, maxLength, validation, autoSave = true, initialEditMode = false, isModified = false, }: EditableCellProps) { const [isEditing, setIsEditing] = React.useState(initialEditMode) const [editValue, setEditValue] = React.useState(value) const [error, setError] = React.useState(null) const handleFinishEdit = useCallback((overrideValue?: T) => { const currentValue = overrideValue !== undefined ? overrideValue : editValue // 유효성 검증 if (validation && currentValue !== value) { const validationError = validation(currentValue) if (validationError) { setError(validationError) return } } // 값이 변경된 경우에만 저장 (일괄 저장용) if (currentValue !== value) { onSave(currentValue) } setIsEditing(false) setError(null) }, [editValue, validation, value, onSave]) const handleCheckboxChange = useCallback((checked: boolean) => { const convertedValue = checked as T setEditValue(convertedValue) if (autoSave) { // 체크박스 변경 시 자동 저장 - 값 직접 전달 handleFinishEdit(convertedValue) } else { // 일괄 저장 모드에서는 실시간 표시를 위해 즉시 onSave 호출 onSave(convertedValue) } }, [autoSave, handleFinishEdit, onSave]) const handleStartEdit = useCallback((e: React.MouseEvent) => { e.stopPropagation() if (disabled) return // 체크박스의 경우 즉시 토글 if (type === "checkbox") { // T가 boolean이라고 가정 const newValue = !value handleCheckboxChange(!!newValue) return } setIsEditing(true) setEditValue(value) setError(null) }, [disabled, value, type, handleCheckboxChange]) const handleCancelEdit = useCallback(() => { // 취소 시 원래 값으로 복원하되, pendingChanges에서도 제거 onCancel?.() setEditValue(value) setIsEditing(false) setError(null) }, [onCancel, value]) const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === "Enter" && type !== "textarea") { e.preventDefault() handleFinishEdit() } else if (e.key === "Escape") { e.stopPropagation() handleCancelEdit() } }, [type, handleFinishEdit, handleCancelEdit]) const handleBlur = useCallback(() => { handleFinishEdit() }, [handleFinishEdit]) // 값 표시를 위한 헬퍼 함수 const getDisplayValue = useCallback((val: T): string => { if (val === null || val === undefined) return "" if (typeof val === 'boolean') return val ? "true" : "false" return String(val) }, []) const handleSelectChange = useCallback((newValue: string) => { // string을 원래 타입으로 변환 시도 let convertedValue: T = newValue as T // number 타입인 경우 변환 if (typeof value === 'number') { const numValue = parseFloat(newValue) if (!isNaN(numValue)) { convertedValue = numValue as T } } setEditValue(convertedValue) if (autoSave) { // 드롭다운 선택 시 자동 저장 - 선택된 값 직접 전달 handleFinishEdit(convertedValue) } else { // 일괄 저장 모드에서는 실시간 표시를 위해 즉시 onSave 호출 onSave(convertedValue) } }, [value, autoSave, handleFinishEdit, onSave]) const handleInputChange = useCallback(async (newValue: string) => { // string을 원래 타입으로 변환 시도 let convertedValue: T = newValue as T // number 타입인 경우 변환 if (type === 'number' && typeof value === 'number') { const numValue = parseFloat(newValue) if (!isNaN(numValue)) { convertedValue = numValue as T } } setEditValue(convertedValue) setError(null) // onChange 콜백 호출 (실시간 값 변경 알림) if (onChange) { await onChange(convertedValue) } // 실시간 표시를 위해 값이 변경될 때마다 onSave 호출 (일괄 저장용) if (!autoSave && convertedValue !== value) { onSave(convertedValue) } }, [type, value, autoSave, onSave, onChange]) // 읽기 전용 모드 if (!isEditing) { return (
{type === "checkbox" ? (
e.stopPropagation()}>
) : ( {value === null || value === undefined || value === "" ? (placeholder || "값 없음") : String(value) } )}
{!disabled && type !== "checkbox" && ( )}
) } // 편집 모드 return (
{type === "text" && ( handleInputChange(e.target.value)} onKeyDown={handleKeyDown} onBlur={handleBlur} placeholder={placeholder} maxLength={maxLength} autoFocus aria-label={`${placeholder || '텍스트 입력'}`} className={cn("h-8", error && "border-red-500", isModified && "bg-yellow-50 border-yellow-300")} /> )} {type === "textarea" && (