519 lines
23 KiB
TypeScript
519 lines
23 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { X, Save, Trash2, Plus, RefreshCw } from 'lucide-react';
|
|
import { comms } from '@/communication';
|
|
import { PartListItem } from '@/types';
|
|
|
|
interface PartListDialogProps {
|
|
projectIdx: number;
|
|
projectName: string;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function PartListDialog({ projectIdx, projectName, onClose }: PartListDialogProps) {
|
|
const [parts, setParts] = useState<PartListItem[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
|
const [editForm, setEditForm] = useState<Partial<PartListItem>>({});
|
|
|
|
// ESC 키 핸들러
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
if (editingIdx !== null) {
|
|
setEditingIdx(null);
|
|
setEditForm({});
|
|
} else {
|
|
onClose();
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [editingIdx, onClose]);
|
|
|
|
// 데이터 로드
|
|
const loadParts = async () => {
|
|
setLoading(true);
|
|
try {
|
|
console.log('[PartList] 로드 시작, projectIdx:', projectIdx);
|
|
const result = await comms.getPartList(projectIdx);
|
|
console.log('[PartList] 결과:', result);
|
|
if (result.Success && result.Data) {
|
|
console.log('[PartList] 데이터 개수:', result.Data.length);
|
|
setParts(result.Data);
|
|
} else {
|
|
console.error('[PartList] 실패:', result.Message);
|
|
alert(result.Message || '파트리스트 로드 실패');
|
|
}
|
|
} catch (error) {
|
|
console.error('파트리스트 로드 실패:', error);
|
|
alert('파트리스트 로드 중 오류: ' + error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadParts();
|
|
}, [projectIdx]);
|
|
|
|
// 편집 시작
|
|
const startEdit = (part: PartListItem) => {
|
|
setEditingIdx(part.idx);
|
|
setEditForm({ ...part });
|
|
};
|
|
|
|
// 편집 취소
|
|
const cancelEdit = () => {
|
|
setEditingIdx(null);
|
|
setEditForm({});
|
|
};
|
|
|
|
// 저장
|
|
const handleSave = async () => {
|
|
if (!editForm.itemname || !editForm.item) {
|
|
alert('품명과 자재번호는 필수입니다.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await comms.savePartList(
|
|
editingIdx || 0,
|
|
projectIdx,
|
|
editForm.itemgroup || '',
|
|
editForm.itemname || '',
|
|
editForm.item || '',
|
|
editForm.itemmodel || '',
|
|
editForm.itemscale || '',
|
|
editForm.itemunit || '',
|
|
editForm.qty || 0,
|
|
editForm.price || 0,
|
|
editForm.itemsupply || '',
|
|
editForm.itemsupplyidx || 0,
|
|
editForm.itemmanu || '',
|
|
editForm.itemsid || '',
|
|
editForm.option1 || '',
|
|
editForm.remark || '',
|
|
editForm.no || 0,
|
|
editForm.qtybuy || 0
|
|
);
|
|
|
|
if (result.Success) {
|
|
await loadParts();
|
|
cancelEdit();
|
|
} else {
|
|
alert(result.Message || '저장 실패');
|
|
}
|
|
} catch (error) {
|
|
console.error('저장 실패:', error);
|
|
alert('저장 중 오류가 발생했습니다.');
|
|
}
|
|
};
|
|
|
|
// 삭제
|
|
const handleDelete = async (idx: number) => {
|
|
if (!confirm('정말 삭제하시겠습니까?')) return;
|
|
|
|
try {
|
|
const result = await comms.deletePartList(idx);
|
|
if (result.Success) {
|
|
await loadParts();
|
|
} else {
|
|
alert(result.Message || '삭제 실패');
|
|
}
|
|
} catch (error) {
|
|
console.error('삭제 실패:', error);
|
|
alert('삭제 중 오류가 발생했습니다.');
|
|
}
|
|
};
|
|
|
|
// 새 항목 추가
|
|
const addNew = () => {
|
|
setEditingIdx(-1);
|
|
setEditForm({
|
|
Project: projectIdx,
|
|
itemgroup: '',
|
|
itemname: '',
|
|
item: '',
|
|
itemmodel: '',
|
|
itemscale: '',
|
|
itemunit: 'EA',
|
|
qty: 1,
|
|
price: 0,
|
|
itemsupply: '',
|
|
itemsupplyidx: 0,
|
|
itemmanu: '',
|
|
itemsid: '',
|
|
option1: '',
|
|
remark: '',
|
|
no: 0,
|
|
qtybuy: 0,
|
|
});
|
|
};
|
|
|
|
// 금액 계산
|
|
const getAmount = (qty: number, price: number) => qty * price;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
|
<div className="dialog-container rounded-lg w-full max-w-7xl max-h-[90vh] flex flex-col overflow-hidden transition-all duration-300">
|
|
{/* 헤더 */}
|
|
<div className="dialog-header flex items-center justify-between p-4 sticky top-0 z-10">
|
|
<div>
|
|
<h2 className="dialog-title">파트리스트</h2>
|
|
<p className="text-sm text-white/60">{projectName}</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={addNew}
|
|
className="flex items-center gap-2 px-3 py-1.5 bg-primary-600 hover:bg-primary-500 text-white rounded transition-colors"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span className="text-sm">추가</span>
|
|
</button>
|
|
<button
|
|
onClick={loadParts}
|
|
disabled={loading}
|
|
className="p-2 hover:bg-white/10 rounded transition-colors disabled:opacity-50"
|
|
title="새로고침"
|
|
>
|
|
<RefreshCw className={`w-5 h-5 text-white/70 ${loading ? 'animate-spin' : ''}`} />
|
|
</button>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-white/10 rounded transition-colors"
|
|
>
|
|
<X className="w-5 h-5 text-white/70" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 테이블 */}
|
|
<div className="flex-1 overflow-auto p-4">
|
|
{loading && parts.length === 0 ? (
|
|
<div className="flex items-center justify-center h-64">
|
|
<RefreshCw className="w-8 h-8 text-primary-500 animate-spin" />
|
|
</div>
|
|
) : (
|
|
<table className="w-full border-collapse">
|
|
<thead className="sticky top-0 bg-slate-700/50 backdrop-blur z-10">
|
|
<tr className="border-b border-white/10">
|
|
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-12">No</th>
|
|
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-24">그룹</th>
|
|
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium">품명</th>
|
|
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-32">모델</th>
|
|
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-32">규격</th>
|
|
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-16">단위</th>
|
|
<th className="px-2 py-2 text-right text-xs text-white/70 font-medium w-20">수량</th>
|
|
<th className="px-2 py-2 text-right text-xs text-white/70 font-medium w-28">단가</th>
|
|
<th className="px-2 py-2 text-right text-xs text-white/70 font-medium w-32">금액</th>
|
|
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-32">공급처</th>
|
|
<th className="px-2 py-2 text-center text-xs text-white/70 font-medium w-20">작업</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{parts.length === 0 && !loading ? (
|
|
<tr>
|
|
<td colSpan={11} className="px-2 py-8 text-center text-white/40 text-sm">
|
|
등록된 파트가 없습니다.
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
parts.map((part) => {
|
|
const isEditing = editingIdx === part.idx;
|
|
return (
|
|
<tr
|
|
key={part.idx}
|
|
className={`border-b border-white/5 hover:bg-white/5 transition-colors ${isEditing ? 'bg-primary-500/10' : ''
|
|
}`}
|
|
>
|
|
<td className="px-2 py-2">
|
|
{isEditing ? (
|
|
<input
|
|
type="number"
|
|
value={editForm.no || 0}
|
|
onChange={(e) => setEditForm({ ...editForm, no: parseInt(e.target.value) || 0 })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
/>
|
|
) : (
|
|
<span className="text-white/70 text-xs">{part.no || ''}</span>
|
|
)}
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
{isEditing ? (
|
|
<input
|
|
type="text"
|
|
value={editForm.itemgroup || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemgroup: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
/>
|
|
) : (
|
|
<span className="text-white/70 text-xs">{part.itemgroup || ''}</span>
|
|
)}
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
{isEditing ? (
|
|
<input
|
|
type="text"
|
|
value={editForm.itemname || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemname: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
required
|
|
/>
|
|
) : (
|
|
<span className="text-white/90 text-xs font-medium">{part.itemname || ''}</span>
|
|
)}
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
{isEditing ? (
|
|
<input
|
|
type="text"
|
|
value={editForm.itemmodel || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemmodel: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
/>
|
|
) : (
|
|
<span className="text-white/70 text-xs">{part.itemmodel || ''}</span>
|
|
)}
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
{isEditing ? (
|
|
<input
|
|
type="text"
|
|
value={editForm.itemscale || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemscale: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
/>
|
|
) : (
|
|
<span className="text-white/70 text-xs">{part.itemscale || ''}</span>
|
|
)}
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
{isEditing ? (
|
|
<input
|
|
type="text"
|
|
value={editForm.itemunit || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemunit: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
/>
|
|
) : (
|
|
<span className="text-white/70 text-xs">{part.itemunit || ''}</span>
|
|
)}
|
|
</td>
|
|
<td className="px-2 py-2 text-right">
|
|
{isEditing ? (
|
|
<input
|
|
type="number"
|
|
value={editForm.qty || 0}
|
|
onChange={(e) => setEditForm({ ...editForm, qty: parseFloat(e.target.value) || 0 })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
|
/>
|
|
) : (
|
|
<span className="text-white/70 text-xs">{part.qty?.toLocaleString() || 0}</span>
|
|
)}
|
|
</td>
|
|
<td className="px-2 py-2 text-right">
|
|
{isEditing ? (
|
|
<input
|
|
type="number"
|
|
value={editForm.price || 0}
|
|
onChange={(e) => setEditForm({ ...editForm, price: parseFloat(e.target.value) || 0 })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
|
/>
|
|
) : (
|
|
<span className="text-white/70 text-xs">{part.price?.toLocaleString() || 0}</span>
|
|
)}
|
|
</td>
|
|
<td className="px-2 py-2 text-right">
|
|
<span className="text-white/90 text-xs font-medium">
|
|
{getAmount(
|
|
isEditing ? editForm.qty || 0 : part.qty || 0,
|
|
isEditing ? editForm.price || 0 : part.price || 0
|
|
).toLocaleString()}
|
|
</span>
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
{isEditing ? (
|
|
<input
|
|
type="text"
|
|
value={editForm.itemsupply || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemsupply: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
/>
|
|
) : (
|
|
<span className="text-white/70 text-xs">{part.itemsupply || ''}</span>
|
|
)}
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
{isEditing ? (
|
|
<div className="flex items-center justify-center gap-1">
|
|
<button
|
|
onClick={handleSave}
|
|
className="p-1 hover:bg-green-500/20 text-green-400 rounded transition-colors"
|
|
title="저장"
|
|
>
|
|
<Save className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={cancelEdit}
|
|
className="p-1 hover:bg-white/10 text-white/50 rounded transition-colors"
|
|
title="취소"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-center gap-1">
|
|
<button
|
|
onClick={() => startEdit(part)}
|
|
className="p-1 hover:bg-white/10 text-white/70 rounded transition-colors text-xs"
|
|
>
|
|
편집
|
|
</button>
|
|
<button
|
|
onClick={() => handleDelete(part.idx)}
|
|
className="p-1 hover:bg-red-500/20 text-red-400 rounded transition-colors"
|
|
title="삭제"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})
|
|
)}
|
|
{/* 새 항목 추가 행 */}
|
|
{editingIdx === -1 && (
|
|
<tr className="border-b border-white/5 bg-primary-500/10">
|
|
<td className="px-2 py-2">
|
|
<input
|
|
type="number"
|
|
value={editForm.no || 0}
|
|
onChange={(e) => setEditForm({ ...editForm, no: parseInt(e.target.value) || 0 })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
/>
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
<input
|
|
type="text"
|
|
value={editForm.itemgroup || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemgroup: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
placeholder="그룹"
|
|
/>
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
<input
|
|
type="text"
|
|
value={editForm.itemname || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemname: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
placeholder="품명 *"
|
|
required
|
|
/>
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
<input
|
|
type="text"
|
|
value={editForm.itemmodel || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemmodel: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
placeholder="모델"
|
|
/>
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
<input
|
|
type="text"
|
|
value={editForm.itemscale || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemscale: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
placeholder="규격"
|
|
/>
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
<input
|
|
type="text"
|
|
value={editForm.itemunit || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemunit: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
placeholder="단위"
|
|
/>
|
|
</td>
|
|
<td className="px-2 py-2 text-right">
|
|
<input
|
|
type="number"
|
|
value={editForm.qty || 0}
|
|
onChange={(e) => setEditForm({ ...editForm, qty: parseFloat(e.target.value) || 0 })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
|
/>
|
|
</td>
|
|
<td className="px-2 py-2 text-right">
|
|
<input
|
|
type="number"
|
|
value={editForm.price || 0}
|
|
onChange={(e) => setEditForm({ ...editForm, price: parseFloat(e.target.value) || 0 })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
|
/>
|
|
</td>
|
|
<td className="px-2 py-2 text-right">
|
|
<span className="text-white/90 text-xs font-medium">
|
|
{getAmount(editForm.qty || 0, editForm.price || 0).toLocaleString()}
|
|
</span>
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
<input
|
|
type="text"
|
|
value={editForm.itemsupply || ''}
|
|
onChange={(e) => setEditForm({ ...editForm, itemsupply: e.target.value })}
|
|
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
|
placeholder="공급처"
|
|
/>
|
|
</td>
|
|
<td className="px-2 py-2">
|
|
<div className="flex items-center justify-center gap-1">
|
|
<button
|
|
onClick={handleSave}
|
|
className="p-1 hover:bg-green-500/20 text-green-400 rounded transition-colors"
|
|
title="저장"
|
|
>
|
|
<Save className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={cancelEdit}
|
|
className="p-1 hover:bg-white/10 text-white/50 rounded transition-colors"
|
|
title="취소"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
|
|
{/* 합계 */}
|
|
{parts.length > 0 && (
|
|
<div className="dialog-footer p-4">
|
|
<div className="flex justify-end gap-4 text-sm">
|
|
<span className="text-white/70">
|
|
총 <span className="text-white font-medium">{parts.length}</span>개 항목
|
|
</span>
|
|
<span className="text-white/70">
|
|
합계: <span className="text-primary-400 font-medium">
|
|
{parts.reduce((sum, part) => sum + getAmount(part.qty || 0, part.price || 0), 0).toLocaleString()}
|
|
</span>원
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|