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:
byeongcheolryu
2025-12-29 15:42:17 +09:00
parent 8af838ab55
commit c749c09dea
10 changed files with 340 additions and 17 deletions

View File

@@ -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);

View 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;

View File

@@ -0,0 +1,2 @@
export { EditableTable } from './EditableTable';
export type { EditableColumn, EditableTableProps } from './EditableTable';

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 색상 =====