261 lines
9.4 KiB
TypeScript
261 lines
9.4 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { Plus, Trash2, Eraser, ChevronUp, ChevronDown } from 'lucide-react';
|
|
import {
|
|
type BendingData,
|
|
createEmptyBendingData,
|
|
calculateRateResult,
|
|
recalculateSums,
|
|
getWidthSum,
|
|
getBendCount,
|
|
} from './types';
|
|
|
|
interface BendingTableProps {
|
|
data: BendingData[];
|
|
onChange: (data: BendingData[]) => void;
|
|
readOnly?: boolean;
|
|
showActions?: boolean;
|
|
showSummary?: boolean;
|
|
}
|
|
|
|
export function BendingTable({
|
|
data,
|
|
onChange,
|
|
readOnly = false,
|
|
showActions = true,
|
|
showSummary = true,
|
|
}: BendingTableProps) {
|
|
const handleInputChange = useCallback(
|
|
(colIndex: number, value: number) => {
|
|
const updated = [...data];
|
|
updated[colIndex] = { ...updated[colIndex], input: isNaN(value) ? 0 : value };
|
|
onChange(recalculateSums(updated));
|
|
},
|
|
[data, onChange]
|
|
);
|
|
|
|
const handleRateChange = useCallback(
|
|
(colIndex: number, value: string) => {
|
|
const cleaned = value.trim();
|
|
// 빈값 또는 숫자(음수 포함) 허용, 입력 중간 '-' 허용
|
|
if (cleaned !== '' && cleaned !== '-' && isNaN(Number(cleaned))) return;
|
|
const updated = [...data];
|
|
updated[colIndex] = { ...updated[colIndex], rate: cleaned };
|
|
if (cleaned === '' || cleaned === '-') {
|
|
onChange(updated);
|
|
} else {
|
|
onChange(recalculateSums(updated));
|
|
}
|
|
},
|
|
[data, onChange]
|
|
);
|
|
|
|
const handleCheckChange = useCallback(
|
|
(colIndex: number, field: 'color' | 'aAngle', checked: boolean) => {
|
|
const updated = [...data];
|
|
updated[colIndex] = { ...updated[colIndex], [field]: checked };
|
|
onChange(updated);
|
|
},
|
|
[data, onChange]
|
|
);
|
|
|
|
const addColumn = useCallback(() => {
|
|
const newData = [...data, createEmptyBendingData(data.length + 1)];
|
|
onChange(recalculateSums(newData));
|
|
}, [data, onChange]);
|
|
|
|
const removeLastColumn = useCallback(() => {
|
|
if (data.length === 0) return;
|
|
const newData = data.slice(0, -1);
|
|
onChange(recalculateSums(newData));
|
|
}, [data, onChange]);
|
|
|
|
const clearAll = useCallback(() => {
|
|
const cleared = data.map((d, i) => createEmptyBendingData(i + 1));
|
|
onChange(recalculateSums(cleared));
|
|
}, [data, onChange]);
|
|
|
|
const widthSum = getWidthSum(data);
|
|
const bendCount = getBendCount(data);
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
{/* 헤더 */}
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold">절곡 입력 테이블</h3>
|
|
{showActions && !readOnly && (
|
|
<div className="flex gap-1">
|
|
<Button type="button" size="sm" variant="outline" onClick={addColumn}>
|
|
<Plus className="h-3.5 w-3.5 mr-1" />열 추가
|
|
</Button>
|
|
<Button type="button" size="sm" variant="destructive" onClick={removeLastColumn}>
|
|
<Trash2 className="h-3.5 w-3.5 mr-1" />열 삭제
|
|
</Button>
|
|
<Button type="button" size="sm" variant="secondary" onClick={clearAll}>
|
|
<Eraser className="h-3.5 w-3.5 mr-1" />비우기
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 가로형 테이블 */}
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm border-collapse">
|
|
<thead>
|
|
<tr className="bg-muted/50">
|
|
<th className="border px-2 py-1.5 text-center w-[70px] font-medium">구분</th>
|
|
{data.map((d) => (
|
|
<th key={d.no} className="border px-2 py-1.5 text-center min-w-[70px] font-medium">
|
|
{d.no}
|
|
</th>
|
|
))}
|
|
{!readOnly && data.length > 0 && (
|
|
<th className="border px-1 py-1.5 w-[36px]">
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-6 w-6 p-0"
|
|
onClick={addColumn}
|
|
>
|
|
<Plus className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</th>
|
|
)}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{/* 입력 */}
|
|
<tr>
|
|
<td className="border px-2 py-1 text-center font-medium bg-muted/30">입력</td>
|
|
{data.map((d, i) => (
|
|
<td key={i} className="border px-1 py-1 bg-yellow-50">
|
|
{readOnly ? (
|
|
<span className="block text-center">{d.input || ''}</span>
|
|
) : (
|
|
<Input
|
|
type="number"
|
|
value={d.input || ''}
|
|
onChange={(e) => handleInputChange(i, Number(e.target.value))}
|
|
className="h-7 text-center px-1 border-0 bg-transparent"
|
|
/>
|
|
)}
|
|
</td>
|
|
))}
|
|
{!readOnly && data.length > 0 && <td className="border" />}
|
|
</tr>
|
|
|
|
{/* 연신율 */}
|
|
<tr>
|
|
<td className="border px-2 py-1 text-center font-medium bg-muted/30">연신율</td>
|
|
{data.map((d, i) => (
|
|
<td key={i} className="border px-1 py-1">
|
|
{readOnly ? (
|
|
<span className="block text-center">{d.rate ?? ''}</span>
|
|
) : (
|
|
<div className="flex items-center justify-center gap-0.5">
|
|
<button
|
|
type="button"
|
|
className="h-6 w-6 flex items-center justify-center rounded hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
onClick={() => handleRateChange(i, String((Number(d.rate) || 0) + 1))}
|
|
>
|
|
<ChevronUp className="h-3.5 w-3.5" />
|
|
</button>
|
|
<span className="w-5 text-center text-sm font-medium">{d.rate || ''}</span>
|
|
<button
|
|
type="button"
|
|
className="h-6 w-6 flex items-center justify-center rounded hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
onClick={() => handleRateChange(i, String((Number(d.rate) || 0) - 1))}
|
|
>
|
|
<ChevronDown className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</td>
|
|
))}
|
|
{!readOnly && data.length > 0 && <td className="border" />}
|
|
</tr>
|
|
|
|
{/* 연신율 후 (자동 계산) */}
|
|
<tr>
|
|
<td className="border px-2 py-1 text-center font-medium bg-muted/30">연신율 후</td>
|
|
{data.map((d, i) => (
|
|
<td key={i} className="border px-1 py-1 text-center text-muted-foreground">
|
|
{d.input ? (calculateRateResult(d.input, d.rate) || '-') : '-'}
|
|
</td>
|
|
))}
|
|
{!readOnly && data.length > 0 && <td className="border" />}
|
|
</tr>
|
|
|
|
{/* 합계 (누적) */}
|
|
<tr className="bg-amber-50">
|
|
<td className="border px-2 py-1 text-center font-semibold bg-amber-100">합계</td>
|
|
{data.map((d, i) => {
|
|
const isLast = i === data.length - 1;
|
|
return (
|
|
<td
|
|
key={i}
|
|
className={`border px-1 py-1 text-center font-semibold ${
|
|
d.color ? 'text-red-600' : ''
|
|
} ${isLast ? 'text-blue-600' : ''}`}
|
|
>
|
|
{d.sum != null && !isNaN(d.sum) ? d.sum : '-'}
|
|
</td>
|
|
);
|
|
})}
|
|
{!readOnly && data.length > 0 && <td className="border" />}
|
|
</tr>
|
|
|
|
{/* 음영 */}
|
|
<tr>
|
|
<td className="border px-2 py-1 text-center font-medium bg-muted/30">음영</td>
|
|
{data.map((d, i) => (
|
|
<td key={i} className="border px-1 py-1 text-center">
|
|
<Checkbox
|
|
checked={d.color}
|
|
onCheckedChange={(checked) =>
|
|
handleCheckChange(i, 'color', checked === true)
|
|
}
|
|
disabled={readOnly}
|
|
/>
|
|
</td>
|
|
))}
|
|
{!readOnly && data.length > 0 && <td className="border" />}
|
|
</tr>
|
|
|
|
{/* A각 */}
|
|
<tr>
|
|
<td className="border px-2 py-1 text-center font-medium bg-muted/30">A각</td>
|
|
{data.map((d, i) => (
|
|
<td key={i} className="border px-1 py-1 text-center">
|
|
<Checkbox
|
|
checked={d.aAngle}
|
|
onCheckedChange={(checked) =>
|
|
handleCheckChange(i, 'aAngle', checked === true)
|
|
}
|
|
disabled={readOnly}
|
|
/>
|
|
</td>
|
|
))}
|
|
{!readOnly && data.length > 0 && <td className="border" />}
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* 합계 표시 */}
|
|
{showSummary && (
|
|
<div className="text-sm">
|
|
폭합계: <span className="font-bold text-blue-600">{widthSum}</span>
|
|
<span className="mx-2">|</span>
|
|
절곡횟수: <span className="font-bold text-blue-600">{bendCount}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|