Files
sam-react-prod/src/components/production/WorkOrders/WipProductionModal.tsx
유병철 81affdc441 feat: ESLint 정리 및 전체 코드 품질 개선
- eslint.config.mjs 규칙 강화 및 정리
- 전역 unused import/변수 제거 (312개 파일)
- next.config.ts, middleware, proxy route 개선
- CopyableCell molecule 추가
- 회계/결재/HR/생산/건설/품질/영업 등 전 도메인 lint 정리
- IntegratedListTemplateV2, DataTable, MobileCard 등 공통 컴포넌트 개선
- execute-server-action 에러 핸들링 보강
2026-03-11 10:27:10 +09:00

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>
);
}