fix: 1:1 문의 라우트 수정 및 빌드 오류 수정
- customer-center/inquiries → customer-center/qna 폴더명 변경 - InquiryManagement 컴포넌트 경로 참조 수정 - BadDebtDetail.tsx 중복 선언 오류 수정 - EditableTable 컴포넌트 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
332
src/components/common/EditableTable/EditableTable.tsx
Normal file
332
src/components/common/EditableTable/EditableTable.tsx
Normal file
@@ -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<T> {
|
||||
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<T extends { id: string | number }> {
|
||||
columns: EditableColumn<T>[];
|
||||
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<T extends { id: string | number }>({
|
||||
columns,
|
||||
data,
|
||||
onChange,
|
||||
createNewRow,
|
||||
title,
|
||||
addButtonLabel = '행 추가',
|
||||
emptyMessage = '데이터가 없습니다. 행을 추가해주세요.',
|
||||
showRowNumber = true,
|
||||
maxRows,
|
||||
minRows = 0,
|
||||
bulkAdd = false,
|
||||
bulkAddLabel = '추가',
|
||||
striped = false,
|
||||
compact = false,
|
||||
}: EditableTableProps<T>) {
|
||||
// 대량 추가 개수 상태
|
||||
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<T>,
|
||||
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 <span className="text-muted-foreground">{String(value ?? '-')}</span>;
|
||||
}
|
||||
|
||||
// 타입별 기본 렌더링
|
||||
switch (column.type) {
|
||||
case 'number':
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={value as number ?? ''}
|
||||
onChange={(e) => 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 (
|
||||
<select
|
||||
value={String(value ?? '')}
|
||||
onChange={(e) => handleCellChange(rowIndex, column.key, e.target.value as T[keyof T])}
|
||||
className={cn(
|
||||
'w-full h-8 px-2 rounded-md border border-input bg-background text-sm',
|
||||
compact && 'h-7'
|
||||
)}
|
||||
>
|
||||
<option value="">{column.placeholder || '선택'}</option>
|
||||
{column.options?.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
value={String(value ?? '')}
|
||||
onChange={(e) => 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 (
|
||||
<Card>
|
||||
{title && (
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">{title}</CardTitle>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{data.length}개 항목
|
||||
{maxRows && ` (최대 ${maxRows}개)`}
|
||||
</span>
|
||||
</div>
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardContent className={cn(!title && 'pt-6')}>
|
||||
{/* 테이블 */}
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{showRowNumber && (
|
||||
<TableHead className="w-[60px] text-center">번호</TableHead>
|
||||
)}
|
||||
{columns.map((column) => (
|
||||
<TableHead
|
||||
key={String(column.key)}
|
||||
className={cn(
|
||||
column.width && `w-[${column.width}]`,
|
||||
column.align === 'center' && 'text-center',
|
||||
column.align === 'right' && 'text-right'
|
||||
)}
|
||||
>
|
||||
{column.header}
|
||||
</TableHead>
|
||||
))}
|
||||
<TableHead className="w-[60px] text-center">삭제</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length + (showRowNumber ? 1 : 0) + 1}
|
||||
className="text-center py-8 text-muted-foreground"
|
||||
>
|
||||
{emptyMessage}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((row, rowIndex) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
className={cn(
|
||||
striped && rowIndex % 2 === 1 && 'bg-muted/30',
|
||||
compact && 'h-10'
|
||||
)}
|
||||
>
|
||||
{showRowNumber && (
|
||||
<TableCell className="text-center text-muted-foreground">
|
||||
{rowIndex + 1}
|
||||
</TableCell>
|
||||
)}
|
||||
{columns.map((column) => (
|
||||
<TableCell
|
||||
key={String(column.key)}
|
||||
className={cn(
|
||||
column.align === 'center' && 'text-center',
|
||||
column.align === 'right' && 'text-right',
|
||||
compact && 'py-1'
|
||||
)}
|
||||
>
|
||||
{renderCell(column, row, rowIndex)}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteRow(rowIndex)}
|
||||
disabled={!canDeleteRow}
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 행 추가 버튼 */}
|
||||
<div className="mt-4 flex gap-2">
|
||||
{bulkAdd ? (
|
||||
<>
|
||||
{/* 대량 추가: 숫자 입력 + 버튼 */}
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={maxRows ? maxRows - data.length : 100}
|
||||
value={bulkCount}
|
||||
onChange={(e) => setBulkCount(Math.max(1, parseInt(e.target.value) || 1))}
|
||||
className="w-20 h-9"
|
||||
placeholder="개수"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">개</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBulkAddRows}
|
||||
disabled={!canAddRow}
|
||||
className="h-9"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{bulkAddLabel}
|
||||
</Button>
|
||||
</div>
|
||||
{/* 단일 추가 버튼도 제공 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleAddRow}
|
||||
disabled={!canAddRow}
|
||||
className="h-9 border border-dashed"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
1개 추가
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
/* 단일 추가만 */
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddRow}
|
||||
disabled={!canAddRow}
|
||||
className="w-full border-dashed"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{addButtonLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableTable;
|
||||
2
src/components/common/EditableTable/index.ts
Normal file
2
src/components/common/EditableTable/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { EditableTable } from './EditableTable';
|
||||
export type { EditableColumn, EditableTableProps } from './EditableTable';
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 색상 =====
|
||||
|
||||
Reference in New Issue
Block a user