feat: 생산/품질/자재/출고/주문 관리 페이지 구현

- 생산관리: 대시보드, 작업지시, 작업실적, 작업자화면
- 품질관리: 검사관리 (리스트/등록/상세)
- 자재관리: 입고관리, 재고현황
- 출고관리: 출하관리 (리스트/등록/상세/수정)
- 주문관리: 수주관리, 생산의뢰
- 기존 컴포넌트 개선: CardTransactionInquiry, VendorDetail, QuoteRegistration
- IntegratedListTemplateV2 개선
- 공통 컴포넌트 분석 문서 추가

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-23 21:13:07 +09:00
parent 2ebcea0255
commit f0e8e51d06
108 changed files with 21156 additions and 84 deletions

View File

@@ -0,0 +1,312 @@
"use client";
/**
* 품목 추가 팝업
*
* 수주 등록 시 품목을 수동으로 추가하는 다이얼로그
*/
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Package } from "lucide-react";
// 품목 타입
export interface OrderItem {
id: string;
itemCode: string; // 품목코드
itemName: string; // 품명
type: string; // 층
symbol: string; // 부호
spec: string; // 규격
width: number; // 가로 (mm)
height: number; // 세로 (mm)
quantity: number; // 수량
unit: string; // 단위
unitPrice: number; // 단가
amount: number; // 금액
guideRailType?: string; // 가이드레일 타입
finish?: string; // 마감
floor?: string; // 층
isFromQuotation?: boolean; // 견적에서 가져온 품목 여부
}
interface ItemAddDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onAdd: (item: OrderItem) => void;
}
// 가이드레일 타입 옵션
const GUIDE_RAIL_TYPES = [
{ value: "back-120-70", label: "백면형 (120-70)" },
{ value: "back-150-70", label: "백면형 (150-70)" },
{ value: "side-120-70", label: "측면형 (120-70)" },
{ value: "side-150-70", label: "측면형 (150-70)" },
];
// 마감 옵션
const FINISH_OPTIONS = [
{ value: "sus", label: "SUS마감" },
{ value: "powder", label: "분체도장" },
{ value: "paint", label: "일반도장" },
];
// 초기 폼 데이터
const INITIAL_FORM = {
floor: "",
symbol: "",
itemName: "",
width: "",
height: "",
guideRailType: "",
finish: "",
unitPrice: "",
};
export function ItemAddDialog({
open,
onOpenChange,
onAdd,
}: ItemAddDialogProps) {
const [form, setForm] = useState(INITIAL_FORM);
const [errors, setErrors] = useState<Record<string, string>>({});
// 폼 리셋
const resetForm = () => {
setForm(INITIAL_FORM);
setErrors({});
};
// 다이얼로그 닫기 시 리셋
const handleOpenChange = (open: boolean) => {
if (!open) {
resetForm();
}
onOpenChange(open);
};
// 유효성 검사
const validate = (): boolean => {
const newErrors: Record<string, string> = {};
if (!form.floor.trim()) {
newErrors.floor = "층을 입력해주세요";
}
if (!form.symbol.trim()) {
newErrors.symbol = "도면부호를 입력해주세요";
}
if (!form.width || Number(form.width) <= 0) {
newErrors.width = "가로 치수를 입력해주세요";
}
if (!form.height || Number(form.height) <= 0) {
newErrors.height = "세로 치수를 입력해주세요";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 추가 핸들러
const handleAdd = () => {
if (!validate()) return;
const width = Number(form.width);
const height = Number(form.height);
const unitPrice = Number(form.unitPrice) || 0;
const newItem: OrderItem = {
id: `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
itemCode: `PRD-${Date.now().toString().slice(-4)}`,
itemName: form.itemName || "국민방화스크린세터",
type: "B1",
symbol: form.symbol,
spec: `${width}×${height}`,
width,
height,
quantity: 1,
unit: "EA",
unitPrice,
amount: unitPrice,
guideRailType: form.guideRailType,
finish: form.finish,
floor: form.floor,
};
onAdd(newItem);
handleOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Package className="h-5 w-5" />
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
{/* 층 / 도면부호 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="floor">
<span className="text-red-500">*</span>
</Label>
<Input
id="floor"
placeholder="예: 4층"
value={form.floor}
onChange={(e) => setForm({ ...form, floor: e.target.value })}
/>
{errors.floor && (
<p className="text-xs text-red-500">{errors.floor}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="symbol">
<span className="text-red-500">*</span>
</Label>
<Input
id="symbol"
placeholder="예: FSS1"
value={form.symbol}
onChange={(e) => setForm({ ...form, symbol: e.target.value })}
/>
{errors.symbol && (
<p className="text-xs text-red-500">{errors.symbol}</p>
)}
</div>
</div>
{/* 품목명 */}
<div className="space-y-2">
<Label htmlFor="itemName"></Label>
<Input
id="itemName"
placeholder="예: 국민방화스크린세터"
value={form.itemName}
onChange={(e) => setForm({ ...form, itemName: e.target.value })}
/>
</div>
{/* 오픈사이즈 (고객 제공 치수) */}
<div className="space-y-2">
<Label className="text-muted-foreground text-sm">
( )
</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="width">
(mm) <span className="text-red-500">*</span>
</Label>
<Input
id="width"
type="number"
placeholder="예: 7260"
value={form.width}
onChange={(e) => setForm({ ...form, width: e.target.value })}
/>
{errors.width && (
<p className="text-xs text-red-500">{errors.width}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="height">
(mm) <span className="text-red-500">*</span>
</Label>
<Input
id="height"
type="number"
placeholder="예: 2600"
value={form.height}
onChange={(e) => setForm({ ...form, height: e.target.value })}
/>
{errors.height && (
<p className="text-xs text-red-500">{errors.height}</p>
)}
</div>
</div>
</div>
{/* 가이드레일 타입 / 마감 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> </Label>
<Select
value={form.guideRailType}
onValueChange={(value) =>
setForm({ ...form, guideRailType: value })
}
>
<SelectTrigger>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{GUIDE_RAIL_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Select
value={form.finish}
onValueChange={(value) => setForm({ ...form, finish: value })}
>
<SelectTrigger>
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{FINISH_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 단가 */}
<div className="space-y-2">
<Label htmlFor="unitPrice"> ()</Label>
<Input
id="unitPrice"
type="number"
placeholder="예: 8000000"
value={form.unitPrice}
onChange={(e) => setForm({ ...form, unitPrice: e.target.value })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
</Button>
<Button onClick={handleAdd}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}