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

@@ -21,6 +21,7 @@ import {
import { Button } from "../ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { Badge } from "../ui/badge";
import { Alert, AlertDescription } from "../ui/alert";
import {
FileText,
Calculator,
@@ -30,6 +31,19 @@ import {
Sparkles,
} from "lucide-react";
import { toast } from "sonner";
// 필드명 매핑
const FIELD_NAME_MAP: Record<string, string> = {
clientId: "발주처",
productCategory: "제품 카테고리",
productName: "제품명",
openWidth: "오픈사이즈(W)",
openHeight: "오픈사이즈(H)",
guideRailType: "가이드레일 설치 유형",
motorPower: "모터 전원",
controller: "연동제어기",
quantity: "수량",
};
import {
ResponsiveFormTemplate,
FormSection,
@@ -226,10 +240,13 @@ export function QuoteRegistration({
const handleSubmit = async () => {
if (!validateForm()) {
toast.error("입력 내용을 확인해주세요.");
// 페이지 상단으로 스크롤
window.scrollTo({ top: 0, behavior: 'smooth' });
return;
}
// 에러 초기화
setErrors({});
setIsSaving(true);
try {
await onSave(formData);
@@ -353,6 +370,44 @@ export function QuoteRegistration({
saveDisabled={isSaving || isLoading}
maxWidth="2xl"
>
{/* Validation 에러 표시 */}
{Object.keys(errors).length > 0 && (
<Alert className="bg-red-50 border-red-200 mb-6">
<AlertDescription className="text-red-900">
<div className="flex items-start gap-2">
<span className="text-lg"></span>
<div className="flex-1">
<strong className="block mb-2">
({Object.keys(errors).length} )
</strong>
<ul className="space-y-1 text-sm">
{Object.entries(errors).map(([field, message]) => {
// item-0-productCategory 형태의 키에서 필드명 추출
const fieldParts = field.split("-");
let fieldName = field;
if (fieldParts.length === 3) {
const itemIndex = parseInt(fieldParts[1]) + 1;
const fieldKey = fieldParts[2];
fieldName = `견적 ${itemIndex} - ${FIELD_NAME_MAP[fieldKey] || fieldKey}`;
} else {
fieldName = FIELD_NAME_MAP[field] || field;
}
return (
<li key={field} className="flex items-start gap-1">
<span></span>
<span>
<strong>{fieldName}</strong>: {message}
</span>
</li>
);
})}
</ul>
</div>
</div>
</AlertDescription>
</Alert>
)}
{/* 1. 기본 정보 */}
<FormSection
title="기본 정보"