Files
sam-react-prod/src/components/items/DynamicItemForm/sections/DynamicTableSection.tsx
유병철 020d74f36c feat(WEB): DynamicItemForm 필드 타입 확장 및 컴포넌트 레지스트리 추가
- DynamicFieldRenderer에 신규 필드 타입 추가 (Currency, File, MultiSelect, Radio, Reference, Toggle, UnitValue, Computed)
- DynamicTableSection 및 TableCellRenderer 추가
- 필드 프리셋 및 설정 구조 분리
- 컴포넌트 레지스트리 개발 도구 페이지 추가
- UniversalListPage 개선
- 근태관리 코드 정리
- 즐겨찾기 기능 및 동적 필드 타입 백엔드 스펙 문서 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 11:17:57 +09:00

200 lines
6.6 KiB
TypeScript

/**
* DynamicTableSection
* config 기반 범용 테이블 섹션 (공정, 품질검사, 구매처, 공정표, 배차 등)
*
* API 연동 시: GET/PUT /v1/items/{itemId}/section-data/{sectionId}
* API 연동 전: rows를 폼 상태(formData)에 로컬 관리
*/
'use client';
import { useCallback } from 'react';
import { Plus, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { TableCellRenderer } from './TableCellRenderer';
import type { ItemSectionResponse } from '@/types/item-master-api';
import type { TableConfig } from '../types';
export interface DynamicTableSectionProps {
section: {
section: ItemSectionResponse;
orderNo: number;
};
tableConfig: TableConfig;
rows: Record<string, unknown>[];
onRowsChange: (rows: Record<string, unknown>[]) => void;
disabled?: boolean;
}
export default function DynamicTableSection({
section,
tableConfig,
rows,
onRowsChange,
disabled,
}: DynamicTableSectionProps) {
const { columns, minRows, maxRows, summaryRow } = tableConfig;
// 행 추가
const handleAddRow = useCallback(() => {
if (maxRows && rows.length >= maxRows) return;
const newRow: Record<string, unknown> = {};
columns.forEach(col => {
newRow[col.key] = null;
});
onRowsChange([...rows, newRow]);
}, [rows, columns, maxRows, onRowsChange]);
// 행 삭제
const handleRemoveRow = useCallback((index: number) => {
if (minRows && rows.length <= minRows) return;
onRowsChange(rows.filter((_, i) => i !== index));
}, [rows, minRows, onRowsChange]);
// 셀 값 변경
const handleCellChange = useCallback((rowIndex: number, columnKey: string, value: unknown) => {
const updated = rows.map((row, i) =>
i === rowIndex ? { ...row, [columnKey]: value } : row
);
onRowsChange(updated);
}, [rows, onRowsChange]);
// 요약행 계산
const computeSummary = (columnKey: string, type: string): string | number => {
const values = rows.map(r => Number(r[columnKey]) || 0);
switch (type) {
case 'sum': return values.reduce((a, b) => a + b, 0);
case 'avg': return values.length > 0
? Math.round(values.reduce((a, b) => a + b, 0) / values.length * 100) / 100
: 0;
case 'count': return rows.length;
default: return '';
}
};
const canAdd = !maxRows || rows.length < maxRows;
const canRemove = !minRows || rows.length > minRows;
return (
<Card>
<CardHeader className="pb-4 flex flex-row items-center justify-between">
<div>
<CardTitle className="text-base font-medium">
{section.section.title}
</CardTitle>
{section.section.description && (
<p className="text-xs text-muted-foreground mt-1">
{section.section.description}
</p>
)}
</div>
{!disabled && canAdd && (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddRow}
>
<Plus className="h-4 w-4 mr-1" />
</Button>
)}
</CardHeader>
<CardContent>
{rows.length === 0 ? (
<div className="text-center py-8 text-muted-foreground text-sm">
. &quot; &quot; .
</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12 text-center">#</TableHead>
{columns.map(col => (
<TableHead
key={col.key}
style={col.width ? { width: col.width } : undefined}
>
{col.label}
{col.isRequired && <span className="text-red-500"> *</span>}
</TableHead>
))}
{!disabled && <TableHead className="w-12" />}
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row, rowIndex) => (
<TableRow key={rowIndex}>
<TableCell className="text-center text-muted-foreground text-sm">
{rowIndex + 1}
</TableCell>
{columns.map(col => (
<TableCell key={col.key} className="p-1">
<TableCellRenderer
column={col}
value={row[col.key] as any}
onChange={(val) => handleCellChange(rowIndex, col.key, val)}
disabled={disabled}
rowIndex={rowIndex}
/>
</TableCell>
))}
{!disabled && (
<TableCell className="p-1 text-center">
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7"
disabled={!canRemove}
onClick={() => handleRemoveRow(rowIndex)}
>
<Trash2 className="h-3.5 w-3.5 text-muted-foreground" />
</Button>
</TableCell>
)}
</TableRow>
))}
{/* 요약행 */}
{summaryRow && summaryRow.length > 0 && (
<TableRow className="bg-muted/50 font-medium">
<TableCell className="text-center text-sm"></TableCell>
{columns.map(col => {
const summary = summaryRow.find(s => s.columnKey === col.key);
return (
<TableCell key={col.key} className="p-2 text-sm">
{summary
? summary.type === 'label'
? summary.label || ''
: computeSummary(col.key, summary.type)
: ''}
</TableCell>
);
})}
{!disabled && <TableCell />}
</TableRow>
)}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
);
}