diff --git a/src/components/material/StockStatus/StockStatusDetail.tsx b/src/components/material/StockStatus/StockStatusDetail.tsx index a600c76e..5781d638 100644 --- a/src/components/material/StockStatus/StockStatusDetail.tsx +++ b/src/components/material/StockStatus/StockStatusDetail.tsx @@ -44,6 +44,7 @@ interface StockDetailData { unit: string; calculatedQty: number; safetyStock: number; + wipStatus: 'active' | 'inactive'; useStatus: 'active' | 'inactive'; } @@ -57,7 +58,7 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - // 폼 데이터 (수정 모드용) + // 폼 데이터 (수정 모드용) - wipStatus는 읽기 전용이므로 제외 const [formData, setFormData] = useState<{ safetyStock: number; useStatus: 'active' | 'inactive'; @@ -90,6 +91,7 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) { unit: data.unit, calculatedQty: data.currentStock, // 재고량 safetyStock: data.safetyStock, + wipStatus: 'active', // 재공품 상태 (기본값: 사용) useStatus: data.status === null ? 'active' : 'active', // 기본값 }; setDetail(detailData); @@ -201,8 +203,9 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) { {renderReadOnlyField('안전재고', detail.safetyStock)} - {/* Row 3: 상태 */} + {/* Row 3: 재공품, 상태 */}
+ {renderReadOnlyField('재공품', USE_STATUS_LABELS[detail.wipStatus])} {renderReadOnlyField('상태', USE_STATUS_LABELS[detail.useStatus])}
@@ -252,8 +255,11 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) { - {/* Row 3: 상태 (수정 가능) */} + {/* Row 3: 재공품 (읽기 전용), 상태 (수정 가능) */}
+ {/* 재공품 (읽기 전용) */} + {renderReadOnlyField('재공품', USE_STATUS_LABELS[detail.wipStatus], true)} + {/* 상태 (수정 가능) */}
} actions={ @@ -361,6 +376,13 @@ export function StockStatusList() { customFilterFn: (items, fv) => { if (!items || items.length === 0) return items; return items.filter((item) => { + // 재공품 필터 (사용: wipQty > 0, 미사용: wipQty === 0) + const wipStatusVal = fv.wipStatus as string; + if (wipStatusVal && wipStatusVal !== 'all') { + if (wipStatusVal === 'active' && item.wipQty === 0) return false; + if (wipStatusVal === 'inactive' && item.wipQty > 0) return false; + } + // 상태 필터 const useStatusVal = fv.useStatus as string; if (useStatusVal && useStatusVal !== 'all' && item.useStatus !== useStatusVal) { return false; diff --git a/src/components/material/StockStatus/actions.ts b/src/components/material/StockStatus/actions.ts index ca66d166..8b77b309 100644 --- a/src/components/material/StockStatus/actions.ts +++ b/src/components/material/StockStatus/actions.ts @@ -137,6 +137,7 @@ function transformApiToListItem(data: ItemApiData): StockItem { actualQty: hasStock ? (parseFloat(String((stock as unknown as Record).actual_qty ?? stock.stock_qty)) || 0) : 0, stockQty: hasStock ? (parseFloat(String(stock.stock_qty)) || 0) : 0, safetyStock: hasStock ? (parseFloat(String(stock.safety_stock)) || 0) : 0, + wipQty: hasStock ? (parseFloat(String((stock as unknown as Record).wip_qty)) || 0) : 0, lotCount: hasStock ? (stock.lot_count || 0) : 0, lotDaysElapsed: hasStock ? (stock.days_elapsed || 0) : 0, status: hasStock ? stock.status : null, diff --git a/src/components/material/StockStatus/mockData.ts b/src/components/material/StockStatus/mockData.ts index 8f7b75cc..3d8cc2cc 100644 --- a/src/components/material/StockStatus/mockData.ts +++ b/src/components/material/StockStatus/mockData.ts @@ -51,6 +51,7 @@ const rawMaterialItems: StockItem[] = [ actualQty: 500, stockQty: 500, safetyStock: 100, + wipQty: 30, lotCount: 3, lotDaysElapsed: 21, status: 'normal', @@ -70,6 +71,7 @@ const rawMaterialItems: StockItem[] = [ actualQty: 350, stockQty: 350, safetyStock: 80, + wipQty: 20, lotCount: 2, lotDaysElapsed: 15, status: 'normal', @@ -89,6 +91,7 @@ const rawMaterialItems: StockItem[] = [ actualQty: 280, stockQty: 280, safetyStock: 70, + wipQty: 15, lotCount: 2, lotDaysElapsed: 18, status: 'normal', @@ -108,6 +111,7 @@ const rawMaterialItems: StockItem[] = [ actualQty: 420, stockQty: 420, safetyStock: 90, + wipQty: 25, lotCount: 4, lotDaysElapsed: 12, status: 'normal', @@ -139,6 +143,7 @@ const bentPartItems: StockItem[] = Array.from({ length: 41 }, (_, i) => { actualQty: stockQty, stockQty, safetyStock, + wipQty: seededInt(seed + 5, 0, 50), lotCount: seededInt(seed + 2, 1, 5), lotDaysElapsed: seededInt(seed + 3, 0, 45), status: getStockStatus(stockQty, safetyStock), @@ -172,6 +177,7 @@ const purchasedPartItems: StockItem[] = [ actualQty: stockQty, stockQty, safetyStock, + wipQty: seededInt(seed + 5, 0, 30), lotCount: seededInt(seed + 2, 2, 5), lotDaysElapsed: seededInt(seed + 3, 0, 40), status: getStockStatus(stockQty, safetyStock), @@ -202,6 +208,7 @@ const purchasedPartItems: StockItem[] = [ actualQty: stockQty, stockQty, safetyStock, + wipQty: seededInt(seed + 5, 0, 25), lotCount: seededInt(seed + 2, 2, 4), lotDaysElapsed: seededInt(seed + 3, 0, 35), status: getStockStatus(stockQty, safetyStock), @@ -234,6 +241,7 @@ const purchasedPartItems: StockItem[] = [ actualQty: stockQty, stockQty, safetyStock, + wipQty: seededInt(seed + 5, 0, 10), lotCount: seededInt(seed + 2, 1, 3), lotDaysElapsed: seededInt(seed + 3, 0, 30), status: getStockStatus(stockQty, safetyStock), @@ -264,6 +272,7 @@ const purchasedPartItems: StockItem[] = [ actualQty: stockQty, stockQty, safetyStock, + wipQty: seededInt(seed + 5, 0, 100), lotCount: seededInt(seed + 2, 3, 6), lotDaysElapsed: seededInt(seed + 3, 0, 25), status: getStockStatus(stockQty, safetyStock), @@ -292,6 +301,7 @@ const purchasedPartItems: StockItem[] = [ actualQty: stockQty, stockQty, safetyStock, + wipQty: seededInt(seed + 5, 0, 20), lotCount: seededInt(seed + 2, 2, 4), lotDaysElapsed: seededInt(seed + 3, 0, 20), status: getStockStatus(stockQty, safetyStock), @@ -322,6 +332,7 @@ const purchasedPartItems: StockItem[] = [ actualQty: stockQty, stockQty, safetyStock, + wipQty: seededInt(seed + 5, 0, 40), lotCount: seededInt(seed + 2, 2, 5), lotDaysElapsed: seededInt(seed + 3, 0, 30), status: getStockStatus(stockQty, safetyStock), @@ -346,6 +357,7 @@ const subMaterialItems: StockItem[] = [ actualQty: 5000, stockQty: 5000, safetyStock: 1000, + wipQty: 100, lotCount: 3, lotDaysElapsed: 28, status: 'normal', @@ -365,6 +377,7 @@ const subMaterialItems: StockItem[] = [ actualQty: 120, stockQty: 120, safetyStock: 30, + wipQty: 10, lotCount: 1, lotDaysElapsed: 5, status: 'normal', @@ -384,6 +397,7 @@ const subMaterialItems: StockItem[] = [ actualQty: 800, stockQty: 800, safetyStock: 200, + wipQty: 50, lotCount: 2, lotDaysElapsed: 12, status: 'normal', @@ -403,6 +417,7 @@ const subMaterialItems: StockItem[] = [ actualQty: 200, stockQty: 200, safetyStock: 50, + wipQty: 15, lotCount: 5, lotDaysElapsed: 37, status: 'normal', @@ -422,6 +437,7 @@ const subMaterialItems: StockItem[] = [ actualQty: 150, stockQty: 150, safetyStock: 40, + wipQty: 8, lotCount: 2, lotDaysElapsed: 10, status: 'normal', @@ -441,6 +457,7 @@ const subMaterialItems: StockItem[] = [ actualQty: 3000, stockQty: 3000, safetyStock: 500, + wipQty: 200, lotCount: 4, lotDaysElapsed: 8, status: 'normal', @@ -460,6 +477,7 @@ const subMaterialItems: StockItem[] = [ actualQty: 2500, stockQty: 2500, safetyStock: 400, + wipQty: 150, lotCount: 3, lotDaysElapsed: 15, status: 'normal', @@ -483,6 +501,7 @@ const consumableItems: StockItem[] = [ actualQty: 200, stockQty: 200, safetyStock: 50, + wipQty: 20, lotCount: 2, lotDaysElapsed: 8, status: 'normal', @@ -502,6 +521,7 @@ const consumableItems: StockItem[] = [ actualQty: 350, stockQty: 350, safetyStock: 80, + wipQty: 30, lotCount: 3, lotDaysElapsed: 5, status: 'normal', diff --git a/src/components/material/StockStatus/types.ts b/src/components/material/StockStatus/types.ts index 077aba2a..6e70791b 100644 --- a/src/components/material/StockStatus/types.ts +++ b/src/components/material/StockStatus/types.ts @@ -64,6 +64,7 @@ export interface StockItem { actualQty: number; // 실제 재고량 (Stock.actual_qty) stockQty: number; // Stock.stock_qty (없으면 0) safetyStock: number; // Stock.safety_stock (없으면 0) + wipQty: number; // 재공품 수량 (Stock.wip_qty, 없으면 0) lotCount: number; // Stock.lot_count (없으면 0) lotDaysElapsed: number; // Stock.days_elapsed (없으면 0) status: StockStatusType | null; // Stock.status (없으면 null) diff --git a/src/components/process-management/InspectionPreviewModal.tsx b/src/components/process-management/InspectionPreviewModal.tsx new file mode 100644 index 00000000..39a02d6a --- /dev/null +++ b/src/components/process-management/InspectionPreviewModal.tsx @@ -0,0 +1,282 @@ +'use client'; + +/** + * 중간검사 미리보기 모달 + * + * 설정된 검사 항목들로 실제 성적서가 어떻게 보일지 미리보기 + */ + +import { Fragment } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import type { InspectionSetting } from '@/types/process'; + +interface InspectionPreviewModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + inspectionSetting?: InspectionSetting; +} + +export function InspectionPreviewModal({ + open, + onOpenChange, + inspectionSetting, +}: InspectionPreviewModalProps) { + if (!inspectionSetting) { + return ( + + + + 중간검사 미리보기 + +
+ 검사 설정이 없습니다. 먼저 검사 설정을 완료해주세요. +
+
+ +
+
+
+ ); + } + + // 활성화된 겉모양 항목들 + const activeAppearanceItems = [ + { key: 'bendingStatus', label: '절곡상태', enabled: inspectionSetting.appearance.bendingStatus.enabled }, + { key: 'processingStatus', label: '가공상태', enabled: inspectionSetting.appearance.processingStatus.enabled }, + { key: 'sewingStatus', label: '재봉상태', enabled: inspectionSetting.appearance.sewingStatus.enabled }, + { key: 'assemblyStatus', label: '조립상태', enabled: inspectionSetting.appearance.assemblyStatus.enabled }, + ].filter((item) => item.enabled); + + // 활성화된 치수 항목들 + const activeDimensionItems = [ + { key: 'length', label: '길이', ...inspectionSetting.dimension.length }, + { key: 'width', label: '너비', ...inspectionSetting.dimension.width }, + { key: 'height1', label: '1 높이', ...inspectionSetting.dimension.height1 }, + { key: 'height2', label: '2 높이', ...inspectionSetting.dimension.height2 }, + { key: 'gap', label: '간격', ...inspectionSetting.dimension.gap }, + ].filter((item) => item.enabled); + + // 샘플 데이터 (미리보기용) + const sampleRows = [1, 2, 3, 4, 5]; + + return ( + + + + 중간검사 미리보기 + + +
+ {/* 헤더 정보 */} +
+
+ 기준서명: + {inspectionSetting.standardName || '미설정'} +
+
+ 활성 항목: + {activeAppearanceItems.length + activeDimensionItems.length}개 +
+
+ + {/* 중간검사 기준서 */} +
+
+ 중간검사 기준서 +
+
+ {/* 도해 이미지 영역 */} +
+ {inspectionSetting.schematicImage ? ( + 도해 이미지 + ) : ( + 도해 이미지 없음 + )} +
+ + {/* 검사기준 이미지 또는 검사 항목 테이블 */} +
+ {inspectionSetting.inspectionStandardImage ? ( + 검사기준 이미지 + ) : ( +
+ + + + + + + + + + {activeAppearanceItems.map((item) => ( + + + + + + ))} + {activeDimensionItems.map((item) => ( + + + + + + ))} + +
검사항목검사방법포인트
{item.label}양자택일-
{item.label}{item.method}{item.point}
+
+ )} +
+
+
+ + {/* 중간검사 DATA */} +
+
+ 중간검사 DATA +
+
+ + + + + {/* 겉모양 항목들 */} + {activeAppearanceItems.map((item) => ( + + ))} + {/* 치수 항목들 */} + {activeDimensionItems.map((item) => ( + + ))} + {/* 판정 */} + {inspectionSetting.judgment && ( + + )} + + {/* 치수 서브헤더 */} + {activeDimensionItems.length > 0 && ( + + + {activeAppearanceItems.map((item) => ( + + ))} + {activeDimensionItems.map((item) => ( + + + + + ))} + {inspectionSetting.judgment && ( + + )} + + )} + + + {sampleRows.map((row) => ( + + + {/* 겉모양 샘플 데이터 */} + {activeAppearanceItems.map((item) => ( + + ))} + {/* 치수 샘플 데이터 */} + {activeDimensionItems.map((item) => ( + + + + + ))} + {/* 판정 샘플 */} + {inspectionSetting.judgment && ( + + )} + + ))} + +
No. + {item.label} + + {item.label} (mm) + + 판정 +
+ (적/부) +
+ 양호/불량 + + 도면치수 + + 측정값 +
{row} + ☐ 양호 +
+ ☐ 불량 +
+ - + + - + + - +
+
+
+ + {/* 부적합 내용 */} + {inspectionSetting.nonConformingContent && ( +
+
+
+ 부적합 내용 +
+
+ 종합판정 +
+
+
+
+ (부적합 사항 입력 영역) +
+
+ 합격 / 불합격 +
+
+
+ )} +
+ + {/* 버튼 영역 */} +
+ +
+
+
+ ); +} diff --git a/src/components/process-management/InspectionSettingModal.tsx b/src/components/process-management/InspectionSettingModal.tsx new file mode 100644 index 00000000..4f2b5cad --- /dev/null +++ b/src/components/process-management/InspectionSettingModal.tsx @@ -0,0 +1,293 @@ +'use client'; + +/** + * 중간검사 설정 모달 + * + * 기획서 Page 9 기준: + * - 왼쪽: 기준서명, 도해 이미지, 검사기준 이미지, 겉모양 항목들 ON/OFF + * - 오른쪽: 치수 항목들 (포인트, 방법, ON/OFF), 판정, 부적합 내용 + */ + +import { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { ImageUpload } from '@/components/ui/image-upload'; +import type { + InspectionSetting, + InspectionPointType, + InspectionMethodType, +} from '@/types/process'; +import { + INSPECTION_POINT_OPTIONS, + INSPECTION_METHOD_OPTIONS, + DEFAULT_INSPECTION_SETTING, +} from '@/types/process'; + +interface InspectionSettingModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + initialData?: InspectionSetting; + onSave: (data: InspectionSetting) => void; +} + +export function InspectionSettingModal({ + open, + onOpenChange, + initialData, + onSave, +}: InspectionSettingModalProps) { + const [formData, setFormData] = useState( + initialData || DEFAULT_INSPECTION_SETTING + ); + + useEffect(() => { + if (open) { + setFormData(initialData || DEFAULT_INSPECTION_SETTING); + } + }, [open, initialData]); + + const handleSave = () => { + onSave(formData); + onOpenChange(false); + }; + + const handleCancel = () => { + onOpenChange(false); + }; + + // 겉모양 항목 토글 + const toggleAppearance = (key: keyof typeof formData.appearance) => { + setFormData((prev) => ({ + ...prev, + appearance: { + ...prev.appearance, + [key]: { ...prev.appearance[key], enabled: !prev.appearance[key].enabled }, + }, + })); + }; + + // 치수 항목 토글 + const toggleDimension = (key: keyof typeof formData.dimension) => { + setFormData((prev) => ({ + ...prev, + dimension: { + ...prev.dimension, + [key]: { ...prev.dimension[key], enabled: !prev.dimension[key].enabled }, + }, + })); + }; + + // 치수 포인트 변경 + const setDimensionPoint = ( + key: keyof typeof formData.dimension, + point: InspectionPointType + ) => { + setFormData((prev) => ({ + ...prev, + dimension: { + ...prev.dimension, + [key]: { ...prev.dimension[key], point }, + }, + })); + }; + + // 치수 방법 변경 + const setDimensionMethod = ( + key: keyof typeof formData.dimension, + method: InspectionMethodType + ) => { + setFormData((prev) => ({ + ...prev, + dimension: { + ...prev.dimension, + [key]: { ...prev.dimension[key], method }, + }, + })); + }; + + return ( + + + + 중간검사 설정 + + +
+ {/* 왼쪽 패널 - 기본 정보 및 겉모양 */} +
+

중간검사 설정

+ + {/* 기준서명 */} +
+ + + setFormData((prev) => ({ ...prev, standardName: e.target.value })) + } + placeholder="예: KDPS-20" + /> +
+ + {/* 중간검사 기준서 이미지 (통합) */} +
+ + { + const url = URL.createObjectURL(file); + // 두 필드 모두 동일 이미지로 설정 (호환성) + setFormData((prev) => ({ + ...prev, + schematicImage: url, + inspectionStandardImage: url, + })); + }} + onRemove={() => { + setFormData((prev) => ({ + ...prev, + schematicImage: undefined, + inspectionStandardImage: undefined, + })); + }} + aspectRatio="wide" + size="lg" + className="w-full" + hint="도해 및 검사기준이 포함된 통합 이미지를 업로드하세요" + /> +
+ + {/* 겉모양 항목들 */} +
+
+ + toggleAppearance('bendingStatus')} + /> +
+
+ + toggleAppearance('processingStatus')} + /> +
+
+ + toggleAppearance('sewingStatus')} + /> +
+
+ + toggleAppearance('assemblyStatus')} + /> +
+
+
+ + {/* 오른쪽 패널 - 치수 및 기타 */} +
+

중간검사

+ + {/* 치수 항목들 */} + {[ + { key: 'length' as const, label: '길이' }, + { key: 'width' as const, label: '너비' }, + { key: 'height1' as const, label: '1 높이' }, + { key: 'height2' as const, label: '2 높이' }, + { key: 'gap' as const, label: '간격' }, + ].map(({ key, label }) => ( +
+ + + + toggleDimension(key)} + /> +
+ ))} + + {/* 판정 */} +
+ + + setFormData((prev) => ({ ...prev, judgment: checked })) + } + /> +
+ + {/* 부적합 내용 */} +
+ + + setFormData((prev) => ({ ...prev, nonConformingContent: checked })) + } + /> +
+
+
+ + {/* 버튼 영역 */} +
+ + +
+
+
+ ); +} diff --git a/src/components/process-management/StepForm.tsx b/src/components/process-management/StepForm.tsx index e1a2578c..9898dc18 100644 --- a/src/components/process-management/StepForm.tsx +++ b/src/components/process-management/StepForm.tsx @@ -14,6 +14,7 @@ import { useRouter } from 'next/navigation'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; import { Select, SelectContent, @@ -23,14 +24,23 @@ import { } from '@/components/ui/select'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { toast } from 'sonner'; -import type { ProcessStep, StepConnectionType, StepCompletionType } from '@/types/process'; +import { Settings, Eye } from 'lucide-react'; +import type { + ProcessStep, + StepConnectionType, + StepCompletionType, + InspectionSetting, +} from '@/types/process'; import { STEP_CONNECTION_TYPE_OPTIONS, STEP_COMPLETION_TYPE_OPTIONS, STEP_CONNECTION_TARGET_OPTIONS, + DEFAULT_INSPECTION_SETTING, } from '@/types/process'; import { createProcessStep, updateProcessStep } from './actions'; import type { DetailConfig } from '@/components/templates/IntegratedDetailTemplate/types'; +import { InspectionSettingModal } from './InspectionSettingModal'; +import { InspectionPreviewModal } from './InspectionPreviewModal'; const stepCreateConfig: DetailConfig = { title: '단계', @@ -94,8 +104,20 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { initialData?.completionType || '클릭 시 완료' ); + // 검사 설정 + const [inspectionSetting, setInspectionSetting] = useState( + initialData?.inspectionSetting || DEFAULT_INSPECTION_SETTING + ); + + // 모달 상태 + const [isInspectionSettingOpen, setIsInspectionSettingOpen] = useState(false); + const [isInspectionPreviewOpen, setIsInspectionPreviewOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + // 검사여부가 "필요"인지 확인 + const isInspectionEnabled = needsInspection === '필요'; + // 제출 const handleSubmit = async (): Promise<{ success: boolean; error?: string }> => { if (!stepName.trim()) { @@ -114,6 +136,7 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { connectionType, connectionTarget: connectionType === '팝업' ? connectionTarget : undefined, completionType, + inspectionSetting: isInspectionEnabled ? inspectionSetting : undefined, }; setIsLoading(true); @@ -236,7 +259,7 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { 연결 정보 -
+
+ {/* 검사여부가 "필요"일 때 버튼 표시 */} + {isInspectionEnabled && ( + <> + + + + )}
@@ -314,19 +359,37 @@ export function StepForm({ mode, processId, initialData }: StepFormProps) { connectionTarget, completionType, initialData?.stepCode, + isInspectionEnabled, ] ); const config = isEdit ? stepEditConfig : stepCreateConfig; return ( - + <> + + + {/* 검사 설정 모달 */} + + + {/* 검사 미리보기 모달 */} + + ); } diff --git a/src/components/production/WorkOrders/WipProductionModal.tsx b/src/components/production/WorkOrders/WipProductionModal.tsx index ea27380d..acdd0e3a 100644 --- a/src/components/production/WorkOrders/WipProductionModal.tsx +++ b/src/components/production/WorkOrders/WipProductionModal.tsx @@ -3,17 +3,17 @@ /** * 재공품 생산 모달 * - * 기획서 기준: - * - 품목 선택 (검색) → 재공품 목록 테이블 추가 + * 기획서 기준 (스크린샷 2026-02-05 오후 6.59.14): + * - 품목 선택: 검색창 + 검색 결과 테이블 (바로 표시) * - 테이블: 품목코드, 품목명, 규격, 단위, 재고량, 안전재고, 수량(입력) - * - 행 삭제 (X 버튼) - * - 우선순위: 긴급/우선/일반 토글 (디폴트: 일반) + * - "총 N건" 표시 + * - 우선순위: 긴급(검정)/우선(검정)/일반(주황) 토글 * - 부서 Select (디폴트: 생산부서) * - 비고 Textarea - * - 하단: 취소 / 생산지시 확정 + * - 하단: 취소(검정) / 생산지시 확정(주황) */ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { Search, X } from 'lucide-react'; import { Dialog, @@ -52,10 +52,13 @@ type Priority = '긴급' | '우선' | '일반'; // Mock 재공품 데이터 (품목관리 > 재공품/사용 상태 '사용' 품목) const MOCK_WIP_ITEMS: Omit[] = [ - { id: 'wip-1', itemCode: 'WIP-GR-001', itemName: '가이드레일(벽면형)', specification: '120X70 EGI 1.6T', unit: 'EA', stockQuantity: 150, safetyStock: 50 }, - { id: 'wip-2', itemCode: 'WIP-CS-001', itemName: '케이스(500X380)', specification: '500X380 EGI 1.6T', unit: 'EA', stockQuantity: 80, safetyStock: 30 }, - { id: 'wip-3', itemCode: 'WIP-BF-001', itemName: '하단마감재(60X40)', specification: '60X40 EGI 1.6T', unit: 'EA', stockQuantity: 200, safetyStock: 100 }, - { id: 'wip-4', itemCode: 'WIP-LB-001', itemName: '하단L-BAR(17X60)', specification: '17X60 EGI 1.6T', unit: 'EA', stockQuantity: 120, safetyStock: 40 }, + { 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 { @@ -65,62 +68,51 @@ interface WipProductionModalProps { export function WipProductionModal({ open, onOpenChange }: WipProductionModalProps) { const [searchTerm, setSearchTerm] = useState(''); - const [selectedItems, setSelectedItems] = useState([]); + const [itemQuantities, setItemQuantities] = useState>({}); const [priority, setPriority] = useState('일반'); const [department, setDepartment] = useState('생산부서'); const [note, setNote] = useState(''); - // 검색 결과 필터링 - const searchResults = searchTerm.trim() - ? MOCK_WIP_ITEMS.filter( - (item) => - !selectedItems.some((s) => s.id === item.id) && - (item.itemCode.toLowerCase().includes(searchTerm.toLowerCase()) || - item.itemName.toLowerCase().includes(searchTerm.toLowerCase())) - ) - : []; - - // 품목 추가 - const handleAddItem = useCallback((item: Omit) => { - setSelectedItems((prev) => [...prev, { ...item, quantity: 0 }]); - setSearchTerm(''); - }, []); - - // 품목 삭제 - const handleRemoveItem = useCallback((id: string) => { - setSelectedItems((prev) => prev.filter((item) => item.id !== id)); - }, []); + // 검색 결과 필터링 (검색어 없으면 전체 표시) + 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; - setSelectedItems((prev) => - prev.map((item) => (item.id === id ? { ...item, quantity: qty } : item)) - ); + setItemQuantities((prev) => ({ + ...prev, + [id]: qty, + })); }, []); // 생산지시 확정 const handleConfirm = useCallback(() => { - if (selectedItems.length === 0) { - toast.error('품목을 추가해주세요.'); - return; - } - const invalidItems = selectedItems.filter((item) => item.quantity <= 0); - if (invalidItems.length > 0) { + const itemsWithQuantity = filteredItems.filter((item) => (itemQuantities[item.id] || 0) > 0); + + if (itemsWithQuantity.length === 0) { toast.error('수량을 입력해주세요.'); return; } // TODO: API 연동 - toast.success(`재공품 생산지시가 확정되었습니다. (${selectedItems.length}건)`); + toast.success(`재공품 생산지시가 확정되었습니다. (${itemsWithQuantity.length}건)`); handleReset(); onOpenChange(false); - }, [selectedItems, onOpenChange]); + }, [filteredItems, itemQuantities, onOpenChange]); // 초기화 const handleReset = useCallback(() => { setSearchTerm(''); - setSelectedItems([]); + setItemQuantities({}); setPriority('일반'); setDepartment('생산부서'); setNote(''); @@ -132,104 +124,87 @@ export function WipProductionModal({ open, onOpenChange }: WipProductionModalPro }, [handleReset, onOpenChange]); const priorityOptions: Priority[] = ['긴급', '우선', '일반']; - const priorityColors: Record = { - '긴급': 'bg-red-500 text-white hover:bg-red-600', - '우선': 'bg-orange-500 text-white hover:bg-orange-600', - '일반': 'bg-gray-500 text-white hover:bg-gray-600', - }; - const priorityInactiveColors: Record = { - '긴급': 'bg-white text-red-500 border-red-300 hover:bg-red-50', - '우선': 'bg-white text-orange-500 border-orange-300 hover:bg-orange-50', - '일반': 'bg-white text-gray-500 border-gray-300 hover:bg-gray-50', + + // 선택된 버튼은 각각 다른 색상, 미선택은 회색 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 ( - + 재공품 생산
- {/* 품목 검색 */} -
+ {/* 품목 선택 섹션 */} +
-
- - setSearchTerm(e.target.value)} - className="pl-9" - /> -
- {/* 검색 결과 드롭다운 */} - {searchResults.length > 0 && ( -
- {searchResults.map((item) => ( - - ))} -
- )} -
- {/* 재공품 목록 테이블 */} - {selectedItems.length > 0 && ( -
- - - - - - - - - - - - - - - {selectedItems.map((item) => ( - - - - - - - - - + {/* 검색창 */} +
+
+ setSearchTerm(e.target.value)} + className="pr-10" + /> + +
+ + {/* 총 건수 */} +

+ 총 {filteredItems.length}건 +

+ + {/* 품목 테이블 */} +
+
품목코드품목명규격단위재고량안전재고수량
{item.itemCode}{item.itemName}{item.specification}{item.unit}{item.stockQuantity}{item.safetyStock} - handleQuantityChange(item.id, e.target.value)} - className="h-8 text-center text-sm" - placeholder="0" - /> - - -
+ + + + + + + + + - ))} - -
품목코드품목명규격단위재고량안전재고수량
+ + + {filteredItems.map((item) => ( + + {item.itemCode} + {item.itemName} + {item.specification} + {item.unit} + {item.stockQuantity} + {item.safetyStock} + + handleQuantityChange(item.id, e.target.value)} + className="h-8 text-center text-sm" + placeholder="" + /> + + + ))} + + +
- )} +
{/* 우선순위 */}
@@ -238,11 +213,8 @@ export function WipProductionModal({ open, onOpenChange }: WipProductionModalPro {priorityOptions.map((opt) => (