diff --git a/src/app/[locale]/(protected)/customer-center/inquiries/[id]/edit/page.tsx b/src/app/[locale]/(protected)/customer-center/qna/[id]/edit/page.tsx similarity index 100% rename from src/app/[locale]/(protected)/customer-center/inquiries/[id]/edit/page.tsx rename to src/app/[locale]/(protected)/customer-center/qna/[id]/edit/page.tsx diff --git a/src/app/[locale]/(protected)/customer-center/inquiries/[id]/page.tsx b/src/app/[locale]/(protected)/customer-center/qna/[id]/page.tsx similarity index 100% rename from src/app/[locale]/(protected)/customer-center/inquiries/[id]/page.tsx rename to src/app/[locale]/(protected)/customer-center/qna/[id]/page.tsx diff --git a/src/app/[locale]/(protected)/customer-center/inquiries/create/page.tsx b/src/app/[locale]/(protected)/customer-center/qna/create/page.tsx similarity index 100% rename from src/app/[locale]/(protected)/customer-center/inquiries/create/page.tsx rename to src/app/[locale]/(protected)/customer-center/qna/create/page.tsx diff --git a/src/app/[locale]/(protected)/customer-center/inquiries/page.tsx b/src/app/[locale]/(protected)/customer-center/qna/page.tsx similarity index 100% rename from src/app/[locale]/(protected)/customer-center/inquiries/page.tsx rename to src/app/[locale]/(protected)/customer-center/qna/page.tsx diff --git a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx index 7607a8e6..625ad803 100644 --- a/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx +++ b/src/components/accounting/BadDebtCollection/BadDebtDetail.tsx @@ -110,17 +110,6 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp }, }); - // Daum 우편번호 서비스 - const { openPostcode } = useDaumPostcode({ - onComplete: (result) => { - setFormData(prev => ({ - ...prev, - zipCode: result.zonecode, - address1: result.address, - })); - }, - }); - // 다이얼로그 상태 const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showSaveDialog, setShowSaveDialog] = useState(false); diff --git a/src/components/common/EditableTable/EditableTable.tsx b/src/components/common/EditableTable/EditableTable.tsx new file mode 100644 index 00000000..ea5339c9 --- /dev/null +++ b/src/components/common/EditableTable/EditableTable.tsx @@ -0,0 +1,332 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Plus, Trash2, GripVertical } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +export interface EditableColumn { + key: keyof T; + header: string; + width?: string; + align?: 'left' | 'center' | 'right'; + type?: 'text' | 'number' | 'select'; + placeholder?: string; + options?: { label: string; value: string }[]; // for select type + render?: (value: T[keyof T], row: T, index: number, onChange: (value: T[keyof T]) => void) => React.ReactNode; + editable?: boolean; // default true +} + +export interface EditableTableProps { + columns: EditableColumn[]; + data: T[]; + onChange: (data: T[]) => void; + + // 새 행 생성 시 기본값 + createNewRow: () => T; + + // 옵션 + title?: string; + addButtonLabel?: string; + emptyMessage?: string; + showRowNumber?: boolean; + maxRows?: number; + minRows?: number; + + // 대량 추가 옵션 + bulkAdd?: boolean; // 여러 행 한번에 추가 기능 + bulkAddLabel?: string; // 대량 추가 버튼 라벨 + + // 스타일 + striped?: boolean; + compact?: boolean; +} + +export function EditableTable({ + columns, + data, + onChange, + createNewRow, + title, + addButtonLabel = '행 추가', + emptyMessage = '데이터가 없습니다. 행을 추가해주세요.', + showRowNumber = true, + maxRows, + minRows = 0, + bulkAdd = false, + bulkAddLabel = '추가', + striped = false, + compact = false, +}: EditableTableProps) { + // 대량 추가 개수 상태 + const [bulkCount, setBulkCount] = useState(1); + + // 행 추가 (단일) + const handleAddRow = useCallback(() => { + if (maxRows && data.length >= maxRows) return; + const newRow = createNewRow(); + onChange([...data, newRow]); + }, [data, onChange, createNewRow, maxRows]); + + // 행 추가 (대량) + const handleBulkAddRows = useCallback(() => { + if (!bulkCount || bulkCount < 1) return; + + // 최대 개수 제한 계산 + let countToAdd = bulkCount; + if (maxRows) { + const remaining = maxRows - data.length; + countToAdd = Math.min(countToAdd, remaining); + } + + if (countToAdd <= 0) return; + + const newRows = Array.from({ length: countToAdd }, () => createNewRow()); + onChange([...data, ...newRows]); + setBulkCount(1); // 입력값 초기화 + }, [bulkCount, data, onChange, createNewRow, maxRows]); + + // 행 삭제 + const handleDeleteRow = useCallback((index: number) => { + if (data.length <= minRows) return; + const newData = data.filter((_, i) => i !== index); + onChange(newData); + }, [data, onChange, minRows]); + + // 셀 값 변경 + const handleCellChange = useCallback((index: number, key: keyof T, value: T[keyof T]) => { + const newData = [...data]; + newData[index] = { ...newData[index], [key]: value }; + onChange(newData); + }, [data, onChange]); + + // 기본 셀 렌더링 + const renderCell = ( + column: EditableColumn, + row: T, + rowIndex: number + ) => { + const value = row[column.key]; + const isEditable = column.editable !== false; + + // 커스텀 렌더러가 있으면 사용 + if (column.render) { + return column.render(value, row, rowIndex, (newValue) => { + handleCellChange(rowIndex, column.key, newValue); + }); + } + + // 편집 불가능한 경우 텍스트로 표시 + if (!isEditable) { + return {String(value ?? '-')}; + } + + // 타입별 기본 렌더링 + switch (column.type) { + case 'number': + return ( + handleCellChange(rowIndex, column.key, Number(e.target.value) as T[keyof T])} + placeholder={column.placeholder} + className={cn('h-8', compact && 'h-7 text-sm')} + /> + ); + + case 'select': + return ( + + ); + + case 'text': + default: + return ( + handleCellChange(rowIndex, column.key, e.target.value as T[keyof T])} + placeholder={column.placeholder} + className={cn('h-8', compact && 'h-7 text-sm')} + /> + ); + } + }; + + const canAddRow = !maxRows || data.length < maxRows; + const canDeleteRow = data.length > minRows; + + return ( + + {title && ( + +
+ {title} + + {data.length}개 항목 + {maxRows && ` (최대 ${maxRows}개)`} + +
+
+ )} + + {/* 테이블 */} +
+ + + + {showRowNumber && ( + 번호 + )} + {columns.map((column) => ( + + {column.header} + + ))} + 삭제 + + + + {data.length === 0 ? ( + + + {emptyMessage} + + + ) : ( + data.map((row, rowIndex) => ( + + {showRowNumber && ( + + {rowIndex + 1} + + )} + {columns.map((column) => ( + + {renderCell(column, row, rowIndex)} + + ))} + + + + + )) + )} + +
+
+ + {/* 행 추가 버튼 */} +
+ {bulkAdd ? ( + <> + {/* 대량 추가: 숫자 입력 + 버튼 */} +
+ setBulkCount(Math.max(1, parseInt(e.target.value) || 1))} + className="w-20 h-9" + placeholder="개수" + /> + + +
+ {/* 단일 추가 버튼도 제공 */} + + + ) : ( + /* 단일 추가만 */ + + )} +
+
+
+ ); +} + +export default EditableTable; diff --git a/src/components/common/EditableTable/index.ts b/src/components/common/EditableTable/index.ts new file mode 100644 index 00000000..b21729e6 --- /dev/null +++ b/src/components/common/EditableTable/index.ts @@ -0,0 +1,2 @@ +export { EditableTable } from './EditableTable'; +export type { EditableColumn, EditableTableProps } from './EditableTable'; \ No newline at end of file diff --git a/src/components/customer-center/InquiryManagement/InquiryDetail.tsx b/src/components/customer-center/InquiryManagement/InquiryDetail.tsx index b59715b3..6b761037 100644 --- a/src/components/customer-center/InquiryManagement/InquiryDetail.tsx +++ b/src/components/customer-center/InquiryManagement/InquiryDetail.tsx @@ -70,11 +70,11 @@ export function InquiryDetail({ // ===== 액션 핸들러 ===== const handleBack = useCallback(() => { - router.push('/ko/customer-center/inquiries'); + router.push('/ko/customer-center/qna'); }, [router]); const handleEdit = useCallback(() => { - router.push(`/ko/customer-center/inquiries/${inquiry.id}/edit`); + router.push(`/ko/customer-center/qna/${inquiry.id}/edit`); }, [router, inquiry.id]); const handleConfirmDelete = useCallback(async () => { @@ -84,7 +84,7 @@ export function InquiryDetail({ const success = await onDeleteInquiry(); if (success) { setShowDeleteDialog(false); - router.push('/ko/customer-center/inquiries'); + router.push('/ko/customer-center/qna'); } } finally { setIsSubmitting(false); diff --git a/src/components/customer-center/InquiryManagement/InquiryForm.tsx b/src/components/customer-center/InquiryManagement/InquiryForm.tsx index 44e0181a..e5e3c765 100644 --- a/src/components/customer-center/InquiryManagement/InquiryForm.tsx +++ b/src/components/customer-center/InquiryManagement/InquiryForm.tsx @@ -115,7 +115,7 @@ export function InquiryForm({ mode, initialData }: InquiryFormProps) { } if (result?.success) { - router.push('/ko/customer-center/inquiries'); + router.push('/ko/customer-center/qna'); } else { // 에러 처리 console.error('Submit error:', result?.error); diff --git a/src/components/customer-center/InquiryManagement/InquiryList.tsx b/src/components/customer-center/InquiryManagement/InquiryList.tsx index 54383b9d..2bce0075 100644 --- a/src/components/customer-center/InquiryManagement/InquiryList.tsx +++ b/src/components/customer-center/InquiryManagement/InquiryList.tsx @@ -165,13 +165,13 @@ export function InquiryList() { const handleRowClick = useCallback( (item: Inquiry) => { - router.push(`/ko/customer-center/inquiries/${item.id}`); + router.push(`/ko/customer-center/qna/${item.id}`); }, [router] ); const handleCreate = useCallback(() => { - router.push('/ko/customer-center/inquiries/create'); + router.push('/ko/customer-center/qna/create'); }, [router]); // ===== 상태 Badge 색상 =====