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>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 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">
|
||||
데이터가 없습니다. "행 추가" 버튼으로 항목을 추가하세요.
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user