Files
sam-react-prod/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx
byeongcheolryu 387672b5b2 refactor(WEB): URL 경로 juil → construction 변경
- /juil/ 경로를 /construction/으로 변경
- 컴포넌트 폴더명 juil → construction 변경
- 컴포넌트명 Juil* → Construction* 변경
- 테스트 URL 페이지 경로 업데이트
- claudedocs 문서 경로 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 17:13:22 +09:00

601 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React from 'react';
import { X, HelpCircle } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import type { EstimateDetailItem } from '../types';
import { formatAmount, MOCK_MATERIALS } from '../utils';
import { calculateItemValuesWithApplied, calculateTotalsWithApplied } from '../hooks/useEstimateCalculations';
// 계산식 정보
const FORMULA_INFO: Record<string, string> = {
weight: '면적 × 25',
area: '(가로 × 0.16) × (세로 × 0.5)',
steelScreen: '면적 × 47,500',
caulking: '(세로 × 4) × 조정단가',
rail: '(세로 × 0.2) × 조정단가',
bottom: '가로 × 조정단가',
boxReinforce: '가로 × 조정단가',
shaft: '가로 × 조정단가',
unitPrice: '철제스크린 + 코킹 + 레일 + 하장 + 박스보강 + 샤프트 + 도장 + 모터 + 제어기 + 가로시공비 + 세로시공비',
expense: '단가 × 공과율',
cost: '단가 + 공과',
costExecution: '원가 × 수량',
marginCost: '원가 × 마진율(1.03)',
marginCostExecution: '마진원가 × 수량',
expenseExecution: '공과 × 수량',
};
// 계산식 툴팁이 있는 헤더 컴포넌트
function FormulaHeader({ label, formulaKey, className }: { label: string; formulaKey: string; className?: string }) {
const formula = FORMULA_INFO[formulaKey];
if (!formula) {
return <span>{label}</span>;
}
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<span className={`inline-flex items-center gap-1 cursor-help ${className || ''}`}>
{label}
<HelpCircle className="h-3.5 w-3.5 text-gray-400 hover:text-gray-600" />
</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<p className="text-xs font-medium text-gray-700">{label} </p>
<p className="text-xs text-gray-500 mt-1">{formula}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
// appliedPrices 타입 정의
export interface AppliedPrices {
caulking: number;
rail: number;
bottom: number;
boxReinforce: number;
shaft: number;
painting: number;
motor: number;
controller: number;
}
interface EstimateDetailTableSectionProps {
detailItems: EstimateDetailItem[];
appliedPrices: AppliedPrices | null;
isViewMode: boolean;
onAddItems: (count: number) => void;
onRemoveItem: (id: string) => void;
onRemoveSelected: () => void;
onItemChange: (id: string, field: keyof EstimateDetailItem, value: string | number) => void;
onSelectItem: (id: string, selected: boolean) => void;
onSelectAll: (selected: boolean) => void;
onApplyAdjustedPrice: () => void;
onReset: () => void;
}
export function EstimateDetailTableSection({
detailItems,
appliedPrices,
isViewMode,
onAddItems,
onRemoveItem,
onRemoveSelected,
onItemChange,
onSelectItem,
onSelectAll,
onApplyAdjustedPrice,
onReset,
}: EstimateDetailTableSectionProps) {
const selectedCount = detailItems.filter((item) => (item as unknown as { selected?: boolean }).selected).length;
const allSelected = detailItems.length > 0 && detailItems.every((item) => (item as unknown as { selected?: boolean }).selected);
const totals = calculateTotalsWithApplied(detailItems, appliedPrices);
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-4">
<div className="flex items-center gap-4">
<CardTitle className="text-lg whitespace-nowrap"> </CardTitle>
{!isViewMode && (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">{selectedCount} </span>
<Button
type="button"
variant="default"
size="sm"
className="bg-gray-900 hover:bg-gray-800"
onClick={onRemoveSelected}
>
</Button>
<Button
type="button"
variant="default"
size="sm"
className="bg-blue-500 hover:bg-blue-600"
onClick={onApplyAdjustedPrice}
>
</Button>
</div>
)}
</div>
{!isViewMode && (
<div className="flex items-center gap-2">
<Input
type="number"
min={1}
defaultValue={1}
className="w-16 text-center"
id="detail-add-count"
/>
<Button
type="button"
variant="default"
size="sm"
className="bg-gray-900 hover:bg-gray-800"
onClick={() => {
const countInput = document.getElementById('detail-add-count') as HTMLInputElement;
const count = Math.max(1, parseInt(countInput?.value || '1', 10));
onAddItems(count);
}}
>
</Button>
{/* TODO: 견적 상세 기획서 수정 후 초기화 버튼 및 테이블 항목/데이터 재작업 필요
<Button type="button" variant="outline" size="sm" onClick={onReset}>
초기화
</Button>
*/}
</div>
)}
</CardHeader>
<CardContent>
<div className="overflow-x-auto max-h-[600px]">
<Table>
<TableHeader className="sticky top-0 bg-white z-10">
<TableRow className="bg-gray-100">
{!isViewMode && (
<TableHead className="w-[40px] text-center sticky left-0 bg-gray-100 z-20">
<input
type="checkbox"
className="h-4 w-4 rounded border-gray-300"
checked={allSelected}
onChange={(e) => onSelectAll(e.target.checked)}
/>
</TableHead>
)}
<TableHead className="w-[100px] text-center"></TableHead>
<TableHead className="w-[80px] text-center"></TableHead>
<TableHead className="w-[70px] text-right"></TableHead>
<TableHead className="w-[70px] text-right"></TableHead>
<TableHead className="w-[70px] text-right">
<FormulaHeader label="무게" formulaKey="weight" />
</TableHead>
<TableHead className="w-[70px] text-right">
<FormulaHeader label="면적" formulaKey="area" />
</TableHead>
<TableHead className="w-[90px] text-right">
<FormulaHeader label="철제,스크린" formulaKey="steelScreen" />
</TableHead>
<TableHead className="w-[80px] text-right">
<FormulaHeader label="코킹" formulaKey="caulking" />
</TableHead>
<TableHead className="w-[80px] text-right">
<FormulaHeader label="레일" formulaKey="rail" />
</TableHead>
<TableHead className="w-[80px] text-right">
<FormulaHeader label="하장" formulaKey="bottom" />
</TableHead>
<TableHead className="w-[90px] text-right">
<FormulaHeader label="박스+보강" formulaKey="boxReinforce" />
</TableHead>
<TableHead className="w-[80px] text-right">
<FormulaHeader label="샤프트" formulaKey="shaft" />
</TableHead>
<TableHead className="w-[80px] text-center"></TableHead>
<TableHead className="w-[80px] text-center"></TableHead>
<TableHead className="w-[80px] text-center"></TableHead>
<TableHead className="w-[100px] text-center"></TableHead>
<TableHead className="w-[100px] text-center"></TableHead>
<TableHead className="w-[90px] text-right">
<FormulaHeader label="단가" formulaKey="unitPrice" />
</TableHead>
<TableHead className="w-[70px] text-right"></TableHead>
<TableHead className="w-[80px] text-right">
<FormulaHeader label="공과" formulaKey="expense" />
</TableHead>
<TableHead className="w-[50px] text-right"></TableHead>
<TableHead className="w-[90px] text-right">
<FormulaHeader label="원가" formulaKey="cost" />
</TableHead>
<TableHead className="w-[90px] text-right">
<FormulaHeader label="원가실행" formulaKey="costExecution" />
</TableHead>
<TableHead className="w-[90px] text-right">
<FormulaHeader label="마진원가" formulaKey="marginCost" />
</TableHead>
<TableHead className="w-[100px] text-right">
<FormulaHeader label="마진원가실행" formulaKey="marginCostExecution" />
</TableHead>
<TableHead className="w-[90px] text-right">
<FormulaHeader label="공과실행" formulaKey="expenseExecution" />
</TableHead>
{!isViewMode && <TableHead className="w-[50px] text-center"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{detailItems.length === 0 ? (
<TableRow>
<TableCell
colSpan={isViewMode ? 27 : 29}
className="text-center text-gray-500 py-8"
>
.
</TableCell>
</TableRow>
) : (
<>
{detailItems.map((item) => {
const values = calculateItemValuesWithApplied(item, appliedPrices);
return (
<TableRow key={item.id}>
{!isViewMode && (
<TableCell className="text-center sticky left-0 bg-white">
<input
type="checkbox"
className="h-4 w-4 rounded border-gray-300"
checked={(item as unknown as { selected?: boolean }).selected || false}
onChange={(e) => onSelectItem(item.id, e.target.checked)}
/>
</TableCell>
)}
{/* 01: 명칭 */}
<TableCell>
<Input
value={item.name}
onChange={(e) => onItemChange(item.id, 'name', e.target.value)}
disabled={isViewMode}
className={`w-full min-w-[80px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 02: 제품 */}
<TableCell>
<Select
value={item.material}
onValueChange={(val) => onItemChange(item.id, 'material', val)}
disabled={isViewMode}
>
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{MOCK_MATERIALS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
{/* 03: 가로 */}
<TableCell>
<Input
type="number"
step="0.01"
value={item.width}
onChange={(e) => onItemChange(item.id, 'width', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 04: 세로 */}
<TableCell>
<Input
type="number"
step="0.01"
value={item.height}
onChange={(e) => onItemChange(item.id, 'height', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 05: 무게 (인풋, 계산값 표시 + 수정 가능) */}
<TableCell>
<Input
type="number"
step="0.01"
value={values.weight.toFixed(2)}
onChange={(e) => onItemChange(item.id, 'calcWeight', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 06: 면적 (인풋, 계산값 표시 + 수정 가능) */}
<TableCell>
<Input
type="number"
step="0.01"
value={values.area.toFixed(2)}
onChange={(e) => onItemChange(item.id, 'calcArea', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 07: 철제,스크린 (인풋, 계산값 표시 + 수정 가능) */}
<TableCell>
<Input
type="number"
value={values.steelScreen}
onChange={(e) => onItemChange(item.id, 'calcSteelScreen', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[80px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 08: 코킹 (인풋, 계산값 표시 + 수정 가능) */}
<TableCell>
<Input
type="number"
value={values.caulking}
onChange={(e) => onItemChange(item.id, 'calcCaulking', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 09: 레일 (인풋, 계산값 표시 + 수정 가능) */}
<TableCell>
<Input
type="number"
value={values.rail}
onChange={(e) => onItemChange(item.id, 'calcRail', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 10: 하장 (인풋, 계산값 표시 + 수정 가능) */}
<TableCell>
<Input
type="number"
value={values.bottom}
onChange={(e) => onItemChange(item.id, 'calcBottom', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 11: 박스+보강 (인풋, 계산값 표시 + 수정 가능) */}
<TableCell>
<Input
type="number"
value={values.boxReinforce}
onChange={(e) => onItemChange(item.id, 'calcBoxReinforce', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[80px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 12: 샤프트 (인풋, 계산값 표시 + 수정 가능) */}
<TableCell>
<Input
type="number"
value={values.shaft}
onChange={(e) => onItemChange(item.id, 'calcShaft', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 13: 도장 */}
<TableCell>
<Select
value={String(item.coating || '')}
onValueChange={(val) => onItemChange(item.id, 'coating', Number(val))}
disabled={isViewMode}
>
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0"></SelectItem>
<SelectItem value="50000">A</SelectItem>
<SelectItem value="80000">B</SelectItem>
</SelectContent>
</Select>
</TableCell>
{/* 14: 모터 */}
<TableCell>
<Select
value={String(item.mounting || '300000')}
onValueChange={(val) => onItemChange(item.id, 'mounting', Number(val))}
disabled={isViewMode}
>
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="300000"> 300,000</SelectItem>
<SelectItem value="500000"> 500,000</SelectItem>
</SelectContent>
</Select>
</TableCell>
{/* 15: 제어기 */}
<TableCell>
<Select
value={String(item.controller || '')}
onValueChange={(val) => onItemChange(item.id, 'controller', Number(val))}
disabled={isViewMode}
>
<SelectTrigger className={`w-full min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="150000"> 150,000</SelectItem>
<SelectItem value="250000"> 250,000</SelectItem>
</SelectContent>
</Select>
</TableCell>
{/* 16: 가로시공비 */}
<TableCell>
<Select
value={String(item.widthConstruction || '')}
onValueChange={(val) => onItemChange(item.id, 'widthConstruction', Number(val))}
disabled={isViewMode}
>
<SelectTrigger className={`w-full min-w-[90px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="300000">3.01~4.0M</SelectItem>
<SelectItem value="400000">4.01~5.0M</SelectItem>
<SelectItem value="500000">5.01~6.0M</SelectItem>
<SelectItem value="600000">6.01~7.0M</SelectItem>
</SelectContent>
</Select>
</TableCell>
{/* 17: 세로시공비 */}
<TableCell>
<Select
value={String(item.heightConstruction || '')}
onValueChange={(val) => onItemChange(item.id, 'heightConstruction', Number(val))}
disabled={isViewMode}
>
<SelectTrigger className={`w-full min-w-[90px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="5000">3.51~4.5M</SelectItem>
<SelectItem value="8000">4.51~5.5M</SelectItem>
<SelectItem value="10000">5.51~6.5M</SelectItem>
</SelectContent>
</Select>
</TableCell>
{/* 18: 단가 (인풋, 계산값 표시 + 수정 가능) */}
<TableCell>
<Input
type="number"
value={values.unitPrice}
onChange={(e) => onItemChange(item.id, 'calcUnitPrice', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[80px] font-medium ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 19: 공과율 */}
<TableCell>
<Input
type="number"
step="0.01"
value={item.expense}
onChange={(e) => onItemChange(item.id, 'expense', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 20: 공과 (인풋, 계산값 표시 + 수정 가능) */}
<TableCell>
<Input
type="number"
value={values.expense}
onChange={(e) => onItemChange(item.id, 'calcExpense', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[70px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 21: 수량 */}
<TableCell>
<Input
type="number"
min={1}
value={item.quantity}
onChange={(e) => onItemChange(item.id, 'quantity', Number(e.target.value))}
disabled={isViewMode}
className={`text-right min-w-[40px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`}
/>
</TableCell>
{/* 22: 원가 */}
<TableCell className="text-right bg-gray-50">{formatAmount(values.cost)}</TableCell>
{/* 23: 원가실행 */}
<TableCell className="text-right bg-gray-50">{formatAmount(values.costExecution)}</TableCell>
{/* 24: 마진원가 */}
<TableCell className="text-right bg-gray-50">{formatAmount(values.marginCost)}</TableCell>
{/* 25: 마진원가실행 */}
<TableCell className="text-right bg-gray-50 font-medium">{formatAmount(values.marginCostExecution)}</TableCell>
{/* 26: 공과실행 */}
<TableCell className="text-right bg-gray-50">{formatAmount(values.expenseExecution)}</TableCell>
{!isViewMode && (
<TableCell className="text-center">
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 text-red-500 hover:text-red-600"
onClick={() => onRemoveItem(item.id)}
>
<X className="h-4 w-4" />
</Button>
</TableCell>
)}
</TableRow>
);
})}
{/* 합계 행 */}
{detailItems.length > 0 && (
<TableRow className="bg-orange-50 font-medium border-t-2 border-orange-300">
{!isViewMode && <TableCell className="sticky left-0 bg-orange-50"></TableCell>}
<TableCell colSpan={4} className="text-center font-bold"></TableCell>
<TableCell className="text-right">{totals.weight.toFixed(2)}</TableCell>
<TableCell className="text-right">{totals.area.toFixed(2)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.steelScreen)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.caulking)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.rail)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.bottom)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.boxReinforce)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.shaft)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.painting)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.motor)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.controller)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.widthConstruction)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.heightConstruction)}</TableCell>
<TableCell className="text-right font-bold">{formatAmount(totals.unitPrice)}</TableCell>
<TableCell className="text-right">-</TableCell>
<TableCell className="text-right">{formatAmount(totals.expense)}</TableCell>
<TableCell className="text-right">{totals.quantity}</TableCell>
<TableCell className="text-right">{formatAmount(totals.cost)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.costExecution)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.marginCost)}</TableCell>
<TableCell className="text-right font-bold">{formatAmount(totals.marginCostExecution)}</TableCell>
<TableCell className="text-right">{formatAmount(totals.expenseExecution)}</TableCell>
{!isViewMode && <TableCell></TableCell>}
</TableRow>
)}
</>
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
);
}