- eslint.config.mjs 규칙 강화 및 정리 - 전역 unused import/변수 제거 (312개 파일) - next.config.ts, middleware, proxy route 개선 - CopyableCell molecule 추가 - 회계/결재/HR/생산/건설/품질/영업 등 전 도메인 lint 정리 - IntegratedListTemplateV2, DataTable, MobileCard 등 공통 컴포넌트 개선 - execute-server-action 에러 핸들링 보강
272 lines
10 KiB
TypeScript
272 lines
10 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 재공품 생산 모달
|
|
*
|
|
* 기획서 기준 (스크린샷 2026-02-05 오후 6.59.14):
|
|
* - 품목 선택: 검색창 + 검색 결과 테이블 (바로 표시)
|
|
* - 테이블: 품목코드, 품목명, 규격, 단위, 재고량, 안전재고, 수량(입력)
|
|
* - "총 N건" 표시
|
|
* - 우선순위: 긴급(검정)/우선(검정)/일반(주황) 토글
|
|
* - 부서 Select (디폴트: 생산부서)
|
|
* - 비고 Textarea
|
|
* - 하단: 취소(검정) / 생산지시 확정(주황)
|
|
*/
|
|
|
|
import { useState, useCallback, useMemo } from 'react';
|
|
import { Search } from 'lucide-react';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { toast } from 'sonner';
|
|
|
|
// 재공품 아이템 타입
|
|
interface WipItem {
|
|
id: string;
|
|
itemCode: string;
|
|
itemName: string;
|
|
specification: string;
|
|
unit: string;
|
|
stockQuantity: number;
|
|
safetyStock: number;
|
|
quantity: number; // 사용자 입력
|
|
}
|
|
|
|
// 우선순위
|
|
type Priority = '긴급' | '우선' | '일반';
|
|
|
|
// Mock 재공품 데이터 (품목관리 > 재공품/사용 상태 '사용' 품목)
|
|
const MOCK_WIP_ITEMS: Omit<WipItem, 'quantity'>[] = [
|
|
{ id: 'wip-1', itemCode: 'WIP-GR-001', itemName: '가이드레일(벽면형)', specification: '120X70 EGI 1.6T', unit: 'EA', stockQuantity: 100, safetyStock: 30 },
|
|
{ id: 'wip-2', itemCode: 'WIP-CS-001', itemName: '케이스(500X380)', specification: '500X380 EGI 1.6T', unit: 'EA', stockQuantity: 100, safetyStock: 30 },
|
|
{ id: 'wip-3', itemCode: 'WIP-BF-001', itemName: '하단마감재(60X40)', specification: '60X40 EGI 1.6T', unit: 'EA', stockQuantity: 100, safetyStock: 30 },
|
|
{ id: 'wip-4', itemCode: 'WIP-LB-001', itemName: '하단L-BAR(17X60)', specification: '17X60 EGI 1.6T', unit: 'EA', stockQuantity: 100, safetyStock: 30 },
|
|
{ id: 'wip-5', itemCode: 'WIP-TB-001', itemName: '상단L-BAR(17X50)', specification: '17X50 EGI 1.6T', unit: 'EA', stockQuantity: 100, safetyStock: 30 },
|
|
{ id: 'wip-6', itemCode: 'WIP-EL-001', itemName: '엘바(16I75)', specification: '16I75 EGI 1.6T', unit: 'EA', stockQuantity: 100, safetyStock: 30 },
|
|
{ id: 'wip-7', itemCode: 'WIP-HJ-001', itemName: '하장바(A각)', specification: '16|75|16|75|16 EGI 1.6T', unit: 'EA', stockQuantity: 100, safetyStock: 30 },
|
|
];
|
|
|
|
interface WipProductionModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
export function WipProductionModal({ open, onOpenChange }: WipProductionModalProps) {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [itemQuantities, setItemQuantities] = useState<Record<string, number>>({});
|
|
const [priority, setPriority] = useState<Priority>('일반');
|
|
const [department, setDepartment] = useState('생산부서');
|
|
const [note, setNote] = useState('');
|
|
|
|
// 검색 결과 필터링 (검색어 없으면 전체 표시)
|
|
const filteredItems = useMemo(() => {
|
|
if (!searchTerm.trim()) {
|
|
return MOCK_WIP_ITEMS;
|
|
}
|
|
return MOCK_WIP_ITEMS.filter(
|
|
(item) =>
|
|
item.itemCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
item.itemName.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
}, [searchTerm]);
|
|
|
|
// 수량 변경
|
|
const handleQuantityChange = useCallback((id: string, value: string) => {
|
|
const qty = parseInt(value) || 0;
|
|
setItemQuantities((prev) => ({
|
|
...prev,
|
|
[id]: qty,
|
|
}));
|
|
}, []);
|
|
|
|
// 생산지시 확정
|
|
const handleConfirm = useCallback(() => {
|
|
const itemsWithQuantity = filteredItems.filter((item) => (itemQuantities[item.id] || 0) > 0);
|
|
|
|
if (itemsWithQuantity.length === 0) {
|
|
toast.error('수량을 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
// TODO: API 연동
|
|
toast.success(`재공품 생산지시가 확정되었습니다. (${itemsWithQuantity.length}건)`);
|
|
handleReset();
|
|
onOpenChange(false);
|
|
}, [filteredItems, itemQuantities, onOpenChange]);
|
|
|
|
// 초기화
|
|
const handleReset = useCallback(() => {
|
|
setSearchTerm('');
|
|
setItemQuantities({});
|
|
setPriority('일반');
|
|
setDepartment('생산부서');
|
|
setNote('');
|
|
}, []);
|
|
|
|
const handleCancel = useCallback(() => {
|
|
handleReset();
|
|
onOpenChange(false);
|
|
}, [handleReset, onOpenChange]);
|
|
|
|
const priorityOptions: Priority[] = ['긴급', '우선', '일반'];
|
|
|
|
// 선택된 버튼은 각각 다른 색상, 미선택은 회색 outline
|
|
const getPriorityStyle = (opt: Priority) => {
|
|
const isSelected = priority === opt;
|
|
if (isSelected) {
|
|
if (opt === '긴급') return 'bg-red-500 text-white hover:bg-red-600';
|
|
if (opt === '우선') return 'bg-amber-500 text-white hover:bg-amber-600';
|
|
return 'bg-orange-400 text-white hover:bg-orange-500'; // 일반
|
|
}
|
|
return 'bg-gray-200 text-gray-700 hover:bg-gray-300';
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>재공품 생산</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-5">
|
|
{/* 품목 선택 섹션 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-sm font-medium">품목 선택</Label>
|
|
|
|
{/* 검색창 */}
|
|
<div className="border rounded-lg p-4 space-y-3">
|
|
<div className="relative">
|
|
<Input
|
|
placeholder="검색..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pr-10"
|
|
/>
|
|
<Search className="absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
|
|
</div>
|
|
|
|
{/* 총 건수 */}
|
|
<p className="text-sm">
|
|
총 <span className="font-bold">{filteredItems.length}</span>건
|
|
</p>
|
|
|
|
{/* 품목 테이블 */}
|
|
<div className="border rounded-md overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="bg-muted">
|
|
<th className="px-3 py-2 text-center font-medium text-xs">품목코드</th>
|
|
<th className="px-3 py-2 text-center font-medium text-xs">품목명</th>
|
|
<th className="px-3 py-2 text-center font-medium text-xs">규격</th>
|
|
<th className="px-3 py-2 text-center font-medium text-xs">단위</th>
|
|
<th className="px-3 py-2 text-center font-medium text-xs">재고량</th>
|
|
<th className="px-3 py-2 text-center font-medium text-xs">안전재고</th>
|
|
<th className="px-3 py-2 text-center font-medium text-xs w-24">수량</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredItems.map((item) => (
|
|
<tr key={item.id} className="border-t hover:bg-gray-50">
|
|
<td className="px-3 py-2 text-center text-xs">{item.itemCode}</td>
|
|
<td className="px-3 py-2 text-center">{item.itemName}</td>
|
|
<td className="px-3 py-2 text-center text-xs text-muted-foreground">{item.specification}</td>
|
|
<td className="px-3 py-2 text-center">{item.unit}</td>
|
|
<td className="px-3 py-2 text-center">{item.stockQuantity}</td>
|
|
<td className="px-3 py-2 text-center">{item.safetyStock}</td>
|
|
<td className="px-3 py-2">
|
|
<Input
|
|
type="number"
|
|
min={0}
|
|
value={itemQuantities[item.id] || ''}
|
|
onChange={(e) => handleQuantityChange(item.id, e.target.value)}
|
|
className="h-8 text-center text-sm"
|
|
placeholder=""
|
|
/>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 우선순위 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">우선순위</Label>
|
|
<div className="flex gap-2">
|
|
{priorityOptions.map((opt) => (
|
|
<Button
|
|
key={opt}
|
|
size="sm"
|
|
className={`flex-1 ${getPriorityStyle(opt)}`}
|
|
onClick={() => setPriority(opt)}
|
|
>
|
|
{opt}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 부서 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">부서</Label>
|
|
<Select value={department} onValueChange={setDepartment}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="생산부서">생산부서</SelectItem>
|
|
<SelectItem value="품질관리부">품질관리부</SelectItem>
|
|
<SelectItem value="자재부">자재부</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 비고 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-sm font-medium">비고</Label>
|
|
<Textarea
|
|
value={note}
|
|
onChange={(e) => setNote(e.target.value)}
|
|
placeholder=""
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="mt-4">
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleCancel}
|
|
className=""
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleConfirm}
|
|
className="bg-orange-400 hover:bg-orange-500"
|
|
>
|
|
생산지시 확정
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|