데이터 등록 플로우 정보가 없습니다.
;\n }\n\n return (\n \n
\n
\n
생산관리자 대시보드
\n
생산관리팀 전용 대시보드
\n
\n
\n \n
\n
\n\n {/* 핵심 지표 */}\n
\n
onNavigate('work-order-list')}\n >\n
\n
{waitingOrders.length}건
\n
\n
onNavigate('work-order-list')}\n >\n
\n
{inProgressOrders.length}건
\n
\n
onNavigate('work-order-list')}\n >\n
\n
{completedOrders.length}건
\n
\n
\n\n
\n {/* 진행중 작업 */}\n
\n
\n
진행중 작업
\n \n \n
\n {inProgressOrders.slice(0, 5).map((wo, i) => (\n
onNavigate('work-order-detail', wo)}\n >\n
\n
{wo.workOrderNo}
\n
{wo.customerName}
\n
\n
\n
\n
\n
0 ? (wo.completedQty / wo.totalQty) * 100 : 0}%` }}\n />\n
\n
{wo.completedQty}/{wo.totalQty}\n
\n
{wo.currentStep}
\n
\n
\n ))}\n {inProgressOrders.length === 0 && (\n
진행중인 작업이 없습니다
\n )}\n
\n
\n\n {/* 납기 임박 */}\n
\n
\n
납기 임박 (D-3)
\n \n
\n {urgentOrders.slice(0, 5).map((wo, i) => {\n const dueDate = new Date(wo.dueDate);\n const diffDays = Math.ceil((dueDate - today) / (1000 * 60 * 60 * 24));\n return (\n
onNavigate('work-order-detail', wo)}\n >\n
\n
{wo.workOrderNo}
\n
{wo.customerName}
\n
\n
\n D-{diffDays}\n \n
\n );\n })}\n {urgentOrders.length === 0 && (\n
납기 임박 작업이 없습니다
\n )}\n
\n
\n
\n
\n );\n};\n\n// 구매팀 대시보드\nconst PurchaseDashboard = ({ materials = [], purchaseOrders = [], onCreatePO, onNavigate }) => {\n // 재고 부족 자재 (안전재고 이하)\n const lowStockMaterials = materials.filter(m => m.stock <= m.minStock);\n\n // 발주 현황\n const pendingPOs = purchaseOrders.filter(po => po.status === '발주대기' || po.status === '승인대기');\n const orderedPOs = purchaseOrders.filter(po => po.status === '발주완료');\n const inTransitPOs = purchaseOrders.filter(po => po.status === '배송중');\n\n // 입고 예정 (더미 데이터)\n const upcomingDeliveries = [\n { date: '2025-03-05', vendor: '철강공업', item: '갈바코일 0.5T', qty: '10톤', status: '배송중' },\n { date: '2025-03-06', vendor: '모터공급사', item: '튜블러모터', qty: '50EA', status: '발주완료' },\n { date: '2025-03-08', vendor: '알루미늄', item: '알루미늄 프레임', qty: '200EA', status: '발주완료' },\n ];\n\n return (\n
\n
\n
\n
구매 대시보드
\n
구매팀 전용 대시보드
\n
\n
\n
\n
\n
\n
\n\n {/* 재고 부족 알림 - 가장 중요! */}\n {lowStockMaterials.length > 0 && (\n
\n
\n
\n
\n
\n
재고 부족 알림
\n
{lowStockMaterials.length}개 품목이 안전재고 이하입니다
\n
\n
\n
\n
\n
\n {lowStockMaterials.slice(0, 5).map((m, i) => (\n
\n
\n
\n
{m.materialCode}
\n
\n {m.stock === 0 ? '재고없음' : '부족'}\n \n
\n
{m.materialName}
\n
\n 현재: {m.stock}{m.unit} / 안전재고: {m.minStock}{m.unit}\n
\n
\n
\n
\n ))}\n {lowStockMaterials.length > 5 && (\n
\n )}\n
\n
\n )}\n\n {/* 재고 부족 없을 때 */}\n {lowStockMaterials.length === 0 && (\n
\n
\n
\n \n
\n
\n
재고 상태 양호
\n
모든 자재가 안전재고 이상입니다.
\n
\n
\n
\n )}\n\n {/* 핵심 지표 */}\n
\n
onNavigate('inventory-list')}\n >\n
\n
{lowStockMaterials.length}건
\n
\n
onNavigate('purchase-order-list')}\n >\n
\n
{pendingPOs.length}건
\n
\n
onNavigate('purchase-order-list')}\n >\n
\n
{inTransitPOs.length}건
\n
\n
onNavigate('inventory-list')}\n >\n
\n
{materials.length}개
\n
\n
\n\n
\n {/* 입고 예정 */}\n
\n
입고 예정
\n
\n {upcomingDeliveries.map((d, i) => (\n
\n
\n
{d.item}
\n
{d.vendor} | {d.qty}
\n
\n
\n
{d.date}
\n
\n {d.status}\n \n
\n
\n ))}\n
\n
\n\n {/* 최근 발주 */}\n
\n
최근 발주
\n {purchaseOrders.length === 0 ? (\n
\n ) : (\n
\n {purchaseOrders.slice(0, 5).map((po, i) => (\n
\n
\n
{po.poNo}
\n
{po.vendor}
\n
\n
\n {po.status}\n \n
\n ))}\n
\n )}\n
\n
\n\n {/* 자재 카테고리별 현황 */}\n
\n
자재 카테고리별 현황
\n
\n {['원자재', '절곡부품', '구매부품', '조립품'].map((cat, i) => {\n const catMaterials = materials.filter(m => m.itemType === cat);\n const lowStock = catMaterials.filter(m => m.stock <= m.minStock).length;\n return (\n
\n
{cat}
\n
{catMaterials.length}개
\n {lowStock > 0 && (\n
부족: {lowStock}개
\n )}\n
\n );\n })}\n
\n
\n
\n );\n};\n\n// 품질관리 대시보드\nconst QualityDashboard = ({ workOrders = [], onNavigate }) => {\n // 검사 대기 목록 (작업완료 상태)\n const pendingInspection = workOrders.filter(wo =>\n wo.status === '작업완료' && wo.inspectionStatus !== '검사완료'\n );\n\n // 중간검사 대기 (작업중)\n const processInspection = workOrders.filter(wo =>\n wo.status === '작업중'\n );\n\n const data = {\n incomingInspection: 3,\n processInspection: processInspection.length,\n finalInspection: pendingInspection.length,\n monthlyDefectRate: 0.5,\n pendingInspections: [\n ...pendingInspection.slice(0, 2).map(wo => ({\n lot: wo.workOrderNo,\n type: '제품검사',\n item: wo.productName,\n })),\n { lot: 'L-251205-001', type: '수입검사', item: '알루미늄 프레임' },\n ],\n defectTypes: [\n { type: '치수불량', rate: 40 },\n { type: '외관불량', rate: 30 },\n { type: '기능불량', rate: 20 },\n { type: '기타', rate: 10 },\n ],\n };\n\n return (\n
\n
\n
품질 대시보드
\n
품질관리팀 전용 대시보드
\n
\n\n {/* 핵심 지표 */}\n
\n
\n
수입검사 대기
\n
{data.incomingInspection}건
\n
\n
\n
중간검사 대기
\n
{data.processInspection}건
\n
\n
\n
제품검사 대기
\n
{data.finalInspection}건
\n
\n
\n
월간 불량률
\n
{data.monthlyDefectRate}%
\n
\n
\n\n
\n {/* 검사 대기 목록 */}\n
\n
🔍 검사 대기 목록
\n
\n {data.pendingInspections.map((p, i) => (\n
\n ))}\n
\n
\n\n {/* 불량 유형별 현황 */}\n
\n
📈 불량 유형별 현황
\n
\n {data.defectTypes.map((d, i) => (\n
\n
{d.type}\n
\n
{d.rate}%\n
\n ))}\n
\n
\n
\n
\n );\n};\n\n// 회계팀 대시보드\nconst AccountingDashboard = ({ orders = [], shipments = [], onApproveShipment, onRejectShipment, onNavigate }) => {\n // C등급 출하 승인대기 목록\n const pendingCGradeShipments = orders.filter(o =>\n o.creditGrade === 'C' &&\n o.productionStatus === '완료' &&\n o.accountingStatus !== '회계확인완료'\n );\n\n // 통계 데이터\n const monthlySales = orders.reduce((sum, o) => sum + (o.totalAmount || 0), 0);\n const cGradeOrders = orders.filter(o => o.creditGrade === 'C');\n const cGradeTotal = cGradeOrders.reduce((sum, o) => sum + (o.totalAmount || 0), 0);\n\n // 수금/지급 예정 (더미 데이터)\n const collectSchedule = [\n { date: '12/10', customer: '삼성전자', amount: 2400 },\n { date: '12/15', customer: 'LG전자', amount: 1800 },\n ];\n const paymentSchedule = [\n { date: '12/15', vendor: '철강공업', amount: 800 },\n { date: '12/20', vendor: '모터공급사', amount: 500 },\n ];\n\n // 출하 승인\n const handleApprove = (order) => {\n if (onApproveShipment) {\n onApproveShipment(order.id);\n }\n };\n\n // 출하 반려\n const handleReject = (order) => {\n const reason = prompt('반려 사유를 입력하세요:');\n if (reason && onRejectShipment) {\n onRejectShipment(order.id, reason);\n }\n };\n\n return (\n
\n
\n
\n
회계 대시보드
\n
회계팀 전용 대시보드
\n
\n
\n \n
\n
\n\n {/* 핵심 지표 */}\n
\n
\n
\n
{(monthlySales / 100000000).toFixed(1)}억
\n
\n
\n
\n
{(cGradeTotal / 10000).toLocaleString()}만원
\n
{cGradeOrders.length}건
\n
\n
\n
\n
{collectSchedule.reduce((s, c) => s + c.amount, 0).toLocaleString()}만원
\n
\n
\n
\n
{paymentSchedule.reduce((s, p) => s + p.amount, 0).toLocaleString()}만원
\n
\n
\n\n
\n {/* 수금 예정 */}\n
\n
\n 수금 예정\n
\n
\n {collectSchedule.map((c, i) => (\n
\n {c.date}\n {c.customer}\n {c.amount.toLocaleString()}만원\n
\n ))}\n
\n
\n\n {/* 지급 예정 */}\n
\n
지급 예정
\n
\n {paymentSchedule.map((p, i) => (\n
\n {p.date}\n {p.vendor}\n {p.amount.toLocaleString()}만원\n
\n ))}\n
\n
\n
\n\n {/* C등급 고객 현황 */}\n
\n
C등급 고객 수주 현황
\n {cGradeOrders.length === 0 ? (\n
C등급 수주가 없습니다
\n ) : (\n
\n {cGradeOrders.slice(0, 5).map((order, i) => (\n
onNavigate('order-detail', order)}\n >\n
\n
{order.orderNo}
\n
{order.customerName}
\n
\n
\n
{(order.totalAmount / 10000).toLocaleString()}만원
\n
\n {order.accountingStatus === '회계확인완료' ? '승인완료' :\n order.productionStatus === '완료' ? '승인대기' : '생산중'}\n \n
\n
\n ))}\n
\n )}\n
\n
\n );\n};\n\n// 생산담당자 대시보드 (모바일 최적화)\nconst WorkerDashboard = ({ workOrders = [], currentUser, onStartWork, onInputWorkLog, onCompleteWork, onNavigate }) => {\n const [showWorkLogModal, setShowWorkLogModal] = useState(false);\n const [selectedWork, setSelectedWork] = useState(null);\n const [workLog, setWorkLog] = useState({\n completedQty: 0,\n defectQty: 0,\n note: '',\n });\n\n // 승인완료된 작업 중 본인에게 배정되었거나 아직 미배정인 작업\n // (생산담당자는 미배정 작업도 볼 수 있어야 함 - 본인이 시작할 수 있도록)\n const myWorks = workOrders.filter(wo =>\n wo.approvalStatus === '승인완료' && (\n wo.worker === currentUser?.name ||\n wo.assignee === currentUser?.name ||\n (!wo.worker && !wo.assignee) || // 미배정 작업\n wo.assignee === '김생산' // 기존 샘플 데이터 호환\n )\n );\n\n // 작업중인 것\n const inProgressWorks = myWorks.filter(wo => wo.status === '작업중');\n // 작업대기\n const waitingWorks = myWorks.filter(wo => wo.status === '작업대기');\n // 오늘 완료\n const today = new Date().toISOString().split('T')[0];\n const todayCompleted = myWorks.filter(wo =>\n wo.status === '작업완료' && wo.completedDate === today\n );\n\n // 작업 시작\n const handleStartWork = (wo) => {\n if (onStartWork) {\n onStartWork(wo.id);\n }\n };\n\n // 작업일지 모달 열기\n const handleOpenWorkLog = (wo) => {\n setSelectedWork(wo);\n setWorkLog({\n completedQty: wo.completedQty || 0,\n defectQty: wo.defectQty || 0,\n note: wo.workNote || '',\n });\n setShowWorkLogModal(true);\n };\n\n // 작업일지 저장\n const handleSaveWorkLog = () => {\n if (onInputWorkLog && selectedWork) {\n onInputWorkLog(selectedWork.id, workLog);\n }\n setShowWorkLogModal(false);\n setSelectedWork(null);\n };\n\n // 작업 완료\n const handleCompleteWork = (wo) => {\n if (onCompleteWork) {\n onCompleteWork(wo.id);\n }\n };\n\n // 총 실적 계산\n const totalCompleted = myWorks.reduce((sum, wo) => sum + (wo.completedQty || 0), 0);\n const totalDefects = myWorks.reduce((sum, wo) => sum + (wo.defectQty || 0), 0);\n\n return (\n
\n {/* 상단 헤더 */}\n
\n
\n
\n \n
\n
\n
👷 {currentUser?.name || '김생산'} 님
\n
생산담당자 | {new Date().toLocaleDateString('ko-KR')}
\n
\n
\n
\n\n {/* 오늘 내 실적 요약 */}\n
\n
\n 오늘 내 실적\n
\n
\n
\n
대기
\n
{waitingWorks.length}
\n
\n
\n
진행중
\n
{inProgressWorks.length}
\n
\n
\n
완료
\n
{totalCompleted}
\n
\n
\n
\n
\n\n {/* 진행중인 작업 (최우선) */}\n {inProgressWorks.length > 0 && (\n
\n
진행중인 작업
\n
\n {inProgressWorks.map((wo, i) => (\n
\n
\n
\n
{wo.workOrderNo}
\n
{wo.productName}
\n
{wo.customerName} | {wo.siteName}
\n
\n
\n ▶ 작업중\n \n
\n {/* 품목 정보 영역 */}\n
\n
\n
\n 품목코드:\n {wo.itemCode || wo.productCode || '-'}\n
\n
\n 규격:\n {wo.width && wo.height ? `${wo.width}×${wo.height}` : wo.spec || '-'}\n
\n
\n LOT번호:\n {wo.lotNo || wo.productionLotNo || '-'}\n
\n
\n 납기일:\n {wo.dueDate || '-'}\n
\n
\n
\n
\n
\n
목표
\n
{wo.totalQty}개소
\n
\n
\n
완료
\n
{wo.completedQty || 0}개소
\n
\n
\n
불량
\n
{wo.defectQty || 0}개소
\n
\n
\n
\n \n \n
\n
\n ))}\n
\n
\n )}\n\n {/* 대기중인 작업 */}\n
\n
대기중인 작업 ({waitingWorks.length}건)
\n {waitingWorks.length === 0 ? (\n
\n ) : (\n
\n {waitingWorks.map((wo, i) => (\n
\n
\n
\n
{wo.workOrderNo}
\n
{wo.productName}
\n
{wo.customerName} | {wo.siteName}
\n
\n
\n ○ 대기\n \n
\n {/* 품목 정보 영역 */}\n
\n
\n
\n 품목:\n {wo.itemCode || wo.productCode || '-'}\n
\n
\n 규격:\n {wo.width && wo.height ? `${wo.width}×${wo.height}` : wo.spec || '-'}\n
\n
\n 수량:\n {wo.totalQty}개소\n
\n
\n 납기:\n {wo.dueDate || '-'}\n
\n
\n
\n
\n
\n ))}\n
\n )}\n
\n\n {/* 오늘 완료한 작업 */}\n {todayCompleted.length > 0 && (\n
\n
오늘 완료 ({todayCompleted.length}건)
\n
\n {todayCompleted.map((wo, i) => (\n
\n
\n
\n
{wo.workOrderNo}
\n
{wo.productName}
\n
\n
\n
{wo.completedQty || wo.totalQty}개소
\n
불량: {wo.defectQty || 0}
\n
\n
\n {/* 품목 정보 */}\n
\n 품목: {wo.itemCode || wo.productCode || '-'}\n 규격: {wo.width && wo.height ? `${wo.width}×${wo.height}` : wo.spec || '-'}\n
\n
\n ))}\n
\n
\n )}\n\n {/* 작업일지 입력 모달 - WL-SCR 문서양식관리 양식 적용 */}\n {showWorkLogModal && selectedWork && (\n
\n
\n {/* ========== 헤더 영역: 로고 + 제목 + 결재란 ========== */}\n
\n
\n {/* 좌측: 로고 + 제목 */}\n
\n
\n KD경동기업\n
\n
\n
작 업 일 지
\n
\n 스크린 생산부서\n
\n
\n
\n\n {/* 우측: 결재란 + 닫기 버튼 */}\n
\n {/* 결재란 테이블 */}\n
\n \n \n | 작성 | \n 검토 | \n 승인 | \n
\n \n \n \n | 결 | \n 재 | \n 란 | \n
\n \n | 판매/전진 | \n 생산 | \n 품질 | \n
\n \n
\n
\n
\n
\n
\n\n {/* ========== 신청업체/신청내용 정보 (WL-HEADER-INFO 블록) ========== */}\n
\n
\n {/* 좌측: 신청업체 정보 */}\n
\n
\n 신 청 업 체\n
\n
\n \n \n | 발주처 | \n {selectedWork.customerName || '-'} | \n
\n \n | 현장명 | \n {selectedWork.siteName || '-'} | \n
\n \n | 제품명 | \n {selectedWork.productName || '-'} | \n
\n \n | 규격(W×H) | \n {selectedWork.width || '-'} × {selectedWork.height || '-'} | \n
\n \n
\n
\n\n {/* 우측: 신청내용 정보 */}\n
\n
\n 신 청 내 용\n
\n
\n \n \n | 작업지시번호 | \n {selectedWork.workOrderNo || '-'} | \n
\n \n | LOT번호 | \n {selectedWork.lotNo || selectedWork.productionLotNo || '-'} | \n
\n \n | 납기일 | \n {selectedWork.dueDate || '-'} | \n
\n \n | 생산담당자 | \n {currentUser?.name || '김생산'} | \n
\n \n
\n
\n
\n
\n\n {/* ========== 작업내역 테이블 (TBL-WORKLOG-SCREEN 블록) ========== */}\n
\n\n {/* ========== 하단 요약 및 버튼 ========== */}\n
\n
\n
\n
목표 수량
\n
{selectedWork.totalQty || 0}개소
\n
\n
\n \n setWorkLog({ ...workLog, completedQty: parseInt(e.target.value) || 0 })}\n className=\"w-full text-xl font-bold text-center text-green-600 border-0 focus:ring-2 focus:ring-green-500 rounded\"\n min=\"0\"\n />\n
\n
\n \n setWorkLog({ ...workLog, defectQty: parseInt(e.target.value) || 0 })}\n className=\"w-full text-xl font-bold text-center text-red-600 border-0 focus:ring-2 focus:ring-red-500 rounded\"\n min=\"0\"\n />\n
\n
\n
진행률
\n
\n {selectedWork.totalQty ? Math.round((workLog.completedQty / selectedWork.totalQty) * 100) : 0}%\n
\n
\n
\n\n
\n \n
\n\n
\n
\n
\n
\n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 탭 필터 (칩 스타일) - 검정/흰색 통일 스타일\n// 선택: 검정 배경 + 흰색 텍스트, 미선택: 흰색 배경 + 회색 테두리 + 검정 텍스트\nconst TabFilter = ({ tabs, activeTab, onTabChange }) => (\n
\n {tabs.map(tab => (\n \n ))}\n
\n);\n\n// 검색 입력\nconst SearchInput = ({ placeholder, value, onChange }) => (\n
\n \n onChange(e.target.value)}\n className=\"w-full pl-12 pr-4 py-3 bg-transparent rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n
\n);\n\n// 버튼\nconst Button = ({ variant = 'primary', size = 'md', children, onClick, disabled, className = '' }) => {\n const variants = {\n primary: 'bg-blue-500 text-white hover:bg-blue-600',\n secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200',\n success: 'bg-green-500 text-white hover:bg-green-600',\n danger: 'bg-red-500 text-white hover:bg-red-600',\n outline: 'border border-gray-300 text-gray-700 hover:bg-gray-50',\n ghost: 'text-gray-600 hover:bg-gray-100',\n };\n const sizes = {\n sm: 'px-3 py-1.5 text-sm',\n md: 'px-4 py-2 text-sm',\n lg: 'px-5 py-2.5 text-base',\n };\n return (\n
\n );\n};\n\n// 카드 섹션\n// ============ 반응형 유틸리티 ============\n\n// 화면 크기 감지 훅\nconst useResponsive = () => {\n const [screenSize, setScreenSize] = useState('desktop');\n\n useEffect(() => {\n const checkSize = () => {\n const width = window.innerWidth;\n if (width < 768) {\n setScreenSize('mobile');\n } else if (width < 1024) {\n setScreenSize('tablet');\n } else {\n setScreenSize('desktop');\n }\n };\n\n checkSize();\n window.addEventListener('resize', checkSize);\n return () => window.removeEventListener('resize', checkSize);\n }, []);\n\n return {\n screenSize,\n isMobile: screenSize === 'mobile',\n isTablet: screenSize === 'tablet',\n isDesktop: screenSize === 'desktop',\n isMobileOrTablet: screenSize === 'mobile' || screenSize === 'tablet',\n };\n};\n\n// 목록 선택 공통 훅 (CLAUDE.md 12번 규칙)\nconst useListSelection = (data = []) => {\n const [selectedIds, setSelectedIds] = useState([]);\n\n useEffect(() => {\n setSelectedIds(prev => prev.filter(id => data.some(item => item.id === id)));\n }, [data]);\n\n const handleSelect = (id) => {\n setSelectedIds(prev =>\n prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]\n );\n };\n\n const handleSelectAll = (e) => {\n setSelectedIds(e.target.checked ? data.map(item => item.id) : []);\n };\n\n const clearSelection = () => setSelectedIds([]);\n\n return {\n selectedIds,\n setSelectedIds,\n handleSelect,\n handleSelectAll,\n clearSelection,\n isAllSelected: data.length > 0 && selectedIds.length === data.length,\n hasSelection: selectedIds.length > 0,\n isMultiSelect: selectedIds.length > 1,\n isSelected: (id) => selectedIds.includes(id),\n };\n};\n\n// 모바일 헤더 컴포넌트\nconst MobileHeader = ({ onMenuToggle, onBack, showBack = false, title = '' }) => {\n return (\n
\n );\n};\n\n// 데스크탑/태블릿 헤더 컴포넌트\nconst DesktopHeader = ({\n user = { name: '김대표', role: '대표이사', roleName: '대표이사' },\n users = [],\n onUserChange,\n showFeatureDescription,\n onToggleFeatureDescription,\n // 버전 히스토리 관련\n currentVersion = 1,\n hasChanges = false,\n onOpenHistory,\n // 유저플로우 패널 관련\n showFlowPanel = false,\n onToggleFlowPanel,\n userNickname,\n // 기능정의문서 패널 관련\n showFeatureDocPanel = false,\n onToggleFeatureDocPanel,\n // 종합 플로우차트/기능정의서 패널 관련\n showComprehensiveFlowPanel = false,\n onToggleComprehensiveFlowPanel,\n showAllMenuFeatureDocPanel = false,\n onToggleAllMenuFeatureDocPanel,\n // 어드민 모드\n isFeatureAdmin = false\n}) => {\n return (\n
\n \n
\n\n
\n {/* 버전 히스토리 버튼 */}\n
\n\n {/* 종합 플로우차트 패널 토글 버튼 */}\n
\n\n {/* 전체 메뉴 기능정의문서 패널 토글 */}\n
\n\n {/* 기능정의서 보기 토글 */}\n
\n
\n
\n
\n
\n \n
\n
\n
{user.name}
\n
{user.roleName || user.role}
\n
\n
\n
\n
\n
\n \n );\n};\n\n// 모바일 슬라이드 메뉴\nconst MobileMenu = ({ isOpen, onClose, activeMenu, onMenuChange, menuConfig }) => {\n const [expandedMenus, setExpandedMenus] = useState(['sales', 'production']);\n\n // 메뉴 그룹 정의\n const allMenuGroups = [\n {\n id: 'master', label: '기준정보', icon: Settings, subMenus: [\n { id: 'item-master', label: '품목기준', icon: Layers },\n { id: 'process-master', label: '공정기준', icon: GitBranch },\n { id: 'quality-master', label: '품질기준', icon: ShieldCheck },\n { id: 'customer-master', label: '거래처', icon: Users },\n { id: 'site-master', label: '현장', icon: MapPin },\n { id: 'order-master', label: '수주기준', icon: Package },\n { id: 'process', label: '공정', icon: GitBranch },\n { id: 'number-rule', label: '채번', icon: ClipboardList },\n { id: 'code-rule', label: '공통코드', icon: Layers },\n { id: 'quote-formula', label: '견적수식', icon: Calculator },\n { id: 'document-template', label: '문서양식', icon: FileText },\n ]\n },\n {\n id: 'sales', label: '판매', icon: DollarSign, subMenus: [\n { id: 'customer', label: '거래처', icon: Users },\n { id: 'quote', label: '견적', icon: FileText },\n { id: 'order', label: '수주', icon: Package },\n { id: 'site', label: '현장', icon: Building },\n { id: 'price', label: '단가', icon: DollarSign },\n ]\n },\n {\n id: 'outbound', label: '출고', icon: Upload, subMenus: [\n { id: 'shipment', label: '출하', icon: Truck },\n ]\n },\n {\n id: 'production', label: '생산', icon: Factory, subMenus: [\n { id: 'item', label: '품목', icon: Package },\n { id: 'production-dashboard', label: '생산 현황판', icon: BarChart3 },\n { id: 'work-order', label: '작업지시', icon: ClipboardList },\n { id: 'work-result', label: '작업실적', icon: Clipboard },\n { id: 'worker-task', label: '작업자 화면', icon: User },\n ]\n },\n {\n id: 'quality', label: '품질', icon: ShieldCheck, subMenus: [\n { id: 'inspection', label: '검사', icon: ShieldCheck },\n { id: 'defect', label: '부적합품', icon: XCircle },\n ]\n },\n {\n id: 'inventory', label: '자재', icon: Warehouse, subMenus: [\n { id: 'stock', label: '재고현황', icon: Box },\n { id: 'inbound', label: '입고', icon: Download },\n ]\n },\n {\n id: 'accounting', label: '회계', icon: CreditCard, subMenus: [\n {\n id: 'acc-customer', label: '거래처', icon: Building, subMenus: [\n { id: 'acc-customer-list', label: '거래처 신용등급 목록' },\n { id: 'acc-customer-register', label: '신규 거래처 신용등급 생성' },\n { id: 'acc-customer-edit', label: '신용등급 변경' },\n { id: 'acc-customer-statement', label: '거래명세서 발행' },\n { id: 'acc-customer-tax', label: '세금계산서 발행' },\n ]\n },\n {\n id: 'sales-account', label: '매출', icon: TrendingUp, subMenus: [\n { id: 'sales-list', label: '판매조회 리스트' },\n ]\n },\n {\n id: 'purchase', label: '매입', icon: ShoppingCart, subMenus: [\n { id: 'purchase-list', label: '품의서 리스트' },\n { id: 'purchase-register', label: '품의서 등록' },\n { id: 'expense-list', label: '지출결의서 리스트' },\n { id: 'expense-register', label: '지출결의서 등록' },\n { id: 'expense-estimate', label: '지출예상내역서' },\n ]\n },\n {\n id: 'cashbook', label: '금전출납부', icon: CreditCard, subMenus: [\n { id: 'cashbook-list', label: '금전출납부' },\n { id: 'cashbook-register', label: '출납 등록' },\n { id: 'cashbook-edit', label: '출납 수정' },\n ]\n },\n {\n id: 'collection', label: '수금', icon: DollarSign, subMenus: [\n { id: 'collection-list', label: '수금 리스트' },\n { id: 'collection-register', label: '수금 등록' },\n { id: 'receivable-list', label: '미수금 현황' },\n { id: 'bill-list', label: '어음 목록' },\n ]\n },\n {\n id: 'cost-analysis', label: '원가', icon: Calculator, subMenus: [\n { id: 'cost-list', label: '수주별 원가 분석' },\n { id: 'cost-summary', label: '원가요약' },\n { id: 'cost-material', label: '자재비' },\n { id: 'cost-outsourcing', label: '외주비' },\n { id: 'cost-labor', label: '인건비' },\n { id: 'cost-expense', label: '경비' },\n { id: 'cost-manufacturing', label: '제조원가' },\n ]\n },\n ]\n },\n ];\n\n const menuGroups = allMenuGroups.filter(g => menuConfig[g.id]?.enabled !== false);\n\n const toggleMenu = (menuId) => {\n setExpandedMenus(prev =>\n prev.includes(menuId) ? prev.filter(id => id !== menuId) : [...prev, menuId]\n );\n };\n\n if (!isOpen) return null;\n\n return (\n
\n
\n
\n {/* 헤더 */}\n
\n
\n
\n S\n
\n
\n
SAM
\n
Smart Automation Management
\n
\n
\n
\n
\n\n {/* 메뉴 */}\n
\n
\n
\n );\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// 메뉴ID-페이지타이틀 매핑 테이블\n// ═══════════════════════════════════════════════════════════════════\nconst menuTitleMap = {\n // 대시보드\n 'dashboard': '대시보드',\n 'sales-dashboard': '판매 대시보드',\n 'production-dashboard': '생산 현황판',\n 'quality-dashboard': '품질 대시보드',\n 'accounting-dashboard': '회계 대시보드',\n 'purchase-dashboard': '구매 대시보드',\n 'worker-dashboard': '작업자 대시보드',\n // 기준정보\n 'item-master': '품목기준관리',\n 'process-master': '공정기준관리',\n 'quality-master': '품질기준관리',\n 'customer-master': '거래처기준관리',\n 'site-master': '현장기준관리',\n 'order-master': '수주기준관리',\n 'process': '공정관리',\n 'number-rule': '채번관리',\n 'code-rule': '공통코드관리',\n 'quote-formula': '견적수식관리',\n 'document-template': '문서양식관리',\n // 판매관리\n 'customer': '거래처관리',\n 'quote': '견적관리',\n 'quote-register': '견적등록',\n 'order': '수주관리',\n 'order-list': '수주목록',\n 'order-detail': '수주상세',\n 'shipment': '출하관리',\n 'site': '현장관리',\n 'price': '단가관리',\n // 생산관리\n 'item': '품목관리',\n 'work-order': '작업지시 관리',\n 'work-result': '작업실적',\n 'worker-task': '작업자 화면',\n // 품질관리\n 'inspection': '검사관리',\n 'defect': '부적합품관리',\n // 자재관리\n 'stock': '재고현황',\n 'inbound': '입고관리',\n 'outbound': '출고관리',\n // 회계관리\n 'sales-account': '매출관리',\n 'tax-invoice': '세금계산서관리',\n 'collection': '수금관리',\n 'acc-customer': '회계 거래처관리',\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// DOM 요소 자동 분석 유틸리티\n// ═══════════════════════════════════════════════════════════════════\nconst analyzeElement = (element) => {\n if (!element) return null;\n\n const result = {\n uiInfo: {\n componentType: '',\n position: '',\n interaction: '',\n style: '',\n },\n funcInfo: {\n purpose: '',\n trigger: '',\n process: '',\n result: '',\n validation: '',\n },\n dataInfo: {\n source: '',\n endpoint: '',\n method: '',\n fields: '',\n relatedTable: '',\n },\n label: '',\n };\n\n // 요소 타입 분석\n const tagName = element.tagName?.toLowerCase();\n const type = element.type;\n const role = element.getAttribute?.('role');\n\n // 컴포넌트 타입 결정\n if (tagName === 'button' || role === 'button') {\n result.uiInfo.componentType = '버튼';\n result.uiInfo.interaction = '클릭';\n } else if (tagName === 'input') {\n if (type === 'checkbox') {\n result.uiInfo.componentType = '체크박스';\n result.uiInfo.interaction = '선택';\n } else if (type === 'radio') {\n result.uiInfo.componentType = '라디오';\n result.uiInfo.interaction = '선택';\n } else {\n result.uiInfo.componentType = '입력필드';\n result.uiInfo.interaction = '입력';\n }\n } else if (tagName === 'select') {\n result.uiInfo.componentType = '셀렉트박스';\n result.uiInfo.interaction = '선택';\n } else if (tagName === 'textarea') {\n result.uiInfo.componentType = '텍스트영역';\n result.uiInfo.interaction = '입력';\n } else if (tagName === 'a') {\n result.uiInfo.componentType = '링크';\n result.uiInfo.interaction = '클릭';\n } else if (tagName === 'table') {\n result.uiInfo.componentType = '테이블';\n result.uiInfo.interaction = '조회';\n } else if (tagName === 'tr') {\n result.uiInfo.componentType = '테이블행';\n result.uiInfo.interaction = '클릭';\n } else if (tagName === 'th' || tagName === 'td') {\n result.uiInfo.componentType = '테이블셀';\n } else if (tagName === 'label') {\n result.uiInfo.componentType = '라벨';\n } else if (tagName === 'img' || tagName === 'svg') {\n result.uiInfo.componentType = '아이콘/이미지';\n } else if (element.className?.includes('modal') || element.className?.includes('dialog')) {\n result.uiInfo.componentType = '모달';\n } else if (element.className?.includes('card')) {\n result.uiInfo.componentType = '카드';\n } else if (element.className?.includes('tab')) {\n result.uiInfo.componentType = '탭';\n result.uiInfo.interaction = '클릭';\n } else {\n result.uiInfo.componentType = '영역';\n }\n\n // 텍스트 내용 추출\n const text = element.innerText?.trim()?.substring(0, 50) ||\n element.value ||\n element.placeholder ||\n element.getAttribute?.('aria-label') ||\n element.title || '';\n\n if (text) {\n result.label = text.split('\\n')[0]; // 첫 줄만\n }\n\n // 스타일 분석\n const className = element.className || '';\n if (className.includes('bg-blue') || className.includes('primary')) {\n result.uiInfo.style = 'Primary 스타일';\n } else if (className.includes('bg-red') || className.includes('danger')) {\n result.uiInfo.style = 'Danger 스타일';\n } else if (className.includes('bg-green') || className.includes('success')) {\n result.uiInfo.style = 'Success 스타일';\n } else if (className.includes('bg-orange') || className.includes('warning')) {\n result.uiInfo.style = 'Warning 스타일';\n } else if (className.includes('bg-gray') || className.includes('secondary')) {\n result.uiInfo.style = 'Secondary 스타일';\n }\n\n // React 내부 정보에서 onClick 핸들러 분석\n try {\n const reactKey = Object.keys(element).find(key => key.startsWith('__reactFiber$'));\n if (reactKey) {\n const fiber = element[reactKey];\n let currentFiber = fiber;\n\n // props에서 onClick 찾기\n while (currentFiber) {\n const props = currentFiber.memoizedProps || currentFiber.pendingProps;\n if (props?.onClick) {\n const onClickStr = props.onClick.toString();\n\n // navigate 패턴 찾기\n const navigateMatch = onClickStr.match(/navigate\\(['\"]([^'\"]+)['\"]\\)/);\n if (navigateMatch) {\n const targetMenu = navigateMatch[1];\n const targetTitle = menuTitleMap[targetMenu] || targetMenu;\n result.funcInfo.result = `→ ${targetTitle} 페이지로 이동`;\n result.funcInfo.purpose = `${targetTitle} 화면으로 이동`;\n }\n\n // setView 패턴 찾기\n const setViewMatch = onClickStr.match(/setView\\(['\"]([^'\"]+)['\"]\\)/);\n if (setViewMatch) {\n const targetView = setViewMatch[1];\n const targetTitle = menuTitleMap[targetView] || targetView;\n result.funcInfo.result = `→ ${targetTitle} 화면으로 전환`;\n result.funcInfo.purpose = `${targetTitle} 화면 표시`;\n }\n\n // setActiveMenu 패턴 찾기\n const menuMatch = onClickStr.match(/setActiveMenu\\(['\"]([^'\"]+)['\"]\\)/);\n if (menuMatch) {\n const targetMenu = menuMatch[1];\n const targetTitle = menuTitleMap[targetMenu] || targetMenu;\n result.funcInfo.result = `→ ${targetTitle} 메뉴로 이동`;\n }\n\n // 모달 열기 패턴\n if (onClickStr.includes('setShow') && onClickStr.includes('true')) {\n const modalMatch = onClickStr.match(/setShow(\\w+)\\(true\\)/);\n if (modalMatch) {\n result.funcInfo.result = `→ ${modalMatch[1]} 모달 열기`;\n } else {\n result.funcInfo.result = '→ 모달/팝업 열기';\n }\n }\n\n // 저장/등록 패턴\n if (onClickStr.includes('save') || onClickStr.includes('Save') ||\n onClickStr.includes('submit') || onClickStr.includes('Submit')) {\n result.funcInfo.purpose = '데이터 저장';\n result.dataInfo.method = 'POST';\n }\n\n // 삭제 패턴\n if (onClickStr.includes('delete') || onClickStr.includes('Delete') ||\n onClickStr.includes('remove') || onClickStr.includes('Remove')) {\n result.funcInfo.purpose = '데이터 삭제';\n result.dataInfo.method = 'DELETE';\n }\n\n // 수정 패턴\n if (onClickStr.includes('edit') || onClickStr.includes('Edit') ||\n onClickStr.includes('update') || onClickStr.includes('Update')) {\n result.funcInfo.purpose = '데이터 수정';\n result.dataInfo.method = 'PUT';\n }\n\n break;\n }\n currentFiber = currentFiber.return;\n }\n }\n } catch (e) {\n // React 내부 접근 실패 시 무시\n }\n\n // href 속성 확인 (링크)\n const href = element.getAttribute?.('href');\n if (href && href !== '#') {\n result.funcInfo.result = `→ ${href} 로 이동`;\n }\n\n // placeholder에서 힌트 추출\n const placeholder = element.placeholder;\n if (placeholder) {\n result.funcInfo.purpose = `${placeholder} 입력`;\n }\n\n return result;\n};\n\n// 좌표로 요소 찾기\nconst getElementAtPoint = (x, y, containerRef) => {\n // 현재 드래그 중인 뱃지는 제외하고 요소 찾기\n const elements = document.elementsFromPoint(x, y);\n\n // 뱃지 오버레이와 패널을 제외한 실제 UI 요소 찾기\n for (const el of elements) {\n // 뱃지 관련 요소 제외\n if (el.closest('[data-badge-overlay]') || el.closest('[data-feature-panel]')) {\n continue;\n }\n // 의미 있는 요소 찾기\n if (el.tagName && !['HTML', 'BODY', 'MAIN', 'DIV', 'SECTION', 'ARTICLE'].includes(el.tagName.toUpperCase()) ||\n el.onclick || el.getAttribute?.('role') || el.className?.includes('btn') || el.className?.includes('button')) {\n return el;\n }\n // div지만 클릭 가능한 요소\n if (el.tagName === 'DIV' && (el.onclick || el.className?.includes('cursor-pointer') || el.className?.includes('hover:'))) {\n return el;\n }\n }\n\n // 못 찾으면 첫 번째 유의미한 요소\n return elements.find(el => el.tagName && !['HTML', 'BODY'].includes(el.tagName.toUpperCase()));\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// 닉네임 입력 모달 컴포넌트\n// ═══════════════════════════════════════════════════════════════════\nconst NicknameModal = ({ onSave, currentNickname }) => {\n const [nickname, setNickname] = useState(currentNickname || '');\n const inputRef = React.useRef(null);\n const isEditMode = !!currentNickname;\n\n React.useEffect(() => {\n if (inputRef.current) {\n inputRef.current.focus();\n // 수정 모드일 때 기존 이름 전체 선택\n if (isEditMode) {\n inputRef.current.select();\n }\n }\n }, [isEditMode]);\n\n const handleSubmit = (e) => {\n e.preventDefault();\n if (nickname.trim().length >= 2) {\n onSave(nickname.trim());\n }\n };\n\n return (\n
\n
\n
\n
{isEditMode ? '이름 변경' : '기능정의서 접근'}
\n
\n {isEditMode ? '새로운 닉네임을 입력해주세요' : '닉네임을 입력해주세요'}\n
\n
\n
\n
\n
\n );\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// 버전 히스토리 관리 모달 컴포넌트\n// ═══════════════════════════════════════════════════════════════════\nconst VersionHistoryModal = ({ isOpen, onClose, history, onCommit, onRollback, currentVersion, hasChanges, autoMessage }) => {\n if (!isOpen) return null;\n\n const handleQuickCommit = () => {\n if (!autoMessage) {\n alert('변경사항이 없습니다.');\n return;\n }\n onCommit(autoMessage);\n };\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n
\n \n 버전 히스토리\n
\n
\n 현재 버전: v{currentVersion} {hasChanges && • 변경사항 있음}\n
\n
\n
\n
\n\n {/* 커밋 버튼 - 자동 메시지 */}\n {hasChanges && (\n
\n
\n
자동 생성된 커밋 메시지:
\n
{autoMessage}
\n
\n
\n
\n )}\n\n {/* 히스토리 목록 */}\n
\n {history.length === 0 ? (\n
\n
\n
아직 커밋된 버전이 없습니다.
\n
변경 후 커밋하면 히스토리가 기록됩니다.
\n
\n ) : (\n
\n {history.map((item, index) => (\n
\n
\n
\n
\n v{item.version}\n {index === 0 && (\n 현재\n )}\n
\n
{item.message}
\n {item.summary && (\n
{item.summary}
\n )}\n
\n \n \n {item.author}\n \n \n \n {new Date(item.timestamp).toLocaleString()}\n \n
\n
\n {index > 0 && (\n
\n )}\n
\n
\n ))}\n
\n )}\n
\n\n {/* 푸터 */}\n
\n 💡 커밋하면 현재 상태가 저장되고, 다른 사용자에게 공개됩니다.\n
\n
\n
\n );\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// 유저플로우 모달 컴포넌트\n// ═══════════════════════════════════════════════════════════════════\nconst UserFlowModal = ({ isOpen, onClose, currentScreen, allBadges, allScreens }) => {\n const [selectedScreen, setSelectedScreen] = useState(currentScreen);\n const [flowData, setFlowData] = useState(() => {\n const saved = localStorage.getItem('pageUserFlows');\n return saved ? JSON.parse(saved) : {};\n });\n\n // 플로우 데이터 저장\n React.useEffect(() => {\n localStorage.setItem('pageUserFlows', JSON.stringify(flowData));\n }, [flowData]);\n\n // 현재 화면 변경 시 선택 화면 업데이트\n React.useEffect(() => {\n if (currentScreen) setSelectedScreen(currentScreen);\n }, [currentScreen]);\n\n if (!isOpen) return null;\n\n // 현재 화면의 뱃지들\n const screenBadges = allBadges[selectedScreen] || [];\n\n // 뱃지에서 데이터 흐름 정보 추출\n const extractDataFlows = (badges) => {\n const flows = [];\n badges.forEach(badge => {\n // 데이터 소스가 있는 경우\n if (badge.dataInfo?.source) {\n flows.push({\n type: 'data_source',\n from: badge.dataInfo.source,\n to: selectedScreen,\n label: badge.dataInfo.fields || badge.description,\n badgeId: badge.id,\n badgeNumber: badge.number\n });\n }\n // API 엔드포인트가 있는 경우\n if (badge.dataInfo?.endpoint) {\n flows.push({\n type: 'api',\n endpoint: badge.dataInfo.endpoint,\n method: badge.dataInfo.method || 'GET',\n label: badge.description,\n badgeId: badge.id,\n badgeNumber: badge.number\n });\n }\n // 관련 테이블이 있는 경우\n if (badge.dataInfo?.relatedTable) {\n flows.push({\n type: 'table',\n table: badge.dataInfo.relatedTable,\n label: badge.description,\n badgeId: badge.id,\n badgeNumber: badge.number\n });\n }\n });\n return flows;\n };\n\n // 액션 플로우 (사용자 인터랙션)\n const extractActionFlows = (badges) => {\n return badges\n .filter(b => b.uiInfo?.interaction || b.funcInfo?.trigger)\n .map(badge => ({\n id: badge.id,\n number: badge.number,\n action: badge.uiInfo?.interaction || badge.funcInfo?.trigger,\n component: badge.uiInfo?.componentType,\n description: badge.description,\n result: badge.funcInfo?.result,\n process: badge.funcInfo?.process\n }));\n };\n\n const dataFlows = extractDataFlows(screenBadges);\n const actionFlows = extractActionFlows(screenBadges);\n\n // 다른 화면과의 연결 관계 찾기\n const findConnectedScreens = () => {\n const connections = [];\n\n // 데이터 소스에서 다른 화면 참조 찾기\n screenBadges.forEach(badge => {\n const desc = (badge.description || '').toLowerCase();\n const source = (badge.dataInfo?.source || '').toLowerCase();\n\n Object.keys(allScreens || {}).forEach(screenKey => {\n if (screenKey !== selectedScreen) {\n const screenName = screenKey.split('-')[0].toLowerCase();\n if (desc.includes(screenName) || source.includes(screenName)) {\n connections.push({\n targetScreen: screenKey,\n type: 'reference',\n badge: badge\n });\n }\n }\n });\n });\n\n return connections;\n };\n\n const connectedScreens = findConnectedScreens();\n\n // 화면 이름 포맷팅\n const formatScreenName = (key) => {\n if (!key) return '';\n const parts = key.split('-');\n return parts[0];\n };\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n
\n \n 페이지 유저플로우\n
\n
데이터 흐름 & 사용자 인터랙션
\n
\n
\n
\n\n {/* 화면 선택 */}\n
\n
\n 화면:\n \n \n 기능정의서 뱃지에서 자동 생성됨\n \n
\n
\n\n {/* 플로우 내용 */}\n
\n {screenBadges.length === 0 ? (\n
\n
\n
이 화면에 등록된 기능정의서가 없습니다
\n
기능정의서에 뱃지를 추가하면 자동으로 플로우가 생성됩니다
\n
\n ) : (\n
\n {/* 화면 개요 */}\n
\n
\n \n {formatScreenName(selectedScreen)} 화면 개요\n
\n
\n
\n
{screenBadges.length}
\n
전체 기능
\n
\n
\n
{actionFlows.length}
\n
인터랙션
\n
\n
\n
{dataFlows.length}
\n
데이터 연결
\n
\n
\n
\n\n {/* 데이터 흐름 */}\n {dataFlows.length > 0 && (\n
\n
\n \n 데이터 흐름\n
\n
\n {dataFlows.map((flow, idx) => (\n
\n
\n {flow.badgeNumber}\n \n {flow.type === 'data_source' && (\n <>\n
\n {flow.from}\n \n
\n
\n {formatScreenName(flow.to)}\n \n
{flow.label}\n >\n )}\n {flow.type === 'api' && (\n <>\n
\n {flow.method}\n \n
{flow.endpoint}\n
{flow.label}\n >\n )}\n {flow.type === 'table' && (\n <>\n
\n 테이블\n \n
{flow.table}\n
{flow.label}\n >\n )}\n
\n ))}\n
\n
\n )}\n\n {/* 사용자 인터랙션 플로우 */}\n {actionFlows.length > 0 && (\n
\n
\n \n 사용자 인터랙션 플로우\n
\n
\n {actionFlows.map((action, idx) => (\n
\n
\n
\n {action.number}\n \n
\n
\n {action.component && (\n \n {action.component}\n \n )}\n {action.action && (\n \n {action.action}\n \n )}\n
\n
{action.description}
\n {action.process && (\n
\n )}\n {action.result && (\n
\n \n {action.result}\n
\n )}\n
\n
\n
\n ))}\n
\n
\n )}\n\n {/* 연결된 화면들 */}\n {connectedScreens.length > 0 && (\n
\n
\n \n 연결된 화면\n
\n
\n {[...new Set(connectedScreens.map(c => c.targetScreen))].map(screen => (\n
\n ))}\n
\n
\n )}\n\n {/* 전체 플로우 다이어그램 */}\n
\n
\n \n 플로우 다이어그램\n
\n
\n {/* 간단한 플로우 시각화 */}\n
\n {/* 데이터 소스 */}\n {dataFlows.filter(f => f.type === 'data_source').length > 0 && (\n
\n {[...new Set(dataFlows.filter(f => f.type === 'data_source').map(f => f.from))].map(source => (\n
\n 📊 {source}\n
\n ))}\n
\n )}\n\n {dataFlows.length > 0 && (\n
\n )}\n\n {/* 현재 화면 */}\n
\n {formatScreenName(selectedScreen)}\n
{screenBadges.length}개 기능
\n
\n\n {/* 인터랙션 표시 */}\n {actionFlows.length > 0 && (\n
\n {actionFlows.slice(0, 6).map(action => (\n
\n {action.action || action.component}\n
\n ))}\n {actionFlows.length > 6 && (\n
\n +{actionFlows.length - 6}개\n
\n )}\n
\n )}\n\n {/* 연결된 화면 */}\n {connectedScreens.length > 0 && (\n <>\n
\n
\n {[...new Set(connectedScreens.map(c => c.targetScreen))].slice(0, 4).map(screen => (\n
\n 🔗 {formatScreenName(screen)}\n
\n ))}\n
\n >\n )}\n
\n
\n
\n
\n )}\n
\n\n {/* 푸터 */}\n
\n 💡 기능정의서 뱃지의 데이터 연동/기능 정보에서 자동 생성됩니다\n \n 마지막 업데이트: {new Date().toLocaleString()}\n \n
\n
\n
\n );\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// 인라인 뱃지 입력 컴포넌트 (클릭한 위치에 표시)\n// ═══════════════════════════════════════════════════════════════════\nconst InlineBadgeInput = ({ position, number, onSave, onCancel, existingBadge, onDelete }) => {\n const [label, setLabel] = useState(existingBadge?.label || '');\n const [description, setDescription] = useState(existingBadge?.description || '');\n const [showDetails, setShowDetails] = useState(false);\n const [uiType, setUiType] = useState(existingBadge?.uiInfo?.componentType || '');\n const [interaction, setInteraction] = useState(existingBadge?.uiInfo?.interaction || '');\n const [selectedColor, setSelectedColor] = useState(existingBadge?.color || 'red');\n const labelInputRef = React.useRef(null);\n const inputRef = React.useRef(null);\n\n const isEditMode = !!existingBadge;\n\n // 뱃지 색상 옵션 - 컬러로 표시\n const colorOptions = [\n { id: 'red', name: '기본', hex: '#ef4444' },\n { id: 'blue', name: '정보', hex: '#3b82f6' },\n { id: 'green', name: '완료', hex: '#22c55e' },\n { id: 'purple', name: '중요', hex: '#a855f7' },\n { id: 'orange', name: '주의', hex: '#f97316' },\n { id: 'gray', name: '참고', hex: '#6b7280' },\n ];\n\n // UI 컴포넌트 타입 옵션\n const uiTypeOptions = ['버튼', '입력필드', '테이블', '모달', '드롭다운', '체크박스', '라디오', '탭', '카드', '아이콘', '레이블', '기타'];\n const interactionOptions = ['클릭', '더블클릭', '호버', '입력', '선택', '드래그', '스크롤', '포커스', '제출', '기타'];\n\n // 자동 포커스 (제목 입력창에 먼저 포커스)\n React.useEffect(() => {\n if (labelInputRef.current) {\n labelInputRef.current.focus();\n }\n }, []);\n\n // Enter로 저장, ESC로 취소\n const handleKeyDown = (e) => {\n if (e.key === 'Enter' && !e.shiftKey && !showDetails) {\n e.preventDefault();\n handleSave();\n }\n if (e.key === 'Escape') {\n onCancel();\n }\n };\n\n const handleSave = () => {\n if (!label.trim() && !description.trim() && !uiType && !interaction) return;\n\n const badgeData = {\n label: label.trim(),\n description: description.trim(),\n color: selectedColor,\n uiInfo: (uiType || interaction) ? {\n componentType: uiType || undefined,\n interaction: interaction || undefined,\n } : undefined,\n };\n onSave(badgeData);\n };\n\n // 입력창 위치 계산 (화면 밖으로 나가지 않도록)\n const getPopupStyle = () => {\n const style = {\n position: 'absolute',\n zIndex: 1000,\n };\n\n if (position.x > 60) {\n style.right = `${100 - position.x}%`;\n } else {\n style.left = `${position.x}%`;\n }\n\n if (position.y > 50) {\n style.bottom = `${100 - position.y}%`;\n } else {\n style.top = `${position.y}%`;\n }\n\n return style;\n };\n\n return (\n
e.stopPropagation()}\n >\n {/* 헤더 - 와이어프레임 스타일 */}\n
\n
\n \n {number}\n \n {isEditMode ? '뱃지 수정' : '뱃지 추가'}\n
\n
\n
\n\n {/* 입력 영역 */}\n
\n {/* 기능 제목 */}\n
\n \n setLabel(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"예: 검색, 등록, 삭제, 목록 테이블...\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-gray-500\"\n />\n
\n\n {/* 기능 설명 */}\n
\n \n
\n\n {/* 색상 선택 */}\n
\n
\n
\n {colorOptions.map(color => (\n
\n
\n\n {/* 상세정보 토글 - 와이어프레임 스타일 */}\n
\n\n {/* 상세정보 입력 - 와이어프레임 스타일 */}\n {showDetails && (\n
\n
\n \n \n
\n
\n \n \n
\n
\n )}\n\n {/* 버튼 영역 */}\n
\n
\n {showDetails ? '' : 'Enter: 저장'}\n
\n
\n {isEditMode && onDelete && (\n \n )}\n \n \n
\n
\n
\n
\n );\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// 기능정의문서 모달 컴포넌트 (화면별 상세 기능정의서)\n// ═══════════════════════════════════════════════════════════════════\nconst FeatureDocumentModal = ({ isOpen, onClose, screenKey, screenName }) => {\n const [expandedSections, setExpandedSections] = useState({});\n const [expandedItems, setExpandedItems] = useState({});\n const [filterType, setFilterType] = useState('all'); // 'all', 'ui', 'function', 'data', 'interaction', 'state', 'validation'\n\n // 화면별 기능정의서 데이터 조회\n const featureDoc = getScreenFeatureDocument(screenKey);\n\n if (!isOpen) return null;\n\n // 필터 타입별 색상 - 와이어프레임 스타일: 그레이스케일\n const filterColors = {\n all: 'bg-gray-100 text-gray-700',\n ui: 'bg-gray-200 text-gray-800',\n function: 'bg-gray-300 text-gray-800',\n data: 'bg-gray-200 text-gray-700',\n interaction: 'bg-gray-300 text-gray-700',\n state: 'bg-gray-200 text-gray-800',\n validation: 'bg-gray-300 text-gray-800'\n };\n\n // 속성 유형별 색상 - 와이어프레임 스타일: 그레이스케일\n const propertyTypeColors = {\n ui: { bg: 'bg-gray-50', border: 'border-gray-300', text: 'text-gray-700', badge: 'bg-gray-200 text-gray-800' },\n function: { bg: 'bg-gray-100', border: 'border-gray-300', text: 'text-gray-700', badge: 'bg-gray-300 text-gray-800' },\n data: { bg: 'bg-gray-50', border: 'border-gray-300', text: 'text-gray-700', badge: 'bg-gray-200 text-gray-700' },\n interaction: { bg: 'bg-gray-100', border: 'border-gray-300', text: 'text-gray-700', badge: 'bg-gray-300 text-gray-700' },\n state: { bg: 'bg-gray-50', border: 'border-gray-300', text: 'text-gray-800', badge: 'bg-gray-200 text-gray-800' },\n validation: { bg: 'bg-gray-100', border: 'border-gray-300', text: 'text-gray-800', badge: 'bg-gray-300 text-gray-800' }\n };\n\n // 섹션 유형별 아이콘\n const sectionTypeIcons = {\n header: '📋',\n search: '🔍',\n table: '📊',\n form: '📝',\n modal: '🔲',\n button: '🔘',\n card: '🃏',\n tab: '📑',\n list: '📃',\n tree: '🌳'\n };\n\n // 항목 유형별 아이콘\n const itemTypeIcons = {\n button: '🔘',\n input: '✏️',\n select: '📋',\n checkbox: '☑️',\n 'checkbox-group': '☑️',\n radio: '🔘',\n 'table-column': '📊',\n 'stat-card': '📈',\n label: '🏷️',\n 'list-item': '•',\n 'tree-node': '🌿',\n 'modal-header': '🔲',\n content: '📄',\n card: '🃏',\n 'tab-button': '📑'\n };\n\n const toggleSection = (sectionIndex) => {\n setExpandedSections(prev => ({\n ...prev,\n [sectionIndex]: !prev[sectionIndex]\n }));\n };\n\n const toggleItem = (sectionIndex, itemIndex) => {\n const key = `${sectionIndex}-${itemIndex}`;\n setExpandedItems(prev => ({\n ...prev,\n [key]: !prev[key]\n }));\n };\n\n // 필터링된 속성 반환\n const getFilteredProperties = (properties) => {\n if (filterType === 'all') return properties;\n return properties.filter(prop => prop.propertyType === filterType);\n };\n\n // 아이템에 필터에 맞는 속성이 있는지 확인\n const hasFilteredProperties = (item) => {\n if (filterType === 'all') return true;\n return item.properties?.some(prop => prop.propertyType === filterType);\n };\n\n return (\n
\n
\n {/* 헤더 - 와이어프레임 스타일 */}\n
\n
\n
\n
\n 기능정의문서\n
\n
\n {featureDoc ? `${featureDoc.menuPath} > ${featureDoc.screenName}` : screenName || screenKey}\n
\n
\n
\n
\n
\n\n {/* 필터 탭 */}\n
\n
\n 필터:\n {[\n { key: 'all', label: '전체' },\n { key: 'ui', label: 'UI 표시' },\n { key: 'interaction', label: '인터랙션' },\n { key: 'function', label: '기능' },\n { key: 'state', label: '상태변화' },\n { key: 'data', label: '데이터' },\n { key: 'validation', label: '유효성검사' }\n ].map(filter => (\n \n ))}\n
\n
\n\n {/* 본문 */}\n
\n {!featureDoc ? (\n
\n
\n
기능정의서가 없습니다
\n
\n 이 화면({screenKey})에 대한 기능정의서가 아직 작성되지 않았습니다.\n
\n
\n ) : (\n
\n {/* 화면 설명 - 와이어프레임 스타일 */}\n {featureDoc.description && (\n
\n
{featureDoc.description}
\n
\n )}\n\n {/* 섹션 목록 */}\n {featureDoc.sections?.map((section, sectionIndex) => {\n // 필터에 맞는 아이템이 있는지 확인\n const hasFilteredItems = section.items?.some(item => hasFilteredProperties(item));\n if (filterType !== 'all' && !hasFilteredItems) return null;\n\n const isExpanded = expandedSections[sectionIndex] !== false; // 기본 펼침\n\n return (\n
\n {/* 섹션 헤더 */}\n
\n\n {/* 항목 목록 */}\n {isExpanded && (\n
\n {section.items?.map((item, itemIndex) => {\n if (filterType !== 'all' && !hasFilteredProperties(item)) return null;\n\n const itemKey = `${sectionIndex}-${itemIndex}`;\n const isItemExpanded = expandedItems[itemKey] !== false; // 기본 펼침\n const filteredProps = getFilteredProperties(item.properties || []);\n\n return (\n
\n {/* 항목 헤더 */}\n
\n\n {/* 속성 목록 */}\n {isItemExpanded && filteredProps.length > 0 && (\n
\n {filteredProps.map((prop, propIndex) => {\n const colors = propertyTypeColors[prop.propertyType] || propertyTypeColors.ui;\n return (\n
\n
\n
\n {prop.propertyType === 'ui' && 'UI'}\n {prop.propertyType === 'function' && '기능'}\n {prop.propertyType === 'data' && '데이터'}\n {prop.propertyType === 'interaction' && '인터랙션'}\n {prop.propertyType === 'state' && '상태'}\n {prop.propertyType === 'validation' && '검증'}\n \n
\n
\n {prop.propertyName}\n
\n
\n {prop.description}\n
\n
\n
\n
\n );\n })}\n
\n )}\n
\n );\n })}\n
\n )}\n
\n );\n })}\n\n {/* 상태 관리 섹션 - 와이어프레임 스타일 */}\n {featureDoc.stateManagement && featureDoc.stateManagement.length > 0 && (\n
\n
\n
\n
\n
\n \n \n | State 이름 | \n 타입 | \n 초기값 | \n 설명 | \n
\n \n \n {featureDoc.stateManagement.map((state, idx) => (\n \n | {state.stateName} | \n {state.type} | \n {state.initialValue} | \n {state.description} | \n
\n ))}\n \n
\n
\n
\n
\n )}\n\n {/* 데이터 흐름 섹션 - 와이어프레임 스타일 */}\n {featureDoc.dataFlow && featureDoc.dataFlow.length > 0 && (\n
\n
\n
\n 데이터 흐름 (Data Flow)\n
\n
\n
\n {featureDoc.dataFlow.map((flow, idx) => (\n
\n
\n {idx + 1}\n
\n
\n
\n \n {flow.trigger}\n \n →\n \n {flow.action}\n \n
\n
{flow.result}
\n
\n
\n ))}\n
\n
\n )}\n
\n )}\n
\n\n {/* 푸터 */}\n
\n
\n 화면 키: {screenKey}\n
\n
\n
\n
\n
\n );\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// 기능정의서 패널 컴포넌트\n// ═══════════════════════════════════════════════════════════════════\nconst FeatureDescriptionPanel = ({\n isOpen,\n onClose,\n badges,\n selectedBadge,\n onSelectBadge,\n onUpdateBadge,\n onDeleteBadge,\n onAddBadge,\n isAddingBadge,\n setIsAddingBadge,\n badgeEditMode,\n setBadgeEditMode,\n screenName,\n screenPath = '',\n screenId = '',\n allBadges = {},\n onImportBadges,\n // 닉네임 관련\n userNickname,\n onChangeNickname,\n // 템플릿 로드 관련\n onLoadTemplate,\n currentScreenKey = '',\n hasTemplate = false,\n // 기능 추출 관련\n onExtractFeatures,\n // 댓글 관련\n onAddComment,\n onDeleteComment,\n // 어드민 모드\n isAdmin = false,\n}) => {\n const fileInputRef = React.useRef(null);\n const [showBackupMenu, setShowBackupMenu] = useState(false);\n const [newComment, setNewComment] = useState('');\n const [expandedComments, setExpandedComments] = useState({}); // 뱃지별 댓글 아코디언 상태\n const [commentInputs, setCommentInputs] = useState({}); // 뱃지별 댓글 입력값\n const [editingBadgeId, setEditingBadgeId] = useState(null); // 인라인 수정 중인 뱃지 ID\n const [editFormData, setEditFormData] = useState({}); // 수정 폼 데이터\n const [lastSaved, setLastSaved] = useState(() => {\n return localStorage.getItem('featureBadges_lastSaved') || null;\n });\n const [autoBackupEnabled, setAutoBackupEnabled] = useState(() => {\n return localStorage.getItem('featureBadges_autoBackup') === 'true';\n });\n const [lastAutoBackup, setLastAutoBackup] = useState(() => {\n return localStorage.getItem('featureBadges_lastAutoBackup') || null;\n });\n\n // 버전 관리 상태\n const [showVersionModal, setShowVersionModal] = useState(false);\n const [showVersionHistory, setShowVersionHistory] = useState(false);\n const [versionChangeLog, setVersionChangeLog] = useState('');\n\n // 기능정의문서 모달 상태\n const [showFeatureDocModal, setShowFeatureDocModal] = useState(false);\n\n // 현재 화면에 기능정의서가 있는지 확인\n const hasFeatureDoc = !!getScreenFeatureDocument(currentScreenKey);\n const [screenVersions, setScreenVersions] = useState(() => {\n const saved = localStorage.getItem('featureDescriptionVersions');\n return saved ? JSON.parse(saved) : {};\n });\n\n // 현재 화면의 버전 정보 가져오기\n const currentVersion = screenVersions[currentScreenKey] || { version: '1.0.0', history: [] };\n\n // 버전 업그레이드 함수\n const upgradeVersion = (type = 'patch') => {\n const [major, minor, patch] = currentVersion.version.split('.').map(Number);\n let newVersion;\n\n if (type === 'major') {\n newVersion = `${major + 1}.0.0`;\n } else if (type === 'minor') {\n newVersion = `${major}.${minor + 1}.0`;\n } else {\n newVersion = `${major}.${minor}.${patch + 1}`;\n }\n\n const historyEntry = {\n version: newVersion,\n previousVersion: currentVersion.version,\n changeLog: versionChangeLog,\n updatedAt: new Date().toISOString(),\n updatedBy: userNickname || '익명',\n badgeCount: badges.length\n };\n\n const updatedVersionData = {\n version: newVersion,\n history: [historyEntry, ...(currentVersion.history || [])]\n };\n\n const updatedVersions = {\n ...screenVersions,\n [currentScreenKey]: updatedVersionData\n };\n\n setScreenVersions(updatedVersions);\n localStorage.setItem('featureDescriptionVersions', JSON.stringify(updatedVersions));\n setVersionChangeLog('');\n setShowVersionModal(false);\n };\n\n // 자동 변경 이력 저장 (뱃지 추가/수정/삭제 시)\n const prevBadgesRef = React.useRef(badges);\n const autoSaveTimeoutRef = React.useRef(null);\n\n React.useEffect(() => {\n // 초기 로드 시 또는 화면 전환 시 이전 상태 업데이트\n if (!currentScreenKey) return;\n\n const prevBadges = prevBadgesRef.current;\n const prevCount = prevBadges?.length || 0;\n const currentCount = badges?.length || 0;\n\n // 변경 감지 (뱃지 수 변경 또는 내용 변경)\n const hasChanged = prevCount !== currentCount ||\n JSON.stringify(prevBadges) !== JSON.stringify(badges);\n\n if (hasChanged && prevBadges !== badges) {\n // 디바운스: 연속 변경 시 마지막 변경만 저장\n if (autoSaveTimeoutRef.current) {\n clearTimeout(autoSaveTimeoutRef.current);\n }\n\n autoSaveTimeoutRef.current = setTimeout(() => {\n // 변경 유형 감지\n let changeType = '';\n let changeDetail = '';\n\n if (currentCount > prevCount) {\n const addedCount = currentCount - prevCount;\n changeType = '뱃지 추가';\n changeDetail = `${addedCount}개 뱃지 추가됨`;\n } else if (currentCount < prevCount) {\n const removedCount = prevCount - currentCount;\n changeType = '뱃지 삭제';\n changeDetail = `${removedCount}개 뱃지 삭제됨`;\n } else {\n changeType = '뱃지 수정';\n changeDetail = '뱃지 내용 수정됨';\n }\n\n // 자동 이력 저장 (패치 버전은 올리지 않고 이력만 추가)\n const autoHistoryEntry = {\n version: currentVersion.version,\n previousVersion: currentVersion.version,\n changeLog: `[자동저장] ${changeType}: ${changeDetail}`,\n updatedAt: new Date().toISOString(),\n updatedBy: userNickname || 'Claude',\n badgeCount: currentCount,\n isAutoSave: true\n };\n\n const updatedVersionData = {\n ...currentVersion,\n lastModified: new Date().toISOString(),\n history: [autoHistoryEntry, ...(currentVersion.history || []).slice(0, 49)] // 최대 50개 이력 유지\n };\n\n const updatedVersions = {\n ...screenVersions,\n [currentScreenKey]: updatedVersionData\n };\n\n setScreenVersions(updatedVersions);\n localStorage.setItem('featureDescriptionVersions', JSON.stringify(updatedVersions));\n }, 2000); // 2초 디바운스\n }\n\n prevBadgesRef.current = badges;\n\n return () => {\n if (autoSaveTimeoutRef.current) {\n clearTimeout(autoSaveTimeoutRef.current);\n }\n };\n }, [badges, currentScreenKey, currentVersion, screenVersions, userNickname]);\n\n // 비밀번호 확인 후 삭제\n const DELETE_PASSWORD = '1234';\n const handleDeleteWithPassword = (badgeId) => {\n const password = window.prompt('삭제하려면 비밀번호를 입력하세요:');\n if (password === null) return; // 취소\n if (password === DELETE_PASSWORD) {\n onDeleteBadge(badgeId);\n setEditingBadgeId(null);\n } else {\n alert('비밀번호가 올바르지 않습니다.');\n }\n };\n\n // 인라인 수정 시작\n const startEditing = (badge) => {\n setEditingBadgeId(badge.id);\n setEditFormData({\n label: badge.label || '',\n description: badge.description || '',\n color: badge.color || 'red',\n uiInfo: badge.uiInfo || {},\n funcInfo: badge.funcInfo || {},\n dataInfo: badge.dataInfo || {},\n });\n };\n\n // 인라인 수정 저장\n const saveEditing = () => {\n if (editingBadgeId && editFormData.label?.trim()) {\n onUpdateBadge(editingBadgeId, editFormData);\n setEditingBadgeId(null);\n setEditFormData({});\n }\n };\n\n // 인라인 수정 취소\n const cancelEditing = () => {\n setEditingBadgeId(null);\n setEditFormData({});\n };\n\n // 자동 백업 실행 함수\n const performAutoBackup = React.useCallback(() => {\n if (Object.keys(allBadges).length === 0) return; // 데이터가 없으면 백업 안함\n\n const exportData = {\n version: '1.0',\n exportedAt: new Date().toISOString(),\n autoBackup: true,\n badges: allBadges\n };\n const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `기능정의서_자동백업_${new Date().toISOString().slice(0, 10)}.json`;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n\n const now = new Date().toISOString();\n localStorage.setItem('featureBadges_lastAutoBackup', now);\n setLastAutoBackup(now);\n }, [allBadges]);\n\n // 자동 백업 체크 - 앱 로드 시 하루가 지났으면 자동 백업\n React.useEffect(() => {\n if (!autoBackupEnabled) return;\n if (Object.keys(allBadges).length === 0) return;\n\n const today = new Date().toISOString().slice(0, 10);\n const lastBackupDate = lastAutoBackup ? lastAutoBackup.slice(0, 10) : null;\n\n // 오늘 이미 백업했으면 스킵\n if (lastBackupDate === today) return;\n\n // 약간의 지연 후 자동 백업 실행 (앱 로드 완료 후)\n const timer = setTimeout(() => {\n performAutoBackup();\n }, 2000);\n\n return () => clearTimeout(timer);\n }, [autoBackupEnabled, lastAutoBackup, allBadges, performAutoBackup]);\n\n // 자동 백업 토글\n const toggleAutoBackup = () => {\n const newValue = !autoBackupEnabled;\n setAutoBackupEnabled(newValue);\n localStorage.setItem('featureBadges_autoBackup', newValue.toString());\n if (newValue) {\n // 자동 백업 활성화 시 즉시 백업 실행\n performAutoBackup();\n }\n };\n\n // 전체 뱃지 내보내기 (JSON 파일 다운로드)\n const exportBadges = () => {\n const exportData = {\n version: '1.0',\n exportedAt: new Date().toISOString(),\n badges: allBadges\n };\n const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `기능정의서_백업_${new Date().toISOString().slice(0, 10)}.json`;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n setShowBackupMenu(false);\n };\n\n // JSON 파일에서 뱃지 가져오기\n const importBadges = (event) => {\n const file = event.target.files[0];\n if (!file) return;\n\n const reader = new FileReader();\n reader.onload = (e) => {\n try {\n const data = JSON.parse(e.target.result);\n if (data.badges) {\n const confirmImport = window.confirm(\n `기존 데이터를 백업 파일로 교체하시겠습니까?\\n\\n` +\n `백업 파일 정보:\\n` +\n `- 내보낸 날짜: ${data.exportedAt ? new Date(data.exportedAt).toLocaleString() : '알 수 없음'}\\n` +\n `- 화면 수: ${Object.keys(data.badges).length}개\\n` +\n `- 전체 뱃지 수: ${Object.values(data.badges).flat().length}개`\n );\n if (confirmImport) {\n onImportBadges(data.badges);\n const now = new Date().toISOString();\n localStorage.setItem('featureBadges_lastSaved', now);\n setLastSaved(now);\n alert('✅ 백업 파일을 성공적으로 불러왔습니다!');\n }\n } else {\n alert('올바른 백업 파일 형식이 아닙니다.');\n }\n } catch (err) {\n alert('파일을 읽는 중 오류가 발생했습니다: ' + err.message);\n }\n };\n reader.readAsText(file);\n event.target.value = ''; // 같은 파일 다시 선택 가능하도록\n setShowBackupMenu(false);\n };\n\n // 수동 저장 (localStorage에 저장 시점 기록)\n const manualSave = () => {\n const now = new Date().toISOString();\n localStorage.setItem('featureBadges', JSON.stringify(allBadges));\n localStorage.setItem('featureBadges_lastSaved', now);\n setLastSaved(now);\n setShowBackupMenu(false);\n alert('✅ 저장되었습니다!');\n };\n\n // 전체 뱃지 통계\n const totalScreens = Object.keys(allBadges).length;\n const totalBadges = Object.values(allBadges).flat().length;\n\n // 아코디언 열림 상태\n const [expandedSections, setExpandedSections] = useState({\n uiInfo: false,\n funcInfo: false,\n dataInfo: false,\n });\n\n // 구조화된 뱃지 데이터\n const [newBadge, setNewBadge] = useState({\n label: '',\n color: 'blue',\n x: 50,\n y: 50,\n description: '', // 자유 텍스트 설명\n // 구조화된 정보\n uiInfo: {\n componentType: '', // 버튼, 입력필드, 테이블, 모달 등\n position: '', // 상단, 좌측, 중앙 등\n interaction: '', // 클릭, 입력, 선택, 드래그 등\n style: '', // 스타일 특징\n },\n funcInfo: {\n purpose: '', // 기능 목적\n trigger: '', // 실행 조건\n process: '', // 처리 과정\n result: '', // 결과/출력\n validation: '', // 유효성 검사\n },\n dataInfo: {\n source: '', // 데이터 출처 (API, State, DB)\n endpoint: '', // API 엔드포인트\n method: '', // GET, POST, PUT, DELETE\n fields: '', // 관련 필드\n relatedTable: '', // 관련 테이블/엔티티\n },\n });\n\n // 아코디언 토글\n const toggleSection = (section) => {\n setExpandedSections(prev => ({\n ...prev,\n [section]: !prev[section]\n }));\n };\n\n // 뱃지 색상 - 컬러로 표시\n const badgeColors = [\n { id: 'red', bg: 'bg-red-500', text: 'text-white', label: '기본', hex: '#ef4444' },\n { id: 'blue', bg: 'bg-blue-500', text: 'text-white', label: '정보', hex: '#3b82f6' },\n { id: 'green', bg: 'bg-green-500', text: 'text-white', label: '완료', hex: '#22c55e' },\n { id: 'purple', bg: 'bg-purple-500', text: 'text-white', label: '중요', hex: '#a855f7' },\n { id: 'orange', bg: 'bg-orange-500', text: 'text-white', label: '주의', hex: '#f97316' },\n { id: 'gray', bg: 'bg-gray-500', text: 'text-white', label: '참고', hex: '#6b7280' },\n ];\n\n // UI 컴포넌트 타입 옵션\n const componentTypes = ['버튼', '입력필드', '셀렉트박스', '테이블', '모달', '탭', '카드', '아이콘', '라벨', '링크', '체크박스', '라디오', '기타'];\n const interactionTypes = ['클릭', '더블클릭', '입력', '선택', '드래그', '호버', '포커스', '스크롤', '키보드', '자동'];\n const dataSourceTypes = ['API 호출', 'State 관리', 'Props 전달', 'LocalStorage', 'URL 파라미터', '하드코딩', '계산값'];\n const httpMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];\n\n const getColorClasses = (colorId) => {\n const color = badgeColors.find(c => c.id === colorId) || badgeColors[0];\n return `${color.bg} ${color.text}`;\n };\n\n // 뱃지 색상 hex 값 반환 (인라인 스타일용)\n const getColorHex = (colorId) => {\n const color = badgeColors.find(c => c.id === colorId) || badgeColors[0];\n return color.hex;\n };\n\n // 뱃지 추가 시 구조화된 설명 생성\n const generateDescription = (badge) => {\n let desc = '';\n\n // 자유 텍스트 설명이 있으면 먼저 추가\n if (badge.description) {\n desc += badge.description;\n }\n\n // UI 정보\n if (badge.uiInfo.componentType || badge.uiInfo.interaction) {\n if (desc) desc += '\\n\\n';\n desc += `【UI 정보】\\n`;\n if (badge.uiInfo.componentType) desc += `• 컴포넌트: ${badge.uiInfo.componentType}\\n`;\n if (badge.uiInfo.position) desc += `• 위치: ${badge.uiInfo.position}\\n`;\n if (badge.uiInfo.interaction) desc += `• 인터랙션: ${badge.uiInfo.interaction}\\n`;\n if (badge.uiInfo.style) desc += `• 스타일: ${badge.uiInfo.style}\\n`;\n }\n\n // 기능 정보\n if (badge.funcInfo.purpose || badge.funcInfo.process) {\n if (desc) desc += '\\n\\n';\n desc += `【기능 정보】\\n`;\n if (badge.funcInfo.purpose) desc += `• 목적: ${badge.funcInfo.purpose}\\n`;\n if (badge.funcInfo.trigger) desc += `• 실행조건: ${badge.funcInfo.trigger}\\n`;\n if (badge.funcInfo.process) desc += `• 처리과정: ${badge.funcInfo.process}\\n`;\n if (badge.funcInfo.result) desc += `• 결과: ${badge.funcInfo.result}\\n`;\n if (badge.funcInfo.validation) desc += `• 유효성검사: ${badge.funcInfo.validation}\\n`;\n }\n\n // 데이터 연동 정보\n if (badge.dataInfo.source || badge.dataInfo.endpoint) {\n if (desc) desc += '\\n\\n';\n desc += `【데이터 연동】\\n`;\n if (badge.dataInfo.source) desc += `• 데이터출처: ${badge.dataInfo.source}\\n`;\n if (badge.dataInfo.endpoint) desc += `• API: ${badge.dataInfo.method ? badge.dataInfo.method + ' ' : ''}${badge.dataInfo.endpoint}\\n`;\n if (badge.dataInfo.fields) desc += `• 필드: ${badge.dataInfo.fields}\\n`;\n if (badge.dataInfo.relatedTable) desc += `• 관련테이블: ${badge.dataInfo.relatedTable}\\n`;\n }\n\n return desc.trim() || '설명 없음';\n };\n\n const resetNewBadge = () => {\n setNewBadge({\n label: '', color: 'blue', x: 50, y: 50,\n description: '',\n uiInfo: { componentType: '', position: '', interaction: '', style: '' },\n funcInfo: { purpose: '', trigger: '', process: '', result: '', validation: '' },\n dataInfo: { source: '', endpoint: '', method: '', fields: '', relatedTable: '' },\n });\n setExpandedSections({ uiInfo: false, funcInfo: false, dataInfo: false });\n };\n\n if (!isOpen) return null;\n\n return (\n
\n {/* 숨겨진 파일 입력 */}\n
\n\n {/* 헤더 - 심플하고 명확하게 */}\n
\n
\n
\n
\n 기능정의서\n
\n \n v{currentVersion.version}\n \n \n
\n \n \n
\n
\n\n {/* 백업 드롭다운 메뉴 */}\n {showBackupMenu && (\n
\n
\n
\n {totalScreens}개 화면 · {totalBadges}개 뱃지\n
\n
\n
\n \n \n \n
\n
\n )}\n
\n\n {/* 뱃지 목록 */}\n
\n {badges.length === 0 ? (\n
\n
등록된 기능 설명이 없습니다
\n
화면에서 C키로 추가하세요
\n
\n ) : (\n
\n {badges.map((badge, index) => (\n
\n {/* 수정 모드 */}\n {editingBadgeId === badge.id ? (\n
e.stopPropagation()}>\n {/* 헤더 */}\n
\n
c.id === editFormData.color)?.hex || '#ef4444', color: '#ffffff', filter: 'none' }}\n >\n {index + 1}\n \n
수정 중\n
\n
\n
\n\n {/* 기능명 */}\n
\n \n setEditFormData(prev => ({ ...prev, label: e.target.value }))}\n className=\"w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500\"\n placeholder=\"기능명을 입력하세요\"\n />\n
\n\n {/* 설명 */}\n
\n \n
\n\n {/* 색상 선택 */}\n
\n
\n
\n {badgeColors.map(color => (\n
\n
\n\n {/* 저장/취소 버튼 */}\n
\n \n \n \n
\n
\n ) : (\n /* 일반 모드 */\n
startEditing(badge)}>\n {/* 뱃지 번호 + 이름 + 수정/삭제 버튼 */}\n
\n {/* 뱃지 번호 */}\n
c.id === badge.color)?.hex || '#ef4444', color: '#ffffff', filter: 'none' }}\n >\n {index + 1}\n \n {/* 기능명 */}\n
\n {badge.label || '(제목없음)'}\n \n {/* 수정/삭제 버튼 */}\n {(isAdmin || badge.author === userNickname) && (\n
\n \n \n
\n )}\n
\n {/* 설명 미리보기 - 2줄만 표시 */}\n {badge.description && (\n
{badge.description}
\n )}\n {/* 작성자 정보 */}\n {badge.author && (\n
\n {badge.author}\n {badge.createdAt && (\n <>\n ·\n {new Date(badge.createdAt).toLocaleDateString()}\n >\n )}\n
\n )}\n
\n )}\n\n {/* 댓글 - 심플화 */}\n {badge.comments && badge.comments.length > 0 && (\n
\n
\n\n {/* 댓글 목록 */}\n {expandedComments[badge.id] && (\n
e.stopPropagation()}>\n {badge.comments.map((comment, idx) => (\n
\n
\n
{comment.text}
\n {onDeleteComment && (isAdmin || comment.author === userNickname) && (\n
\n )}\n
\n
\n {comment.author || '익명'} · {comment.createdAt ? new Date(comment.createdAt).toLocaleDateString('ko-KR') : ''}\n
\n
\n ))}\n\n {/* 댓글 입력 */}\n {onAddComment && (\n
\n setCommentInputs(prev => ({ ...prev, [badge.id]: e.target.value }))}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && (commentInputs[badge.id] || '').trim()) {\n e.stopPropagation();\n onAddComment(badge.id, commentInputs[badge.id].trim());\n setCommentInputs(prev => ({ ...prev, [badge.id]: '' }));\n }\n }}\n onClick={(e) => e.stopPropagation()}\n placeholder=\"댓글 입력...\"\n className=\"flex-1 px-2 py-1.5 text-xs border border-gray-300 rounded focus:ring-1 focus:ring-gray-500 focus:border-gray-500\"\n />\n \n
\n )}\n
\n )}\n
\n )}\n
\n ))}\n
\n )}\n
\n\n {/* 사용자 정보 */}\n {userNickname && (\n
\n
\n
\n {userNickname.charAt(0).toUpperCase()}\n
\n
{userNickname}\n
\n
\n
\n )}\n\n {/* 뱃지 추가 폼 - 심플화 */}\n {isAddingBadge && (\n
\n
\n {/* 기본 정보 */}\n
\n
\n
setNewBadge(prev => ({ ...prev, label: e.target.value }))}\n className=\"w-full px-3 py-2.5 border-2 border-gray-300 rounded-lg text-sm text-black focus:ring-2 focus:ring-gray-500 focus:border-gray-500\"\n />\n
\n 색상\n {badgeColors.map(color => (\n
\n
\n\n {/* 설명 */}\n
\n \n
\n\n {/* UI 정보 섹션 */}\n
\n
\n {expandedSections.uiInfo && (\n
\n )}\n
\n\n {/* 기능 정보 섹션 */}\n
\n\n {/* 데이터 연동 섹션 */}\n
\n
\n {expandedSections.dataInfo && (\n
\n
\n
\n \n setNewBadge(prev => ({ ...prev, dataInfo: { ...prev.dataInfo, endpoint: e.target.value } }))}\n className=\"flex-1 px-3 py-2 border border-gray-300 rounded text-sm text-black\"\n />\n
\n
setNewBadge(prev => ({ ...prev, dataInfo: { ...prev.dataInfo, fields: e.target.value } }))}\n className=\"w-full px-3 py-2 border border-gray-300 rounded text-sm text-black\"\n />\n
setNewBadge(prev => ({ ...prev, dataInfo: { ...prev.dataInfo, relatedTable: e.target.value } }))}\n className=\"w-full px-3 py-2 border border-gray-300 rounded text-sm text-black\"\n />\n
\n )}\n
\n\n {/* 버튼 - 와이어프레임 스타일 */}\n
\n \n \n
\n
\n
\n )}\n\n {/* 버전 업그레이드 모달 - 와이어프레임 스타일 */}\n {showVersionModal && (\n
\n
\n
\n
버전 업그레이드
\n
현재 버전: v{currentVersion.version}
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
\n )}\n\n {/* 버전 이력 조회 모달 - 와이어프레임 스타일 */}\n {showVersionHistory && (\n
\n
\n
\n
\n
버전 이력
\n
현재 버전: v{currentVersion.version}
\n
\n
\n
\n
\n {currentVersion.history && currentVersion.history.length > 0 ? (\n
\n {currentVersion.history.map((entry, idx) => (\n
\n
\n
\n {entry.isAutoSave ? (\n \n 자동저장\n \n ) : (\n <>\n \n v{entry.version}\n \n {entry.version !== entry.previousVersion && (\n <>\n ←\n v{entry.previousVersion}\n >\n )}\n >\n )}\n
\n
\n {new Date(entry.updatedAt).toLocaleString('ko-KR', {\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit'\n })}\n \n
\n {entry.changeLog && (\n
{entry.changeLog}
\n )}\n
\n 작성자: {entry.updatedBy}\n 뱃지 수: {entry.badgeCount}개\n
\n
\n ))}\n
\n ) : (\n
\n
버전 이력이 없습니다
\n
버전을 업그레이드하면 이력이 기록됩니다
\n
\n )}\n
\n
\n \n
\n
\n
\n )}\n\n {/* 기능정의문서 모달 */}\n
setShowFeatureDocModal(false)}\n screenKey={currentScreenKey}\n screenName={screenName}\n />\n\n \n );\n};\n\n// 뱃지 오버레이 컴포넌트 (화면에 표시되는 뱃지들)\nconst FeatureBadgeOverlay = ({ badges, selectedBadge, onSelectBadge, badgeEditMode, onUpdatePosition, onAnalyzeAndUpdate, onEditBadge }) => {\n const [dragging, setDragging] = useState(null);\n const [dragStart, setDragStart] = useState(null);\n const [hoveredElement, setHoveredElement] = useState(null);\n const containerRef = React.useRef(null);\n\n // 뱃지 색상 hex 값 (인라인 스타일용) - 컬러로 표시\n const badgeColorHex = {\n red: '#ef4444',\n blue: '#3b82f6',\n green: '#22c55e',\n purple: '#a855f7',\n orange: '#f97316',\n gray: '#6b7280',\n };\n\n // 드래그 시작 (isFixed가 false인 경우만 드래그 가능)\n const handleMouseDown = (e, badge) => {\n // isFixed인 뱃지는 드래그 불가, 클릭만 가능\n if (badge.isFixed) {\n onSelectBadge(badge);\n return;\n }\n e.preventDefault();\n e.stopPropagation();\n setDragging(badge.id);\n setDragStart({ x: e.clientX, y: e.clientY });\n };\n\n // 드래그 중\n const handleMouseMove = React.useCallback((e) => {\n if (!dragging || !containerRef.current) return;\n const rect = containerRef.current.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / rect.width) * 100;\n const y = ((e.clientY - rect.top) / rect.height) * 100;\n onUpdatePosition(dragging, {\n x: Math.max(2, Math.min(98, x)),\n y: Math.max(2, Math.min(98, y)),\n });\n\n // 드래그 중 UI 요소 탐지하여 하이라이트\n const targetElement = getElementAtPoint(e.clientX, e.clientY, containerRef);\n if (targetElement) {\n setHoveredElement(targetElement);\n // 하이라이트 효과 추가\n targetElement.style.outline = '2px dashed #6b7280';\n targetElement.style.outlineOffset = '2px';\n }\n }, [dragging, onUpdatePosition]);\n\n // 드래그 종료\n const handleMouseUp = React.useCallback((e) => {\n // 하이라이트 제거\n if (hoveredElement) {\n hoveredElement.style.outline = '';\n hoveredElement.style.outlineOffset = '';\n setHoveredElement(null);\n }\n\n if (dragging && dragStart) {\n const distance = Math.sqrt(\n Math.pow(e.clientX - dragStart.x, 2) + Math.pow(e.clientY - dragStart.y, 2)\n );\n\n // 이동 거리가 5px 미만이면 클릭으로 간주 → 인라인 수정 모드로 열기\n if (distance < 5) {\n const clickedBadge = badges.find(b => b.id === dragging);\n if (clickedBadge && onEditBadge) {\n onEditBadge(clickedBadge);\n }\n } else {\n // 드래그로 이동한 경우: 해당 위치의 UI 요소 분석\n const targetElement = getElementAtPoint(e.clientX, e.clientY, containerRef);\n if (targetElement && onAnalyzeAndUpdate) {\n const analysis = analyzeElement(targetElement);\n if (analysis) {\n onAnalyzeAndUpdate(dragging, analysis);\n }\n }\n // 드래그 완료 후 해당 뱃지 선택\n const movedBadge = badges.find(b => b.id === dragging);\n if (movedBadge) {\n onSelectBadge(movedBadge);\n }\n }\n }\n setDragging(null);\n setDragStart(null);\n }, [dragging, dragStart, badges, onSelectBadge, hoveredElement, onAnalyzeAndUpdate]);\n\n React.useEffect(() => {\n if (dragging) {\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n // 드래그 중 텍스트 선택 방지\n document.body.style.userSelect = 'none';\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n document.body.style.userSelect = '';\n // 하이라이트 제거\n if (hoveredElement) {\n hoveredElement.style.outline = '';\n hoveredElement.style.outlineOffset = '';\n }\n };\n }\n }, [dragging, handleMouseMove, handleMouseUp, hoveredElement]);\n\n if (badges.length === 0) return null;\n\n return (\n
\n {badges.map((badge, index) => (\n
handleMouseDown(e, badge)}\n >\n
\n
\n {badge.number || index + 1}\n
\n {badge.label && (\n
\n {badge.label}\n \n )}\n
\n {/* 고정 표시 아이콘 - 와이어프레임 스타일 */}\n {badge.isFixed && (\n
\n )}\n {/* 연결선 (선택된 경우) - 와이어프레임 스타일 */}\n {selectedBadge?.id === badge.id && !dragging && (\n
\n )}\n
\n ))}\n {/* 드래그 중 안내 */}\n {dragging && (\n
\n UI 요소 위에 드롭하면 자동으로 분석됩니다\n
\n )}\n
\n );\n};\n\n// 모달용 뱃지 오버레이 컴포넌트 (기능정의서 모드에서 모달 위에 뱃지 표시)\nconst ModalFeatureBadgeOverlay = ({\n badges,\n selectedBadge,\n onSelectBadge,\n badgeEditMode,\n onUpdatePosition,\n onAnalyzeAndUpdate,\n onEditBadge,\n modalId,\n screenId\n}) => {\n const [dragging, setDragging] = useState(null);\n const [dragStart, setDragStart] = useState(null);\n const overlayRef = React.useRef(null);\n\n // 뱃지 색상 hex 값 - 컬러로 표시\n const badgeColorHex = {\n red: '#ef4444',\n blue: '#3b82f6',\n green: '#22c55e',\n purple: '#a855f7',\n orange: '#f97316',\n gray: '#6b7280',\n };\n\n // 드래그 시작\n const handleMouseDown = (e, badge) => {\n if (badge.isFixed) {\n onSelectBadge(badge);\n return;\n }\n e.preventDefault();\n e.stopPropagation();\n setDragging(badge.id);\n setDragStart({ x: e.clientX, y: e.clientY });\n };\n\n // 드래그 중\n const handleMouseMove = React.useCallback((e) => {\n if (!dragging || !overlayRef.current) return;\n const rect = overlayRef.current.getBoundingClientRect();\n const x = ((e.clientX - rect.left) / rect.width) * 100;\n const y = ((e.clientY - rect.top) / rect.height) * 100;\n onUpdatePosition(dragging, {\n x: Math.max(2, Math.min(98, x)),\n y: Math.max(2, Math.min(98, y)),\n });\n }, [dragging, onUpdatePosition]);\n\n // 드래그 종료\n const handleMouseUp = React.useCallback((e) => {\n if (dragging && dragStart) {\n const distance = Math.sqrt(\n Math.pow(e.clientX - dragStart.x, 2) + Math.pow(e.clientY - dragStart.y, 2)\n );\n if (distance < 5) {\n const clickedBadge = badges.find(b => b.id === dragging);\n if (clickedBadge && onEditBadge) {\n onEditBadge(clickedBadge);\n }\n } else {\n const movedBadge = badges.find(b => b.id === dragging);\n if (movedBadge) {\n onSelectBadge(movedBadge);\n }\n }\n }\n setDragging(null);\n setDragStart(null);\n }, [dragging, dragStart, badges, onSelectBadge, onEditBadge]);\n\n React.useEffect(() => {\n if (dragging) {\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n document.body.style.userSelect = 'none';\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n document.body.style.userSelect = '';\n };\n }\n }, [dragging, handleMouseMove, handleMouseUp]);\n\n return (\n
\n {/* 모달 ID 표시 - 항상 표시 (와이어프레임 스타일) */}\n
\n {screenId}\n
\n\n {badges.map((badge, index) => (\n
handleMouseDown(e, badge)}\n >\n
\n
\n {badge.number || index + 1}\n
\n {badge.label && (\n
\n {badge.label}\n \n )}\n
\n
\n ))}\n
\n );\n};\n\n// 수주 카드 컴포넌트 (태블릿/모바일용)\nconst OrderCard = ({ order, onClick }) => {\n const getStatusStyle = (status) => {\n const styles = {\n '수주확정': 'bg-gray-100 text-gray-700',\n '생산중': 'bg-orange-100 text-orange-700',\n '생산완료': 'bg-green-100 text-green-700',\n '출하완료': 'bg-blue-100 text-blue-700',\n '취소': 'bg-red-100 text-red-700',\n };\n return styles[status] || 'bg-gray-100 text-gray-700';\n };\n\n const getQuoteStatusBadge = (order) => {\n if (order.quoteStatus === '최종확정') {\n return
최종확정;\n }\n if (order.modificationCount > 0) {\n return
{order.modificationCount}차 수정;\n }\n return null;\n };\n\n return (\n
\n {/* 상단: 번호, 상태 */}\n
\n
\n e.stopPropagation()} />\n #{order.id}\n
\n
\n {order.status}\n \n
\n\n {/* 로트번호 */}\n
\n \n {order.orderNo}\n \n
\n\n {/* 발주처 (큰 텍스트) */}\n
{order.customerName}
\n\n {/* 견적번호, 현장명 */}\n
\n
\n
견적번호
\n
{order.quoteNo || '-'}
\n
\n
\n
현장명
\n
{order.siteName}
\n
\n
\n\n {/* 견적 상태 뱃지 */}\n {getQuoteStatusBadge(order) && (\n
\n {getQuoteStatusBadge(order)}\n
\n )}\n\n {/* 생산지시 */}\n
\n
\n
생산지시
\n
\n {order.productionOrdered ? '완료' : '아니요'}\n \n
\n
\n
\n );\n};\n\nconst Card = ({ title, children, className = '' }) => (\n
\n {title && (\n
\n
{title}
\n \n )}\n
{children}
\n
\n);\n\n// 슬라이드 패널 (오른쪽에서 열림)\nconst SlidePanel = ({ isOpen, onClose, title, width = 'max-w-2xl', children }) => {\n if (!isOpen) return null;\n\n return (\n
\n {/* 배경 오버레이 */}\n
\n\n {/* 패널 */}\n
\n {/* 헤더 */}\n
\n
{title}
\n \n \n\n {/* 내용 */}\n
\n {children}\n
\n
\n
\n );\n};\n\n// 폼 필드 (에러 상태 지원)\nconst FormField = ({ label, required, children, hint, error, errorMessage }) => (\n
\n
\n
\n {children}\n
\n {error && errorMessage && (\n
\n ⚠ {errorMessage}\n
\n )}\n {!error && hint &&
{hint}
}\n
\n);\n\n// 유효성 검사 훅\nconst useFormValidation = (formData, validationRules) => {\n const [errors, setErrors] = useState({});\n const [touched, setTouched] = useState({});\n\n // 단일 필드 유효성 검사\n const validateField = (fieldName, value) => {\n const rule = validationRules[fieldName];\n if (!rule) return null;\n\n // 필수 항목 체크\n if (rule.required) {\n if (value === undefined || value === null || value === '' ||\n (Array.isArray(value) && value.length === 0)) {\n return rule.message || `${rule.label || fieldName}은(는) 필수 항목입니다.`;\n }\n }\n\n // 최소 길이 체크\n if (rule.minLength && typeof value === 'string' && value.length < rule.minLength) {\n return `${rule.label || fieldName}은(는) 최소 ${rule.minLength}자 이상이어야 합니다.`;\n }\n\n // 패턴 체크\n if (rule.pattern && typeof value === 'string' && !rule.pattern.test(value)) {\n return rule.patternMessage || `${rule.label || fieldName} 형식이 올바르지 않습니다.`;\n }\n\n // 커스텀 유효성 검사\n if (rule.validate && typeof rule.validate === 'function') {\n const customError = rule.validate(value, formData);\n if (customError) return customError;\n }\n\n return null;\n };\n\n // 전체 폼 유효성 검사\n const validateForm = () => {\n const newErrors = {};\n let isValid = true;\n\n Object.keys(validationRules).forEach(fieldName => {\n const error = validateField(fieldName, formData[fieldName]);\n if (error) {\n newErrors[fieldName] = error;\n isValid = false;\n }\n });\n\n setErrors(newErrors);\n // 모든 필드를 touched로 표시\n const allTouched = {};\n Object.keys(validationRules).forEach(key => { allTouched[key] = true; });\n setTouched(allTouched);\n\n return isValid;\n };\n\n // 필드 터치 처리\n const handleBlur = (fieldName) => {\n setTouched(prev => ({ ...prev, [fieldName]: true }));\n const error = validateField(fieldName, formData[fieldName]);\n setErrors(prev => ({ ...prev, [fieldName]: error }));\n };\n\n // 필드 변경 시 에러 클리어\n const clearFieldError = (fieldName) => {\n if (errors[fieldName]) {\n setErrors(prev => ({ ...prev, [fieldName]: null }));\n }\n };\n\n // 에러 상태 확인 (터치된 필드만)\n const getFieldError = (fieldName) => {\n return touched[fieldName] ? errors[fieldName] : null;\n };\n\n // 에러가 있는지 확인\n const hasError = (fieldName) => {\n return touched[fieldName] && !!errors[fieldName];\n };\n\n // 모든 에러 초기화\n const resetErrors = () => {\n setErrors({});\n setTouched({});\n };\n\n return {\n errors,\n touched,\n validateForm,\n validateField,\n handleBlur,\n clearFieldError,\n getFieldError,\n hasError,\n resetErrors,\n setErrors,\n setTouched,\n };\n};\n\n// 에러 상태 input 스타일 클래스\nconst getInputClassName = (hasError, baseClass = '') => {\n const base = baseClass || 'w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:border-transparent';\n if (hasError) {\n return `${base} border-red-500 focus:ring-red-500 bg-red-50`;\n }\n return `${base} border-gray-200 focus:ring-blue-500`;\n};\n\n// 텍스트 입력\nconst TextInput = ({ placeholder, value, onChange, disabled }) => (\n
onChange(e.target.value)}\n disabled={disabled}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500\"\n />\n);\n\n// 셀렉트\nconst Select = ({ options, value, onChange, placeholder }) => (\n
\n);\n\n// 정보 행\nconst InfoRow = ({ label, value }) => (\n
\n
{label}
\n
{value || '-'}
\n
\n);\n\n// 페이지 헤더 - 타이틀과 버튼 영역 분리 (타이틀 아래에 버튼)\nconst PageHeader = ({ icon: Icon, title, onBack, actions, leftActions }) => (\n
\n {/* 타이틀 영역 */}\n
\n {onBack && (\n
\n )}\n {Icon &&
}\n
{title}
\n
\n {/* 버튼 영역 - 타이틀 아래, 좌측/우측 분리 */}\n {(actions || leftActions) && (\n
\n
{leftActions}
\n
{actions}
\n
\n )}\n
\n);\n\n// 상세 페이지 탭\nconst DetailTabs = ({ tabs, activeTab, onTabChange }) => (\n
\n {tabs.map(tab => (\n \n ))}\n
\n);\n\n// ============ 더미 데이터 ============\n\n// ============ 거래처 데이터 ============\n/*\n * 신용등급별 프로세스 제어\n * A등급(우량): 수주→생산→출고 자동, 세금계산서 자동\n * B등급(관리): 생산 자동, 입금확인 후 출고\n * C등급(위험): 경리승인 후 생산, 입금확인 후 출고\n */\nconst customers = [\n {\n id: 1,\n code: 'CUS-001',\n name: '삼성물산(주)',\n ceo: '김삼성',\n bizNo: '123-45-67890',\n type: '고객',\n bizType: '건설업',\n bizItem: '종합건설',\n address: '서울시 강남구 테헤란로 100',\n phone: '02-1111-1111',\n fax: '02-1111-1112',\n email: 'samsung@samsung-const.co.kr',\n manager: '이건설',\n managerPhone: '010-1111-1111',\n creditGrade: 'A', // 우량\n creditNote: '정상거래처, 입금지연 없음',\n paymentTerms: '월말마감 익월15일',\n requireApproval: false, // 경리승인 불필요\n requirePaymentBeforeShip: false, // 입금 전 출고 가능\n },\n {\n id: 2,\n code: 'CUS-002',\n name: '현대건설(주)',\n ceo: '박현대',\n bizNo: '234-56-78901',\n type: '고객',\n bizType: '건설업',\n bizItem: '시설물관리',\n address: '인천시 연수구 송도동 200',\n phone: '032-2222-2222',\n fax: '032-2222-2223',\n email: 'hyundai@hyundai-dev.co.kr',\n manager: '최개발',\n managerPhone: '010-2222-2222',\n creditGrade: 'B', // 관리\n creditNote: '가끔 입금 지연, 할인 요청 발생',\n paymentTerms: '입금확인 후 출고',\n requireApproval: false, // 경리승인 불필요\n requirePaymentBeforeShip: true, // 입금 후 출고\n },\n {\n id: 3,\n code: 'CUS-003',\n name: '대우건설(주)',\n ceo: '정한화',\n bizNo: '345-67-89012',\n type: '고객',\n bizType: '건설업',\n bizItem: '인테리어',\n address: '대전시 서구 둔산동 300',\n phone: '042-3333-3333',\n fax: '042-3333-3334',\n email: 'hanwha@hanwha-const.co.kr',\n manager: '강시공',\n managerPhone: '010-3333-3333',\n creditGrade: 'C', // 위험\n creditNote: '입금약속 미이행 이력 2회, 주의 필요',\n paymentTerms: '선입금 50% + 잔금 출고 전',\n requireApproval: true, // 경리승인 필요\n requirePaymentBeforeShip: true, // 입금 후 출고\n },\n {\n id: 4,\n code: 'CUS-004',\n name: '(주)서울인테리어',\n ceo: '이부산',\n bizNo: '456-78-90123',\n type: '고객',\n bizType: '건설업',\n bizItem: '종합건설',\n address: '부산시 해운대구 우동 400',\n phone: '051-4444-4444',\n fax: '051-4444-4445',\n email: 'busan@busan-const.co.kr',\n manager: '박해운',\n managerPhone: '010-4444-4444',\n creditGrade: 'A', // 우량\n creditNote: '우량 거래처',\n paymentTerms: '월말마감 익월말',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n },\n];\n\n// 신용등급 색상 매핑\nconst creditGradeColors = {\n 'A': { bg: 'bg-green-100', text: 'text-green-700', label: '우량' },\n 'B': { bg: 'bg-yellow-100', text: 'text-yellow-700', label: '관리' },\n 'C': { bg: 'bg-red-100', text: 'text-red-700', label: '위험' },\n};\n\n/*\n * ============================================================\n * 📋 발주서/생산지시서 상세 데이터 구조 (발주서 양식 기반)\n * ============================================================\n * \n * 1. productionSpec: 스크린 품목별 제작 사양\n * 2. bomData: 절곡물 BOM (자재 소요량)\n * 3. motorSpec: 모터/전장품 사양\n * \n * ============================================================\n */\n\n// 제작 스펙 자동 생성 함수 (��라메트릭 계산)\nconst generateProductionSpec = (item, index = 0) => {\n // spec에서 크기 파싱 (예: \"7660×2550\")\n const [openW, openH] = (item.spec || item.width && item.height ? `${item.width}×${item.height}` : '3000×2400')\n .split('×').map(s => parseInt(s) || 0);\n\n // 제작 사이즈 계산 (오픈 사이즈 + 여유분)\n const prodWidth = openW + 140; // 가로 140mm 추가\n const prodHeight = Math.max(openH + 400, 2950); // 세로 최소 2950mm\n\n // 샤프트 인치 결정 (가로 기준)\n const shaft = openW > 6000 ? 5 : 4;\n\n // 모터 용량 결정\n const capacity = openW > 6000 ? 300 : 160;\n\n // 가이드레일 타입 결정 (기본: 백면형)\n const guideRailType = '백면형';\n const guideRailSpec = '120-70';\n\n return {\n type: '와이어',\n drawingNo: `${item.floor || '1층'} ${item.location || `FSS${index + 1}`}`,\n openWidth: openW,\n openHeight: openH,\n prodWidth,\n prodHeight,\n guideRailType,\n guideRailSpec,\n shaft,\n caseSpec: '500-330',\n motorBracket: '380-180',\n capacity,\n finish: 'SUS마감',\n };\n};\n\n// 모터 스펙 자동 계산\nconst generateMotorSpec = (items) => {\n const specs = items.map(item => item.productionSpec || generateProductionSpec(item));\n\n const motor150Count = specs.filter(s => s.capacity <= 160).length;\n const motor300Count = specs.filter(s => s.capacity > 160 && s.capacity <= 300).length;\n const motor400Count = specs.filter(s => s.capacity > 300).length;\n\n return {\n motors220V: [\n { model: 'KD-150K', qty: 0 },\n { model: 'KD-300K', qty: 0 },\n { model: 'KD-400K', qty: 0 },\n ],\n motors380V: [\n { model: 'KD-150K', qty: motor150Count },\n { model: 'KD-300K', qty: motor300Count },\n { model: 'KD-400K', qty: motor400Count },\n ],\n brackets: [\n { spec: '380-180 [2-4\"]', qty: 0 },\n { spec: '380-180 [2-5\"]', qty: items.length },\n ],\n heatSinks: [\n { spec: '40-60', qty: items.length * 2 },\n { spec: 'L-380', qty: 0 },\n ],\n controllers: [\n { type: '매입형', qty: 0 },\n { type: '노출형', qty: 0 },\n { type: '핫라스', qty: 0 },\n ],\n };\n};\n\n// BOM 자동 계산\nconst generateBomData = (items) => {\n const totalItems = items.length;\n\n return {\n guideRails: {\n description: '가이드레일 - EGI 1.55T + 마감재 EGI 1.15T + 별도마감재 SUS 1.15T',\n items: [\n {\n type: '백면형', spec: '120-70', code: 'KSE01/KWE01', lengths: [\n { length: 3000, qty: totalItems * 2 },\n ]\n },\n {\n type: '측면형', spec: '120-120', code: 'KSS01', lengths: [\n { length: 4300, qty: 0 },\n { length: 4000, qty: 0 },\n { length: 3500, qty: 0 },\n { length: 3000, qty: 0 },\n ]\n },\n {\n type: '하부BASE', spec: '130-80', code: '', lengths: [\n { length: 0, qty: totalItems * 2 },\n ]\n },\n ],\n smokeBarrier: {\n spec: 'W80',\n material: '원 0.8T 화이바글라스크탑직물',\n lengths: [{ length: 2950, qty: totalItems * 2 }],\n note: '* 전면부, 린텔부 양쪽에 설치',\n },\n },\n cases: {\n description: '케이스(셔터박스) - EGI 1.55T',\n mainSpec: '500-330 (150,300,400/K용)',\n items: [\n { length: 4150, qty: Math.ceil(totalItems * 0.1) },\n { length: 4000, qty: Math.ceil(totalItems * 0.4) },\n { length: 3500, qty: Math.ceil(totalItems * 0.3) },\n { length: 3000, qty: Math.ceil(totalItems * 0.2) },\n ],\n sideCover: { spec: '500-355', qty: totalItems * 2 },\n topCover: { qty: Math.ceil(totalItems * 0.1) },\n extension: { spec: '1219+무게', qty: totalItems * 3 },\n smokeBarrier: { spec: 'W80', length: 3000, qty: totalItems * 2 },\n },\n bottomFinish: {\n description: '하단마감재 - 마단마감재(EGI 1.55T) + 하단보강별바(EGI 1.55T) + 하단 보강횡철(EGI 1.15T) + 하단 부재횡철(50-12T)',\n items: [\n {\n name: '하단마감재', spec: '50-40', lengths: [\n { length: 4000, qty: totalItems },\n { length: 3000, qty: 0 },\n ]\n },\n {\n name: '하단보강빔바', spec: '80-17', lengths: [\n { length: 4000, qty: totalItems * 2 },\n { length: 3000, qty: 0 },\n ]\n },\n {\n name: '하단보강철', spec: '', lengths: [\n { length: 4000, qty: totalItems },\n { length: 3000, qty: 0 },\n ]\n },\n {\n name: '하단부재횡철', spec: '50-12T', lengths: [\n { length: 2000, qty: totalItems * 4 },\n ]\n },\n ],\n },\n };\n};\n\n// ============ 견적수식관리 마스터 데이터 ============\n\n// 제품 유형 정의\nconst quoteProducts = [\n { id: 'common', name: '공통', formulaCount: 24 },\n { id: 'screen-basic', name: '스크린 셔터 기본형', formulaCount: 0 },\n { id: 'screen-open', name: '스크린 셔터 오픈형', formulaCount: 0 },\n { id: 'steel-basic', name: '철재 셔터 기본형', formulaCount: 0 },\n { id: 'slat', name: '슬랫', formulaCount: 6 },\n];\n\n// 수식 카테고리 정의 (스크린샷 기준 전체 확장)\nconst formulaCategories = [\n { id: 'basic-info', name: '기본정보', order: 1, description: '입력 변수 정의' },\n { id: 'production-size', name: '제작사이즈', order: 2, description: '제작 가로/세로 산출' },\n { id: 'area-weight', name: '면적&중량', order: 3, description: '면적(M) 및 중량(K) 산출' },\n { id: 'motor-calc', name: '모터용량산출', order: 4, description: '샤프트 인치별 모터 선정' },\n { id: 'bracket-angle', name: '브라켓&받침용앵글', order: 5, description: '모터용량별 브라켓/앵글 산출' },\n { id: 'shaft', name: '감기샤프트', order: 6, description: '메인샤프트/보조샤프트 산출' },\n { id: 'guide-rail', name: '가이드레일', order: 7, description: '가이드레일 자재/수량 산출' },\n { id: 'smoke-block', name: '연기차단재', order: 8, description: '연기차단재 산출' },\n { id: 'shutter-box', name: '셔터박스(케이스)', order: 9, description: '케이스 구성품 산출' },\n { id: 'bottom-finish', name: '하단마감재', order: 10, description: '하장바/엘바/보강평철/무게평철' },\n { id: 'sub-material', name: '부자재', order: 11, description: '환봉/조인트바/각파이프' },\n { id: 'purchased-parts', name: '구매부품', order: 12, description: '모터/연동제어기/앵글 등' },\n { id: 'sheet-count', name: '장수산출', order: 13, description: '스크린 원단 장수/슬랫 매수 계산' },\n];\n\n// ═══════════════════════════════════════════════════════════════════\n// 품목 수식 데이터 (스크린샷 기반 완전 구현)\n// ═══════════════════════════════════════════════════════════════════\nconst initialQuoteFormulas = [\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 1. 기본정보 (입력 변수)\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n { id: 1, productId: 'common', categoryId: 'basic-info', order: 1, name: '제품구분', variable: 'PC', type: 'input', formula: '', resultType: 'variable', description: '스크린/철재', unit: '', options: ['스크린', '철재'] },\n { id: 2, productId: 'common', categoryId: 'basic-info', order: 2, name: '오픈사이즈 가로', variable: 'W0', type: 'input', formula: '', resultType: 'variable', description: '고객 주문 가로 (mm)', unit: 'mm' },\n { id: 3, productId: 'common', categoryId: 'basic-info', order: 3, name: '오픈사이즈 세로', variable: 'H0', type: 'input', formula: '', resultType: 'variable', description: '고객 주문 세로 (mm)', unit: 'mm' },\n { id: 4, productId: 'common', categoryId: 'basic-info', order: 4, name: '샤프트 규격', variable: 'SHAFT_INCH', type: 'input', formula: '', resultType: 'variable', description: '4\"/5\"/6\"/8\"', unit: '', options: ['4', '5', '6', '8'] },\n { id: 5, productId: 'common', categoryId: 'basic-info', order: 5, name: '설치유형', variable: 'INSTALL_TYPE', type: 'input', formula: '', resultType: 'variable', description: '벽면형/측면형/혼합형', unit: '', options: ['벽면형', '측면형', '혼합형'] },\n { id: 6, productId: 'common', categoryId: 'basic-info', order: 6, name: '모터 전원', variable: 'MP', type: 'input', formula: '', resultType: 'variable', description: '220V/380V', unit: 'V', options: ['220', '380'] },\n { id: 7, productId: 'common', categoryId: 'basic-info', order: 7, name: '유선/무선', variable: 'WIRE', type: 'input', formula: '', resultType: 'variable', description: '유선/무선', unit: '', options: ['유선', '무선'] },\n { id: 8, productId: 'common', categoryId: 'basic-info', order: 8, name: '연동제어기 타입', variable: 'CT', type: 'input', formula: '', resultType: 'variable', description: '매립/노출', unit: '', options: ['매립', '노출'] },\n { id: 9, productId: 'common', categoryId: 'basic-info', order: 9, name: '수량', variable: 'QTY', type: 'input', formula: '', resultType: 'variable', description: '주문 수량', unit: 'SET' },\n { id: 10, productId: 'common', categoryId: 'basic-info', order: 10, name: '케이스 사이즈', variable: 'CASE_SIZE', type: 'input', formula: '', resultType: 'variable', description: '스크린500*350/철재650*500', unit: 'mm' },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 2. 제작사이즈 산출 (스크린샷: 오픈사이즈 기반 제작사이즈)\n // 스크린: W1 = W0+140, H1 = H0+350 (일부 케이스는 고정값 900 사용)\n // 철재: W1 = W0+110, H1 = H0+350\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n { id: 11, productId: 'screen', categoryId: 'production-size', order: 1, name: '제작가로(스크린)', variable: 'W1', type: 'formula', formula: 'W0 + 140', resultType: 'variable', description: '스크린(실리카/와이어) 제작가로', unit: 'mm', condition: \"PC === '스크린'\" },\n { id: 12, productId: 'screen', categoryId: 'production-size', order: 2, name: '제작세로(스크린)', variable: 'H1', type: 'formula', formula: 'H0 + 350', resultType: 'variable', description: '스크린 제작세로 (일부케이스 고정값 900)', unit: 'mm', condition: \"PC === '스크린'\" },\n { id: 13, productId: 'steel', categoryId: 'production-size', order: 3, name: '제작가로(철재)', variable: 'W1', type: 'formula', formula: 'W0 + 110', resultType: 'variable', description: '철재 셔터 제작가로', unit: 'mm', condition: \"PC === '철재'\" },\n { id: 14, productId: 'steel', categoryId: 'production-size', order: 4, name: '제작세로(철재)', variable: 'H1', type: 'formula', formula: 'H0 + 350', resultType: 'variable', description: '철재 셔터 제작세로', unit: 'mm', condition: \"PC === '철재'\" },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 3. 면적(M) & 중량(K) 계산 (스크린샷: 면적 및 중량 산출)\n // M = W1 × H1 ÷ 1,000,000 (㎡)\n // 스크린: K = (M × 2) + (W0 × 14.17 ÷ 1000)\n // 철재: K = M × 25\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n { id: 15, productId: 'common', categoryId: 'area-weight', order: 1, name: '면적(M)', variable: 'M', type: 'formula', formula: '(W1 * H1) / 1000000', resultType: 'variable', description: '면적 = W1 × H1 ÷ 1,000,000', unit: '㎡' },\n { id: 16, productId: 'screen', categoryId: 'area-weight', order: 2, name: '중량(스크린)', variable: 'K', type: 'formula', formula: '(M * 2) + (W0 * 14.17 / 1000)', resultType: 'variable', description: 'K = (M×2) + (W0×14.17÷1000)', unit: 'kg', condition: \"PC === '스크린'\" },\n { id: 17, productId: 'steel', categoryId: 'area-weight', order: 3, name: '중량(철재)', variable: 'K', type: 'formula', formula: 'M * 25', resultType: 'variable', description: 'K = M × 25', unit: 'kg', condition: \"PC === '철재'\" },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 4. 모터용량 산출 (스크린샷: 모터 용량 산정 기준)\n // [스크린] 샤프트 인치 + 산출중량(K) → 적용모터\n // [철재] 샤프트 인치 + 산출중량(K) → 적용모터\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n {\n id: 20, productId: 'common', categoryId: 'motor-calc', order: 1, name: '모터용량(KG)', variable: 'MOTOR_KG', type: 'lookup',\n formula: 'MOTOR_LOOKUP', resultType: 'variable', description: '샤프트인치+중량K로 모터용량 결정', unit: 'KG',\n lookupTable: 'motorCapacityTable'\n },\n {\n id: 21, productId: 'common', categoryId: 'motor-calc', order: 2, name: '모터코드', variable: 'MOTOR_CODE', type: 'formula',\n formula: \"'E-' + MOTOR_KG + 'K' + MP + 'V-' + (WIRE === '유선' ? 'W' : 'R')\", resultType: 'variable', description: '모터 품목코드 생성', unit: ''\n },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 5. 브라켓 & 받침용앵글 산출 (스크린샷: 모터용량별 기준표)\n // [스크린용] 150K:380×180/40×40×380, 300K:380×180/40×40×380, 400K:530×320/50×50×530\n // [철재용] 300K:530×320/50×50×530, 400K:530×320/50×50×530, 500K~:600×350/50×50×600\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n {\n id: 25, productId: 'common', categoryId: 'bracket-angle', order: 1, name: '브라켓 규격', variable: 'BRACKET_SPEC', type: 'lookup',\n formula: 'BRACKET_LOOKUP', resultType: 'variable', description: '모터용량별 브라켓 규격', unit: 'mm',\n lookupTable: 'bracketTable'\n },\n {\n id: 26, productId: 'common', categoryId: 'bracket-angle', order: 2, name: '받침용앵글 규격', variable: 'ANGLE_SPEC', type: 'lookup',\n formula: 'ANGLE_LOOKUP', resultType: 'variable', description: '모터용량별 받침용앵글 규격', unit: 'mm',\n lookupTable: 'angleTable'\n },\n // 구매부품 - 규칙: 품목명-규격\n {\n id: 27, productId: 'common', categoryId: 'bracket-angle', order: 3, name: '브라켓', variable: 'BRACKET', type: 'output',\n formula: 'BRACKET_SPEC', resultType: 'item', description: '브라켓 품목', itemCode: '브라켓', itemName: '브라켓', unit: 'SET', qtyFormula: 'QTY'\n },\n {\n id: 28, productId: 'common', categoryId: 'bracket-angle', order: 4, name: '받침용앵글', variable: 'SUPPORT_ANGLE', type: 'output',\n formula: 'ANGLE_SPEC', resultType: 'item', description: '받침용앵글', itemCode: '받침용앵글', itemName: '받침용앵글', unit: 'SET', qtyFormula: 'QTY'\n },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 6. 감기샤프트 산출 (문서: 감기샤프트(스크린용/철재용) 산출 로직)\n // 스크린: W1 기준 샤프트 규격 결정 (기본 5인치)\n // 철재: W1과 K(산출중량) 두 조건을 모두 만족해야 함\n // 메인샤프트: 자재단위 3000/4500/6000/7000/8000/8200, 통자재 1개만 납품(길이조합 불가)\n // 보조샤프트(스크린전용): 항상 3인치, W1≤8200→300자재, W1>8200→500자재, 수량 무조건 1개\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n {\n id: 30, productId: 'steel', categoryId: 'shaft', order: 1, name: '메인샤프트 규격(철재)', variable: 'MAIN_SHAFT_INCH_STEEL', type: 'lookup',\n formula: 'SHAFT_INCH_STEEL_LOOKUP', resultType: 'variable', description: '철재: W1,K 두 조건으로 샤프트인치 결정 (4/5/6/8인치)',\n lookupTable: 'shaftInchTableSteel', condition: \"PC === '철재'\"\n },\n {\n id: 97, productId: 'screen', categoryId: 'shaft', order: 1, name: '메인샤프트 규격(스크린)', variable: 'MAIN_SHAFT_INCH_SCREEN', type: 'lookup',\n formula: 'SHAFT_INCH_SCREEN_LOOKUP', resultType: 'variable', description: '스크린: W1 기준 샤프트인치 결정 (기본 5인치)',\n lookupTable: 'shaftInchTableScreen', condition: \"PC === '스크린'\"\n },\n {\n id: 31, productId: 'common', categoryId: 'shaft', order: 2, name: '메인샤프트 자재길이', variable: 'MAIN_SHAFT_LEN', type: 'lookup',\n formula: 'SHAFT_LEN_LOOKUP', resultType: 'variable', description: '인치별 W1구간에 따라 자재길이 선택 (항상 W1 이상 길이)',\n lookupTable: 'shaftLengthTable'\n },\n // 구매부품 - 규칙: 품목명-규격\n {\n id: 32, productId: 'common', categoryId: 'shaft', order: 3, name: '메인샤프트', variable: 'MAIN_SHAFT', type: 'output',\n formula: 'MAIN_SHAFT_LEN', resultType: 'item', description: '메인샤프트(통자재 1개)',\n itemCode: '감기샤프트', itemName: '감기샤프트(메인)', unit: 'EA', qtyFormula: 'QTY'\n },\n {\n id: 33, productId: 'screen', categoryId: 'shaft', order: 4, name: '보조샤프트 자재', variable: 'SUB_SHAFT_MAT', type: 'lookup',\n formula: 'SUB_SHAFT_LOOKUP', resultType: 'variable', description: '보조샤프트(스크린전용, 항상 3인치) W1≤8200→300, W1>8200→500',\n lookupTable: 'subShaftTable', condition: \"PC === '스크린'\"\n },\n {\n id: 34, productId: 'screen', categoryId: 'shaft', order: 5, name: '보조샤프트', variable: 'SUB_SHAFT', type: 'output',\n formula: 'SUB_SHAFT_MAT.length', resultType: 'item', description: '보조샤프트(스크린전용, 3인치, 수량 무조건 1개)',\n itemCode: '감기샤프트-보조3인치', itemName: '감기샤프트(보조3인치)', unit: 'EA', qtyFormula: 'QTY', condition: \"PC === '스크린'\"\n },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 7. 가이드레일 산출 (스크린샷: 가이드레일 및 연기차단재 산출 기준서)\n // G(제작길이) = H0 + 250\n // 스크린: 가이드레일 내부 삽입 2개 (가이드레일1랑과 동일한 길이)\n // 철재: 가이드레일 내부 삽입 1개\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n {\n id: 40, productId: 'common', categoryId: 'guide-rail', order: 1, name: '가이드레일 제작길이', variable: 'G', type: 'formula',\n formula: 'H0 + 250', resultType: 'variable', description: 'G(제작길이) = H0 + 250', unit: 'mm'\n },\n {\n id: 41, productId: 'common', categoryId: 'guide-rail', order: 2, name: '가이드레일 자재길이', variable: 'GR_MAT_LEN', type: 'lookup',\n formula: 'GR_LENGTH_LOOKUP', resultType: 'variable', description: 'G값으로 자재길이 결정(2438/3000/3500/4000/4300)', unit: 'mm',\n lookupTable: 'guideRailLengthTable'\n },\n // 절곡부품 - 규칙: 품목코드+종류코드+모양&길이코드\n {\n id: 42, productId: 'common', categoryId: 'guide-rail', order: 3, name: '가이드레일', variable: 'GUIDE_RAIL', type: 'output',\n formula: 'GR_MAT_LEN', resultType: 'item', description: '가이드레일(벽면형)',\n itemCode: 'RC', itemName: '가이드레일', unit: 'EA', qtyFormula: 'QTY * 2'\n },\n {\n id: 43, productId: 'common', categoryId: 'guide-rail', order: 4, name: '하부BASE1 규격', variable: 'BASE1_SPEC', type: 'lookup',\n formula: 'BASE1_LOOKUP', resultType: 'variable', description: '제품구분+설치유형별 하부BASE1 규격',\n lookupTable: 'baseSpecTable'\n },\n {\n id: 44, productId: 'common', categoryId: 'guide-rail', order: 5, name: '하부BASE2 규격', variable: 'BASE2_SPEC', type: 'lookup',\n formula: 'BASE2_LOOKUP', resultType: 'variable', description: '제품구분+설치유형별 하부BASE2 규격',\n lookupTable: 'baseSpecTable'\n },\n // 부자재 - 규칙: 품목명-규격\n {\n id: 95, productId: 'common', categoryId: 'guide-rail', order: 6, name: '하부BASE1', variable: 'GR_BASE1', type: 'output',\n formula: 'BASE1_SPEC', resultType: 'item', description: '하부BASE1(스크린:130×80/130×130, 철재:140×85/140×135)',\n itemCode: '하부BASE-벽면형', itemName: '하부BASE1', unit: 'SET', qtyFormula: 'QTY'\n },\n {\n id: 96, productId: 'common', categoryId: 'guide-rail', order: 7, name: '하부BASE2', variable: 'GR_BASE2', type: 'output',\n formula: 'BASE2_SPEC', resultType: 'item', description: '하부BASE2(설치유형별 규격)',\n itemCode: '하부BASE-코너형', itemName: '하부BASE2', unit: 'SET', qtyFormula: 'QTY'\n },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 8. 연기차단재 산출 (문서: 가이드레일 1량과 동일한 길이로 제작)\n // 스크린: 가이드레일 내부 삽입 2개/1량당 (1SET=2량, 총4개)\n // 철재: 가이드레일 내부 삽입 1개/1량당 (1SET=2량, 총2개)\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 절곡부품 - 규칙: 품목코드+종류코드+모양&길이코드 (GI: 연기차단재 화이바원단)\n {\n id: 45, productId: 'screen', categoryId: 'smoke-block', order: 1, name: '연기차단재(스크린)', variable: 'SMOKE_BLOCK', type: 'output',\n formula: 'G', resultType: 'item', description: '연기차단재(스크린) 2개/1량당×2량',\n itemCode: 'GI', itemName: '연기차단재', unit: 'EA', qtyFormula: 'QTY * 2 * 2', condition: \"PC === '스크린'\"\n },\n {\n id: 46, productId: 'steel', categoryId: 'smoke-block', order: 2, name: '연기차단재(철재)', variable: 'SMOKE_BLOCK', type: 'output',\n formula: 'G', resultType: 'item', description: '연기차단재(철재) 1개/1량당×2량',\n itemCode: 'GI', itemName: '연기차단재', unit: 'EA', qtyFormula: 'QTY * 2 * 1', condition: \"PC === '철재'\"\n },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 9. 셔터박스(케이스) 구성품 산출 (스크린샷: 케이스 구성품 산출 기준서)\n // 제작사이즈(S) = 케이스 제작사이즈, 적용자재 규칙\n // 케이스판재/상부덮개: ceil(S / 1219)\n // 마구리: 1SET = 2EA (제작사이즈 무관하게 1SET)\n // 케이스용 연기차단재: ceil(S / 3000) * 2\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n {\n id: 50, productId: 'common', categoryId: 'shutter-box', order: 1, name: '케이스 제작사이즈', variable: 'S', type: 'formula',\n formula: 'W1', resultType: 'variable', description: '케이스 제작사이즈 = W1', unit: 'mm'\n },\n {\n id: 51, productId: 'common', categoryId: 'shutter-box', order: 2, name: '케이스판재 자재길이', variable: 'CASE_MAT_LEN', type: 'lookup',\n formula: 'CASE_LENGTH_LOOKUP', resultType: 'variable', description: 'S값으로 자재길이 결정', unit: 'mm',\n lookupTable: 'caseLengthTable'\n },\n {\n id: 52, productId: 'common', categoryId: 'shutter-box', order: 3, name: '케이스판재 수량', variable: 'CASE_MAT_QTY', type: 'lookup',\n formula: 'CASE_QTY_LOOKUP', resultType: 'variable', description: 'S범위별 수량 (1~2개)', unit: 'EA',\n lookupTable: 'caseQtyTable'\n },\n // 절곡부품 - 규칙: 품목코드+종류코드+모양&길이코드 (CB: 케이스)\n {\n id: 53, productId: 'common', categoryId: 'shutter-box', order: 4, name: '케이스판재', variable: 'CASE_PANEL', type: 'output',\n formula: 'CASE_MAT_LEN', resultType: 'item', description: '케이스판재',\n itemCode: 'CB', itemName: '케이스판재', unit: 'EA', qtyFormula: 'CASE_MAT_QTY * QTY'\n },\n // 부자재 - 규칙: 품목명-규격\n {\n id: 54, productId: 'common', categoryId: 'shutter-box', order: 5, name: '마구리', variable: 'MARGURI', type: 'output',\n formula: '', resultType: 'item', description: '마구리(1SET=2EA, 제작사이즈 무관)',\n itemCode: '마구리-', itemName: '마구리', unit: 'SET', qtyFormula: 'QTY'\n },\n {\n id: 55, productId: 'common', categoryId: 'shutter-box', order: 6, name: '상부덮개 자재길이', variable: 'TOP_COVER_LEN', type: 'lookup',\n formula: 'TOP_COVER_LOOKUP', resultType: 'variable', description: '상부덮개 자재길이(케이스판재와 동일 테이블)', unit: 'mm',\n lookupTable: 'caseLengthTable'\n },\n // 부자재 - 규칙: 품목명-규격\n {\n id: 56, productId: 'common', categoryId: 'shutter-box', order: 7, name: '상부덮개', variable: 'TOP_COVER', type: 'output',\n formula: 'TOP_COVER_LEN', resultType: 'item', description: '상부덮개(케이스판재와 동일 규격, EGI 1.5ST)',\n itemCode: '상부덮개', itemName: '상부덮개', unit: 'EA', qtyFormula: 'CASE_MAT_QTY * QTY'\n },\n {\n id: 57, productId: 'common', categoryId: 'shutter-box', order: 8, name: '케이스연기차단재 수량', variable: 'CASE_SMOKE_QTY', type: 'formula',\n formula: 'Math.ceil(S / 3000) * 2', resultType: 'variable', description: '케이스용 연기차단재 = ceil(S/3000)*2', unit: 'EA'\n },\n // 절곡부품 - 규칙: 품목코드+종류코드+모양&길이코드 (GI: 연기차단재)\n {\n id: 58, productId: 'common', categoryId: 'shutter-box', order: 9, name: '케이스연기차단재', variable: 'CASE_SMOKE', type: 'output',\n formula: '3000', resultType: 'item', description: '케이스용 연기차단재(3000mm)',\n itemCode: 'GI30', itemName: '케이스용연기차단재', unit: 'EA', qtyFormula: 'CASE_SMOKE_QTY * QTY'\n },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 10. 하단마감재 구성품 산출 (스크린샷: 하단마감재 구성품 산출 기준서)\n // B = W0 (하단마감재 제작사이즈)\n // 하장바: 철재60×30, 스크린60×40, B범위별 3000/4000 자재\n // 엘바(스크린전용): 17×60 / 2량 1세트\n // 보강평철(스크린전용): 50mm\n // 무게평철(스크린전용): 50×12T / 오픈사이즈 기준 두줄 삽입\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n {\n id: 60, productId: 'common', categoryId: 'bottom-finish', order: 1, name: '하단마감재 제작사이즈', variable: 'B', type: 'formula',\n formula: 'W0', resultType: 'variable', description: 'B = W0 (오픈사이즈 가로)', unit: 'mm'\n },\n // 하장바: 철재(60×30), 스크린(60×40) - bottomBarTable 참조\n {\n id: 61, productId: 'common', categoryId: 'bottom-finish', order: 2, name: '하장바 자재', variable: 'HJ_MAT', type: 'lookup',\n formula: 'HJ_LOOKUP', resultType: 'variable', description: 'B범위별 하장바 자재길이 및 수량 결정',\n lookupTable: 'bottomBarTable'\n },\n // 부자재 - 규칙: 품목명-규격\n {\n id: 62, productId: 'steel', categoryId: 'bottom-finish', order: 3, name: '하장바(철재)', variable: 'HAJANGBAR_STEEL', type: 'output',\n formula: 'HJ_MAT', resultType: 'item', description: '하장바(철재 60×30)',\n itemCode: '하장바-60×30', itemName: '하장바 60×30', unit: 'EA', qtyFormula: 'HJ_MAT.qty * QTY', condition: \"PC === '철재'\"\n },\n {\n id: 63, productId: 'screen', categoryId: 'bottom-finish', order: 4, name: '하장바(스크린)', variable: 'HAJANGBAR_SCREEN', type: 'output',\n formula: 'HJ_MAT', resultType: 'item', description: '하장바(스크린 60×40)',\n itemCode: '하장바-60×40', itemName: '하장바 60×40', unit: 'EA', qtyFormula: 'HJ_MAT.qty * QTY', condition: \"PC === '스크린'\"\n },\n // 엘바: 스크린전용, 규격 17×60, 2량 1세트 - elbarTable 참조\n {\n id: 64, productId: 'screen', categoryId: 'bottom-finish', order: 5, name: '엘바 자재', variable: 'ELBAR_MAT', type: 'lookup',\n formula: 'ELBAR_LOOKUP', resultType: 'variable', description: '엘바(스크린전용) B범위별 자재',\n lookupTable: 'elbarTable', condition: \"PC === '스크린'\"\n },\n // 부자재 - 규칙: 품목명-규격\n {\n id: 65, productId: 'screen', categoryId: 'bottom-finish', order: 6, name: '엘바', variable: 'ELBAR', type: 'output',\n formula: 'ELBAR_MAT', resultType: 'item', description: '엘바(스크린전용 17×60, 2량1세트)',\n itemCode: '엘바-17×60', itemName: '엘바 17×60', unit: 'EA', qtyFormula: 'ELBAR_MAT.qty * QTY', condition: \"PC === '스크린'\"\n },\n // 보강평철: 스크린전용, 규격 50mm - reinforceTable 참조\n {\n id: 66, productId: 'screen', categoryId: 'bottom-finish', order: 7, name: '보강평철 자재', variable: 'REINFORCE_MAT', type: 'lookup',\n formula: 'REINFORCE_LOOKUP', resultType: 'variable', description: '보강평철(스크린전용) B범위별 자재',\n lookupTable: 'reinforceTable', condition: \"PC === '스크린'\"\n },\n // 부자재 - 규칙: 품목명-규격\n {\n id: 67, productId: 'screen', categoryId: 'bottom-finish', order: 8, name: '보강평철', variable: 'REINFORCE', type: 'output',\n formula: 'REINFORCE_MAT', resultType: 'item', description: '보강평철(스크린전용 50mm)',\n itemCode: '보강평철-50mm', itemName: '보강평철 50mm', unit: 'EA', qtyFormula: 'REINFORCE_MAT.qty * QTY', condition: \"PC === '스크린'\"\n },\n // 무게평철: 스크린전용, 규격 50×12T, 단위 2000mm, 두줄 삽입 - weightFlatTable 참조\n {\n id: 68, productId: 'screen', categoryId: 'bottom-finish', order: 9, name: '무게평철 자재', variable: 'WEIGHT_MAT', type: 'lookup',\n formula: 'WEIGHT_LOOKUP', resultType: 'variable', description: '무게평철(스크린전용) B범위별 자재(두줄 삽입)',\n lookupTable: 'weightFlatTable', condition: \"PC === '스크린'\"\n },\n // 부자재 - 규칙: 품목명-규격\n {\n id: 69, productId: 'screen', categoryId: 'bottom-finish', order: 10, name: '무게평철', variable: 'WEIGHT_FLAT', type: 'output',\n formula: '2000', resultType: 'item', description: '무게평철(스크린전용 50×12T, 2000단위, 두줄)',\n itemCode: '무게평철-50×12TL2000', itemName: '무게평철 50×12T', unit: 'EA', qtyFormula: 'WEIGHT_MAT.qty * QTY', condition: \"PC === '스크린'\"\n },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 11. 부자재 산출 (문서: 환봉, 조인트바 산출 기준 / 각파이프 자재 수량 산출 기준)\n // 환봉(스크린전용): 3000mm고정, W1구간별 수량 결정\n // 조인트바: 기본2개(양 끝 250mm 지점) + FLOOR((W1-500)/1000) 추가\n // 각파이프: 스크린용(500*350), 철재용(650*500) 케이스 제작사이즈별 산출\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n {\n id: 70, productId: 'screen', categoryId: 'sub-material', order: 1, name: '환봉 자재', variable: 'HWANBONG_MAT', type: 'lookup',\n formula: 'HWANBONG_LOOKUP', resultType: 'variable', description: '환봉(스크린전용) W1구간별 자재 및 수량',\n lookupTable: 'hwanbongQtyTable', condition: \"PC === '스크린'\"\n },\n // 부자재 - 규칙: 품목명-규격\n {\n id: 71, productId: 'screen', categoryId: 'sub-material', order: 2, name: '환봉', variable: 'HWANBONG', type: 'output',\n formula: '3000', resultType: 'item', description: '환봉(스크린전용, 3000mm고정)',\n itemCode: '환봉-3000', itemName: '환봉 3000', unit: 'EA', qtyFormula: 'HWANBONG_MAT.qty * QTY', condition: \"PC === '스크린'\"\n },\n {\n id: 72, productId: 'common', categoryId: 'sub-material', order: 3, name: '조인트바 수량', variable: 'JOINTBAR_QTY', type: 'formula',\n formula: '2 + Math.floor((W1 - 500) / 1000)', resultType: 'variable', description: '조인트바 = 기본2개 + FLOOR((W1-500)/1000) (중앙여유공간 내 1000mm당 추가)', unit: 'EA'\n },\n // 부자재 - 규칙: 품목명-규격\n {\n id: 73, productId: 'common', categoryId: 'sub-material', order: 4, name: '조인트바', variable: 'JOINTBAR', type: 'output',\n formula: '', resultType: 'item', description: '조인트바(양끝 250mm 지점 설치, 중앙 1000mm 간격 추가)',\n itemCode: '조인트바-', itemName: '조인트바', unit: 'EA', qtyFormula: 'JOINTBAR_QTY * QTY'\n },\n // 각파이프: 스크린용(squarePipeTableScreen), 철재용(squarePipeTableSteel) 별도 참조\n {\n id: 74, productId: 'screen', categoryId: 'sub-material', order: 5, name: '각파이프 자재(스크린)', variable: 'SQP_MAT_SCREEN', type: 'lookup',\n formula: 'SQP_SCREEN_LOOKUP', resultType: 'variable', description: '각파이프(스크린용 500*350) L구간별 3000/6000 자재 수량',\n lookupTable: 'squarePipeTableScreen', condition: \"PC === '스크린'\"\n },\n {\n id: 98, productId: 'steel', categoryId: 'sub-material', order: 5, name: '각파이프 자재(철재)', variable: 'SQP_MAT_STEEL', type: 'lookup',\n formula: 'SQP_STEEL_LOOKUP', resultType: 'variable', description: '각파이프(철재용 650*500) L구간별 3000/6000 자재 수량',\n lookupTable: 'squarePipeTableSteel', condition: \"PC === '철재'\"\n },\n // 부자재 - 규칙: 품목명-규격\n {\n id: 76, productId: 'common', categoryId: 'sub-material', order: 6, name: '각파이프 3000', variable: 'SQP3000', type: 'output',\n formula: '3000', resultType: 'item', description: '각파이프(3000mm)',\n itemCode: '각파이프-30×30L3000', itemName: '각파이프 30×30 3000', unit: 'EA', qtyFormula: \"(PC === '스크린' ? SQP_MAT_SCREEN.mat3000 : SQP_MAT_STEEL.mat3000) * QTY\"\n },\n {\n id: 77, productId: 'common', categoryId: 'sub-material', order: 7, name: '각파이프 6000', variable: 'SQP6000', type: 'output',\n formula: '6000', resultType: 'item', description: '각파이프(6000mm)',\n itemCode: '각파이프-30×30L6000', itemName: '각파이프 30×30 6000', unit: 'EA', qtyFormula: \"(PC === '스크린' ? SQP_MAT_SCREEN.mat6000 : SQP_MAT_STEEL.mat6000) * QTY\"\n },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 12. 구매부품 산출 (전동개폐기, 연동제어기)\n // 구매부품 - 규칙: 품목명-규격 (규격 = 전원+용량, 공백제거)\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n {\n id: 80, productId: 'common', categoryId: 'purchased-parts', order: 1, name: '전동개폐기', variable: 'MOTOR', type: 'output',\n formula: 'MOTOR_CODE', resultType: 'item', description: '전동개폐기(모터)',\n itemCode: '전동개폐기', itemName: '전동개폐기', unit: 'EA', qtyFormula: 'QTY', isDynamic: true\n },\n {\n id: 81, productId: 'common', categoryId: 'purchased-parts', order: 2, name: '연동제어기 코드', variable: 'CTL_CODE', type: 'formula',\n formula: \"'연동제어기-' + (CT === '매립' ? '매립형' : '노출형') + (WIRE === '유선' ? '유선' : '무선')\", resultType: 'variable', description: '연동제어기 품목코드 생성', unit: ''\n },\n {\n id: 82, productId: 'common', categoryId: 'purchased-parts', order: 3, name: '연동제어기', variable: 'CONTROLLER', type: 'output',\n formula: 'CTL_CODE', resultType: 'item', description: '연동제어기',\n itemCode: '연동제어기', itemName: '연동제어기', unit: 'EA', qtyFormula: 'QTY', isDynamic: true\n },\n\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 13. 장수산출 (스크린 원단 장수 / 슬랫 매수 계산)\n // 스크린 원단 장수: 총 중량(kg) ÷ 장당 중량 = 투입 장수\n // 슬랫 매수: 코일 길이 ÷ 제품 높이 = 절단 매수\n // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n // 스크린 원단 계산용 입력값\n {\n id: 83, productId: 'screen', categoryId: 'sheet-count', order: 1, name: '원단 롤 중량', variable: 'FABRIC_ROLL_KG', type: 'input',\n formula: '', resultType: 'variable', description: '원단 롤 1개당 중량 (기본 25kg)', unit: 'kg', defaultValue: 25,\n condition: \"PC === '스크린'\"\n },\n {\n id: 84, productId: 'screen', categoryId: 'sheet-count', order: 2, name: '장당 중량', variable: 'FABRIC_SHEET_KG', type: 'formula',\n formula: '(W1 * H1 * 0.00035) / 1000', resultType: 'variable', description: '장당 중량 = 제작가로(mm) × 제작세로(mm) × 원단밀도(0.00035) ÷ 1000', unit: 'kg',\n condition: \"PC === '스크린'\"\n },\n {\n id: 85, productId: 'screen', categoryId: 'sheet-count', order: 3, name: '스크린 원단 장수', variable: 'SCREEN_SHEET_CNT', type: 'formula',\n formula: 'Math.floor(FABRIC_ROLL_KG / FABRIC_SHEET_KG)', resultType: 'variable', description: '원단 롤 중량 ÷ 장당 중량 = 투입 장수 (소수점 버림)', unit: '장',\n condition: \"PC === '스크린'\"\n },\n {\n id: 86, productId: 'screen', categoryId: 'sheet-count', order: 4, name: '필요 롤 수량', variable: 'SCREEN_ROLL_QTY', type: 'formula',\n formula: 'Math.ceil(QTY / SCREEN_SHEET_CNT)', resultType: 'variable', description: '주문수량 ÷ 롤당 장수 = 필요 롤 수량 (올림)', unit: '롤',\n condition: \"PC === '스크린'\"\n },\n\n // 슬랫 매수 계산용 입력값\n {\n id: 87, productId: 'slat', categoryId: 'sheet-count', order: 5, name: '코일 길이', variable: 'COIL_LENGTH', type: 'input',\n formula: '', resultType: 'variable', description: '슬랫 코일 1롤 길이 (기본 305000mm = 305m)', unit: 'mm', defaultValue: 305000,\n condition: \"PC === '슬랫'\"\n },\n {\n id: 88, productId: 'slat', categoryId: 'sheet-count', order: 6, name: '슬랫 피치', variable: 'SLAT_PITCH', type: 'input',\n formula: '', resultType: 'variable', description: '슬랫 1단 피치 높이 (기본 76mm)', unit: 'mm', defaultValue: 76,\n condition: \"PC === '슬랫'\"\n },\n {\n id: 89, productId: 'slat', categoryId: 'sheet-count', order: 7, name: '제품 높이별 슬랫 단수', variable: 'SLAT_ROW_CNT', type: 'formula',\n formula: 'Math.ceil(H0 / SLAT_PITCH)', resultType: 'variable', description: '제품 높이 ÷ 피치 = 슬랫 단수 (올림)', unit: '단',\n condition: \"PC === '슬랫'\"\n },\n {\n id: 90, productId: 'slat', categoryId: 'sheet-count', order: 8, name: '슬랫 절단 매수', variable: 'SLAT_SHEET_CNT', type: 'formula',\n formula: 'Math.floor(COIL_LENGTH / H0)', resultType: 'variable', description: '코일 길이 ÷ 제품 높이 = 절단 매수 (소수점 버림)', unit: '매',\n condition: \"PC === '슬랫'\"\n },\n {\n id: 91, productId: 'slat', categoryId: 'sheet-count', order: 9, name: '1SET 슬랫 총매수', variable: 'SLAT_TOTAL_SHEET', type: 'formula',\n formula: 'SLAT_ROW_CNT * 2', resultType: 'variable', description: '1SET = 2량이므로 슬랫 단수 × 2', unit: '매',\n condition: \"PC === '슬랫'\"\n },\n {\n id: 92, productId: 'slat', categoryId: 'sheet-count', order: 10, name: '필요 코일 수량', variable: 'SLAT_COIL_QTY', type: 'formula',\n formula: 'Math.ceil((SLAT_TOTAL_SHEET * QTY) / SLAT_SHEET_CNT)', resultType: 'variable', description: '총 필요매수 ÷ 코일당 절단매수 = 필요 코일 수량 (올림)', unit: '롤',\n condition: \"PC === '슬랫'\"\n },\n];\n\n// ═══════════════════════════════════════════════════════════════════\n// 장수 계산 유틸리티 함수 (작업일지 데이터 연동용)\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * 스크린 원단 장수 계산\n * @param {number} width - 제작 가로 (mm)\n * @param {number} height - 제작 세로 (mm)\n * @param {number} fabricRollKg - 원단 롤 중량 (기본 25kg)\n * @param {number} fabricDensity - 원단 밀도 (기본 0.00035)\n * @returns {object} { sheetPerRoll: 롤당 장수, sheetKg: 장당 중량 }\n */\nconst calculateScreenSheetCount = (width, height, fabricRollKg = 25, fabricDensity = 0.00035) => {\n // 장당 중량 = 제작가로(mm) × 제작세로(mm) × 원단밀도 ÷ 1000\n const sheetKg = (width * height * fabricDensity) / 1000;\n // 롤당 장수 = 롤 중량 ÷ 장당 중량 (소수점 버림)\n const sheetPerRoll = sheetKg > 0 ? Math.floor(fabricRollKg / sheetKg) : 0;\n return { sheetPerRoll, sheetKg: sheetKg.toFixed(3) };\n};\n\n/**\n * 슬랫 매수(절단 매수) 계산\n * @param {number} productHeight - 제품 높이 (mm) - H0\n * @param {number} coilLength - 코일 길이 (기본 305000mm = 305m)\n * @param {number} slatPitch - 슬랫 피치 (기본 76mm)\n * @returns {object} { sheetCount: 절단 매수, rowCount: 슬랫 단수, totalSheetPerSet: 1SET당 총매수 }\n */\nconst calculateSlatSheetCount = (productHeight, coilLength = 305000, slatPitch = 76) => {\n // 절단 매수 = 코일 길이 ÷ 제품 높이 (소수점 버림)\n const sheetCount = productHeight > 0 ? Math.floor(coilLength / productHeight) : 0;\n // 슬랫 단수 = 제품 높이 ÷ 피치 (올림)\n const rowCount = slatPitch > 0 ? Math.ceil(productHeight / slatPitch) : 0;\n // 1SET = 2량이므로 슬랫 단수 × 2\n const totalSheetPerSet = rowCount * 2;\n return { sheetCount, rowCount, totalSheetPerSet };\n};\n\n/**\n * 작업일지용 슬랫 매수 자동 계산 (수주 품목 데이터 기반)\n * @param {object} orderItem - 수주 품목 데이터 (width, height 포함)\n * @returns {number} 계산된 매수(세로)\n */\nconst calculateWorkLogSlatSheetCount = (orderItem) => {\n if (!orderItem || !orderItem.height) return 0;\n const { sheetCount } = calculateSlatSheetCount(orderItem.height);\n return sheetCount;\n};\n\n/**\n * 작업일지용 스크린 규격별 장수 자동 계산\n * @param {number} totalHeight - 제품 총 높이 (mm)\n * @param {Array} standardHeights - 규격 높이 배열 [1180, 900, 600, 400, 300]\n * @returns {object} { spec1180: n, spec900: n, spec600: n, spec400: n, spec300: n, remainHeight: n }\n */\nconst calculateScreenSpecSheetCount = (totalHeight, standardHeights = [1180, 900, 600, 400, 300]) => {\n let remaining = totalHeight;\n const result = {\n spec1180: 0,\n spec900: 0,\n spec600: 0,\n spec400: 0,\n spec300: 0,\n remainHeight: 0\n };\n\n // 큰 규격부터 순차적으로 배분\n standardHeights.forEach(specHeight => {\n const specKey = `spec${specHeight}`;\n if (remaining >= specHeight) {\n result[specKey] = Math.floor(remaining / specHeight);\n remaining = remaining % specHeight;\n }\n });\n\n result.remainHeight = remaining;\n return result;\n};\n\n/**\n * 절곡 부품 수량 자동 계산 (1SET = 2량 기준)\n * @param {string} partType - 부품 유형 (가이드레일, 하부BASE, 연기차단재 등)\n * @param {number} productHeight - 제품 높이 (mm) - H0\n * @param {number} productWidth - 제품 가로 (mm) - W0\n * @param {number} qty - 주문 수량 (SET)\n * @returns {number} 필요 수량\n */\nconst calculateBendingPartQty = (partType, productHeight, productWidth, qty = 1) => {\n // 1SET = 2량 기준\n const setMultiplier = 2;\n\n switch (partType) {\n case '가이드레일':\n case '가이드레일(본체)':\n case '가이드레일(C형)':\n case '가이드레일(D형)':\n // 가이드레일: 1SET당 2개 (좌우 각 1개)\n return qty * setMultiplier;\n\n case '하부BASE1':\n case '하부BASE2':\n // 하부BASE: 1SET당 2개 (좌우 각 1개)\n return qty * setMultiplier;\n\n case '연기차단재':\n // 연기차단재: 1량당 2개 (상하) → 1SET당 4개\n return qty * setMultiplier * 2;\n\n case '케이스':\n case '케이스판재':\n // 케이스: 가로 기준으로 1219mm당 1개\n const casePanelQty = Math.ceil(productWidth / 1219);\n return casePanelQty * qty;\n\n case '마구리':\n // 마구리: 1SET당 1세트 (좌우 한쌍)\n return qty;\n\n case '상부덮개':\n // 상부덮개: 케이스와 동일\n const topCoverQty = Math.ceil(productWidth / 1219);\n return topCoverQty * qty;\n\n default:\n // 기본: 1SET당 2개\n return qty * setMultiplier;\n }\n};\n\n/**\n * 절곡 작업일지용 부품 목록 자동 생성\n * @param {object} orderData - 수주 데이터 (productHeight, productWidth, qty 포함)\n * @returns {Array} 절곡 부품 목록 (name, material, spec, qty)\n */\nconst generateBendingWorkLogParts = (orderData) => {\n const { productHeight = 3000, productWidth = 3000, qty = 1 } = orderData;\n\n // 가이드레일 제작 길이 = H0 + 250\n const guideRailLength = productHeight + 250;\n\n return [\n {\n name: '가이드레일(본체)',\n material: 'EGI 1.5T',\n spec: `${guideRailLength}mm`,\n qty: calculateBendingPartQty('가이드레일', productHeight, productWidth, qty)\n },\n {\n name: '가이드레일(C형)',\n material: 'EGI 1.5T',\n spec: `${guideRailLength}mm`,\n qty: calculateBendingPartQty('가이드레일', productHeight, productWidth, qty)\n },\n {\n name: '가이드레일(D형)',\n material: 'EGI 1.5T',\n spec: `${guideRailLength}mm`,\n qty: calculateBendingPartQty('가이드레일', productHeight, productWidth, qty)\n },\n {\n name: '하부BASE1',\n material: 'EGI 1.5T',\n spec: '130×80',\n qty: calculateBendingPartQty('하부BASE1', productHeight, productWidth, qty)\n },\n {\n name: '하부BASE2',\n material: 'EGI 1.5T',\n spec: '130×130',\n qty: calculateBendingPartQty('하부BASE2', productHeight, productWidth, qty)\n },\n {\n name: '연기차단재',\n material: '화이바',\n spec: `${guideRailLength}mm`,\n qty: calculateBendingPartQty('연기차단재', productHeight, productWidth, qty)\n },\n ];\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// 조회 테이블 (Lookup Tables) - 스크린샷 기반\n// ═══════════════════════════════════════════════════════════════════\n\n// 모터 용량 산출 테이블 (스크린샷: 모터 용량 산정 기준)\nconst motorCapacityTable = {\n // [스크린] 제품 기준 (샤프트인치 → 산출중량범위 → 적용모터)\n screen: {\n '4': [\n { min: 0, max: 123, motor: 150 },\n { min: 124, max: 208, motor: 300 },\n ],\n '5': [\n { min: 0, max: 150, motor: 150 },\n { min: 151, max: 246, motor: 300 },\n { min: 247, max: 277, motor: 400 },\n ],\n '6': [\n { min: 0, max: 208, motor: 150 },\n { min: 209, max: 246, motor: 300 },\n { min: 247, max: 324, motor: 400 },\n { min: 325, max: 388, motor: 500 },\n { min: 389, max: 494, motor: 600 },\n ],\n },\n // [철재] 제품 기준\n steel: {\n '4': [\n { min: 0, max: 277, motor: 300 },\n { min: 278, max: 327, motor: 400 },\n { min: 328, max: 400, motor: 500 },\n ],\n '5': [\n { min: 0, max: 300, motor: 300 },\n { min: 301, max: 424, motor: 400 },\n { min: 425, max: 600, motor: 500 },\n ],\n '6': [\n { min: 0, max: 327, motor: 300 },\n { min: 328, max: 494, motor: 400 },\n { min: 495, max: 600, motor: 500 },\n { min: 601, max: 800, motor: 600 },\n ],\n '8': [\n { min: 0, max: 600, motor: 500 },\n { min: 601, max: 800, motor: 800 },\n { min: 801, max: 1000, motor: 1000 },\n ],\n },\n};\n\n// 브라켓 규격 테이블 (스크린샷: 모터용량별 브라켓 & 받침용앵글 산출 기준서)\nconst bracketTable = {\n screen: {\n 150: { bracket: '380×180', shaft: '3\"~4\"', angle: '40×40×380' },\n 300: { bracket: '380×180', shaft: '3\"~5\"', angle: '40×40×380' },\n 400: { bracket: '530×320', shaft: '3\"~6\"', angle: '50×50×530' },\n },\n steel: {\n 300: { bracket: '530×320', shaft: '4\"', angle: '50×50×530' },\n 400: { bracket: '530×320', shaft: '5\"', angle: '50×50×530' },\n 500: { bracket: '600×350', shaft: '5\"', angle: '50×50×600' },\n 600: { bracket: '600×350', shaft: '6\"', angle: '50×50×600' },\n 800: { bracket: '600×350', shaft: '6\"', angle: '50×50×600' },\n 1000: { bracket: '690×390', shaft: '8\"', angle: '50×50×690' },\n },\n};\n\n// ═══════════════════════════════════════════════════════════════════\n// 감기샤프트 산출 로직 (문서: 감기샤프트(스크린용/철재용) 산출 로직)\n// ═══════════════════════════════════════════════════════════════════\n\n// 철재용 샤프트 인치 선택 테이블 (W1과 K 두 조건을 모두 만족해야 함)\nconst shaftInchTableSteel = [\n { w1Min: 0, w1Max: 4500, kMin: 0, kMax: 400, inch: '4' },\n { w1Min: 0, w1Max: 4500, kMin: 400, kMax: 600, inch: '5' },\n { w1Min: 4500, w1Max: 5600, kMin: 0, kMax: 600, inch: '5' },\n { w1Min: 5600, w1Max: 7800, kMin: 0, kMax: 600, inch: '5' },\n { w1Min: 5600, w1Max: 7800, kMin: 600, kMax: 800, inch: '6' },\n { w1Min: 5600, w1Max: 8200, kMin: 800, kMax: 9999, inch: '8' },\n];\n\n// 스크린용 샤프트 인치 선택 (W1 기준)\nconst shaftInchTableScreen = [\n { w1Min: 0, w1Max: 8200, inch: '5' },\n { w1Min: 8200, w1Max: 99999, inch: '6' },\n];\n\n// 메인샤프트 자재 길이 테이블 (인치별 W1 구간)\n// 자재 규격: 3000 / 4500 / 6000 / 7000 / 8000 / 8200\n// 자재는 통자재로 1개만 납품, 길이 조합 불가\n// 샤프트 자재는 제작사이즈보다 작으면 안됨 (항상 W1 이상 길이 선택)\nconst shaftLengthTable = {\n '4': [\n { w1Min: 0, w1Max: 3000, length: 3000, qty: 1 },\n { w1Min: 3000, w1Max: 4500, length: 4500, qty: 1 },\n ],\n '5': [\n { w1Min: 0, w1Max: 3000, length: 3000, qty: 1 },\n { w1Min: 3000, w1Max: 4500, length: 4500, qty: 1 },\n { w1Min: 4500, w1Max: 6000, length: 6000, qty: 1 },\n { w1Min: 6000, w1Max: 7000, length: 7000, qty: 1 },\n ],\n '6': [\n { w1Min: 0, w1Max: 3000, length: 3000, qty: 1 },\n { w1Min: 3000, w1Max: 4500, length: 4500, qty: 1 },\n { w1Min: 4500, w1Max: 6000, length: 6000, qty: 1 },\n { w1Min: 6000, w1Max: 7000, length: 7000, qty: 1 },\n { w1Min: 7000, w1Max: 7800, length: 8000, qty: 1 },\n ],\n '8': [\n { w1Min: 0, w1Max: 3000, length: 3000, qty: 1 },\n { w1Min: 3000, w1Max: 4500, length: 4500, qty: 1 },\n { w1Min: 4500, w1Max: 6000, length: 6000, qty: 1 },\n { w1Min: 6000, w1Max: 7000, length: 7000, qty: 1 },\n { w1Min: 7000, w1Max: 7800, length: 8000, qty: 1 },\n { w1Min: 7800, w1Max: 8200, length: 8200, qty: 1 },\n ],\n};\n\n// 보조샤프트 (스크린 전용)\n// 규격: 항상 3인치, 수량: 무조건 1개\n// 자재 길이: W1 ≤ 8200 → 300 자재, W1 > 8200 → 500 자재\nconst subShaftTable = [\n { w1Min: 0, w1Max: 8200, length: 300, qty: 1 },\n { w1Min: 8200, w1Max: 99999, length: 500, qty: 1 },\n];\n\n// 가이드레일 자재 길이 테이블 (문서: 가이드레일 및 연기차단재 산출 기준서)\n// 제작길이 G(mm) 구간별 자재 선택 (1SET = 2량)\nconst guideRailLengthTable = [\n { gMin: 1219, gMax: 2438, materials: [{ length: 2438, qty: 2 }] },\n { gMin: 2438, gMax: 3000, materials: [{ length: 3000, qty: 2 }] },\n { gMin: 3000, gMax: 3500, materials: [{ length: 3500, qty: 2 }] },\n { gMin: 3500, gMax: 4000, materials: [{ length: 4000, qty: 2 }] },\n { gMin: 4000, gMax: 4300, materials: [{ length: 4300, qty: 2 }] },\n { gMin: 4300, gMax: 5438, materials: [{ length: 3000, qty: 2 }, { length: 2438, qty: 2 }] },\n { gMin: 5438, gMax: 6000, materials: [{ length: 3000, qty: 4 }] },\n { gMin: 6000, gMax: 6500, materials: [{ length: 4000, qty: 2 }] },\n { gMin: 6500, gMax: 7000, materials: [{ length: 4300, qty: 2 }] },\n { gMin: 7000, gMax: 7300, materials: [{ length: 4300, qty: 2 }] },\n];\n\n// 하부BASE 규격 테이블 (문서: 가이드레일 설치유형별 하부BASE 규격)\n// 스크린용\nconst baseSpecTableScreen = {\n '벽면형': { base1: '130×80', base2: '130×80', note: '120*70' },\n '측면형': { base1: '130×130', base2: '130×130', note: '120*120' },\n '혼합형': { base1: '130×80', base2: '130×130', note: '' },\n};\n// 철재용\nconst baseSpecTableSteel = {\n '벽면형': { base1: '140×85', base2: '140×85', note: '130*75' },\n '측면형': { base1: '140×135', base2: '140×135', note: '130*125' },\n '혼합형': { base1: '140×85', base2: '140×135', note: '' },\n};\n\n// 케이스 자재 테이블 (문서: 케이스(셔터박스) 구성품 산출 기준서)\n// 케이스판재, 상부덮개 동일 적용, 자재두께: EGI 1.5ST\nconst caseLengthTable = [\n { sMin: 0, sMax: 1219, length: 1219, qty: 1 },\n { sMin: 1219, sMax: 2438, length: 2438, qty: 1 },\n { sMin: 2438, sMax: 3000, length: 3000, qty: 1 },\n { sMin: 3000, sMax: 3500, length: 3500, qty: 1 },\n { sMin: 3500, sMax: 4000, length: 4000, qty: 1 },\n { sMin: 4000, sMax: 4150, length: 4150, qty: 1 },\n { sMin: 4150, sMax: 4719, materials: [{ length: 2438, qty: 1 }, { length: 1219, qty: 1 }], qty: 2, combo: '2438+1219' },\n { sMin: 4719, sMax: 6000, materials: [{ length: 3000, qty: 2 }], qty: 2, combo: '3000×2' },\n { sMin: 6000, sMax: 7000, materials: [{ length: 3500, qty: 2 }], qty: 2, combo: '3500×2' },\n { sMin: 7000, sMax: 8000, materials: [{ length: 4000, qty: 2 }], qty: 2, combo: '4000×2' },\n { sMin: 8000, sMax: 8300, materials: [{ length: 4150, qty: 2 }], qty: 2, combo: '4150×2' },\n];\n\n// ═══════════════════════════════════════════════════════════════════\n// 하단마감재 구성품 산출 기준서 (문서 기반)\n// B = W0 (오픈사이즈 가로), 자재 단위길이: 3000, 4000 (무게평철은 2000)\n// 철재: 하장바만 사용 (60×30)\n// 스크린: 하장바(60×40) + 엘바 + 보강평철 + 무게평철\n// ═══════════════════════════════════════════════════════════════════\n\n// 하장바 자재 테이블 (철재: 60×30, 스크린: 60×40)\nconst bottomBarTable = [\n { bMin: 0, bMax: 3000, materials: [{ length: 3000, qty: 1 }] },\n { bMin: 3000, bMax: 4000, materials: [{ length: 4000, qty: 1 }] },\n { bMin: 4000, bMax: 6000, materials: [{ length: 3000, qty: 2 }] },\n { bMin: 6000, bMax: 7000, materials: [{ length: 3000, qty: 1 }, { length: 4000, qty: 1 }], combo: '3000+4000' },\n { bMin: 7000, bMax: 8000, materials: [{ length: 4000, qty: 2 }] },\n];\n\n// 엘바 자재 테이블 (스크린 전용, 규격: 17×60 / 2량 1세트)\nconst elbarTable = [\n { bMin: 0, bMax: 3000, materials: [{ length: 3000, qty: 2 }] },\n { bMin: 3000, bMax: 4000, materials: [{ length: 4000, qty: 2 }] },\n { bMin: 4000, bMax: 6000, materials: [{ length: 3000, qty: 4 }] },\n { bMin: 6000, bMax: 7000, materials: [{ length: 3000, qty: 2 }, { length: 4000, qty: 2 }], combo: '3000+4000×2' },\n { bMin: 7000, bMax: 8000, materials: [{ length: 4000, qty: 4 }] },\n];\n\n// 보강평철 자재 테이블 (스크린 전용, 규격: 50mm)\nconst reinforceTable = [\n { bMin: 0, bMax: 3000, materials: [{ length: 3000, qty: 1 }] },\n { bMin: 3000, bMax: 4000, materials: [{ length: 4000, qty: 1 }] },\n { bMin: 4000, bMax: 6000, materials: [{ length: 3000, qty: 2 }] },\n { bMin: 6000, bMax: 7000, materials: [{ length: 3000, qty: 1 }, { length: 4000, qty: 1 }], combo: '3000+4000' },\n { bMin: 7000, bMax: 8000, materials: [{ length: 4000, qty: 2 }] },\n];\n\n// 무게평철 자재 테이블 (스크린 전용, 규격: 50×12T, 단위: 2000mm, 두줄 삽입)\nconst weightFlatTable = [\n { bMin: 0, bMax: 3000, materials: [{ length: 2000, qty: 2 }] },\n { bMin: 3000, bMax: 4000, materials: [{ length: 2000, qty: 2 }] },\n { bMin: 4000, bMax: 6000, materials: [{ length: 2000, qty: 4 }] },\n { bMin: 6000, bMax: 7000, materials: [{ length: 2000, qty: 4 }] },\n { bMin: 7000, bMax: 8000, materials: [{ length: 2000, qty: 4 }] },\n];\n\n// ═══════════════════════════════════════════════════════════════════\n// 환봉, 조인트바 산출 기준 (문서: 환봉, 조인트바 산출 기준)\n// ═══════════════════════════════════════════════════════════════════\n\n// 환봉 (스크린 전용 부자재)\n// 자재 규격: 3000mm 고정, W1 구간별 수량 결정\nconst hwanbongQtyTable = [\n { w1Min: 0, w1Max: 3000, length: 3000, qty: 1 },\n { w1Min: 3000, w1Max: 6000, length: 3000, qty: 2 },\n { w1Min: 6000, w1Max: 9000, length: 3000, qty: 3 },\n { w1Min: 9000, w1Max: 12000, length: 3000, qty: 4 },\n];\n\n// 조인트바 산출 기준\n// 기본 2개 고정 (양 끝 250mm 지점 설치)\n// 중앙 여유공간 = W1 - 500 (mm)\n// 여유 공간 내에 완전히 1000mm가 들어가는 횟수만큼 조인트바 추가\n// 공식: 조인트바 수량 = 2 + FLOOR((W1 - 500) / 1000)\n// 1000mm가 안 되는 나머지 구간에는 추가하지 않음\n\n// ═══════════════════════════════════════════════════════════════════\n// 각파이프 자재 수량 산출 기준 (문서: 각파이프 자재 수량 산출 기준)\n// ═══════════════════════════════════════════════════════════════════\n// 공통 기준:\n// - 각파이프 자재는 3000mm, 6000mm 두 종류\n// - 케이스 제작사이즈 L은 자재 수량 계산 기준\n// - 자재 수량은 \"기본 2개 + 자보강 갯수\"에 따라 상수길이(P)를 계산하고,\n// 그 길이에 따라 각파이프 자재가 몇 개 들어가는지 결정됨\n\n// 스크린용 케이스 500*350\nconst squarePipeTableScreen = [\n { lMin: 0, lMax: 1600, mat3000: 3, mat6000: 0 },\n { lMin: 1600, lMax: 2800, mat3000: 4, mat6000: 0 },\n { lMin: 2800, lMax: 3000, mat3000: 5, mat6000: 0 },\n { lMin: 3000, lMax: 4000, mat3000: 1, mat6000: 3 },\n { lMin: 4000, lMax: 5200, mat3000: 1, mat6000: 4 },\n { lMin: 5200, lMax: 6400, mat3000: 1, mat6000: 5 },\n];\n\n// 철재용 케이스 650*500\nconst squarePipeTableSteel = [\n { lMin: 0, lMax: 1600, mat3000: 4, mat6000: 0 },\n { lMin: 1600, lMax: 2800, mat3000: 5, mat6000: 0 },\n { lMin: 2800, lMax: 3000, mat3000: 6, mat6000: 0 },\n { lMin: 3000, lMax: 4000, mat3000: 1, mat6000: 3 },\n { lMin: 4000, lMax: 5200, mat3000: 1, mat6000: 4 },\n { lMin: 5200, lMax: 6400, mat3000: 1, mat6000: 5 },\n];\n\n// ═══════════════════════════════════════════════════════════════════\n// 품목별 단가 마스터 데이터\n// ═══════════════════════════════════════════════════════════════════\nconst itemPriceMaster = [\n // ═══════════════════════════════════════════════════════════════════\n // 절곡부품 - 규칙: 품목코드+종류코드+모양&길이코드 (예: RC24, CB30)\n // ═══════════════════════════════════════════════════════════════════\n // 절곡부품 - 가이드레일 (R: 가이드레일, C: C형)\n { itemCode: 'RC24', itemName: '가이드레일(벽면형) C형 2438', category: '가이드레일', unitPrice: 15000, sellingPrice: 22000, unit: 'EA' },\n { itemCode: 'RC30', itemName: '가이드레일(벽면형) C형 3000', category: '가이드레일', unitPrice: 18000, sellingPrice: 27000, unit: 'EA' },\n { itemCode: 'RC35', itemName: '가이드레일(벽면형) C형 3500', category: '가이드레일', unitPrice: 21000, sellingPrice: 31500, unit: 'EA' },\n { itemCode: 'RC40', itemName: '가이드레일(벽면형) C형 4000', category: '가이드레일', unitPrice: 24000, sellingPrice: 36000, unit: 'EA' },\n { itemCode: 'RC43', itemName: '가이드레일(벽면형) C형 4300', category: '가이드레일', unitPrice: 26000, sellingPrice: 39000, unit: 'EA' },\n\n // 절곡부품 - 케이스 (C: 케이스, B: 후면코너부)\n { itemCode: 'CB12', itemName: '케이스(후면코너부) 1219', category: '케이스', unitPrice: 8000, sellingPrice: 12000, unit: 'EA' },\n { itemCode: 'CB24', itemName: '케이스(후면코너부) 2438', category: '케이스', unitPrice: 16000, sellingPrice: 24000, unit: 'EA' },\n { itemCode: 'CB30', itemName: '케이스(후면코너부) 3000', category: '케이스', unitPrice: 20000, sellingPrice: 30000, unit: 'EA' },\n { itemCode: 'CB35', itemName: '케이스(후면코너부) 3500', category: '케이스', unitPrice: 23000, sellingPrice: 34500, unit: 'EA' },\n { itemCode: 'CB40', itemName: '케이스(후면코너부) 4000', category: '케이스', unitPrice: 26000, sellingPrice: 39000, unit: 'EA' },\n\n // 절곡부품 - 하단마감재 (B: 하단마감재, E: EGI, S: SUS)\n { itemCode: 'BE30', itemName: '하단마감재(스크린) EGI 3000', category: '하단마감재', unitPrice: 12000, sellingPrice: 18000, unit: 'EA' },\n { itemCode: 'BE40', itemName: '하단마감재(스크린) EGI 4000', category: '하단마감재', unitPrice: 16000, sellingPrice: 24000, unit: 'EA' },\n { itemCode: 'BS30', itemName: '하단마감재(스크린) SUS 3000', category: '하단마감재', unitPrice: 18000, sellingPrice: 27000, unit: 'EA' },\n { itemCode: 'BS40', itemName: '하단마감재(스크린) SUS 4000', category: '하단마감재', unitPrice: 24000, sellingPrice: 36000, unit: 'EA' },\n\n // 절곡부품 - 연기차단재 (G: 연기차단재, I: 화이바원단)\n { itemCode: 'GI24', itemName: '연기차단재 화이바원단 2438', category: '연기차단재', unitPrice: 8000, sellingPrice: 12000, unit: 'EA' },\n { itemCode: 'GI30', itemName: '연기차단재 화이바원단 3000', category: '연기차단재', unitPrice: 10000, sellingPrice: 15000, unit: 'EA' },\n { itemCode: 'GI35', itemName: '연기차단재 화이바원단 3500', category: '연기차단재', unitPrice: 12000, sellingPrice: 18000, unit: 'EA' },\n { itemCode: 'GI40', itemName: '연기차단재 화이바원단 4000', category: '연기차단재', unitPrice: 13000, sellingPrice: 19500, unit: 'EA' },\n\n // ═══════════════════════════════════════════════════════════════════\n // 구매부품 - 규칙: 품목명-규격 (규격 = 전원+용량, 공백제거)\n // 예: 전동개폐기-220V150KG유선\n // ═══════════════════════════════════════════════════════════════════\n // 구매부품 - 전동개폐기\n { itemCode: '전동개폐기-220V150KG유선', itemName: '전동개폐기 150KG 220V 유선', category: '전동개폐기', unitPrice: 280000, sellingPrice: 380000, unit: 'EA' },\n { itemCode: '전동개폐기-220V150KG무선', itemName: '전동개폐기 150KG 220V 무선', category: '전동개폐기', unitPrice: 320000, sellingPrice: 430000, unit: 'EA' },\n { itemCode: '전동개폐기-220V300KG유선', itemName: '전동개폐기 300KG 220V 유선', category: '전동개폐기', unitPrice: 350000, sellingPrice: 480000, unit: 'EA' },\n { itemCode: '전동개폐기-220V300KG무선', itemName: '전동개폐기 300KG 220V 무선', category: '전동개폐기', unitPrice: 390000, sellingPrice: 530000, unit: 'EA' },\n { itemCode: '전동개폐기-220V400KG유선', itemName: '전동개폐기 400KG 220V 유선', category: '전동개폐기', unitPrice: 420000, sellingPrice: 580000, unit: 'EA' },\n { itemCode: '전동개폐기-220V400KG무선', itemName: '전동개폐기 400KG 220V 무선', category: '전동개폐기', unitPrice: 460000, sellingPrice: 630000, unit: 'EA' },\n { itemCode: '전동개폐기-220V500KG유선', itemName: '전동개폐기 500KG 220V 유선', category: '전동개폐기', unitPrice: 520000, sellingPrice: 720000, unit: 'EA' },\n { itemCode: '전동개폐기-220V600KG유선', itemName: '전동개폐기 600KG 220V 유선', category: '전동개폐기', unitPrice: 620000, sellingPrice: 860000, unit: 'EA' },\n { itemCode: '전동개폐기-220V800KG유선', itemName: '전동개폐기 800KG 220V 유선', category: '전동개폐기', unitPrice: 780000, sellingPrice: 1080000, unit: 'EA' },\n { itemCode: '전동개폐기-220V1000KG유선', itemName: '전동개폐기 1000KG 220V 유선', category: '전동개폐기', unitPrice: 950000, sellingPrice: 1320000, unit: 'EA' },\n { itemCode: '전동개폐기-380V150KG유선', itemName: '전동개폐기 150KG 380V 유선', category: '전동개폐기', unitPrice: 290000, sellingPrice: 395000, unit: 'EA' },\n { itemCode: '전동개폐기-380V300KG유선', itemName: '전동개폐기 300KG 380V 유선', category: '전동개폐기', unitPrice: 360000, sellingPrice: 495000, unit: 'EA' },\n { itemCode: '전동개폐기-380V400KG유선', itemName: '전동개폐기 400KG 380V 유선', category: '전동개폐기', unitPrice: 430000, sellingPrice: 595000, unit: 'EA' },\n { itemCode: '전동개폐기-380V500KG유선', itemName: '전동개폐기 500KG 380V 유선', category: '전동개폐기', unitPrice: 530000, sellingPrice: 735000, unit: 'EA' },\n { itemCode: '전동개폐기-380V600KG유선', itemName: '전동개폐기 600KG 380V 유선', category: '전동개폐기', unitPrice: 630000, sellingPrice: 875000, unit: 'EA' },\n\n // 구매부품 - 연동제어기\n { itemCode: '연동제어기-매립형유선', itemName: '연동제어기 매립형 유선', category: '연동제어기', unitPrice: 85000, sellingPrice: 120000, unit: 'EA' },\n { itemCode: '연동제어기-매립형무선', itemName: '연동제어기 매립형 무선', category: '연동제어기', unitPrice: 95000, sellingPrice: 135000, unit: 'EA' },\n { itemCode: '연동제어기-노출형유선', itemName: '연동제어기 노출형 유선', category: '연동제어기', unitPrice: 75000, sellingPrice: 105000, unit: 'EA' },\n { itemCode: '연동제어기-노출형무선', itemName: '연동제어기 노출형 무선', category: '연동제어기', unitPrice: 85000, sellingPrice: 120000, unit: 'EA' },\n\n // 구매부품 - 감기샤프트\n { itemCode: '감기샤프트-60.5×2.9TL3000', itemName: '감기샤프트 60.5×2.9T L:3000', category: '감기샤프트', unitPrice: 45000, sellingPrice: 65000, unit: 'EA' },\n { itemCode: '감기샤프트-60.5×2.9TL4500', itemName: '감기샤프트 60.5×2.9T L:4500', category: '감기샤프트', unitPrice: 65000, sellingPrice: 95000, unit: 'EA' },\n { itemCode: '감기샤프트-76.3×2.8TL3000', itemName: '감기샤프트 76.3×2.8T L:3000', category: '감기샤프트', unitPrice: 55000, sellingPrice: 80000, unit: 'EA' },\n { itemCode: '감기샤프트-76.3×2.8TL4500', itemName: '감기샤프트 76.3×2.8T L:4500', category: '감기샤프트', unitPrice: 75000, sellingPrice: 110000, unit: 'EA' },\n { itemCode: '감기샤프트-89.1×2.8TL6000', itemName: '감기샤프트 89.1×2.8T L:6000', category: '감기샤프트', unitPrice: 95000, sellingPrice: 140000, unit: 'EA' },\n { itemCode: '감기샤프트-101.6×2.8TL6000', itemName: '감기샤프트 101.6×2.8T L:6000', category: '감기샤프트', unitPrice: 110000, sellingPrice: 160000, unit: 'EA' },\n { itemCode: '감기샤프트-114.3×2TL7000', itemName: '감기샤프트 114.3×2T L:7000', category: '감기샤프트', unitPrice: 130000, sellingPrice: 190000, unit: 'EA' },\n { itemCode: '감기샤프트-139.8×2.9TL8000', itemName: '감기샤프트 139.8×2.9T L:8000', category: '감기샤프트', unitPrice: 165000, sellingPrice: 240000, unit: 'EA' },\n { itemCode: '감기샤프트-165.2×3.3TL8200', itemName: '감기샤프트 165.2×3.3T L:8200', category: '감기샤프트', unitPrice: 195000, sellingPrice: 285000, unit: 'EA' },\n\n // 구매부품 - 브라켓/앵글\n { itemCode: '브라켓-380×180', itemName: '브라켓 380×180', category: '브라켓', unitPrice: 25000, sellingPrice: 38000, unit: 'SET' },\n { itemCode: '브라켓-530×320', itemName: '브라켓 530×320', category: '브라켓', unitPrice: 35000, sellingPrice: 52000, unit: 'SET' },\n { itemCode: '브라켓-600×350', itemName: '브라켓 600×350', category: '브라켓', unitPrice: 42000, sellingPrice: 63000, unit: 'SET' },\n { itemCode: '브라켓-690×390', itemName: '브라켓 690×390', category: '브라켓', unitPrice: 50000, sellingPrice: 75000, unit: 'SET' },\n { itemCode: '받침용앵글-40×40×380', itemName: '받침용앵글 40×40×380', category: '앵글', unitPrice: 18000, sellingPrice: 27000, unit: 'SET' },\n { itemCode: '받침용앵글-50×50×530', itemName: '받침용앵글 50×50×530', category: '앵글', unitPrice: 25000, sellingPrice: 38000, unit: 'SET' },\n { itemCode: '받침용앵글-50×50×600', itemName: '받침용앵글 50×50×600', category: '앵글', unitPrice: 28000, sellingPrice: 42000, unit: 'SET' },\n { itemCode: '받침용앵글-50×50×690', itemName: '받침용앵글 50×50×690', category: '앵글', unitPrice: 32000, sellingPrice: 48000, unit: 'SET' },\n\n // ═══════════════════════════════════════════════════════════════════\n // 부자재 - 규칙: 품목명-규격\n // 예: 환봉-3000, 각파이프-30×30L3000\n // ═══════════════════════════════════════════════════════════════════\n { itemCode: '환봉-3000', itemName: '환봉 3000', category: '환봉', unitPrice: 5000, sellingPrice: 7500, unit: 'EA' },\n { itemCode: '조인트바-', itemName: '조인트바', category: '조인트바', unitPrice: 3500, sellingPrice: 5200, unit: 'EA' },\n { itemCode: '각파이프-30×30L3000', itemName: '각파이프 30×30 L:3000', category: '각파이프', unitPrice: 8000, sellingPrice: 12000, unit: 'EA' },\n { itemCode: '각파이프-30×30L6000', itemName: '각파이프 30×30 L:6000', category: '각파이프', unitPrice: 15000, sellingPrice: 22500, unit: 'EA' },\n\n // 하단마감재 부자재\n { itemCode: '하장바-60×30L3000', itemName: '하장바 60×30 L:3000', category: '하장바', unitPrice: 12000, sellingPrice: 18000, unit: 'EA' },\n { itemCode: '하장바-60×30L4000', itemName: '하장바 60×30 L:4000', category: '하장바', unitPrice: 16000, sellingPrice: 24000, unit: 'EA' },\n { itemCode: '엘바-17×60L3000', itemName: '엘바 17×60 L:3000', category: '엘바', unitPrice: 8000, sellingPrice: 12000, unit: 'EA' },\n { itemCode: '엘바-17×60L4000', itemName: '엘바 17×60 L:4000', category: '엘바', unitPrice: 10000, sellingPrice: 15000, unit: 'EA' },\n { itemCode: '보강평철-50mmL3000', itemName: '보강평철 50mm L:3000', category: '보강평철', unitPrice: 6000, sellingPrice: 9000, unit: 'EA' },\n { itemCode: '보강평철-50mmL4000', itemName: '보강평철 50mm L:4000', category: '보강평철', unitPrice: 8000, sellingPrice: 12000, unit: 'EA' },\n { itemCode: '무게평철-50×12TL2000', itemName: '무게평철 50×12T L:2000', category: '무게평철', unitPrice: 15000, sellingPrice: 22500, unit: 'EA' },\n\n // 마구리/상부덮개\n { itemCode: '마구리-', itemName: '마구리', category: '마구리', unitPrice: 15000, sellingPrice: 22500, unit: 'SET' },\n { itemCode: '상부덮개-1219', itemName: '상부덮개 1219', category: '상부덮개', unitPrice: 8000, sellingPrice: 12000, unit: 'EA' },\n\n // 하부 BASE\n { itemCode: '하부BASE-벽면형', itemName: '하부BASE(벽면형)', category: 'BASE', unitPrice: 25000, sellingPrice: 37500, unit: 'SET' },\n { itemCode: '하부BASE-코너형', itemName: '하부BASE(코너형)', category: 'BASE', unitPrice: 30000, sellingPrice: 45000, unit: 'SET' },\n\n // 검사비\n { itemCode: '검사비-', itemName: '검사비', category: '검사비', unitPrice: 0, sellingPrice: 50000, unit: 'SET' },\n];\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// 통합 샘플 데이터 (거래처→현장→견적→수주→생산→출하→품질→회계 전 과정 연동)\n// 100개 프로젝트 기준 완전한 데이터셋\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// 1. 거래처 마스터 (10개) - 공통코드관리 규칙 적용: C-###\nconst integratedCustomerMaster = [\n {\n id: 1, code: 'C-001', name: '삼성물산(주)', bizNo: '124-81-00001', ceo: '오세철', bizType: '건설업', bizItem: '종합건설',\n address: '서울시 강남구 테헤란로 128', tel: '02-2145-5000', fax: '02-2145-5001', email: 'contract@samsung.com',\n creditGrade: 'A', creditLimit: 5000000000, currentBalance: 0, manager: '김건설', managerTel: '010-1234-5678',\n paymentTerms: '월말마감익월25일', discountRate: 5, status: '활성', createdAt: '2020-01-15'\n },\n {\n id: 2, code: 'C-002', name: '현대건설(주)', bizNo: '104-81-00002', ceo: '윤영준', bizType: '건설업', bizItem: '종합건설',\n address: '서울시 종로구 율곡로 75', tel: '02-746-1114', fax: '02-746-1115', email: 'contract@hdec.kr',\n creditGrade: 'A', creditLimit: 5000000000, currentBalance: 0, manager: '이현대', managerTel: '010-2345-6789',\n paymentTerms: '월말마감익월25일', discountRate: 5, status: '활성', createdAt: '2020-02-20'\n },\n {\n id: 3, code: 'C-003', name: '대우건설(주)', bizNo: '117-81-00003', ceo: '백정완', bizType: '건설업', bizItem: '종합건설',\n address: '서울시 중구 을지로 170', tel: '02-2129-3114', fax: '02-2129-3115', email: 'order@daewoo.com',\n creditGrade: 'B', creditLimit: 3000000000, currentBalance: 150000000, manager: '박대우', managerTel: '010-3456-7890',\n paymentTerms: '입금후출고', discountRate: 3, status: '활성', createdAt: '2020-03-10'\n },\n {\n id: 4, code: 'C-004', name: 'GS건설(주)', bizNo: '104-81-00004', ceo: '허윤홍', bizType: '건설업', bizItem: '종합건설',\n address: '서울시 강남구 논현로 508', tel: '02-3429-1114', fax: '02-3429-1115', email: 'purchase@gsconst.co.kr',\n creditGrade: 'A', creditLimit: 4000000000, currentBalance: 0, manager: '최지에스', managerTel: '010-4567-8901',\n paymentTerms: '월말마감익월25일', discountRate: 4, status: '활성', createdAt: '2020-04-05'\n },\n {\n id: 5, code: 'C-005', name: '포스코건설(주)', bizNo: '602-81-00005', ceo: '한성희', bizType: '건설업', bizItem: '종합건설',\n address: '인천시 연수구 아카데미로 86', tel: '032-748-0114', fax: '032-748-0115', email: 'order@poscoenc.com',\n creditGrade: 'A', creditLimit: 4000000000, currentBalance: 0, manager: '정포스코', managerTel: '010-5678-9012',\n paymentTerms: '월말마감익월25일', discountRate: 5, status: '활성', createdAt: '2020-05-15'\n },\n {\n id: 6, code: 'C-006', name: '롯데건설(주)', bizNo: '110-81-00006', ceo: '하석주', bizType: '건설업', bizItem: '종합건설',\n address: '서울시 영등포구 국제금융로 2길 15', tel: '02-3479-5114', fax: '02-3479-5115', email: 'contract@lotteenc.co.kr',\n creditGrade: 'B', creditLimit: 3000000000, currentBalance: 80000000, manager: '강롯데', managerTel: '010-6789-0123',\n paymentTerms: '입금후출고', discountRate: 3, status: '활성', createdAt: '2020-06-20'\n },\n {\n id: 7, code: 'C-007', name: '호반건설(주)', bizNo: '134-81-00007', ceo: '김상열', bizType: '건설업', bizItem: '주택건설',\n address: '서울시 서초구 강남대로 465', tel: '02-532-3000', fax: '02-532-3001', email: 'purchase@hoban.co.kr',\n creditGrade: 'B', creditLimit: 2000000000, currentBalance: 45000000, manager: '임호반', managerTel: '010-7890-1234',\n paymentTerms: '입금후출고', discountRate: 2, status: '활성', createdAt: '2020-07-10'\n },\n {\n id: 8, code: 'C-008', name: '한화건설(주)', bizNo: '135-81-00008', ceo: '최광호', bizType: '건설업', bizItem: '종합건설',\n address: '서울시 중구 청계천로 86', tel: '02-729-2700', fax: '02-729-2701', email: 'order@hwconst.co.kr',\n creditGrade: 'A', creditLimit: 3500000000, currentBalance: 0, manager: '윤한화', managerTel: '010-8901-2345',\n paymentTerms: '월말마감익월25일', discountRate: 4, status: '활성', createdAt: '2020-08-05'\n },\n {\n id: 9, code: 'C-009', name: '태영건설(주)', bizNo: '106-81-00009', ceo: '박인규', bizType: '건설업', bizItem: '종합건설',\n address: '서울시 강남구 영동대로 511', tel: '02-2191-7114', fax: '02-2191-7115', email: 'contract@tyconst.co.kr',\n creditGrade: 'C', creditLimit: 1000000000, currentBalance: 200000000, manager: '서태영', managerTel: '010-9012-3456',\n paymentTerms: '경리승인', discountRate: 0, status: '활성', createdAt: '2020-09-15'\n },\n {\n id: 10, code: 'C-010', name: '두산건설(주)', bizNo: '101-81-00010', ceo: '박홍식', bizType: '건설업', bizItem: '종합건설',\n address: '서울시 영등포구 여의대로 24', tel: '02-3398-0114', fax: '02-3398-0115', email: 'purchase@doosanenc.com',\n creditGrade: 'B', creditLimit: 2500000000, currentBalance: 120000000, manager: '한두산', managerTel: '010-0123-4567',\n paymentTerms: '입금후출고', discountRate: 3, status: '활성', createdAt: '2020-10-20'\n },\n];\n\n// 2. 현장 마스터 (50개) - 각 거래처별 5개 현장\nconst integratedSiteMaster = [\n // 삼성물산 현장 (5개)\n { id: 1, code: 'S-001', name: '래미안 원베일리', customerId: 1, customerName: '삼성물산(주)', address: '서울시 서초구 반포동 1-1', manager: '김현장', tel: '010-1111-0001', status: '진행중', startDate: '2024-06-01', endDate: '2026-12-31', totalAmount: 850000000 },\n { id: 2, code: 'S-002', name: '래미안 포레카운티', customerId: 1, customerName: '삼성물산(주)', address: '경기도 수원시 영통구 매탄동', manager: '이현장', tel: '010-1111-0002', status: '진행중', startDate: '2024-08-15', endDate: '2027-02-28', totalAmount: 650000000 },\n { id: 3, code: 'S-003', name: '삼성 서초사옥', customerId: 1, customerName: '삼성물산(주)', address: '서울시 서초구 서초동 1320-10', manager: '박현장', tel: '010-1111-0003', status: '진행중', startDate: '2024-10-01', endDate: '2026-06-30', totalAmount: 420000000 },\n { id: 4, code: 'S-004', name: '래미안 리더스원', customerId: 1, customerName: '삼성물산(주)', address: '서울시 송파구 문정동', manager: '최현장', tel: '010-1111-0004', status: '계약', startDate: '2025-03-01', endDate: '2027-08-31', totalAmount: 780000000 },\n { id: 5, code: 'S-005', name: '삼성 부산사옥', customerId: 1, customerName: '삼성물산(주)', address: '부산시 해운대구 우동', manager: '정현장', tel: '010-1111-0005', status: '계약', startDate: '2025-04-01', endDate: '2026-09-30', totalAmount: 350000000 },\n // 현대건설 현장 (5개)\n { id: 6, code: 'S-006', name: '힐스테이트 광교중앙역', customerId: 2, customerName: '현대건설(주)', address: '경기도 수원시 영통구 이의동', manager: '강현장', tel: '010-2222-0001', status: '진행중', startDate: '2024-05-01', endDate: '2026-10-31', totalAmount: 920000000 },\n { id: 7, code: 'S-007', name: '디에이치 아너힐즈', customerId: 2, customerName: '현대건설(주)', address: '서울시 강동구 상일동', manager: '임현장', tel: '010-2222-0002', status: '진행중', startDate: '2024-07-15', endDate: '2027-01-31', totalAmount: 1100000000 },\n { id: 8, code: 'S-008', name: '현대 판교사옥', customerId: 2, customerName: '현대건설(주)', address: '경기도 성남시 분당구 판교동', manager: '조현장', tel: '010-2222-0003', status: '진행중', startDate: '2024-09-01', endDate: '2026-03-31', totalAmount: 480000000 },\n { id: 9, code: 'S-009', name: '힐스테이트 송도더스카이', customerId: 2, customerName: '현대건설(주)', address: '인천시 연수구 송도동', manager: '윤현장', tel: '010-2222-0004', status: '계약', startDate: '2025-02-01', endDate: '2027-06-30', totalAmount: 880000000 },\n { id: 10, code: 'S-010', name: '힐스테이트 대구역', customerId: 2, customerName: '현대건설(주)', address: '대구시 중구 동인동', manager: '장현장', tel: '010-2222-0005', status: '계약', startDate: '2025-05-01', endDate: '2027-09-30', totalAmount: 720000000 },\n // 대우건설 현장 (5개)\n { id: 11, code: 'S-011', name: '푸르지오 서울숲', customerId: 3, customerName: '대우건설(주)', address: '서울시 성동구 성수동', manager: '배현장', tel: '010-3333-0001', status: '진행중', startDate: '2024-04-01', endDate: '2026-08-31', totalAmount: 680000000 },\n { id: 12, code: 'S-012', name: '푸르지오 시티', customerId: 3, customerName: '대우건설(주)', address: '경기도 안양시 동안구', manager: '나현장', tel: '010-3333-0002', status: '진행중', startDate: '2024-06-15', endDate: '2026-11-30', totalAmount: 520000000 },\n { id: 13, code: 'S-013', name: '대우 마곡사옥', customerId: 3, customerName: '대우건설(주)', address: '서울시 강서구 마곡동', manager: '우현장', tel: '010-3333-0003', status: '진행중', startDate: '2024-08-01', endDate: '2026-01-31', totalAmount: 380000000 },\n { id: 14, code: 'S-014', name: '푸르지오 더샵', customerId: 3, customerName: '대우건설(주)', address: '경기도 화성시 동탄', manager: '공현장', tel: '010-3333-0004', status: '계약', startDate: '2025-01-01', endDate: '2027-04-30', totalAmount: 950000000 },\n { id: 15, code: 'S-015', name: '푸르지오 벨라듀', customerId: 3, customerName: '대우건설(주)', address: '부산시 남구 대연동', manager: '마현장', tel: '010-3333-0005', status: '계약', startDate: '2025-03-01', endDate: '2027-07-31', totalAmount: 620000000 },\n // GS건설 현장 (5개)\n { id: 16, code: 'S-016', name: '자이 분당센트럴', customerId: 4, customerName: 'GS건설(주)', address: '경기도 성남시 분당구 정자동', manager: '하현장', tel: '010-4444-0001', status: '진행중', startDate: '2024-03-01', endDate: '2026-07-31', totalAmount: 1050000000 },\n { id: 17, code: 'S-017', name: '자이 안산그랑시티', customerId: 4, customerName: 'GS건설(주)', address: '경기도 안산시 상록구', manager: '주현장', tel: '010-4444-0002', status: '진행중', startDate: '2024-05-15', endDate: '2026-09-30', totalAmount: 780000000 },\n { id: 18, code: 'S-018', name: 'GS 판교R&D센터', customerId: 4, customerName: 'GS건설(주)', address: '경기도 성남시 분당구 삼평동', manager: '차현장', tel: '010-4444-0003', status: '진행중', startDate: '2024-07-01', endDate: '2025-12-31', totalAmount: 420000000 },\n { id: 19, code: 'S-019', name: '자이 대치에코', customerId: 4, customerName: 'GS건설(주)', address: '서울시 강남구 대치동', manager: '고현장', tel: '010-4444-0004', status: '완료', startDate: '2023-06-01', endDate: '2025-01-31', totalAmount: 890000000 },\n { id: 20, code: 'S-020', name: '자이 광교엘리포레', customerId: 4, customerName: 'GS건설(주)', address: '경기도 수원시 영통구 광교', manager: '노현장', tel: '010-4444-0005', status: '계약', startDate: '2025-04-01', endDate: '2027-08-31', totalAmount: 1200000000 },\n // 포스코건설 현장 (5개)\n { id: 21, code: 'S-021', name: '더샵 송도센트럴시티', customerId: 5, customerName: '포스코건설(주)', address: '인천시 연수구 송도동', manager: '도현장', tel: '010-5555-0001', status: '진행중', startDate: '2024-02-01', endDate: '2026-06-30', totalAmount: 980000000 },\n { id: 22, code: 'S-022', name: '더샵 청라시티', customerId: 5, customerName: '포스코건설(주)', address: '인천시 서구 청라동', manager: '라현장', tel: '010-5555-0002', status: '진행중', startDate: '2024-04-15', endDate: '2026-08-31', totalAmount: 720000000 },\n { id: 23, code: 'S-023', name: '포스코 광양연구소', customerId: 5, customerName: '포스코건설(주)', address: '전남 광양시 금호동', manager: '마현장', tel: '010-5555-0003', status: '진행중', startDate: '2024-06-01', endDate: '2025-11-30', totalAmount: 350000000 },\n { id: 24, code: 'S-024', name: '더샵 광교테크노밸리', customerId: 5, customerName: '포스코건설(주)', address: '경기도 수원시 영통구', manager: '바현장', tel: '010-5555-0004', status: '완료', startDate: '2023-04-01', endDate: '2024-12-31', totalAmount: 650000000 },\n { id: 25, code: 'S-025', name: '더샵 위례신도시', customerId: 5, customerName: '포스코건설(주)', address: '경기도 성남시 위례동', manager: '사현장', tel: '010-5555-0005', status: '계약', startDate: '2025-06-01', endDate: '2027-10-31', totalAmount: 1100000000 },\n // 롯데건설 현장 (5개)\n { id: 26, code: 'S-026', name: '롯데캐슬 골드파크', customerId: 6, customerName: '롯데건설(주)', address: '서울시 강서구 가양동', manager: '아현장', tel: '010-6666-0001', status: '진행중', startDate: '2024-01-15', endDate: '2026-05-31', totalAmount: 850000000 },\n { id: 27, code: 'S-027', name: '롯데캐슬 시그니처', customerId: 6, customerName: '롯데건설(주)', address: '경기도 부천시 중동', manager: '자현장', tel: '010-6666-0002', status: '진행중', startDate: '2024-03-01', endDate: '2026-07-31', totalAmount: 680000000 },\n { id: 28, code: 'S-028', name: '롯데월드타워 증축', customerId: 6, customerName: '롯데건설(주)', address: '서울시 송파구 잠실동', manager: '차현장', tel: '010-6666-0003', status: '진행중', startDate: '2024-05-01', endDate: '2025-10-31', totalAmount: 450000000 },\n { id: 29, code: 'S-029', name: '롯데캐슬 더퍼스트', customerId: 6, customerName: '롯데건설(주)', address: '대전시 서구 둔산동', manager: '카현장', tel: '010-6666-0004', status: '완료', startDate: '2023-03-01', endDate: '2024-11-30', totalAmount: 520000000 },\n { id: 30, code: 'S-030', name: '롯데캐슬 프리미어', customerId: 6, customerName: '롯데건설(주)', address: '경기도 김포시 장기동', manager: '타현장', tel: '010-6666-0005', status: '계약', startDate: '2025-07-01', endDate: '2027-11-30', totalAmount: 920000000 },\n // 호반건설 현장 (5개)\n { id: 31, code: 'S-031', name: '호반써밋 개포', customerId: 7, customerName: '호반건설(주)', address: '서울시 강남구 개포동', manager: '파현장', tel: '010-7777-0001', status: '진행중', startDate: '2024-02-15', endDate: '2026-06-30', totalAmount: 580000000 },\n { id: 32, code: 'S-032', name: '호반베르디움 위례', customerId: 7, customerName: '호반건설(주)', address: '경기도 하남시 위례동', manager: '하현장', tel: '010-7777-0002', status: '진행중', startDate: '2024-04-01', endDate: '2026-08-31', totalAmount: 450000000 },\n { id: 33, code: 'S-033', name: '호반써밋 스카이폴리스', customerId: 7, customerName: '호반건설(주)', address: '경기도 의왕시 포일동', manager: '거현장', tel: '010-7777-0003', status: '진행중', startDate: '2024-06-15', endDate: '2026-10-31', totalAmount: 620000000 },\n { id: 34, code: 'S-034', name: '호반베르디움 산본', customerId: 7, customerName: '호반건설(주)', address: '경기도 군포시 산본동', manager: '너현장', tel: '010-7777-0004', status: '완료', startDate: '2023-05-01', endDate: '2024-10-31', totalAmount: 380000000 },\n { id: 35, code: 'S-035', name: '호반써밋 동탄', customerId: 7, customerName: '호반건설(주)', address: '경기도 화성시 동탄', manager: '더현장', tel: '010-7777-0005', status: '계약', startDate: '2025-08-01', endDate: '2027-12-31', totalAmount: 750000000 },\n // 한화건설 현장 (5개)\n { id: 36, code: 'S-036', name: '포레나 용산', customerId: 8, customerName: '한화건설(주)', address: '서울시 용산구 한남동', manager: '러현장', tel: '010-8888-0001', status: '진행중', startDate: '2024-03-15', endDate: '2026-07-31', totalAmount: 920000000 },\n { id: 37, code: 'S-037', name: '포레나 세종시티', customerId: 8, customerName: '한화건설(주)', address: '세종시 나성동', manager: '머현장', tel: '010-8888-0002', status: '진행중', startDate: '2024-05-01', endDate: '2026-09-30', totalAmount: 780000000 },\n { id: 38, code: 'S-038', name: '한화 대전본사', customerId: 8, customerName: '한화건설(주)', address: '대전시 유성구 덕명동', manager: '버현장', tel: '010-8888-0003', status: '진행중', startDate: '2024-07-01', endDate: '2025-12-31', totalAmount: 380000000 },\n { id: 39, code: 'S-039', name: '포레나 청주', customerId: 8, customerName: '한화건설(주)', address: '충북 청주시 서원구', manager: '서현장', tel: '010-8888-0004', status: '완료', startDate: '2023-07-01', endDate: '2025-01-31', totalAmount: 520000000 },\n { id: 40, code: 'S-040', name: '포레나 해운대', customerId: 8, customerName: '한화건설(주)', address: '부산시 해운대구 우동', manager: '어현장', tel: '010-8888-0005', status: '계약', startDate: '2025-09-01', endDate: '2028-01-31', totalAmount: 1050000000 },\n // 태영건설 현장 (5개)\n { id: 41, code: 'S-041', name: '데시앙 마포', customerId: 9, customerName: '태영건설(주)', address: '서울시 마포구 상암동', manager: '저현장', tel: '010-9999-0001', status: '진행중', startDate: '2024-04-01', endDate: '2026-08-31', totalAmount: 480000000 },\n { id: 42, code: 'S-042', name: '데시앙 부평', customerId: 9, customerName: '태영건설(주)', address: '인천시 부평구 삼산동', manager: '처현장', tel: '010-9999-0002', status: '진행중', startDate: '2024-06-15', endDate: '2026-10-31', totalAmount: 350000000 },\n { id: 43, code: 'S-043', name: '태영 물류센터', customerId: 9, customerName: '태영건설(주)', address: '경기도 이천시 마장면', manager: '커현장', tel: '010-9999-0003', status: '진행중', startDate: '2024-08-01', endDate: '2025-11-30', totalAmount: 280000000 },\n { id: 44, code: 'S-044', name: '데시앙 광명', customerId: 9, customerName: '태영건설(주)', address: '경기도 광명시 철산동', manager: '터현장', tel: '010-9999-0004', status: '완료', startDate: '2023-08-01', endDate: '2024-12-31', totalAmount: 420000000 },\n { id: 45, code: 'S-045', name: '데시앙 천안', customerId: 9, customerName: '태영건설(주)', address: '충남 천안시 서북구', manager: '퍼현장', tel: '010-9999-0005', status: '계약', startDate: '2025-10-01', endDate: '2028-02-28', totalAmount: 680000000 },\n // 두산건설 현장 (5개)\n { id: 46, code: 'S-046', name: '위브더제니스', customerId: 10, customerName: '두산건설(주)', address: '서울시 영등포구 여의도동', manager: '허현장', tel: '010-0000-0001', status: '진행중', startDate: '2024-01-01', endDate: '2026-05-31', totalAmount: 880000000 },\n { id: 47, code: 'S-047', name: '위브센티움', customerId: 10, customerName: '두산건설(주)', address: '경기도 고양시 일산동구', manager: '고현장', tel: '010-0000-0002', status: '진행중', startDate: '2024-03-15', endDate: '2026-07-31', totalAmount: 650000000 },\n { id: 48, code: 'S-048', name: '두산 인천연구소', customerId: 10, customerName: '두산건설(주)', address: '인천시 연수구 송도동', manager: '노현장', tel: '010-0000-0003', status: '진행중', startDate: '2024-05-01', endDate: '2025-09-30', totalAmount: 320000000 },\n { id: 49, code: 'S-049', name: '위브트레보', customerId: 10, customerName: '두산건설(주)', address: '경기도 용인시 수지구', manager: '모현장', tel: '010-0000-0004', status: '완료', startDate: '2023-04-01', endDate: '2024-09-30', totalAmount: 450000000 },\n { id: 50, code: 'S-050', name: '위브프리모', customerId: 10, customerName: '두산건설(주)', address: '대전시 중구 대흥동', manager: '보현장', tel: '010-0000-0005', status: '계약', startDate: '2025-11-01', endDate: '2028-03-31', totalAmount: 780000000 },\n // ======== 추가 현장 (51~100) - 각 거래처별 5개 추가 ========\n // 삼성물산 추가현장\n { id: 51, code: 'S-051', name: '래미안 역삼퍼스트', customerId: 1, customerName: '삼성물산(주)', address: '서울시 강남구 역삼동', manager: '권현장', tel: '010-1111-0006', status: '진행중', startDate: '2024-11-01', endDate: '2027-03-31', totalAmount: 920000000 },\n { id: 52, code: 'S-052', name: '래미안 목동센트럴', customerId: 1, customerName: '삼성물산(주)', address: '서울시 양천구 목동', manager: '금현장', tel: '010-1111-0007', status: '계약', startDate: '2025-02-01', endDate: '2027-06-30', totalAmount: 780000000 },\n { id: 53, code: 'S-053', name: '삼성 강북물류센터', customerId: 1, customerName: '삼성물산(주)', address: '서울시 강북구 미아동', manager: '기현장', tel: '010-1111-0008', status: '진행중', startDate: '2024-09-15', endDate: '2026-01-31', totalAmount: 450000000 },\n { id: 54, code: 'S-054', name: '래미안 일산엘포레', customerId: 1, customerName: '삼성물산(주)', address: '경기도 고양시 일산서구', manager: '남현장', tel: '010-1111-0009', status: '계약', startDate: '2025-05-01', endDate: '2027-09-30', totalAmount: 1100000000 },\n { id: 55, code: 'S-055', name: '래미안 광명센트럴', customerId: 1, customerName: '삼성물산(주)', address: '경기도 광명시 광명동', manager: '단현장', tel: '010-1111-0010', status: '진행중', startDate: '2024-07-01', endDate: '2026-11-30', totalAmount: 680000000 },\n // 현대건설 추가현장\n { id: 56, code: 'S-056', name: '힐스테이트 천안', customerId: 2, customerName: '현대건설(주)', address: '충남 천안시 서북구', manager: '도현장', tel: '010-2222-0006', status: '진행중', startDate: '2024-08-01', endDate: '2026-12-31', totalAmount: 820000000 },\n { id: 57, code: 'S-057', name: '디에이치 레이크시티', customerId: 2, customerName: '현대건설(주)', address: '경기도 김포시 구래동', manager: '라현장', tel: '010-2222-0007', status: '계약', startDate: '2025-03-01', endDate: '2027-07-31', totalAmount: 950000000 },\n { id: 58, code: 'S-058', name: '현대 수원연구소', customerId: 2, customerName: '현대건설(주)', address: '경기도 수원시 팔달구', manager: '마현장', tel: '010-2222-0008', status: '진행중', startDate: '2024-10-15', endDate: '2026-02-28', totalAmount: 380000000 },\n { id: 59, code: 'S-059', name: '힐스테이트 제주', customerId: 2, customerName: '현대건설(주)', address: '제주시 노형동', manager: '바현장', tel: '010-2222-0009', status: '계약', startDate: '2025-06-01', endDate: '2027-10-31', totalAmount: 720000000 },\n { id: 60, code: 'S-060', name: '힐스테이트 평택', customerId: 2, customerName: '현대건설(주)', address: '경기도 평택시 고덕동', manager: '사현장', tel: '010-2222-0010', status: '진행중', startDate: '2024-12-01', endDate: '2027-04-30', totalAmount: 1050000000 },\n // 대우건설 추가현장\n { id: 61, code: 'S-061', name: '푸르지오 인천파크', customerId: 3, customerName: '대우건설(주)', address: '인천시 서구 검단동', manager: '아현장', tel: '010-3333-0006', status: '진행중', startDate: '2024-06-01', endDate: '2026-10-31', totalAmount: 720000000 },\n { id: 62, code: 'S-062', name: '푸르지오 대전유성', customerId: 3, customerName: '대우건설(주)', address: '대전시 유성구 봉명동', manager: '자현장', tel: '010-3333-0007', status: '계약', startDate: '2025-04-01', endDate: '2027-08-31', totalAmount: 580000000 },\n { id: 63, code: 'S-063', name: '대우 부산물류', customerId: 3, customerName: '대우건설(주)', address: '부산시 강서구 녹산동', manager: '차현장', tel: '010-3333-0008', status: '진행중', startDate: '2024-09-01', endDate: '2026-01-31', totalAmount: 420000000 },\n { id: 64, code: 'S-064', name: '푸르지오 세종시티', customerId: 3, customerName: '대우건설(주)', address: '세종시 어진동', manager: '카현장', tel: '010-3333-0009', status: '계약', startDate: '2025-07-01', endDate: '2027-11-30', totalAmount: 890000000 },\n { id: 65, code: 'S-065', name: '푸르지오 수원역', customerId: 3, customerName: '대우건설(주)', address: '경기도 수원시 팔달구', manager: '타현장', tel: '010-3333-0010', status: '진행중', startDate: '2024-11-15', endDate: '2027-03-31', totalAmount: 750000000 },\n // GS건설 추가현장\n { id: 66, code: 'S-066', name: '자이 용산파크', customerId: 4, customerName: 'GS건설(주)', address: '서울시 용산구 용산동', manager: '파현장', tel: '010-4444-0006', status: '진행중', startDate: '2024-04-01', endDate: '2026-08-31', totalAmount: 1150000000 },\n { id: 67, code: 'S-067', name: '자이 청라호수', customerId: 4, customerName: 'GS건설(주)', address: '인천시 서구 청라동', manager: '하현장', tel: '010-4444-0007', status: '계약', startDate: '2025-08-01', endDate: '2027-12-31', totalAmount: 920000000 },\n { id: 68, code: 'S-068', name: 'GS 창원공장', customerId: 4, customerName: 'GS건설(주)', address: '경남 창원시 성산구', manager: '거현장', tel: '010-4444-0008', status: '진행중', startDate: '2024-08-15', endDate: '2025-12-31', totalAmount: 520000000 },\n { id: 69, code: 'S-069', name: '자이 울산파크', customerId: 4, customerName: 'GS건설(주)', address: '울산시 남구 삼산동', manager: '너현장', tel: '010-4444-0009', status: '계약', startDate: '2025-10-01', endDate: '2028-02-28', totalAmount: 780000000 },\n { id: 70, code: 'S-070', name: '자이 인천루원시티', customerId: 4, customerName: 'GS건설(주)', address: '인천시 서구 가좌동', manager: '더현장', tel: '010-4444-0010', status: '진행중', startDate: '2024-10-01', endDate: '2027-02-28', totalAmount: 980000000 },\n // 포스코건설 추가현장\n { id: 71, code: 'S-071', name: '더샵 광명역', customerId: 5, customerName: '포스코건설(주)', address: '경기도 광명시 일직동', manager: '러현장', tel: '010-5555-0006', status: '진행중', startDate: '2024-05-01', endDate: '2026-09-30', totalAmount: 880000000 },\n { id: 72, code: 'S-072', name: '더샵 부산센텀', customerId: 5, customerName: '포스코건설(주)', address: '부산시 해운대구 우동', manager: '머현장', tel: '010-5555-0007', status: '계약', startDate: '2025-09-01', endDate: '2028-01-31', totalAmount: 1020000000 },\n { id: 73, code: 'S-073', name: '포스코 울산연구소', customerId: 5, customerName: '포스코건설(주)', address: '울산시 남구 매암동', manager: '버현장', tel: '010-5555-0008', status: '진행중', startDate: '2024-07-01', endDate: '2025-11-30', totalAmount: 380000000 },\n { id: 74, code: 'S-074', name: '더샵 서울숲', customerId: 5, customerName: '포스코건설(주)', address: '서울시 성동구 성수동', manager: '서현장', tel: '010-5555-0009', status: '계약', startDate: '2025-11-01', endDate: '2028-03-31', totalAmount: 1250000000 },\n { id: 75, code: 'S-075', name: '더샵 안양역', customerId: 5, customerName: '포스코건설(주)', address: '경기도 안양시 만안구', manager: '어현장', tel: '010-5555-0010', status: '진행중', startDate: '2024-09-01', endDate: '2027-01-31', totalAmount: 720000000 },\n // 롯데건설 추가현장\n { id: 76, code: 'S-076', name: '롯데캐슬 평택역', customerId: 6, customerName: '롯데건설(주)', address: '경기도 평택시 평택동', manager: '저현장', tel: '010-6666-0006', status: '진행중', startDate: '2024-03-01', endDate: '2026-07-31', totalAmount: 680000000 },\n { id: 77, code: 'S-077', name: '롯데캐슬 광주', customerId: 6, customerName: '롯데건설(주)', address: '광주시 서구 치평동', manager: '처현장', tel: '010-6666-0007', status: '계약', startDate: '2025-05-01', endDate: '2027-09-30', totalAmount: 920000000 },\n { id: 78, code: 'S-078', name: '롯데 목포센터', customerId: 6, customerName: '롯데건설(주)', address: '전남 목포시 하당동', manager: '커현장', tel: '010-6666-0008', status: '진행중', startDate: '2024-06-15', endDate: '2025-10-31', totalAmount: 350000000 },\n { id: 79, code: 'S-079', name: '롯데캐슬 원주', customerId: 6, customerName: '롯데건설(주)', address: '강원도 원주시 무실동', manager: '터현장', tel: '010-6666-0009', status: '계약', startDate: '2025-08-01', endDate: '2027-12-31', totalAmount: 580000000 },\n { id: 80, code: 'S-080', name: '롯데캐슬 동탄', customerId: 6, customerName: '롯데건설(주)', address: '경기도 화성시 동탄', manager: '퍼현장', tel: '010-6666-0010', status: '진행중', startDate: '2024-11-01', endDate: '2027-03-31', totalAmount: 1050000000 },\n // 호반건설 추가현장\n { id: 81, code: 'S-081', name: '호반써밋 부천', customerId: 7, customerName: '호반건설(주)', address: '경기도 부천시 원미구', manager: '허현장', tel: '010-7777-0006', status: '진행중', startDate: '2024-05-15', endDate: '2026-09-30', totalAmount: 520000000 },\n { id: 82, code: 'S-082', name: '호반베르디움 수원', customerId: 7, customerName: '호반건설(주)', address: '경기도 수원시 영통구', manager: '고현장', tel: '010-7777-0007', status: '계약', startDate: '2025-06-01', endDate: '2027-10-31', totalAmount: 680000000 },\n { id: 83, code: 'S-083', name: '호반 안성물류', customerId: 7, customerName: '호반건설(주)', address: '경기도 안성시 공도읍', manager: '노현장', tel: '010-7777-0008', status: '진행중', startDate: '2024-08-01', endDate: '2025-12-31', totalAmount: 320000000 },\n { id: 84, code: 'S-084', name: '호반써밋 김포', customerId: 7, customerName: '호반건설(주)', address: '경기도 김포시 장기동', manager: '모현장', tel: '010-7777-0009', status: '계약', startDate: '2025-09-01', endDate: '2028-01-31', totalAmount: 820000000 },\n { id: 85, code: 'S-085', name: '호반베르디움 용인', customerId: 7, customerName: '호반건설(주)', address: '경기도 용인시 기흥구', manager: '보현장', tel: '010-7777-0010', status: '진행중', startDate: '2024-10-15', endDate: '2027-02-28', totalAmount: 650000000 },\n // 한화건설 추가현장\n { id: 86, code: 'S-086', name: '포레나 수원', customerId: 8, customerName: '한화건설(주)', address: '경기도 수원시 장안구', manager: '소현장', tel: '010-8888-0006', status: '진행중', startDate: '2024-04-01', endDate: '2026-08-31', totalAmount: 720000000 },\n { id: 87, code: 'S-087', name: '포레나 전주', customerId: 8, customerName: '한화건설(주)', address: '전북 전주시 덕진구', manager: '오현장', tel: '010-8888-0007', status: '계약', startDate: '2025-07-01', endDate: '2027-11-30', totalAmount: 580000000 },\n { id: 88, code: 'S-088', name: '한화 여수공장', customerId: 8, customerName: '한화건설(주)', address: '전남 여수시 중흥동', manager: '조현장', tel: '010-8888-0008', status: '진행중', startDate: '2024-09-01', endDate: '2026-01-31', totalAmount: 450000000 },\n { id: 89, code: 'S-089', name: '포레나 김해', customerId: 8, customerName: '한화건설(주)', address: '경남 김해시 내동', manager: '우현장', tel: '010-8888-0009', status: '계약', startDate: '2025-10-01', endDate: '2028-02-28', totalAmount: 680000000 },\n { id: 90, code: 'S-090', name: '포레나 인천검단', customerId: 8, customerName: '한화건설(주)', address: '인천시 서구 검단동', manager: '주현장', tel: '010-8888-0010', status: '진행중', startDate: '2024-12-01', endDate: '2027-04-30', totalAmount: 950000000 },\n // 태영건설 추가현장\n { id: 91, code: 'S-091', name: '데시앙 분당', customerId: 9, customerName: '태영건설(주)', address: '경기도 성남시 분당구', manager: '추현장', tel: '010-9999-0006', status: '진행중', startDate: '2024-06-01', endDate: '2026-10-31', totalAmount: 620000000 },\n { id: 92, code: 'S-092', name: '데시앙 안산', customerId: 9, customerName: '태영건설(주)', address: '경기도 안산시 단원구', manager: '국현장', tel: '010-9999-0007', status: '계약', startDate: '2025-08-01', endDate: '2027-12-31', totalAmount: 480000000 },\n { id: 93, code: 'S-093', name: '태영 시흥센터', customerId: 9, customerName: '태영건설(주)', address: '경기도 시흥시 정왕동', manager: '백현장', tel: '010-9999-0008', status: '진행중', startDate: '2024-10-01', endDate: '2026-02-28', totalAmount: 380000000 },\n { id: 94, code: 'S-094', name: '데시앙 대구', customerId: 9, customerName: '태영건설(주)', address: '대구시 달서구 상인동', manager: '남현장', tel: '010-9999-0009', status: '계약', startDate: '2025-11-01', endDate: '2028-03-31', totalAmount: 720000000 },\n { id: 95, code: 'S-095', name: '데시앙 파주', customerId: 9, customerName: '태영건설(주)', address: '경기도 파주시 금촌동', manager: '유현장', tel: '010-9999-0010', status: '진행중', startDate: '2024-07-15', endDate: '2026-11-30', totalAmount: 550000000 },\n // 두산건설 추가현장\n { id: 96, code: 'S-096', name: '위브더비스타', customerId: 10, customerName: '두산건설(주)', address: '서울시 마포구 합정동', manager: '손현장', tel: '010-0000-0006', status: '진행중', startDate: '2024-02-01', endDate: '2026-06-30', totalAmount: 780000000 },\n { id: 97, code: 'S-097', name: '위브프라임', customerId: 10, customerName: '두산건설(주)', address: '경기도 의정부시 녹양동', manager: '양현장', tel: '010-0000-0007', status: '계약', startDate: '2025-04-01', endDate: '2027-08-31', totalAmount: 620000000 },\n { id: 98, code: 'S-098', name: '두산 구미공장', customerId: 10, customerName: '두산건설(주)', address: '경북 구미시 공단동', manager: '홍현장', tel: '010-0000-0008', status: '진행중', startDate: '2024-08-01', endDate: '2025-12-31', totalAmount: 420000000 },\n { id: 99, code: 'S-099', name: '위브엘리트', customerId: 10, customerName: '두산건설(주)', address: '경기도 안양시 동안구', manager: '강현장', tel: '010-0000-0009', status: '계약', startDate: '2025-12-01', endDate: '2028-04-30', totalAmount: 850000000 },\n { id: 100, code: 'S-100', name: '위브더시티', customerId: 10, customerName: '두산건설(주)', address: '경기도 부천시 상동', manager: '신현장', tel: '010-0000-0010', status: '진행중', startDate: '2024-11-01', endDate: '2027-03-31', totalAmount: 920000000 },\n];\n\n// 3. 견적 마스터 (100개) - 사전 계산된 견적 데이터\nconst integratedQuoteMaster = (() => {\n const quotes = [];\n const sizeOptions = [\n { W0: 2000, H0: 2500 }, { W0: 2500, H0: 2800 }, { W0: 3000, H0: 3000 }, { W0: 3500, H0: 3200 }, { W0: 4000, H0: 3500 },\n { W0: 4500, H0: 3000 }, { W0: 5000, H0: 3500 }, { W0: 5500, H0: 4000 }, { W0: 6000, H0: 4000 }, { W0: 6500, H0: 4500 },\n { W0: 7000, H0: 4000 }, { W0: 7500, H0: 4500 }, { W0: 8000, H0: 5000 }, { W0: 2200, H0: 2600 }, { W0: 2800, H0: 2900 },\n ];\n const productTypes = ['스크린', '철재'];\n const voltages = ['220', '380'];\n const wireTypes = ['유선', '무선'];\n const ctTypes = ['매립', '노출'];\n const gtTypes = ['벽면형', '코너형'];\n\n // ★ 통합 유틸리티 연동 견적 계산 함수 (mesIntegrationUtils.calculateBOM 활용)\n const calculateQuoteWithBOM = (W0, H0, qty, productType, options = {}) => {\n // 통합 BOM 계산 유틸리티 사용\n const bomResult = calculateBOM({\n openWidth: W0,\n openHeight: H0,\n productType: productType === '스크린' ? '스크린' : '철재',\n shaftInch: options.shaftInch || '5',\n installType: options.installType || '벽면형',\n motorPower: options.motorPower || '380',\n wireType: options.wireType || '유선',\n controllerType: options.controllerType || '매립',\n qty,\n });\n\n // BOM 결과를 견적 형식으로 변환\n const totalAmount = bomResult.summary.totalAmount;\n const unitPrice = Math.round(totalAmount / qty);\n\n return {\n area: bomResult.variables.M.toFixed(2),\n weight: bomResult.variables.K.toFixed(1),\n motorCapacity: `${bomResult.variables.motorCapacity}KG`,\n shaftInch: `${bomResult.variables.shaftInch}인치`,\n unitPrice,\n totalAmount,\n variables: bomResult.variables, // BOM 계산 변수들 포함\n items: bomResult.items.map(item => ({\n itemCode: item.itemCode,\n itemName: item.itemName,\n spec: item.spec,\n qty: item.qty,\n unit: item.unit,\n unitPrice: item.unitPrice || 0,\n amount: item.qty * (item.unitPrice || 0),\n category: item.category,\n })),\n };\n };\n\n // 레거시 호환용 간단 계산 함수 (기존 로직 유지)\n const calculateQuote = (W0, H0, qty, productType) => {\n const area = (W0 * H0) / 1000000; // 면적 (㎡)\n const weight = area * 25; // 대략적인 중량\n const basePrice = productType === '스크린' ? 850000 : 750000; // 기본 단가/㎡\n const unitPrice = Math.round(area * basePrice);\n const totalAmount = unitPrice * qty;\n\n // 모터 용량 결정\n const motorCapacity = weight > 150 ? '500KG' : weight > 100 ? '400KG' : weight > 60 ? '300KG' : '200KG';\n const shaftInch = weight > 100 ? '5인치' : weight > 60 ? '4인치' : '3인치';\n\n return {\n area: area.toFixed(2),\n weight: weight.toFixed(1),\n motorCapacity,\n shaftInch,\n unitPrice,\n totalAmount,\n items: [\n { itemCode: 'SCR-SCREEN', itemName: '스크린', qty, unit: 'EA', unitPrice: Math.round(area * 350000), amount: Math.round(area * 350000 * qty) },\n { itemCode: 'STL-GUIDE', itemName: '가이드레일', qty: qty * 2, unit: 'EA', unitPrice: Math.round(H0 * 15), amount: Math.round(H0 * 15 * qty * 2) },\n { itemCode: 'STL-CASE', itemName: '케이스', qty, unit: 'EA', unitPrice: Math.round(W0 * 20), amount: Math.round(W0 * 20 * qty) },\n { itemCode: 'MTR-MOTOR', itemName: '튜블러모터', qty, unit: 'EA', unitPrice: 180000, amount: 180000 * qty },\n { itemCode: 'CTL-CTRL', itemName: '제어기', qty, unit: 'EA', unitPrice: 85000, amount: 85000 * qty },\n { itemCode: 'STL-SHAFT', itemName: '샤프트', qty, unit: 'EA', unitPrice: Math.round(W0 * 12), amount: Math.round(W0 * 12 * qty) },\n { itemCode: 'STL-BRACKET', itemName: '브라켓', qty: qty * 2, unit: 'EA', unitPrice: 25000, amount: 25000 * qty * 2 },\n { itemCode: 'INSP-FEE', itemName: '검사비', qty, unit: 'SET', unitPrice: 50000, amount: 50000 * qty },\n ]\n };\n };\n\n for (let i = 0; i < 100; i++) {\n const siteId = i + 1;\n const site = integratedSiteMaster.find(s => s.id === siteId);\n const customer = integratedCustomerMaster.find(c => c.id === site.customerId);\n const sizeOpt = sizeOptions[i % sizeOptions.length];\n const qty = (i % 5) + 1; // 1~5개\n const productType = productTypes[i % 2];\n\n // 견적 계산\n const calcResult = calculateQuote(sizeOpt.W0, sizeOpt.H0, qty, productType);\n const discountRate = customer.discountRate || 0;\n const discountAmount = Math.round(calcResult.totalAmount * discountRate / 100);\n const finalAmount = calcResult.totalAmount - discountAmount;\n\n // 상태 결정 (다양한 상태 분포)\n const statusList = ['견적중', '견적중', '제출완료', '제출완료', '수주전환', '수주전환', '수주전환', '취소', '만료'];\n const status = statusList[i % statusList.length];\n\n const quoteDate = new Date(2025, 0, 1 + i); // 2025년 1월부터\n // 견적번호 생성 (KD-PR-YYMMDD-## 형식) - 채번관리 규칙 적용\n const qYY = String(quoteDate.getFullYear()).slice(-2);\n const qMM = String(quoteDate.getMonth() + 1).padStart(2, '0');\n const qDD = String(quoteDate.getDate()).padStart(2, '0');\n const qSeq = String((i % 99) + 1).padStart(2, '0');\n\n quotes.push({\n id: i + 1,\n quoteNo: `KD-PR-${qYY}${qMM}${qDD}-${qSeq}`,\n quoteDate: quoteDate.toISOString().split('T')[0],\n validUntil: new Date(quoteDate.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],\n status,\n customerId: customer.id,\n customerCode: customer.code,\n customerName: customer.name,\n creditGrade: customer.creditGrade,\n siteId: site.id,\n siteCode: site.code,\n siteName: site.name,\n siteAddress: site.address,\n siteManager: site.manager,\n siteTel: site.tel,\n // 제품 정보\n productType,\n width: sizeOpt.W0,\n height: sizeOpt.H0,\n productionSizeW: sizeOpt.W0 + 140,\n productionSizeH: sizeOpt.H0 + 350,\n area: parseFloat(calcResult.area),\n weight: parseFloat(calcResult.weight),\n motorCapacity: calcResult.motorCapacity,\n shaftInch: calcResult.shaftInch,\n guideType: gtTypes[i % 2],\n voltage: voltages[i % 2],\n wireType: wireTypes[i % 2],\n controllerType: ctTypes[i % 2],\n qty,\n // 금액 정보\n supplyAmount: calcResult.totalAmount,\n discountRate,\n discountAmount,\n finalAmount,\n vatAmount: Math.round(finalAmount * 0.1),\n totalWithVat: Math.round(finalAmount * 1.1),\n // 품목 상세\n items: calcResult.items.map((item, idx) => ({\n ...item,\n lineNo: idx + 1,\n quoteId: i + 1,\n })),\n // 메타 정보\n createdBy: '판매팀',\n createdAt: quoteDate.toISOString(),\n updatedAt: quoteDate.toISOString(),\n note: `${site.name} 방화셔터 견적건`,\n });\n }\n\n // ========== 절곡 공정 E2E 테스트용 견적 2건 추가 ==========\n // 테스트 1: 강남 타워 절곡 테스트 (수주전환 상태)\n const testQuote1Date = new Date(2025, 11, 15); // 2025-12-15\n quotes.push({\n id: 101,\n quoteNo: 'KD-PR-251215-T1',\n quoteDate: testQuote1Date.toISOString().split('T')[0],\n validUntil: new Date(testQuote1Date.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],\n status: '수주전환',\n customerId: 1,\n customerCode: 'CUS-001',\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteId: 1,\n siteCode: 'PJ-251215-01',\n siteName: '[E2E테스트1] 강남 타워 절곡현장',\n siteAddress: '서울시 강남구 역삼동 123-45',\n siteManager: '김현장',\n siteTel: '02-1234-5678',\n productType: '절곡',\n width: 3500,\n height: 4000,\n productionSizeW: 3640,\n productionSizeH: 4350,\n area: 14.0,\n weight: 350.0,\n motorCapacity: '500KG',\n shaftInch: '5인치',\n guideType: '벽면형',\n voltage: '380',\n wireType: '유선',\n controllerType: '매립',\n qty: 2,\n supplyAmount: 8500000,\n discountRate: 5,\n discountAmount: 425000,\n finalAmount: 8075000,\n vatAmount: 807500,\n totalWithVat: 8882500,\n items: [\n { lineNo: 1, itemCode: 'FLD-GUIDE', itemName: '가이드레일(절곡)', qty: 4, unit: 'EA', unitPrice: 180000, amount: 720000, quoteId: 101 },\n { lineNo: 2, itemCode: 'FLD-CASE', itemName: '케이스(절곡)', qty: 2, unit: 'EA', unitPrice: 250000, amount: 500000, quoteId: 101 },\n { lineNo: 3, itemCode: 'FLD-SLAT', itemName: '슬랫조립체(절곡)', qty: 2, unit: 'SET', unitPrice: 1500000, amount: 3000000, quoteId: 101 },\n { lineNo: 4, itemCode: 'FLD-BOTTOM', itemName: '바텀바(절곡)', qty: 2, unit: 'EA', unitPrice: 120000, amount: 240000, quoteId: 101 },\n { lineNo: 5, itemCode: 'MTR-MOTOR', itemName: '튜블러모터', qty: 2, unit: 'EA', unitPrice: 280000, amount: 560000, quoteId: 101 },\n { lineNo: 6, itemCode: 'CTL-CTRL', itemName: '제어기', qty: 2, unit: 'EA', unitPrice: 120000, amount: 240000, quoteId: 101 },\n { lineNo: 7, itemCode: 'FLD-BRACKET', itemName: '브라켓(절곡)', qty: 4, unit: 'EA', unitPrice: 45000, amount: 180000, quoteId: 101 },\n { lineNo: 8, itemCode: 'INSP-FEE', itemName: '검사비', qty: 2, unit: 'SET', unitPrice: 80000, amount: 160000, quoteId: 101 },\n ],\n // 절곡 전용 전개도 데이터\n developedParts: [\n { itemCode: 'SD30', itemName: '엘바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3500, qty: 4, weight: 1.05, dimensions: '75', note: '테스트1' },\n { itemCode: 'SD31', itemName: '하장바', material: 'E.G.I 1.6T', totalWidth: 520, length: 3500, qty: 2, weight: 1.35, dimensions: '67→126→165→178→193', note: '' },\n { itemCode: 'SD32', itemName: '바텀커버', material: 'E.G.I 1.0T', totalWidth: 350, length: 3500, qty: 2, weight: 0.42, dimensions: '50→15→215→15→50', note: '' },\n { itemCode: 'SD33', itemName: '가이드레일', material: 'E.G.I 2.0T', totalWidth: 187.5, length: 4000, qty: 4, weight: 1.18, dimensions: '40→80→42.5→25', note: '' },\n { itemCode: 'SD34', itemName: '행거브라켓', material: 'E.G.I 2.3T', totalWidth: 300, length: 350, qty: 4, weight: 0.65, dimensions: '100→50→100→50', note: '' },\n { itemCode: 'SD35', itemName: '케이스상판', material: 'E.G.I 1.0T', totalWidth: 450, length: 3500, qty: 2, weight: 0.58, dimensions: '30→390→30', note: '' },\n { itemCode: 'SD36', itemName: '케이스측판', material: 'E.G.I 1.0T', totalWidth: 450, length: 350, qty: 4, weight: 0.12, dimensions: '30→390→30', note: '' },\n { itemCode: 'SD37', itemName: '케이스전면', material: 'E.G.I 0.8T', totalWidth: 380, length: 3500, qty: 2, weight: 0.41, dimensions: '20→340→20', note: '' },\n { itemCode: 'SD38', itemName: '중간가이드', material: 'E.G.I 1.6T', totalWidth: 200, length: 500, qty: 2, weight: 0.13, dimensions: '50→100→50', note: '' },\n ],\n createdBy: '테스트팀',\n createdAt: testQuote1Date.toISOString(),\n updatedAt: testQuote1Date.toISOString(),\n note: '[E2E테스트1] 강남 타워 절곡 테스트 - 견적→수주→생산지시→작업지시 연동 테스트',\n });\n\n // 테스트 2: 판교 물류센터 절곡 테스트 (수주전환 상태)\n const testQuote2Date = new Date(2025, 11, 16); // 2025-12-16\n quotes.push({\n id: 102,\n quoteNo: 'KD-PR-251216-T2',\n quoteDate: testQuote2Date.toISOString().split('T')[0],\n validUntil: new Date(testQuote2Date.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],\n status: '수주전환',\n customerId: 2,\n customerCode: 'CUS-002',\n customerName: '현대건설(주)',\n creditGrade: 'A',\n siteId: 2,\n siteCode: 'PJ-251216-01',\n siteName: '[E2E테스트2] 판교 물류센터 절곡현장',\n siteAddress: '경기도 성남시 분당구 판교동 789-12',\n siteManager: '이현장',\n siteTel: '031-987-6543',\n productType: '절곡',\n width: 4000,\n height: 4500,\n productionSizeW: 4140,\n productionSizeH: 4850,\n area: 18.0,\n weight: 450.0,\n motorCapacity: '500KG',\n shaftInch: '5인치',\n guideType: '코너형',\n voltage: '380',\n wireType: '무선',\n controllerType: '노출',\n qty: 3,\n supplyAmount: 12500000,\n discountRate: 3,\n discountAmount: 375000,\n finalAmount: 12125000,\n vatAmount: 1212500,\n totalWithVat: 13337500,\n items: [\n { lineNo: 1, itemCode: 'FLD-GUIDE', itemName: '가이드레일(절곡)', qty: 6, unit: 'EA', unitPrice: 200000, amount: 1200000, quoteId: 102 },\n { lineNo: 2, itemCode: 'FLD-CASE', itemName: '케이스(절곡)', qty: 3, unit: 'EA', unitPrice: 280000, amount: 840000, quoteId: 102 },\n { lineNo: 3, itemCode: 'FLD-SLAT', itemName: '슬랫조립체(절곡)', qty: 3, unit: 'SET', unitPrice: 1800000, amount: 5400000, quoteId: 102 },\n { lineNo: 4, itemCode: 'FLD-BOTTOM', itemName: '바텀바(절곡)', qty: 3, unit: 'EA', unitPrice: 140000, amount: 420000, quoteId: 102 },\n { lineNo: 5, itemCode: 'MTR-MOTOR', itemName: '튜블러모터', qty: 3, unit: 'EA', unitPrice: 320000, amount: 960000, quoteId: 102 },\n { lineNo: 6, itemCode: 'CTL-CTRL', itemName: '제어기', qty: 3, unit: 'EA', unitPrice: 140000, amount: 420000, quoteId: 102 },\n { lineNo: 7, itemCode: 'FLD-BRACKET', itemName: '브라켓(절곡)', qty: 6, unit: 'EA', unitPrice: 50000, amount: 300000, quoteId: 102 },\n { lineNo: 8, itemCode: 'INSP-FEE', itemName: '검사비', qty: 3, unit: 'SET', unitPrice: 90000, amount: 270000, quoteId: 102 },\n ],\n // 절곡 전용 전개도 데이터\n developedParts: [\n { itemCode: 'SD40', itemName: '엘바', material: 'E.G.I 2.0T', totalWidth: 550, length: 4000, qty: 6, weight: 1.38, dimensions: '85', note: '테스트2' },\n { itemCode: 'SD41', itemName: '하장바', material: 'E.G.I 2.0T', totalWidth: 580, length: 4000, qty: 3, weight: 1.82, dimensions: '72→135→175→188→210', note: '' },\n { itemCode: 'SD42', itemName: '바텀커버', material: 'E.G.I 1.2T', totalWidth: 400, length: 4000, qty: 3, weight: 0.58, dimensions: '55→20→250→20→55', note: '' },\n { itemCode: 'SD43', itemName: '가이드레일', material: 'E.G.I 2.3T', totalWidth: 210, length: 4500, qty: 6, weight: 1.72, dimensions: '45→90→47.5→27.5', note: '' },\n { itemCode: 'SD44', itemName: '행거브라켓', material: 'E.G.I 2.3T', totalWidth: 350, length: 400, qty: 6, weight: 0.92, dimensions: '120→55→120→55', note: '' },\n { itemCode: 'SD45', itemName: '케이스상판', material: 'E.G.I 1.2T', totalWidth: 500, length: 4000, qty: 3, weight: 0.74, dimensions: '35→430→35', note: '' },\n { itemCode: 'SD46', itemName: '케이스측판', material: 'E.G.I 1.2T', totalWidth: 500, length: 400, qty: 6, weight: 0.18, dimensions: '35→430→35', note: '' },\n { itemCode: 'SD47', itemName: '케이스전면', material: 'E.G.I 1.0T', totalWidth: 420, length: 4000, qty: 3, weight: 0.55, dimensions: '25→370→25', note: '' },\n { itemCode: 'SD48', itemName: '중간가이드', material: 'E.G.I 2.0T', totalWidth: 230, length: 550, qty: 3, weight: 0.19, dimensions: '55→120→55', note: '' },\n { itemCode: 'SD49', itemName: '보강재', material: 'E.G.I 1.6T', totalWidth: 180, length: 600, qty: 3, weight: 0.14, dimensions: '40→100→40', note: '' },\n ],\n createdBy: '테스트팀',\n createdAt: testQuote2Date.toISOString(),\n updatedAt: testQuote2Date.toISOString(),\n note: '[E2E테스트2] 판교 물류센터 절곡 테스트 - 견적→수주→생산지시→작업지시 연동 테스트',\n });\n // ========== 절곡 공정 E2E 테스트용 견적 끝 ==========\n\n return quotes;\n})();\n\n// 4. 수주 마스터 - 견적에서 수주전환된 건 (35건)\nconst integratedOrderMaster = (() => {\n const orders = [];\n const convertedQuotes = integratedQuoteMaster.filter(q => q.status === '수주전환');\n\n convertedQuotes.forEach((quote, idx) => {\n const orderDate = new Date(quote.quoteDate);\n orderDate.setDate(orderDate.getDate() + 5); // 견적일 + 5일 = 수주일\n const dueDate = new Date(orderDate);\n dueDate.setDate(dueDate.getDate() + 45); // 수주일 + 45일 = 납기일\n\n // 상태 결정 (E2E 테스트 데이터는 생산중으로 고정)\n // 수주 상태는 생산지시완료까지만 표시 (출하는 출하관리에서 처리)\n const statusList = ['수주확정', '수주확정', '생산지시', '생산중', '생산완료', '생산지시완료'];\n const isTestData = quote.id === 101 || quote.id === 102;\n const status = isTestData ? '생산중' : statusList[idx % statusList.length];\n\n // 수주번호 생성 (채번관리 규칙 적용)\n // 스크린: KD-TS, 슬랫: KD-SL, 절곡: KD-BD\n const oYY = String(orderDate.getFullYear()).slice(-2);\n const oMM = String(orderDate.getMonth() + 1).padStart(2, '0');\n const oDD = String(orderDate.getDate()).padStart(2, '0');\n const oSeq = String((idx % 99) + 1).padStart(2, '0');\n const orderPrefix = quote.productType === '스크린' ? 'KD-TS' : 'KD-BD'; // 스크린/철재(절곡)\n\n // 절곡 공정 관련 제품 여부 확인\n const isFoldProduct = ['절곡', '방화셔터', '방연셔터', '방화문', '철재'].includes(quote.productType) ||\n orderPrefix === 'KD-BD';\n\n // 견적에 developedParts가 있으면 그대로 사용, 없으면 샘플 데이터 생성\n const developedParts = quote.developedParts ? quote.developedParts : (isFoldProduct ? [\n { itemCode: 'SD30', itemName: '엘바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 4, weight: 0.9, dimensions: '75', note: '' },\n { itemCode: 'SD31', itemName: '하장바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 1.158, dimensions: '67→126→165→178→193', note: '' },\n { itemCode: 'SD32', itemName: '짜부가스켓', material: 'E.G.I 0.8T', totalWidth: 500, length: 3000, qty: 4, weight: 0.576, dimensions: '48', note: '80*4,50*8' },\n { itemCode: 'SD33', itemName: '가이드레일', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 2.304, dimensions: '40→80→42.5→25', note: '' },\n { itemCode: 'SD34', itemName: '바텀바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.44, dimensions: '30→95→30', note: '' },\n { itemCode: 'SD35', itemName: '바텀커버', material: 'E.G.I 1.2T', totalWidth: 500, length: 3000, qty: 1, weight: 0.864, dimensions: '65→50→16.5→25', note: '' },\n { itemCode: 'SD36', itemName: '전면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.92, dimensions: '25→210→140→30→50→28', note: '' },\n { itemCode: 'SD37', itemName: '후면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.728, dimensions: '25→165→90→155', note: '' },\n { itemCode: 'SD38', itemName: '측면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 0.96, dimensions: '35→50→35', note: '' },\n { itemCode: 'SD39', itemName: '상부덮개', material: 'E.G.I 1.2T', totalWidth: 500, length: 3000, qty: 1, weight: 0.648, dimensions: '25→140→330→25', note: '' },\n ] : null);\n\n orders.push({\n id: idx + 1,\n orderNo: `${orderPrefix}-${oYY}${oMM}${oDD}-${oSeq}`,\n orderDate: orderDate.toISOString().split('T')[0],\n status,\n // 견적 연결\n quoteId: quote.id,\n quoteNo: quote.quoteNo,\n // 거래처/현장\n customerId: quote.customerId,\n customerCode: quote.customerCode,\n customerName: quote.customerName,\n creditGrade: quote.creditGrade,\n siteId: quote.siteId,\n siteCode: quote.siteCode,\n siteName: quote.siteName,\n siteAddress: quote.siteAddress,\n siteManager: quote.siteManager,\n siteTel: quote.siteTel,\n // 제품 정보\n productType: quote.productType,\n openSizeW: quote.openSizeW,\n openSizeH: quote.openSizeH,\n productionSizeW: quote.productionSizeW,\n productionSizeH: quote.productionSizeH,\n area: quote.area,\n weight: quote.weight,\n motorCapacity: quote.motorCapacity,\n shaftInch: quote.shaftInch,\n guideType: quote.guideType,\n voltage: quote.voltage,\n wireType: quote.wireType,\n controllerType: quote.controllerType,\n qty: quote.qty,\n // 금액\n orderAmount: quote.finalAmount,\n vatAmount: quote.vatAmount,\n totalAmount: quote.totalWithVat,\n // 납기\n dueDate: dueDate.toISOString().split('T')[0],\n deliveryAddress: quote.siteAddress,\n // 품목\n items: quote.items,\n // 절곡 공정 전개도 데이터 (있는 경우만)\n ...(developedParts && { developedParts }),\n // 메타\n createdBy: '판매팀',\n createdAt: orderDate.toISOString(),\n approvedBy: '판매팀장',\n approvedAt: orderDate.toISOString(),\n note: quote.note,\n });\n });\n return orders;\n})();\n\n// 5. 생산지시 마스터 - 수주에서 생산지시 생성 (중간 단계)\n// BOM 품목 → 공정 매핑 테이블 (processMasterConfig.sampleProcesses와 동일한 코드 형식 사용)\n// SCR-001: 스크린 생산, SLT-001: 슬랫 생산, FLD-001: 절곡 생산, STK-001: 재고 생산(포밍)\nconst itemProcessMapping = {\n 'SCR-SCREEN': { processCode: 'SCR-001', processName: '스크린 생산', workflowCode: 'SCREEN', processSeq: 1 },\n 'SLT-SLAT': { processCode: 'SLT-001', processName: '슬랫 생산', workflowCode: 'SLAT', processSeq: 2 },\n 'STL-GUIDE': { processCode: 'FLD-001', processName: '절곡 생산', workflowCode: 'FOLD', processSeq: 3 },\n 'STL-CASE': { processCode: 'FLD-001', processName: '절곡 생산', workflowCode: 'FOLD', processSeq: 3 },\n 'STL-BRACKET': { processCode: 'FLD-001', processName: '절곡 생산', workflowCode: 'FOLD', processSeq: 3 },\n 'STL-SHAFT': { processCode: 'STK-001', processName: '재고 생산', workflowCode: 'STOCK', processSeq: 4 },\n 'MTR-MOTOR': { processCode: null, processName: '구매품', workflowCode: null, processSeq: 99, isPurchased: true },\n 'CTL-CTRL': { processCode: null, processName: '구매품', workflowCode: null, processSeq: 99, isPurchased: true },\n 'INSP-FEE': { processCode: 'QC-001', processName: '중간검사', workflowCode: null, processSeq: 5, isService: true },\n};\n\nconst integratedProductionOrderMaster = (() => {\n const productionOrders = [];\n // 생산중 이상 상태의 수주에서 생산지시 생성\n const eligibleOrders = integratedOrderMaster.filter(o =>\n ['생산중', '생산완료', '출하중', '출하완료', '완료'].includes(o.status)\n );\n\n eligibleOrders.forEach((order, idx) => {\n const poDate = new Date(order.orderDate);\n poDate.setDate(poDate.getDate() + 2); // 수주일 + 2일 = 생산지시일\n\n // 상태 결정\n let status = '생산완료';\n if (order.status === '생산중') status = '생산중';\n if (order.status === '수주확정') status = '생산대기';\n\n // BOM 품목별 공정 분류 (구매품/서비스 제외, workflowCode 포함)\n const bomItems = order.items || [];\n const processGroups = {};\n\n bomItems.forEach(item => {\n const mapping = itemProcessMapping[item.itemCode];\n // 구매품(isPurchased), 서비스(isService), 검사(중간검사) 제외\n if (mapping && mapping.processCode && !mapping.isPurchased && !mapping.isService) {\n const key = mapping.processCode;\n if (!processGroups[key]) {\n processGroups[key] = {\n processCode: mapping.processCode,\n processName: mapping.processName,\n workflowCode: mapping.workflowCode, // 공정 워크플로우 코드 추가\n processSeq: mapping.processSeq,\n items: []\n };\n }\n processGroups[key].items.push(item);\n }\n });\n\n // 절곡 공정 여부 확인 (processGroups에 FOLD가 있거나 productType이 절곡 관련인 경우)\n const hasFoldProcess = Object.values(processGroups).some(pg =>\n pg.processName === 'FOLD' || pg.processName === '절곡'\n ) || ['절곡', '방화셔터', '방연셔터', '방화문'].includes(order.productType);\n\n // 수주에 developedParts가 있으면 그대로 사용, 없으면 샘플 데이터 생성\n const developedParts = order.developedParts ? order.developedParts : (hasFoldProcess ? [\n { itemCode: 'SD30', itemName: '엘바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 4, weight: 0.9, dimensions: '75', note: '' },\n { itemCode: 'SD31', itemName: '하장바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 1.158, dimensions: '67→126→165→178→193', note: '' },\n { itemCode: 'SD32', itemName: '짜부가스켓', material: 'E.G.I 0.8T', totalWidth: 500, length: 3000, qty: 4, weight: 0.576, dimensions: '48', note: '80*4,50*8' },\n { itemCode: 'SD33', itemName: '가이드레일', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 2.304, dimensions: '40→80→42.5→25', note: '' },\n { itemCode: 'SD34', itemName: '바텀바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.44, dimensions: '30→95→30', note: '' },\n { itemCode: 'SD35', itemName: '바텀커버', material: 'E.G.I 1.2T', totalWidth: 500, length: 3000, qty: 1, weight: 0.864, dimensions: '65→50→16.5→25', note: '' },\n { itemCode: 'SD36', itemName: '전면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.92, dimensions: '25→210→140→30→50→28', note: '' },\n { itemCode: 'SD37', itemName: '후면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.728, dimensions: '25→165→90→155', note: '' },\n { itemCode: 'SD38', itemName: '측면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 0.96, dimensions: '35→50→35', note: '' },\n { itemCode: 'SD39', itemName: '상부덮개', material: 'E.G.I 1.2T', totalWidth: 500, length: 3000, qty: 1, weight: 0.648, dimensions: '25→140→330→25', note: '' },\n ] : null);\n\n productionOrders.push({\n id: idx + 1,\n productionOrderNo: `PO-${order.orderNo.replace('SO-', '')}`,\n productionOrderDate: poDate.toISOString().split('T')[0],\n status,\n // 수주 연결\n orderId: order.id,\n orderNo: order.orderNo,\n quoteNo: order.quoteNo,\n // 거래처/현장\n customerId: order.customerId,\n customerName: order.customerName,\n siteId: order.siteId,\n siteName: order.siteName,\n // 제품 정보\n productType: order.productType,\n qty: order.qty,\n // 납기\n dueDate: order.dueDate,\n // 공정 그룹 (BOM 기반)\n processGroups: Object.values(processGroups).sort((a, b) => a.processSeq - b.processSeq),\n // 진행 현황\n totalProcesses: Object.keys(processGroups).length,\n completedProcesses: status === '생산완료' ? Object.keys(processGroups).length :\n status === '생산중' ? Math.floor(Object.keys(processGroups).length / 2) : 0,\n // 작업지시서 생성 여부\n workOrdersGenerated: true, // 기존 데이터는 이미 생성됨\n // 절곡 공정 전개도 데이터 (있는 경우만)\n ...(developedParts && { developedParts }),\n // 메타\n createdBy: '생산관리',\n createdAt: poDate.toISOString(),\n approvedBy: status !== '생산대기' ? '생산팀장' : null,\n approvedAt: status !== '생산대기' ? poDate.toISOString() : null,\n note: `${order.siteName} 생산지시`,\n });\n });\n return productionOrders;\n})();\n\n// 6. 작업지시 마스터 - 생산지시에서 공정별 자동 생성\n// ★ 통합 유틸리티(mesIntegrationUtils) 연동: processSteps, processAssignees, initializeStepStatus 활용\nconst integratedWorkOrderMaster = (() => {\n const workOrders = [];\n const productionOrders = integratedOrderMaster.filter(o =>\n ['생산중', '생산완료', '출하중', '출하완료', '완료'].includes(o.status)\n );\n\n // ★ 통합 유틸리티의 공정 정의 사용 (processSteps 연동)\n const availableProcesses = Object.keys(processSteps); // ['스크린', '슬랫', '절곡', '재고생산', '전기']\n const processes = ['스크린', '절곡', '슬랫', '조립']; // 실제 사용할 공정\n\n productionOrders.forEach((order, orderIdx) => {\n processes.forEach((process, procIdx) => {\n const woDate = new Date(order.orderDate);\n woDate.setDate(woDate.getDate() + 3 + procIdx * 2);\n\n // 상태 결정\n let status = '완료';\n if (order.status === '생산중' && procIdx >= 2) status = '진행중';\n if (order.status === '생산중' && procIdx >= 3) status = '대기';\n\n // ★ 채번관리 연동: 공정별 문서유형 결정\n // WO-SCR(스크린), WO-SLT(슬랫), WO-FLD(절곡), WO(일반)\n const woDocType = process === '스크린' ? 'WO-SCR'\n : process === '슬랫' ? 'WO-SLT'\n : process === '절곡' ? 'WO-FLD'\n : 'WO';\n\n // 작업지시(생산LOT) 번호 생성 (KD-PL-YYMMDD-## 형식) - 채번관리 규칙 적용\n const wYY = String(woDate.getFullYear()).slice(-2);\n const wMM = String(woDate.getMonth() + 1).padStart(2, '0');\n const wDD = String(woDate.getDate()).padStart(2, '0');\n const wSeq = String((orderIdx * 4 + procIdx + 1) % 99 || 1).padStart(2, '0');\n\n // 절곡 공정인 경우 수주에서 developedParts 연동\n const isFoldProcess = process === '절곡';\n const developedParts = isFoldProcess && order.developedParts ? order.developedParts : null;\n\n // ★ 공정관리 연동: 작업단계 상태 초기화 (initializeStepStatus 활용)\n const steps = processSteps[process] || processSteps['스크린'] || [];\n const stepStatus = {};\n steps.forEach((step, idx) => {\n if (status === '완료') {\n stepStatus[step] = { status: '완료', startTime: woDate.toISOString(), endTime: woDate.toISOString() };\n } else if (status === '진행중' && idx < Math.ceil(steps.length / 2)) {\n stepStatus[step] = { status: '완료', startTime: woDate.toISOString(), endTime: woDate.toISOString() };\n } else if (status === '진행중' && idx === Math.ceil(steps.length / 2)) {\n stepStatus[step] = { status: '진행중', startTime: woDate.toISOString(), endTime: null };\n } else {\n stepStatus[step] = { status: '대기', startTime: null, endTime: null };\n }\n });\n\n // ★ 공정관리 연동: 담당자 배정 (processAssignees 활용)\n const assigneeInfo = processAssignees[process] || processAssignees['스크린'] || { team: '생산팀', members: ['작업자'] };\n\n workOrders.push({\n id: orderIdx * 4 + procIdx + 1,\n workOrderNo: `${woDocType}-${wYY}${wMM}${wDD}-${wSeq}`,\n workOrderDate: woDate.toISOString().split('T')[0],\n status,\n processType: process, // 공정 유형 추가\n // 수주 연결\n orderId: order.id,\n orderNo: order.orderNo,\n // 거래처/현장 정보 추가\n customerName: order.customerName,\n siteName: order.siteName,\n // 공정 정보\n processCode: `P-00${procIdx + 1}`,\n processName: process,\n processSeq: procIdx + 1,\n // 제품 정보\n productType: order.productType,\n productionSizeW: order.productionSizeW,\n productionSizeH: order.productionSizeH,\n qty: order.qty,\n totalQty: order.qty,\n completedQty: status === '완료' ? order.qty : (status === '진행중' ? Math.floor(order.qty / 2) : 0),\n defectQty: 0,\n // LOT 정보\n lotNo: `${order.orderNo}-${String(procIdx + 1).padStart(2, '0')}`,\n // 작업 계획\n plannedStartDate: woDate.toISOString().split('T')[0],\n plannedEndDate: new Date(woDate.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],\n plannedQty: order.qty,\n // 작업 실적\n actualStartDate: status !== '대기' ? woDate.toISOString().split('T')[0] : null,\n actualEndDate: status === '완료' ? new Date(woDate.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] : null,\n actualQty: status === '완료' ? order.qty : (status === '진행중' ? Math.floor(order.qty / 2) : 0),\n // ★ 공정관리 연동: 작업단계 상태\n stepStatus,\n currentStep: status === '완료' ? steps[steps.length - 1] : (status === '진행중' ? steps[Math.ceil(steps.length / 2)] : steps[0]),\n // ★ 공정관리 연동: 담당자\n assignee: assigneeInfo.team,\n assignees: assigneeInfo.members,\n worker: assigneeInfo.members[0] || `${process}작업자`,\n workCenter: `${process}라인`,\n // 절곡 공정 전개도 데이터 (있는 경우만)\n ...(developedParts && { developedParts }),\n // 메타\n createdBy: '생산관리',\n createdAt: woDate.toISOString(),\n note: `${order.siteName} ${process} 공정`,\n });\n });\n });\n return workOrders;\n})();\n\n// 6. 품질검사 데이터 - 작업지시 완료건에 대해\nconst integratedQualityMaster = (() => {\n const inspections = [];\n const completedWOs = integratedWorkOrderMaster.filter(wo => wo.status === '완료');\n\n completedWOs.forEach((wo, idx) => {\n const inspDate = new Date(wo.actualEndDate);\n inspDate.setDate(inspDate.getDate() + 1);\n\n // 결과 (대부분 합격, 일부 불합격)\n const result = idx % 10 === 0 ? '불합격' : '합격';\n\n // 검사LOT 번호 생성 (채번관리 규칙 적용)\n // 중간검사: KD-WE, 제품검사: KD-SA\n const iYY = String(inspDate.getFullYear()).slice(-2);\n const iMM = String(inspDate.getMonth() + 1).padStart(2, '0');\n const iDD = String(inspDate.getDate()).padStart(2, '0');\n const iSeq = String((idx % 99) + 1).padStart(2, '0');\n const inspPrefix = wo.processSeq === 4 ? 'KD-SA' : 'KD-WE';\n\n inspections.push({\n id: idx + 1,\n inspectionNo: `${inspPrefix}-${iYY}${iMM}${iDD}-${iSeq}`,\n inspectionDate: inspDate.toISOString().split('T')[0],\n inspectionType: wo.processSeq === 4 ? '제품검사' : '중간검사',\n status: result === '합격' ? '완료' : '재작업',\n result,\n // 작업지시 연결\n workOrderId: wo.id,\n workOrderNo: wo.workOrderNo,\n orderId: wo.orderId,\n orderNo: wo.orderNo,\n // 공정 정보\n processName: wo.processName,\n // 검사 정보\n inspectedQty: wo.actualQty,\n passedQty: result === '합격' ? wo.actualQty : 0,\n failedQty: result === '불합격' ? wo.actualQty : 0,\n // 검사 항목\n checkItems: [\n { item: '외관검사', spec: '흠집/찍힘 없음', result: result === '합격' ? 'OK' : 'NG', note: '' },\n { item: '치수검사', spec: '±5mm', result: 'OK', note: '' },\n { item: '작동검사', spec: '정상 작동', result: 'OK', note: '' },\n ],\n // 메타\n inspector: '품질검사원',\n createdAt: inspDate.toISOString(),\n note: result === '불합격' ? '외관 불량으로 재작업 필요' : '',\n });\n });\n return inspections;\n})();\n\n// 7. 출하 데이터 - 생산완료 이상 상태\n// ★ 채번관리 연동: SL(출하지시서) 번호 규칙 적용\nconst integratedShipmentMaster = (() => {\n const shipments = [];\n const shippableOrders = integratedOrderMaster.filter(o =>\n ['출하중', '출하완료', '완료'].includes(o.status)\n );\n\n shippableOrders.forEach((order, idx) => {\n const shipDate = new Date(order.dueDate);\n shipDate.setDate(shipDate.getDate() - 3);\n\n const status = order.status === '완료' ? '배송완료' : (order.status === '출하완료' ? '배송중' : '출하대기');\n\n // ★ 채번관리 연동: 출하번호 생성 (SL-YYMMDD-## 형식)\n const sYY = String(shipDate.getFullYear()).slice(-2);\n const sMM = String(shipDate.getMonth() + 1).padStart(2, '0');\n const sDD = String(shipDate.getDate()).padStart(2, '0');\n const sSeq = String((idx % 99) + 1).padStart(2, '0');\n\n // 연결된 작업지시 조회\n const relatedWorkOrders = integratedWorkOrderMaster.filter(wo => wo.orderId === order.id);\n const relatedWorkOrderNo = relatedWorkOrders.length > 0 ? relatedWorkOrders[0].workOrderNo : null;\n\n // 연결된 검사 조회\n const relatedInspections = integratedQualityMaster.filter(insp =>\n relatedWorkOrders.some(wo => wo.id === insp.workOrderId)\n );\n const relatedInspectionNo = relatedInspections.length > 0 ? relatedInspections[0].inspectionNo : null;\n\n shipments.push({\n id: idx + 1,\n shipmentNo: `SL-${sYY}${sMM}${sDD}-${sSeq}`,\n shipmentDate: shipDate.toISOString().split('T')[0],\n status,\n // 수주 연결\n orderId: order.id,\n orderNo: order.orderNo,\n // ★ 작업지시/검사 연결 (워크플로우 추적용)\n workOrderId: relatedWorkOrders[0]?.id || null,\n workOrderNo: relatedWorkOrderNo,\n inspectionId: relatedInspections[0]?.id || null,\n inspectionNo: relatedInspectionNo,\n // 거래처/현장\n customerId: order.customerId,\n customerName: order.customerName,\n siteId: order.siteId,\n siteName: order.siteName,\n deliveryAddress: order.deliveryAddress,\n siteManager: order.siteManager,\n siteTel: order.siteTel,\n // 제품 정보\n productName: order.productType ? `${order.productType} 셔터` : '방화셔터',\n productSpec: `${order.width || order.productionSizeW}×${order.height || order.productionSizeH}`,\n lotNo: relatedWorkOrders[0]?.lotNo || `${order.orderNo}-01`,\n // 출하 정보\n shipmentQty: order.qty,\n shippedQty: order.qty,\n // 배송 정보\n deliveryType: '자사배송',\n carrier: '자사배송',\n driverName: `배송기사${idx + 1}`,\n driverTel: `010-5000-${String(idx + 1).padStart(4, '0')}`,\n vehicleNo: `12가${1234 + idx}`,\n // 배송 시간\n departureTime: status !== '출하대기' ? `${shipDate.toISOString().split('T')[0]} 08:00` : null,\n arrivalTime: status === '배송완료' ? `${shipDate.toISOString().split('T')[0]} 14:00` : null,\n // 메타\n createdBy: '물류팀',\n createdAt: shipDate.toISOString(),\n confirmedBy: status === '배송완료' ? order.siteManager : null,\n confirmedAt: status === '배송완료' ? `${shipDate.toISOString().split('T')[0]} 14:30` : null,\n note: '',\n });\n });\n return shipments;\n})();\n\n// 8. 매출/세금계산서 데이터 - 출하완료건\nconst integratedSalesMaster = (() => {\n const sales = [];\n const completedShipments = integratedShipmentMaster.filter(s => s.status === '배송완료');\n\n completedShipments.forEach((shipment, idx) => {\n const order = integratedOrderMaster.find(o => o.id === shipment.orderId);\n const customer = integratedCustomerMaster.find(c => c.id === order.customerId);\n const salesDate = new Date(shipment.shipmentDate);\n salesDate.setDate(salesDate.getDate() + 1);\n\n sales.push({\n id: idx + 1,\n salesNo: `SL-${order.orderNo.replace('SO-', '')}`,\n salesDate: salesDate.toISOString().split('T')[0],\n salesType: '제품매출',\n status: idx % 3 === 0 ? '수금완료' : '미수금',\n // 수주/출하 연결\n orderId: order.id,\n orderNo: order.orderNo,\n shipmentId: shipment.id,\n shipmentNo: shipment.shipmentNo,\n // 거래처\n customerId: customer.id,\n customerCode: customer.code,\n customerName: customer.name,\n bizNo: customer.bizNo,\n // 금액\n supplyAmount: order.orderAmount,\n vatAmount: order.vatAmount,\n totalAmount: order.totalAmount,\n // 세금계산서\n invoiceNo: `INV-${salesDate.getFullYear()}${String(salesDate.getMonth() + 1).padStart(2, '0')}${String(idx + 1).padStart(4, '0')}`,\n invoiceDate: salesDate.toISOString().split('T')[0],\n invoiceStatus: '발행완료',\n // 수금 정보\n paidAmount: idx % 3 === 0 ? order.totalAmount : 0,\n unpaidAmount: idx % 3 === 0 ? 0 : order.totalAmount,\n dueDate: new Date(salesDate.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],\n // 메타\n createdBy: '경리팀',\n createdAt: salesDate.toISOString(),\n note: '',\n });\n });\n return sales;\n})();\n\n// 9. 수금 데이터 - 수금완료건\nconst integratedCollectionMaster = (() => {\n const collections = [];\n const paidSales = integratedSalesMaster.filter(s => s.status === '수금완료');\n\n paidSales.forEach((sale, idx) => {\n const collectDate = new Date(sale.salesDate);\n collectDate.setDate(collectDate.getDate() + 25);\n\n collections.push({\n id: idx + 1,\n collectionNo: `CL-${sale.salesNo.replace('SL-', '')}`,\n collectionDate: collectDate.toISOString().split('T')[0],\n collectionType: idx % 2 === 0 ? '계좌이체' : '어음',\n status: '완료',\n // 매출 연결\n salesId: sale.id,\n salesNo: sale.salesNo,\n invoiceNo: sale.invoiceNo,\n // 거래처\n customerId: sale.customerId,\n customerName: sale.customerName,\n // 금액\n amount: sale.totalAmount,\n // 입금 정보\n bankName: idx % 2 === 0 ? '우리은행' : '어음',\n accountNo: idx % 2 === 0 ? '1005-XXX-XXXXXX' : null,\n billNo: idx % 2 === 0 ? null : `BILL-${collectDate.getFullYear()}${String(idx + 1).padStart(4, '0')}`,\n billDueDate: idx % 2 === 0 ? null : new Date(collectDate.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],\n // 메타\n createdBy: '경리팀',\n createdAt: collectDate.toISOString(),\n note: '',\n });\n });\n return collections;\n})();\n\n// 통합 데이터 요약 정보\nconst integratedDataSummary = {\n customers: integratedCustomerMaster.length,\n sites: integratedSiteMaster.length,\n quotes: integratedQuoteMaster.length,\n orders: integratedOrderMaster.length,\n workOrders: integratedWorkOrderMaster.length,\n qualityInspections: integratedQualityMaster.length,\n shipments: integratedShipmentMaster.length,\n sales: integratedSalesMaster.length,\n collections: integratedCollectionMaster.length,\n totalQuoteAmount: integratedQuoteMaster.reduce((sum, q) => sum + q.finalAmount, 0),\n totalOrderAmount: integratedOrderMaster.reduce((sum, o) => sum + o.totalAmount, 0),\n totalSalesAmount: integratedSalesMaster.reduce((sum, s) => sum + s.totalAmount, 0),\n totalCollectedAmount: integratedCollectionMaster.reduce((sum, c) => sum + c.amount, 0),\n};\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// ★ 통합 워크플로우 데이터 생성 (mesIntegrationUtils 완전 연동)\n// 견적 → 수주 → 작업지시 → 검사 → 출하 전체 흐름 자동 생성\n// ═══════════════════════════════════════════════════════════════════════════════\n\n/**\n * 통합 워크플로우 테스트 데이터 생성\n * - 채번관리 규칙 적용 (documentTemplateConfig)\n * - 견적수식 → BOM 자동계산 (calculateBOM)\n * - 공정관리 연동 (processSteps, processAssignees)\n *\n * @param {number} count - 생성할 워크플로우 수\n * @returns {object} 생성된 통합 테스트 데이터\n */\nconst generateIntegratedWorkflowData = (count = 10) => {\n const workflows = [];\n\n // 테스트용 고객/현장 정보\n const testCustomers = [\n { customerName: '삼성물산(주)', siteName: '[통합테스트] 래미안 강남 프레스티지', siteCode: 'S-INT-001' },\n { customerName: '현대건설(주)', siteName: '[통합테스트] 힐스테이트 판교', siteCode: 'S-INT-002' },\n { customerName: '대우건설(주)', siteName: '[통합테스트] 푸르지오 송도', siteCode: 'S-INT-003' },\n { customerName: 'GS건설(주)', siteName: '[통합테스트] 자이 위례', siteCode: 'S-INT-004' },\n { customerName: '포스코건설(주)', siteName: '[통합테스트] 더샵 마린시티', siteCode: 'S-INT-005' },\n { customerName: '롯데건설(주)', siteName: '[통합테스트] 롯데캐슬 동탄', siteCode: 'S-INT-006' },\n { customerName: '호반건설(주)', siteName: '[통합테스트] 써밋 광교', siteCode: 'S-INT-007' },\n { customerName: '한화건설(주)', siteName: '[통합테스트] 포레나 수지', siteCode: 'S-INT-008' },\n { customerName: '태영건설(주)', siteName: '[통합테스트] 데시앙 동탄', siteCode: 'S-INT-009' },\n { customerName: '두산건설(주)', siteName: '[통합테스트] 위브 청라', siteCode: 'S-INT-010' },\n ];\n\n // 테스트용 제품 규격\n const testSpecs = [\n { openWidth: 3000, openHeight: 3000, productType: '스크린', qty: 2 },\n { openWidth: 3500, openHeight: 3500, productType: '스크린', qty: 3 },\n { openWidth: 4000, openHeight: 4000, productType: '스크린', qty: 1 },\n { openWidth: 4500, openHeight: 3000, productType: '철재', qty: 2 },\n { openWidth: 5000, openHeight: 3500, productType: '철재', qty: 4 },\n { openWidth: 2500, openHeight: 2800, productType: '스크린', qty: 5 },\n { openWidth: 3200, openHeight: 3200, productType: '스크린', qty: 2 },\n { openWidth: 3800, openHeight: 4200, productType: '철재', qty: 3 },\n { openWidth: 6000, openHeight: 4000, productType: '스크린', qty: 1 },\n { openWidth: 5500, openHeight: 3800, productType: '철재', qty: 2 },\n ];\n\n for (let i = 0; i < count; i++) {\n const customer = testCustomers[i % testCustomers.length];\n const spec = testSpecs[i % testSpecs.length];\n\n // ★ simulateFullWorkflow 호출 - 전체 워크플로우 자동 생성\n const workflow = simulateFullWorkflow(\n {\n openWidth: spec.openWidth,\n openHeight: spec.openHeight,\n productType: spec.productType,\n shaftInch: spec.openWidth > 4000 ? '6' : '5',\n installType: '벽면형',\n motorPower: '380',\n wireType: i % 2 === 0 ? '유선' : '무선',\n controllerType: i % 2 === 0 ? '매립' : '노출',\n qty: spec.qty,\n },\n {\n customerName: customer.customerName,\n siteName: customer.siteName,\n siteCode: customer.siteCode,\n }\n );\n\n workflows.push({\n id: i + 1,\n ...workflow,\n // 메타정보 추가\n createdAt: new Date().toISOString(),\n testScenario: `통합테스트 #${i + 1}: ${customer.customerName} - ${spec.productType} ${spec.openWidth}×${spec.openHeight}`,\n });\n }\n\n // 요약 정보 생성\n const summary = {\n totalWorkflows: workflows.length,\n totalQuotes: workflows.length,\n totalOrders: workflows.length,\n totalWorkOrders: workflows.reduce((sum, w) => sum + (w.workOrders?.length || 0), 0),\n totalInspections: workflows.filter(w => w.inspection).length,\n totalShipments: workflows.filter(w => w.shipment).length,\n totalBomItems: workflows.reduce((sum, w) => sum + (w.bom?.items?.length || 0), 0),\n totalAmount: workflows.reduce((sum, w) => sum + (w.bom?.summary?.totalAmount || 0), 0),\n processDistribution: {\n '스크린': workflows.filter(w => w.workOrders?.some(wo => wo.processType === '스크린')).length,\n '슬랫': workflows.filter(w => w.workOrders?.some(wo => wo.processType === '슬랫')).length,\n '절곡': workflows.filter(w => w.workOrders?.some(wo => wo.processType === '절곡')).length,\n },\n };\n\n console.log('=== 통합 워크플로우 테스트 데이터 생성 완료 ===');\n console.log('생성된 워크플로우:', summary.totalWorkflows, '건');\n console.log('총 작업지시:', summary.totalWorkOrders, '건');\n console.log('총 BOM 품목:', summary.totalBomItems, '건');\n console.log('총 금액:', new Intl.NumberFormat('ko-KR').format(summary.totalAmount), '원');\n\n return {\n workflows,\n summary,\n };\n};\n\n// 테스트 데이터 생성 (개발용)\n// const integratedWorkflowTestData = generateIntegratedWorkflowData(10);\n\nconsole.log('=== 통합 샘플 데이터 로드 완료 ===');\nconsole.log('거래처:', integratedDataSummary.customers, '개');\nconsole.log('현장:', integratedDataSummary.sites, '개');\nconsole.log('견적:', integratedDataSummary.quotes, '건');\nconsole.log('수주:', integratedDataSummary.orders, '건');\nconsole.log('작업지시:', integratedDataSummary.workOrders, '건');\nconsole.log('품질검사:', integratedDataSummary.qualityInspections, '건');\nconsole.log('출하:', integratedDataSummary.shipments, '건');\nconsole.log('매출:', integratedDataSummary.sales, '건');\nconsole.log('수금:', integratedDataSummary.collections, '건');\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// 통합 테스트 대시보드 - 전체 프로세스 데이터 흐름 확인\n// ═══════════════════════════════════════════════════════════════════════════════\nconst IntegratedTestDashboard = () => {\n const [activeTab, setActiveTab] = useState('process-integration');\n const [selectedCustomer, setSelectedCustomer] = useState(null);\n const [selectedQuote, setSelectedQuote] = useState(null);\n\n // 금액 포맷\n const formatCurrency = (amount) => {\n return new Intl.NumberFormat('ko-KR').format(amount) + '원';\n };\n\n // 상태별 색상\n const getStatusColor = (status) => {\n const colors = {\n '견적중': 'bg-blue-100 text-blue-800',\n '제출완료': 'bg-purple-100 text-purple-800',\n '수주전환': 'bg-green-100 text-green-800',\n '수주확정': 'bg-green-200 text-green-900',\n '생산중': 'bg-yellow-100 text-yellow-800',\n '생산완료': 'bg-orange-100 text-orange-800',\n '출하중': 'bg-indigo-100 text-indigo-800',\n '출하완료': 'bg-teal-100 text-teal-800',\n '완료': 'bg-gray-200 text-gray-800',\n '합격': 'bg-green-100 text-green-800',\n '불합격': 'bg-red-100 text-red-800',\n '배송완료': 'bg-green-100 text-green-800',\n '배송중': 'bg-blue-100 text-blue-800',\n '수금완료': 'bg-green-100 text-green-800',\n '미수금': 'bg-red-100 text-red-800',\n };\n return colors[status] || 'bg-gray-100 text-gray-800';\n };\n\n // 탭 정의\n const tabs = [\n { id: 'process-integration', label: '프로세스별 통합 테스트', icon: Layers },\n { id: 'complete-integration', label: '완전 통합 테스트 (10건)', icon: Database },\n { id: 'full-integration', label: '전체 통합 테스트 (5건)', icon: Zap },\n { id: 'workflow', label: '워크플로우 테스트', icon: PlayCircle },\n { id: 'e2e-test', label: 'E2E 50건 테스트', icon: Activity },\n { id: 'summary', label: '요약', icon: BarChart3 },\n { id: 'customers', label: '거래처', icon: Building },\n { id: 'quotes', label: '견적', icon: FileText },\n { id: 'orders', label: '수주', icon: Package },\n { id: 'production', label: '생산', icon: Factory },\n { id: 'quality', label: '품질', icon: ShieldCheck },\n { id: 'shipment', label: '출하', icon: Truck },\n { id: 'accounting', label: '회계', icon: DollarSign },\n { id: 'flow', label: '전체흐름', icon: GitBranch },\n ];\n\n // ═══════════════════════════════════════════════════════════════════════════\n // 전체 통합 테스트 탭 (5건 실데이터) - 기준정보 연동 검증\n // ═══════════════════════════════════════════════════════════════════════════\n const FullIntegrationTestTab = () => {\n const [testResults, setTestResults] = useState(null);\n const [isRunning, setIsRunning] = useState(false);\n const [selectedTest, setSelectedTest] = useState(null);\n\n // 테스트 실행\n const handleRunTests = () => {\n setIsRunning(true);\n setSelectedTest(null);\n\n // 약간의 딜레이 후 테스트 실행 (UI 반응을 위해)\n setTimeout(() => {\n try {\n const results = runAllIntegrationTests();\n setTestResults(results);\n } catch (error) {\n console.error('테스트 실행 오류:', error);\n setTestResults({ error: error.message });\n }\n setIsRunning(false);\n }, 500);\n };\n\n // 마스터 데이터 요약\n const masterData = getAllMasterData();\n\n return (\n
\n {/* 헤더 */}\n
\n
🔥 MES 전체 시스템 통합 테스트
\n
\n 기준정보(공정/채번/코드/수식/양식) → 거래처 → 현장 → 품목 → 견적 → 수주 → 생산 → 검사 → 출하 전체 연동 검증\n
\n
\n\n {/* 마스터 데이터 현황 */}\n
\n
\n \n 실데이터 마스터 현황\n
\n
\n
\n
{masterData.customers.length}
\n
거래처
\n
\n
\n
{masterData.sites.length}
\n
현장
\n
\n
\n
{masterData.products.length}
\n
제품
\n
\n
\n
\n {masterData.parts.purchased.length + masterData.parts.assembly.length + masterData.parts.bending.length}\n
\n
부품
\n
\n
\n
{masterData.rawMaterials.length}
\n
원자재
\n
\n
\n
{masterData.consumables.length}
\n
소모품
\n
\n
\n
\n\n {/* 테스트 실행 버튼 */}\n
\n
\n
\n\n {/* 테스트 결과 */}\n {testResults && !testResults.error && (\n
\n {/* 결과 요약 */}\n
\n
\n \n 테스트 결과 요약\n
\n
\n
\n
{testResults.summary.totalTests}
\n
총 테스트
\n
\n
\n
{testResults.summary.successCount}
\n
성공
\n
\n
\n
{testResults.summary.failCount}
\n
실패
\n
\n
\n\n {/* 개별 테스트 결과 목록 */}\n
\n {testResults.results.map((result, idx) => (\n
setSelectedTest(selectedTest === idx ? null : idx)}\n className={`p-4 rounded-lg border cursor-pointer transition-all ${result.success\n ? 'bg-green-50 border-green-200 hover:bg-green-100'\n : 'bg-red-50 border-red-200 hover:bg-red-100'\n } ${selectedTest === idx ? 'ring-2 ring-blue-500' : ''}`}\n >\n
\n
\n \n {result.success ? '✅' : '❌'}\n \n {result.testName}\n
\n
\n
\n\n {/* 상세 정보 (펼쳐졌을 때) */}\n {selectedTest === idx && result.summary && (\n
\n
\n
\n
견적번호
\n
{result.summary.quoteNo}
\n
\n
\n
수주번호
\n
{result.summary.orderNo}
\n
\n
\n
작업지시
\n
{result.summary.workOrderCount}건
\n
\n
\n
검사
\n
{result.summary.inspectionCount}건
\n
\n
\n
출하번호
\n
{result.summary.shipmentNo}
\n
\n
\n
매출번호
\n
{result.summary.salesNo}
\n
\n
\n
총 금액
\n
\n {result.summary.totalAmount?.toLocaleString()}원\n
\n
\n
\n
잔금
\n
\n {result.summary.remainingAmount?.toLocaleString()}원\n
\n
\n
\n
\n
작업지시번호
\n
\n {result.summary.workOrderNos?.map((woNo, i) => (\n \n {woNo}\n \n ))}\n
\n
\n
\n
프로세스 흐름
\n
{result.summary.processFlow}
\n
\n
\n )}\n
\n ))}\n
\n
\n\n {/* 연동 검증 체크리스트 */}\n
\n
\n \n 기준정보 연동 검증 체크리스트\n
\n
\n {[\n { name: '채번관리', desc: 'QT/SO/WO/IQC/PQC/FQC/SL 문서번호 자동생성', status: 'pass' },\n { name: '공통코드', desc: '품목코드/거래처코드/현장코드 규칙 적용', status: 'pass' },\n { name: '공정관리', desc: '스크린/슬랫/절곡 공정별 작업단계 연동', status: 'pass' },\n { name: '견적수식', desc: 'BOM 자동계산 (제작사이즈/면적/중량)', status: 'pass' },\n { name: '문서양식', desc: '견적서/수주서/작업지시서/검사성적서', status: 'pass' },\n { name: '단가관리', desc: '품목별 단가 조회 및 적용', status: 'pass' },\n { name: '재고연동', desc: '자재 소요량 산출 및 출고 처리', status: 'pass' },\n { name: '품질연동', desc: '중간검사/최종검사 결과 연동', status: 'pass' },\n { name: '회계연동', desc: '입금/매출/수금 데이터 연동', status: 'pass' },\n { name: '추적성', desc: '견적→출하 전 과정 문서번호 연결', status: 'pass' },\n ].map((item, idx) => (\n
\n
✅\n
\n
{item.name}
\n
{item.desc}
\n
\n
\n ))}\n
\n
\n
\n )}\n\n {/* 에러 표시 */}\n {testResults?.error && (\n
\n
테스트 실행 오류
\n
{testResults.error}
\n
\n )}\n
\n );\n };\n\n // ═══════════════════════════════════════════════════════════════════════════\n // 워크플로우 테스트 탭 - 견적 입력부터 회계까지 전 과정 실시간 테스트\n // ═══════════════════════════════════════════════════════════════════════════\n const WorkflowTestTab = () => {\n // 워크플로우 상태\n const [currentStep, setCurrentStep] = useState(0);\n const [isProcessing, setIsProcessing] = useState(false);\n const [workflowData, setWorkflowData] = useState({\n // 입력 정보\n inputs: {\n customerId: integratedCustomerMaster[0]?.id,\n siteId: integratedSiteMaster[0]?.id,\n PC: '스크린',\n W0: 2500,\n H0: 3000,\n QTY: 2,\n GT: '벽면형',\n V: '220',\n WIRE: '유선',\n CT: '매립',\n floor: '1층',\n symbol: 'A',\n },\n // 계산 결과\n calculatedResult: null,\n // 생성된 데이터\n quote: null,\n order: null,\n workOrders: [],\n qualityInspections: [],\n shipment: null,\n sales: null,\n collection: null,\n });\n\n // 단계 정의\n const steps = [\n { id: 0, label: '견적 입력', icon: Calculator, color: 'blue' },\n { id: 1, label: '자동 산출', icon: Zap, color: 'purple' },\n { id: 2, label: '견적 확정', icon: FileText, color: 'green' },\n { id: 3, label: '수주 전환', icon: Package, color: 'orange' },\n { id: 4, label: '생산 지시', icon: Factory, color: 'yellow' },\n { id: 5, label: '품질 검사', icon: ShieldCheck, color: 'indigo' },\n { id: 6, label: '출하', icon: Truck, color: 'teal' },\n { id: 7, label: '회계 처리', icon: DollarSign, color: 'pink' },\n { id: 8, label: '완료', icon: CheckCircle, color: 'emerald' },\n ];\n\n // 선택된 거래처의 현장 목록\n const customerSites = integratedSiteMaster.filter(s => s.customerId === workflowData.inputs.customerId);\n const selectedCustomer = integratedCustomerMaster.find(c => c.id === workflowData.inputs.customerId);\n const selectedSite = integratedSiteMaster.find(s => s.id === workflowData.inputs.siteId);\n\n // 입력값 변경\n const handleInputChange = (key, value) => {\n setWorkflowData(prev => ({\n ...prev,\n inputs: { ...prev.inputs, [key]: value }\n }));\n // 거래처 변경시 현장 초기화\n if (key === 'customerId') {\n const sites = integratedSiteMaster.filter(s => s.customerId === parseInt(value));\n if (sites.length > 0) {\n setWorkflowData(prev => ({\n ...prev,\n inputs: { ...prev.inputs, customerId: parseInt(value), siteId: sites[0].id }\n }));\n }\n }\n };\n\n // 견적 자동 산출\n const calculateQuote = () => {\n const { PC, W0, H0, QTY, GT, V, WIRE, CT } = workflowData.inputs;\n\n // 제작사이즈 계산\n const W1 = PC === '스크린' ? W0 + 140 : W0 + 110;\n const H1 = H0 + 350;\n\n // 면적 및 중량 계산\n const M = (W1 * H1) / 1000000;\n const K = PC === '스크린' ? (M * 2) + (W0 * 14.17 / 1000) : M * 25;\n\n // 모터 용량 결정\n let motorCapacity = '200KG';\n if (K > 150) motorCapacity = '500KG';\n else if (K > 100) motorCapacity = '400KG';\n else if (K > 60) motorCapacity = '300KG';\n\n // 샤프트 인치 결정\n let shaftInch = '3인치';\n if (W1 > 5000) shaftInch = '5인치';\n else if (W1 > 3500) shaftInch = '4인치';\n\n // 품목별 산출\n const items = [\n { id: 1, itemCode: 'SCR-001', itemName: '스크린', spec: `${W1}x${H1}`, qty: QTY, unit: 'EA', unitPrice: Math.round(M * 350000), amount: Math.round(M * 350000 * QTY), process: '스크린', category: '스크린' },\n { id: 2, itemCode: 'GR-001', itemName: '가이드레일', spec: `${H0 + 250}mm`, qty: QTY * 2, unit: 'EA', unitPrice: Math.round((H0 + 250) * 85), amount: Math.round((H0 + 250) * 85 * QTY * 2), process: '절곡', category: '철재' },\n { id: 3, itemCode: 'CS-001', itemName: '케이스', spec: `${W1}mm`, qty: QTY, unit: 'SET', unitPrice: Math.round(W1 * 120), amount: Math.round(W1 * 120 * QTY), process: '절곡', category: '철재' },\n { id: 4, itemCode: `MTR-${motorCapacity}`, itemName: `튜블러모터 ${motorCapacity}`, spec: motorCapacity, qty: QTY, unit: 'EA', unitPrice: motorCapacity === '500KG' ? 450000 : motorCapacity === '400KG' ? 380000 : motorCapacity === '300KG' ? 320000 : 280000, amount: 0, process: '조립', category: '모터' },\n { id: 5, itemCode: `SFT-${shaftInch}`, itemName: `샤프트 ${shaftInch}`, spec: `${shaftInch} x ${W1}mm`, qty: QTY, unit: 'EA', unitPrice: shaftInch === '5인치' ? 180000 : shaftInch === '4인치' ? 150000 : 120000, amount: 0, process: '샤프트', category: '샤프트' },\n { id: 6, itemCode: 'BRK-001', itemName: '브라켓', spec: motorCapacity, qty: QTY * 2, unit: 'EA', unitPrice: 25000, amount: 25000 * QTY * 2, process: '조립', category: '부자재' },\n { id: 7, itemCode: 'CTR-001', itemName: `연동제어기 ${CT}`, spec: CT === '매립' ? '매립형' : '노출형', qty: QTY, unit: 'EA', unitPrice: CT === '매립' ? 85000 : 65000, amount: 0, process: '전장', category: '제어' },\n { id: 8, itemCode: 'BTM-001', itemName: '하단마감재', spec: `${W0}mm`, qty: QTY, unit: 'SET', unitPrice: Math.round(W0 * 45), amount: Math.round(W0 * 45 * QTY), process: '절곡', category: '철재' },\n ];\n\n // 금액 계산 (아직 계산되지 않은 항목)\n items.forEach(item => {\n if (item.amount === 0) {\n item.amount = item.unitPrice * item.qty;\n }\n });\n\n const totalAmount = items.reduce((sum, item) => sum + item.amount, 0);\n\n return {\n summary: {\n productType: PC,\n openSize: `${W0} x ${H0}`,\n productionSize: `${W1} x ${H1}`,\n area: M.toFixed(2),\n weight: K.toFixed(1),\n motorCapacity,\n shaftInch,\n qty: QTY,\n },\n items,\n totalAmount,\n vatAmount: Math.round(totalAmount * 0.1),\n grandTotal: Math.round(totalAmount * 1.1),\n };\n };\n\n // 다음 단계 실행\n const executeNextStep = async () => {\n setIsProcessing(true);\n\n // 각 단계별 처리\n await new Promise(resolve => setTimeout(resolve, 800)); // 애니메이션 효과\n\n switch (currentStep) {\n case 0: // 견적 입력 → 자동 산출\n const result = calculateQuote();\n setWorkflowData(prev => ({ ...prev, calculatedResult: result }));\n break;\n\n case 1: // 자동 산출 → 견적 확정\n const quoteNo = `QT-${Date.now().toString().slice(-8)}`;\n const newQuote = {\n id: Date.now(),\n quoteNo,\n quoteDate: new Date().toISOString().slice(0, 10),\n customerId: workflowData.inputs.customerId,\n customerName: selectedCustomer?.name,\n siteId: workflowData.inputs.siteId,\n siteName: selectedSite?.name,\n productType: workflowData.calculatedResult.summary.productType,\n width: workflowData.inputs.W0,\n height: workflowData.inputs.H0,\n qty: workflowData.inputs.QTY,\n items: workflowData.calculatedResult.items,\n totalAmount: workflowData.calculatedResult.totalAmount,\n vatAmount: workflowData.calculatedResult.vatAmount,\n finalAmount: workflowData.calculatedResult.grandTotal,\n status: '최종확정',\n discountRate: selectedCustomer?.discountRate || 0,\n };\n setWorkflowData(prev => ({ ...prev, quote: newQuote }));\n break;\n\n case 2: // 견적 확정 → 수주 전환\n const orderNo = `SO-${Date.now().toString().slice(-8)}`;\n const newOrder = {\n id: Date.now(),\n orderNo,\n orderDate: new Date().toISOString().slice(0, 10),\n quoteId: workflowData.quote.id,\n quoteNo: workflowData.quote.quoteNo,\n customerId: workflowData.quote.customerId,\n customerName: workflowData.quote.customerName,\n siteId: workflowData.quote.siteId,\n siteName: workflowData.quote.siteName,\n items: workflowData.quote.items,\n totalAmount: workflowData.quote.finalAmount,\n dueDate: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),\n status: '수주확정',\n };\n setWorkflowData(prev => ({\n ...prev,\n order: newOrder,\n quote: { ...prev.quote, status: '수주전환', convertedOrderNo: orderNo }\n }));\n break;\n\n case 3: // 수주 전환 → 생산 지시\n const processes = ['스크린', '절곡', '샤프트', '조립', '전장'];\n const newWorkOrders = processes.map((process, idx) => ({\n id: Date.now() + idx,\n workOrderNo: `WO-${Date.now().toString().slice(-6)}-${idx + 1}`,\n orderId: workflowData.order.id,\n orderNo: workflowData.order.orderNo,\n processName: process,\n qty: workflowData.inputs.QTY,\n producedQty: 0,\n defectQty: 0,\n status: '대기',\n startDate: null,\n endDate: null,\n }));\n setWorkflowData(prev => ({\n ...prev,\n workOrders: newWorkOrders,\n order: { ...prev.order, status: '생산중' }\n }));\n break;\n\n case 4: // 생산 지시 → 품질 검사 (생산 완료 처리 포함)\n // 생산 완료 처리\n const completedWorkOrders = workflowData.workOrders.map(wo => ({\n ...wo,\n producedQty: wo.qty,\n status: '완료',\n startDate: new Date().toISOString().slice(0, 10),\n endDate: new Date().toISOString().slice(0, 10),\n }));\n\n // 품질 검사 생성\n const inspections = completedWorkOrders.map((wo, idx) => ({\n id: Date.now() + idx,\n inspectionNo: `QI-${Date.now().toString().slice(-6)}-${idx + 1}`,\n workOrderId: wo.id,\n workOrderNo: wo.workOrderNo,\n inspectionType: wo.processName === '조립' ? '제품검사' : '중간검사',\n inspectionItems: ['외관검사', '치수검사', '작동검사'],\n inspectionDate: new Date().toISOString().slice(0, 10),\n result: '합격',\n inspector: '검사원A',\n }));\n\n setWorkflowData(prev => ({\n ...prev,\n workOrders: completedWorkOrders,\n qualityInspections: inspections,\n order: { ...prev.order, status: '생산완료' }\n }));\n break;\n\n case 5: // 품질 검사 → 출하\n const shipmentNo = `SH-${Date.now().toString().slice(-8)}`;\n const newShipment = {\n id: Date.now(),\n shipmentNo,\n orderId: workflowData.order.id,\n orderNo: workflowData.order.orderNo,\n customerId: workflowData.order.customerId,\n customerName: workflowData.order.customerName,\n siteId: workflowData.order.siteId,\n siteName: workflowData.order.siteName,\n shipmentDate: new Date().toISOString().slice(0, 10),\n carrier: '자체배송',\n status: '배송완료',\n deliveryDate: new Date().toISOString().slice(0, 10),\n };\n setWorkflowData(prev => ({\n ...prev,\n shipment: newShipment,\n order: { ...prev.order, status: '출하완료' }\n }));\n break;\n\n case 6: // 출하 → 회계 처리\n const salesNo = `SL-${Date.now().toString().slice(-8)}`;\n const supplyAmount = workflowData.order.totalAmount / 1.1;\n const newSales = {\n id: Date.now(),\n salesNo,\n orderId: workflowData.order.id,\n orderNo: workflowData.order.orderNo,\n customerId: workflowData.order.customerId,\n customerName: workflowData.order.customerName,\n supplyAmount: Math.round(supplyAmount),\n vatAmount: Math.round(supplyAmount * 0.1),\n totalAmount: workflowData.order.totalAmount,\n salesDate: new Date().toISOString().slice(0, 10),\n paymentStatus: '미수금',\n };\n\n // 수금 처리 (A등급 거래처는 자동 수금 처리)\n let newCollection = null;\n if (selectedCustomer?.creditGrade === 'A') {\n const collectionNo = `CL-${Date.now().toString().slice(-8)}`;\n newCollection = {\n id: Date.now() + 1,\n collectionNo,\n salesId: newSales.id,\n salesNo: newSales.salesNo,\n customerId: workflowData.order.customerId,\n customerName: workflowData.order.customerName,\n amount: newSales.totalAmount,\n collectionDate: new Date().toISOString().slice(0, 10),\n paymentMethod: '계좌이체',\n };\n newSales.paymentStatus = '수금완료';\n }\n\n setWorkflowData(prev => ({\n ...prev,\n sales: newSales,\n collection: newCollection,\n order: { ...prev.order, status: '완료' }\n }));\n break;\n\n default:\n break;\n }\n\n setCurrentStep(prev => Math.min(prev + 1, 8));\n setIsProcessing(false);\n };\n\n // 초기화\n const resetWorkflow = () => {\n setCurrentStep(0);\n setWorkflowData({\n inputs: {\n customerId: integratedCustomerMaster[0]?.id,\n siteId: integratedSiteMaster[0]?.id,\n PC: '스크린',\n W0: 2500,\n H0: 3000,\n QTY: 2,\n GT: '벽면형',\n V: '220',\n WIRE: '유선',\n CT: '매립',\n floor: '1층',\n symbol: 'A',\n },\n calculatedResult: null,\n quote: null,\n order: null,\n workOrders: [],\n qualityInspections: [],\n shipment: null,\n sales: null,\n collection: null,\n });\n };\n\n // 자동 실행 (전체 프로세스)\n const [isAutoRunning, setIsAutoRunning] = useState(false);\n const [selectedScenario, setSelectedScenario] = useState('normal');\n const [testResults, setTestResults] = useState([]);\n\n // 테스트 시나리오 정의\n const testScenarios = [\n // ═══════════════════════════════════════════════════════════════════════════\n // MVP 핵심 시나리오 (수주→작업지시→자재투입→작업실적→검사→출하)\n // ═══════════════════════════════════════════════════════════════════════════\n { id: 'mvp-core', label: 'MVP 핵심흐름', desc: '수주→작업지시→자재투입→실적→검사→출하', icon: '🎯' },\n { id: 'mvp-material-input', label: 'MVP 자재투입', desc: '작업지시 자재투입 테스트', icon: '🔧' },\n { id: 'mvp-work-result', label: 'MVP 작업실적', desc: '공정별 작업실적 입력 테스트', icon: '📝' },\n { id: 'mvp-worker-screen', label: 'MVP 작업자화면', desc: '작업자 현장 입력 화면 테스트', icon: '👷' },\n { id: 'mvp-production-board', label: 'MVP 생산현황판', desc: '실시간 생산현황 모니터링', icon: '📺' },\n { id: 'mvp-stock-status', label: 'MVP 재고현황', desc: '재고현황 조회 테스트', icon: '📦' },\n { id: 'mvp-inbound', label: 'MVP 입고관리', desc: '자재입고 처리 테스트', icon: '📥' },\n { id: 'mvp-full-flow', label: 'MVP 전체흐름', desc: 'MVP 전 과정 통합 테스트', icon: '🔄' },\n // ═══════════════════════════════════════════════════════════════════════════\n // 기존 시나리오\n // ═══════════════════════════════════════════════════════════════════════════\n { id: 'normal', label: '정상 프로세스', desc: '모든 단계 정상 완료', icon: '✅' },\n { id: 'quote-cancel', label: '견적 취소', desc: '견적 단계에서 취소', icon: '❌' },\n { id: 'quote-expire', label: '견적 만료', desc: '유효기간 초과로 만료', icon: '⏰' },\n { id: 'quality-fail', label: '품질 불합격', desc: '품질검사 불합격 → 재작업', icon: '🔴' },\n { id: 'quality-conditional', label: '조건부 합격', desc: '품질검사 조건부 합격', icon: '🟡' },\n { id: 'partial-shipment', label: '부분 출하', desc: '일부 수량만 출하', icon: '📦' },\n { id: 'payment-delay', label: '수금 지연', desc: '미수금 발생 (B/C등급)', icon: '💰' },\n { id: 'rush-order', label: '긴급 주문', desc: '납기 단축 긴급 처리', icon: '🚨' },\n { id: 'size-change', label: '사이즈 변경', desc: '생산 중 사이즈 변경', icon: '📐' },\n { id: 'new-item', label: '신규 품목', desc: '품목등록 → 견적 테스트', icon: '📋' },\n { id: 'multi-inspection', label: '검사유형전환', desc: '입고/중간/제품검사 통합', icon: '🔍' },\n { id: 'numbering-test', label: '채번 테스트', desc: '자동채번 규칙 검증', icon: '🔢' },\n { id: 'code-reference', label: '코드참조', desc: '공통코드 활용 테스트', icon: '📑' },\n { id: 'accounting-sales', label: '매출관리', desc: '매출 등록/수정/조회 테스트', icon: '📊' },\n { id: 'accounting-purchase', label: '매입관리', desc: '매입 등록/수정/조회 테스트', icon: '🛒' },\n { id: 'accounting-cashbook', label: '금전출납부', desc: '입출금 내역 관리 테스트', icon: '💵' },\n { id: 'accounting-collection', label: '수금관리', desc: '미수금/수금 처리 테스트', icon: '💳' },\n { id: 'accounting-cost', label: '원가관리', desc: '원가계산/분석 테스트', icon: '🧮' },\n // 문서 출력 테스트 시나리오\n { id: 'doc-quote', label: '견적서 출력', desc: '견적서 생성/출력 테스트', icon: '📄' },\n { id: 'doc-order-confirm', label: '수주확인서', desc: '수주확인서 생성/출력', icon: '📋' },\n { id: 'doc-work-order', label: '작업지시서', desc: '작업지시서 생성/출력', icon: '📝' },\n { id: 'doc-inspection', label: '검사성적서', desc: '검사성적서 생성/출력', icon: '🔬' },\n { id: 'doc-shipment', label: '출하지시서', desc: '출하지시서 생성/출력', icon: '🚚' },\n { id: 'doc-invoice', label: '거래명세서', desc: '거래명세서 생성/출력', icon: '🧾' },\n { id: 'doc-tax', label: '세금계산서', desc: '세금계산서 발행 테스트', icon: '💹' },\n { id: 'doc-full-flow', label: '문서 전체흐름', desc: '전 과정 문서출력 통합', icon: '📚' },\n // ═══════════════════════════════════════════════════════════════════════════\n // E2E 통합 테스트 시나리오 (견적산출 → 수주 → 생산지시 → 공정분리 → 출하)\n // ═══════════════════════════════════════════════════════════════════════════\n { id: 'e2e-quote-calc', label: 'E2E 견적산출', desc: '새 수식 기준 자동견적 산출', icon: '🧮', isE2E: true },\n { id: 'e2e-order-convert', label: 'E2E 수주전환', desc: '견적→수주 전환 테스트', icon: '📦', isE2E: true },\n { id: 'e2e-order-confirm', label: 'E2E 수주확정', desc: '입금/승인 후 수주 확정', icon: '✅', isE2E: true },\n { id: 'e2e-production-order', label: 'E2E 생산지시', desc: '수주→생산지시 발행', icon: '🏭', isE2E: true },\n { id: 'e2e-process-split', label: 'E2E 공정분리', desc: '스크린/슬랫/절곡 공정 분리', icon: '🔀', isE2E: true },\n { id: 'e2e-work-order', label: 'E2E 작업지시', desc: '공정별 작업지시서 생성', icon: '📋', isE2E: true },\n { id: 'e2e-work-result', label: 'E2E 작업실적', desc: '공정별 작업 완료 처리', icon: '✔️', isE2E: true },\n { id: 'e2e-inspection', label: 'E2E 품질검사', desc: '완제품 품질검사 통과', icon: '🔍', isE2E: true },\n { id: 'e2e-shipment', label: 'E2E 출하완료', desc: '출하 처리 및 문서발행', icon: '🚚', isE2E: true },\n { id: 'e2e-full-flow', label: 'E2E 전체흐름', desc: '견적→출하 전과정 통합', icon: '🎯', isE2E: true },\n { id: 'full-cycle', label: '전체 사이클', desc: '모든 경우의 수 테스트', icon: '🔄' },\n ];\n\n // 전체 테스트 결과 상세\n const [allTestResults, setAllTestResults] = useState({});\n const [testSummary, setTestSummary] = useState(null);\n\n // 시나리오별 자동 실행\n const runAutoWorkflow = async () => {\n setIsAutoRunning(true);\n resetWorkflow();\n setTestResults([]);\n setAllTestResults({});\n setTestSummary(null);\n await new Promise(resolve => setTimeout(resolve, 300));\n\n const results = [];\n const scenario = selectedScenario;\n\n // 시나리오별 처리\n if (scenario === 'full-cycle') {\n // 전체 사이클: 모든 시나리오 순차 실행\n const startTime = Date.now();\n const detailedResults = {};\n let passCount = 0;\n let failCount = 0;\n\n for (const sc of testScenarios.filter(s => s.id !== 'full-cycle')) {\n const scenarioStartTime = Date.now();\n results.push({\n scenario: sc.label,\n scenarioId: sc.id,\n status: '실행중',\n icon: sc.icon,\n startTime: new Date().toLocaleTimeString(),\n });\n setTestResults([...results]);\n\n try {\n await runSingleScenario(sc.id);\n const duration = ((Date.now() - scenarioStartTime) / 1000).toFixed(1);\n results[results.length - 1].status = '✅ 통과';\n results[results.length - 1].duration = `${duration}s`;\n results[results.length - 1].passed = true;\n detailedResults[sc.id] = { passed: true, duration, label: sc.label };\n passCount++;\n } catch (error) {\n const duration = ((Date.now() - scenarioStartTime) / 1000).toFixed(1);\n results[results.length - 1].status = '❌ 실패';\n results[results.length - 1].duration = `${duration}s`;\n results[results.length - 1].passed = false;\n results[results.length - 1].error = error.message;\n detailedResults[sc.id] = { passed: false, duration, label: sc.label, error: error.message };\n failCount++;\n }\n\n setTestResults([...results]);\n await new Promise(resolve => setTimeout(resolve, 300));\n resetWorkflow();\n }\n\n const totalDuration = ((Date.now() - startTime) / 1000).toFixed(1);\n setAllTestResults(detailedResults);\n setTestSummary({\n totalScenarios: testScenarios.length - 1,\n passed: passCount,\n failed: failCount,\n passRate: ((passCount / (testScenarios.length - 1)) * 100).toFixed(1),\n totalDuration: `${totalDuration}s`,\n completedAt: new Date().toLocaleString(),\n });\n setIsAutoRunning(false);\n return;\n }\n\n await runSingleScenario(scenario);\n setIsAutoRunning(false);\n };\n\n // 단일 시나리오 실행\n const runSingleScenario = async (scenario) => {\n // 번호 시퀀스 카운터 (실제 운영에서는 DB에서 관리)\n const seqCounters = {\n quote: Math.floor(Math.random() * 50) + 1,\n order: Math.floor(Math.random() * 50) + 1,\n production: Math.floor(Math.random() * 20) + 1,\n inspection: Math.floor(Math.random() * 20) + 1,\n };\n const today = new Date();\n\n // Step 0: 견적 입력 → 자동 산출\n await new Promise(resolve => setTimeout(resolve, 400));\n const calcResult = calculateQuote();\n setWorkflowData(prev => ({ ...prev, calculatedResult: calcResult }));\n setCurrentStep(1);\n\n // 견적 취소 시나리오\n if (scenario === 'quote-cancel') {\n await new Promise(resolve => setTimeout(resolve, 400));\n const quoteNo = generateQuoteNo(today, seqCounters.quote);\n setWorkflowData(prev => ({\n ...prev,\n quote: {\n id: Date.now(),\n quoteNo,\n quoteDate: today.toISOString().slice(0, 10),\n customerName: selectedCustomer?.name,\n siteName: selectedSite?.name,\n status: '취소',\n cancelReason: '고객 요청으로 취소',\n cancelDate: today.toISOString().slice(0, 10),\n }\n }));\n setCurrentStep(8);\n return;\n }\n\n // 견적 만료 시나리오\n if (scenario === 'quote-expire') {\n await new Promise(resolve => setTimeout(resolve, 400));\n const expireDate = new Date();\n expireDate.setDate(expireDate.getDate() - 31);\n const quoteNo = generateQuoteNo(expireDate, seqCounters.quote);\n setWorkflowData(prev => ({\n ...prev,\n quote: {\n id: Date.now(),\n quoteNo,\n quoteDate: expireDate.toISOString().slice(0, 10),\n validUntil: new Date(expireDate.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),\n customerName: selectedCustomer?.name,\n siteName: selectedSite?.name,\n status: '만료',\n expireReason: '유효기간 30일 초과',\n }\n }));\n setCurrentStep(8);\n return;\n }\n\n // Step 1: 자동 산출 → 견적 확정\n await new Promise(resolve => setTimeout(resolve, 400));\n setWorkflowData(prev => {\n const quoteNo = generateQuoteNo(today, seqCounters.quote);\n return {\n ...prev,\n quote: {\n id: Date.now(),\n quoteNo,\n quoteDate: today.toISOString().slice(0, 10),\n validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),\n customerId: prev.inputs.customerId,\n customerName: selectedCustomer?.name,\n creditGrade: selectedCustomer?.creditGrade,\n siteId: prev.inputs.siteId,\n siteName: selectedSite?.name,\n productType: prev.calculatedResult?.summary?.productType || '스크린',\n width: prev.inputs.W0,\n height: prev.inputs.H0,\n qty: prev.inputs.QTY,\n items: prev.calculatedResult?.items || [],\n totalAmount: prev.calculatedResult?.totalAmount || 0,\n vatAmount: prev.calculatedResult?.vatAmount || 0,\n finalAmount: prev.calculatedResult?.grandTotal || 0,\n status: '최종확정',\n discountRate: selectedCustomer?.discountRate || 0,\n }\n };\n });\n setCurrentStep(2);\n\n // Step 2: 견적 확정 → 수주 전환\n await new Promise(resolve => setTimeout(resolve, 400));\n const isRushOrder = scenario === 'rush-order';\n setWorkflowData(prev => {\n // 수주번호: KD-모델코드-YYMMDD-## (예: KD-TS-251209-02)\n const productType = prev.quote?.productType || '스크린';\n const orderNo = generateOrderNo(productType, today, seqCounters.order);\n const productionLot = generateProductionLot(today, seqCounters.production);\n const dueDate = new Date();\n dueDate.setDate(dueDate.getDate() + (isRushOrder ? 7 : 14)); // 긴급주문은 7일\n return {\n ...prev,\n order: {\n id: Date.now(),\n orderNo,\n productionLot,\n orderDate: today.toISOString().slice(0, 10),\n quoteId: prev.quote?.id,\n quoteNo: prev.quote?.quoteNo,\n customerId: prev.quote?.customerId,\n customerName: prev.quote?.customerName,\n creditGrade: prev.quote?.creditGrade,\n siteId: prev.quote?.siteId,\n siteName: prev.quote?.siteName,\n productType: prev.quote?.productType,\n items: prev.quote?.items || [],\n totalAmount: prev.quote?.finalAmount || 0,\n dueDate: dueDate.toISOString().slice(0, 10),\n status: '수주확정',\n isRush: isRushOrder,\n rushReason: isRushOrder ? '고객 긴급 요청' : null,\n priority: isRushOrder ? '긴급' : '일반',\n },\n quote: { ...prev.quote, status: '수주전환', convertedOrderNo: orderNo }\n };\n });\n setCurrentStep(3);\n\n // Step 3: 수주 전환 → 생산 지시\n await new Promise(resolve => setTimeout(resolve, 400));\n setWorkflowData(prev => {\n const processes = ['스크린', '절곡', '샤프트', '조립', '전장'];\n const newWorkOrders = processes.map((process, idx) => {\n // 중간검사 LOT: KD-WE-YYMMDD-##-(공정단계)\n const processInspLot = generateProcessInspectionLot(today, seqCounters.inspection, idx + 1);\n return {\n id: Date.now() + idx,\n workOrderNo: `${prev.order?.orderNo}-W${String(idx + 1).padStart(2, '0')}`,\n processInspectionLot: processInspLot,\n orderId: prev.order?.id,\n orderNo: prev.order?.orderNo,\n processName: process,\n processStep: idx + 1,\n qty: prev.inputs.QTY,\n producedQty: 0,\n defectQty: 0,\n status: '대기',\n priority: prev.order?.priority || '일반',\n startDate: null,\n endDate: null,\n };\n });\n return {\n ...prev,\n workOrders: newWorkOrders,\n order: { ...prev.order, status: '생산중' }\n };\n });\n setCurrentStep(4);\n\n // 사이즈 변경 시나리오\n if (scenario === 'size-change') {\n await new Promise(resolve => setTimeout(resolve, 400));\n setWorkflowData(prev => ({\n ...prev,\n sizeChangeLog: {\n originalSize: `${prev.inputs.W0}x${prev.inputs.H0}`,\n newSize: `${prev.inputs.W0 + 100}x${prev.inputs.H0 + 50}`,\n changeDate: new Date().toISOString().slice(0, 10),\n reason: '현장 실측 후 변경',\n approver: '생산관리자',\n },\n inputs: {\n ...prev.inputs,\n W0: prev.inputs.W0 + 100,\n H0: prev.inputs.H0 + 50,\n }\n }));\n }\n\n // Step 4: 생산 지시 → 품질 검사\n await new Promise(resolve => setTimeout(resolve, 400));\n const qualityResult = scenario === 'quality-fail' ? '불합격' :\n scenario === 'quality-conditional' ? '조건부합격' : '합격';\n\n setWorkflowData(prev => {\n const completedWO = prev.workOrders.map((wo, idx) => ({\n ...wo,\n producedQty: wo.qty,\n defectQty: scenario === 'quality-fail' && idx === 3 ? 1 : 0, // 조립 공정에서 불량\n status: '완료',\n startDate: today.toISOString().slice(0, 10),\n endDate: today.toISOString().slice(0, 10),\n }));\n\n // 검사 LOT 번호 생성\n const inspections = completedWO.map((wo, idx) => {\n const isFinalInspection = wo.processName === '조립';\n // 중간검사: KD-WE-YYMMDD-##-(공정단계), 제품검사: KD-SA-YYMMDD-##\n const inspLot = isFinalInspection\n ? generateProductInspectionLot(today, seqCounters.inspection + idx)\n : wo.processInspectionLot || generateProcessInspectionLot(today, seqCounters.inspection, wo.processStep);\n\n return {\n id: Date.now() + idx,\n inspectionLot: inspLot,\n inspectionNo: inspLot,\n workOrderId: wo.id,\n workOrderNo: wo.workOrderNo,\n inspectionType: isFinalInspection ? '제품검사' : '중간검사',\n inspectionItems: ['외관검사', '치수검사', '작동검사'],\n inspectionDate: today.toISOString().slice(0, 10),\n result: isFinalInspection ? qualityResult : '합격',\n defectType: qualityResult === '불합격' && isFinalInspection ? '외관불량' : null,\n defectDescription: qualityResult === '불합격' && isFinalInspection ? '스크래치 발견' : null,\n conditionalNote: qualityResult === '조건부합격' && isFinalInspection ? '경미한 외관 손상, 터치업 후 출하 가능' : null,\n inspector: '검사원A',\n };\n });\n\n // 제품검사 시 완제품/부품 LOT 발급\n const productLot = generateProductLot(prev.order?.orderNo, 1);\n\n return {\n ...prev,\n workOrders: completedWO,\n qualityInspections: inspections,\n finishedProductLot: productLot,\n installationLot: productLot, // 설치 LOT = 완제품 LOT\n order: { ...prev.order, status: qualityResult === '불합격' ? '재작업필요' : '생산완료' }\n };\n });\n setCurrentStep(5);\n\n // 품질 불합격 → 재작업 처리\n if (scenario === 'quality-fail') {\n await new Promise(resolve => setTimeout(resolve, 400));\n setWorkflowData(prev => ({\n ...prev,\n reworkOrder: {\n id: Date.now(),\n reworkNo: `RW-${Date.now().toString().slice(-8)}`,\n originalWorkOrderNo: prev.workOrders[3]?.workOrderNo,\n reason: '품질검사 불합격',\n defectType: '외관불량',\n reworkProcess: '조립 재작업',\n status: '재작업완료',\n completedDate: new Date().toISOString().slice(0, 10),\n },\n qualityInspections: prev.qualityInspections.map((qi, idx) =>\n idx === 3 ? { ...qi, result: '합격', reinspectionDate: new Date().toISOString().slice(0, 10), reinspectionNote: '재작업 후 재검사 합격' } : qi\n ),\n order: { ...prev.order, status: '생산완료' }\n }));\n }\n\n // Step 5: 품질 검사 → 출하\n await new Promise(resolve => setTimeout(resolve, 400));\n const isPartialShipment = scenario === 'partial-shipment';\n const shippedQty = isPartialShipment ? Math.ceil(workflowData.inputs.QTY / 2) : workflowData.inputs.QTY;\n\n setWorkflowData(prev => ({\n ...prev,\n shipment: {\n id: Date.now(),\n shipmentNo: `SH-${Date.now().toString().slice(-8)}`,\n orderId: prev.order?.id,\n orderNo: prev.order?.orderNo,\n customerId: prev.order?.customerId,\n customerName: prev.order?.customerName,\n siteId: prev.order?.siteId,\n siteName: prev.order?.siteName,\n shipmentDate: new Date().toISOString().slice(0, 10),\n carrier: '자체배송',\n status: isPartialShipment ? '부분출하' : '배송완료',\n shippedQty,\n totalQty: prev.inputs.QTY,\n remainingQty: prev.inputs.QTY - shippedQty,\n deliveryDate: new Date().toISOString().slice(0, 10),\n partialReason: isPartialShipment ? '재고 부족으로 부분 출하' : null,\n },\n order: { ...prev.order, status: isPartialShipment ? '부분출하' : '출하완료' }\n }));\n setCurrentStep(6);\n\n // 부분 출하 → 잔여 출하\n if (isPartialShipment) {\n await new Promise(resolve => setTimeout(resolve, 400));\n setWorkflowData(prev => ({\n ...prev,\n shipment2: {\n id: Date.now() + 1,\n shipmentNo: `SH-${Date.now().toString().slice(-8)}-2`,\n orderId: prev.order?.id,\n orderNo: prev.order?.orderNo,\n shipmentDate: new Date().toISOString().slice(0, 10),\n shippedQty: prev.shipment.remainingQty,\n status: '배송완료',\n note: '잔여 수량 출하',\n },\n shipment: { ...prev.shipment, status: '전량출하완료' },\n order: { ...prev.order, status: '출하완료' }\n }));\n }\n\n // Step 6: 출하 → 회계 처리\n await new Promise(resolve => setTimeout(resolve, 400));\n const creditGrade = selectedCustomer?.creditGrade || 'B';\n const isPaymentDelay = scenario === 'payment-delay' || creditGrade !== 'A';\n\n setWorkflowData(prev => {\n const supplyAmount = (prev.order?.totalAmount || 0) / 1.1;\n const salesNo = `SL-${Date.now().toString().slice(-8)}`;\n\n return {\n ...prev,\n sales: {\n id: Date.now(),\n salesNo,\n orderId: prev.order?.id,\n orderNo: prev.order?.orderNo,\n customerId: prev.order?.customerId,\n customerName: prev.order?.customerName,\n creditGrade,\n supplyAmount: Math.round(supplyAmount),\n vatAmount: Math.round(supplyAmount * 0.1),\n totalAmount: prev.order?.totalAmount || 0,\n salesDate: new Date().toISOString().slice(0, 10),\n paymentStatus: isPaymentDelay ? '미수금' : '수금완료',\n paymentDueDate: new Date(Date.now() + (creditGrade === 'A' ? 30 : creditGrade === 'B' ? 45 : 60) * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),\n },\n collection: !isPaymentDelay ? {\n id: Date.now() + 1,\n collectionNo: `CL-${Date.now().toString().slice(-8)}`,\n salesId: Date.now(),\n salesNo,\n customerId: prev.order?.customerId,\n customerName: prev.order?.customerName,\n amount: prev.order?.totalAmount || 0,\n collectionDate: new Date().toISOString().slice(0, 10),\n paymentMethod: '계좌이체',\n } : null,\n order: { ...prev.order, status: isPaymentDelay ? '수금대기' : '완료' }\n };\n });\n setCurrentStep(7);\n\n // 수금 지연 → 수금 처리\n if (isPaymentDelay) {\n await new Promise(resolve => setTimeout(resolve, 400));\n setWorkflowData(prev => ({\n ...prev,\n collection: {\n id: Date.now() + 1,\n collectionNo: `CL-${Date.now().toString().slice(-8)}`,\n salesId: prev.sales?.id,\n salesNo: prev.sales?.salesNo,\n customerId: prev.order?.customerId,\n customerName: prev.order?.customerName,\n amount: prev.order?.totalAmount || 0,\n collectionDate: new Date().toISOString().slice(0, 10),\n paymentMethod: creditGrade === 'C' ? '어음' : '계좌이체',\n delayDays: creditGrade === 'A' ? 0 : creditGrade === 'B' ? 15 : 30,\n note: creditGrade !== 'A' ? `${creditGrade}등급 거래처 - 지연 수금` : null,\n },\n sales: { ...prev.sales, paymentStatus: '수금완료' },\n order: { ...prev.order, status: '완료' }\n }));\n }\n\n // 신규 품목 등록 시나리오 - 품목관리(생산관리 메뉴)에서 신규 품목 등록 후 견적 진행\n if (scenario === 'new-item') {\n setWorkflowData(prev => ({\n ...prev,\n newItem: {\n id: Date.now(),\n itemCode: `ITM-${Date.now().toString().slice(-6)}`,\n itemName: '신규 스크린 제품',\n category: '제품',\n itemType: 'FG',\n unit: 'EA',\n basePrice: 850000,\n registeredDate: new Date().toISOString().slice(0, 10),\n registeredBy: '생산팀',\n menuPath: '생산관리 > 품목관리',\n status: '등록완료',\n }\n }));\n }\n\n // 검사유형 전환 시나리오 - 입고검사/중간검사/제품검사 통합 테스트\n if (scenario === 'multi-inspection') {\n setWorkflowData(prev => ({\n ...prev,\n inspectionFlow: {\n iqc: { // 입고검사 (IQC)\n inspectionNo: `IQC-${Date.now().toString().slice(-6)}`,\n inspectionType: '입고검사',\n typeCode: 'IQC',\n targetItem: '원자재',\n inspectionItems: ['외관검사', '성적서확인', '수량검수'],\n result: '합격',\n date: new Date().toISOString().slice(0, 10),\n },\n pqc: { // 중간검사 (PQC)\n inspectionNo: `PQC-${Date.now().toString().slice(-6)}`,\n inspectionType: '중간검사',\n typeCode: 'PQC',\n targetProcess: '절곡공정',\n inspectionItems: ['치수검사', '가공상태', '규격확인'],\n result: '합격',\n date: new Date().toISOString().slice(0, 10),\n },\n fqc: { // 제품검사 (FQC)\n inspectionNo: `FQC-${Date.now().toString().slice(-6)}`,\n inspectionType: '제품검사',\n typeCode: 'FQC',\n targetItem: '완제품',\n inspectionItems: ['외관검사', '작동검사', '포장상태', '부속품확인'],\n result: '합격',\n date: new Date().toISOString().slice(0, 10),\n },\n menuPath: '품질관리 > 검사관리',\n note: '품질기준관리에서 정의된 3단계 검사유형(IQC/PQC/FQC) 통합 테스트',\n }\n }));\n }\n\n // 채번 테스트 시나리오 - 자동채번 규칙 검증\n if (scenario === 'numbering-test') {\n const today = new Date();\n const yymmdd = `${String(today.getFullYear()).slice(-2)}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`;\n setWorkflowData(prev => ({\n ...prev,\n numberingResults: {\n menuPath: '기준정보 > 채번관리',\n rules: [\n {\n ruleName: '견적번호',\n pattern: 'QT-{YYYYMMDD}-{SEQ:3}',\n example: `QT-${today.toISOString().slice(0, 10).replace(/-/g, '')}-001`,\n description: '견적 문서번호 자동생성',\n },\n {\n ruleName: '수주번호',\n pattern: 'KD-{모델}-{YYMMDD}-{SEQ:2}',\n example: `KD-TS-${yymmdd}-01`,\n description: '수주번호 (모델코드 포함)',\n },\n {\n ruleName: '생산LOT',\n pattern: 'KD-WE-{YYMMDD}-{SEQ:2}',\n example: `KD-WE-${yymmdd}-01`,\n description: '생산 LOT번호',\n },\n {\n ruleName: '중간검사LOT',\n pattern: 'KD-WE-{YYMMDD}-{SEQ:2}-{공정단계}',\n example: `KD-WE-${yymmdd}-01-1`,\n description: '공정별 중간검사 LOT',\n },\n {\n ruleName: '제품검사LOT',\n pattern: 'KD-SA-{YYMMDD}-{SEQ:2}',\n example: `KD-SA-${yymmdd}-01`,\n description: '완제품 검사 LOT',\n },\n {\n ruleName: '완제품LOT',\n pattern: '{수주번호}-F{SEQ:2}',\n example: `KD-TS-${yymmdd}-01-F01`,\n description: '완제품/부품 LOT',\n },\n ],\n testStatus: '모든 채번규칙 검증 완료',\n }\n }));\n }\n\n // 공통코드 참조 시나리오 - 공통코드 활용 테스트\n if (scenario === 'code-reference') {\n setWorkflowData(prev => ({\n ...prev,\n codeReferences: {\n menuPath: '기준정보 > 공통코드관리',\n codeGroups: [\n {\n groupCode: 'ITEM_TYPE',\n groupName: '품목유형',\n codes: [\n { code: 'FG', name: '제품', desc: '완제품' },\n { code: 'PT', name: '부품', desc: '부품/파트' },\n { code: 'WIP', name: '반제품', desc: '반제품/공정품' },\n { code: 'RM', name: '원자재', desc: '원자재' },\n ],\n usedIn: ['품목관리', '재고관리', 'BOM관리'],\n },\n {\n groupCode: 'CUST_TYPE',\n groupName: '거래처유형',\n codes: [\n { code: 'CUST', name: '고객사', desc: '제품 구매 고객' },\n { code: 'SUPP', name: '공급업체', desc: '자재 공급업체' },\n { code: 'BOTH', name: '고객/공급', desc: '고객 및 공급업체' },\n { code: 'PARTNER', name: '협력사', desc: '외주/협력 업체' },\n ],\n usedIn: ['거래처관리', '발주관리', '매출관리'],\n },\n {\n groupCode: 'INSP_TYPE',\n groupName: '검사유형',\n codes: [\n { code: 'IQC', name: '입고검사', desc: '자재 입고 시 검사' },\n { code: 'PQC', name: '중간검사', desc: '생산 중 품질검사' },\n { code: 'FQC', name: '제품검사', desc: '출하 후 고객요청 검사' },\n ],\n usedIn: ['검사관리', '품질관리', '품질기준관리'],\n },\n {\n groupCode: 'PROCESS_TYPE',\n groupName: '공정유형',\n codes: [\n { code: 'MAIN', name: '주공정', desc: '메인 생산공정' },\n { code: 'SUB', name: '보조공정', desc: '보조 작업공정' },\n { code: 'QC', name: '검사공정', desc: '품질검사 공정' },\n { code: 'PACK', name: '포장공정', desc: '포장/출하 공정' },\n ],\n usedIn: ['공정관리', '작업지시', '생산실적'],\n },\n {\n groupCode: 'SITE_TYPE',\n groupName: '현장유형',\n codes: [\n { code: 'NEW', name: '신규현장', desc: '신규 설치 현장' },\n { code: 'MAINT', name: '유지보수', desc: '유지보수 현장' },\n { code: 'REPLACE', name: '교체현장', desc: '기존 설비 교체' },\n { code: 'AS', name: 'AS현장', desc: 'AS 대응 현장' },\n ],\n usedIn: ['현장관리', '견적관리', '출하관리'],\n },\n ],\n testStatus: '모든 공통코드 참조 검증 완료',\n }\n }));\n }\n\n // 회계관리 - 매출관리 시나리오\n if (scenario === 'accounting-sales') {\n setWorkflowData(prev => ({\n ...prev,\n accountingSales: {\n menuPath: '회계관리 > 매출관리',\n testCases: [\n { id: 1, action: '매출전표 등록', data: { orderNo: 'ORD-202501-001', amount: 52000000, customer: '삼성물산(주)' }, result: '등록완료' },\n { id: 2, action: '매출 수정', data: { salesNo: 'KD-SH-250101-01', field: '금액', before: 52000000, after: 53000000 }, result: '수정완료' },\n { id: 3, action: '매출 조회', data: { period: '2025-01', filter: '거래처별' }, result: '3건 조회' },\n { id: 4, action: '세금계산서 발행', data: { salesNo: 'KD-SH-250101-01', type: '전자세금계산서' }, result: '발행완료' },\n ],\n summary: { total: 4, success: 4, fail: 0 },\n testStatus: '매출관리 전체 테스트 통과',\n }\n }));\n }\n\n // 회계관리 - 매입관리 시나리오\n if (scenario === 'accounting-purchase') {\n setWorkflowData(prev => ({\n ...prev,\n accountingPurchase: {\n menuPath: '회계관리 > 매입관리',\n testCases: [\n { id: 1, action: '매입전표 등록', data: { poNo: 'KD-SO-250101-01', amount: 12000000, supplier: '대한알루미늄' }, result: '등록완료' },\n { id: 2, action: '매입 수정', data: { purchaseNo: 'PR-202501-001', field: '단가', before: 50000, after: 48000 }, result: '수정완료' },\n { id: 3, action: '매입 조회', data: { period: '2025-01', filter: '공급업체별' }, result: '5건 조회' },\n { id: 4, action: '지급 예정 확인', data: { dueDate: '2025-02-10' }, result: '3건 지급예정' },\n ],\n summary: { total: 4, success: 4, fail: 0 },\n testStatus: '매입관리 전체 테스트 통과',\n }\n }));\n }\n\n // 회계관리 - 금전출납부 시나리오\n if (scenario === 'accounting-cashbook') {\n setWorkflowData(prev => ({\n ...prev,\n accountingCashbook: {\n menuPath: '회계관리 > 금전출납부',\n testCases: [\n { id: 1, action: '입금 등록', data: { type: '매출입금', amount: 50000000, account: '기업은행', customer: '삼성물산' }, result: '등록완료' },\n { id: 2, action: '출금 등록', data: { type: '경비지출', amount: 2500000, account: '기업은행', purpose: '원자재비' }, result: '등록완료' },\n { id: 3, action: '잔액 조회', data: { account: '기업은행', date: '2025-01-31' }, result: '잔액: 150,000,000원' },\n { id: 4, action: '월별 현금흐름', data: { period: '2025-01' }, result: '입금 200M / 출금 150M' },\n ],\n summary: { total: 4, success: 4, fail: 0 },\n testStatus: '금전출납부 전체 테스트 통과',\n }\n }));\n }\n\n // 회계관리 - 수금관리 시나리오\n if (scenario === 'accounting-collection') {\n setWorkflowData(prev => ({\n ...prev,\n accountingCollection: {\n menuPath: '회계관리 > 수금관리',\n testCases: [\n { id: 1, action: '미수금 조회', data: { customer: '전체', status: '미수' }, result: '5건 / 85,000,000원' },\n { id: 2, action: '수금 등록', data: { customer: '삼성물산(주)', amount: 52000000, method: '계좌이체' }, result: '수금완료' },\n { id: 3, action: '연체 알림', data: { overdueDays: 30 }, result: '연체 2건 알림발송' },\n { id: 4, action: '수금 현황표', data: { period: '2025-01', groupBy: '거래처' }, result: '리포트 생성완료' },\n ],\n summary: { total: 4, success: 4, fail: 0 },\n testStatus: '수금관리 전체 테스트 통과',\n }\n }));\n }\n\n // 회계관리 - 원가관리 시나리오\n if (scenario === 'accounting-cost') {\n setWorkflowData(prev => ({\n ...prev,\n accountingCost: {\n menuPath: '회계관리 > 원가관리',\n testCases: [\n { id: 1, action: '제품별 원가계산', data: { product: 'KS60 스크린', period: '2025-01' }, result: '원가: 385,000원/EA' },\n { id: 2, action: '원가 구성분석', data: { product: 'KS60 스크린' }, result: '재료비 60%, 노무비 25%, 경비 15%' },\n { id: 3, action: '원가 추이분석', data: { period: '2024-01 ~ 2025-01' }, result: '원가 5.2% 상승' },\n { id: 4, action: '손익분석표', data: { period: '2025-01', type: '제품별' }, result: '리포트 생성완료' },\n ],\n summary: { total: 4, success: 4, fail: 0 },\n testStatus: '원가관리 전체 테스트 통과',\n }\n }));\n }\n\n // ═══════════════════════════════════════════════════════════════════════════\n // 문서 출력 테스트 시나리오\n // ═══════════════════════════════════════════════════════════════════════════\n\n // 견적서 출력 테스트\n if (scenario === 'doc-quote') {\n setWorkflowData(prev => ({\n ...prev,\n documentTest: {\n docType: '견적서',\n docTypeCode: 'QT',\n menuPath: '판매관리 > 견적관리',\n testCases: [\n { id: 1, action: '견적서 생성', status: '완료', timestamp: new Date().toLocaleTimeString() },\n { id: 2, action: '결재 상신', status: '완료', timestamp: new Date().toLocaleTimeString() },\n { id: 3, action: '검토자 승인', status: '완료', approver: '판매팀장', timestamp: new Date().toLocaleTimeString() },\n { id: 4, action: '최종 승인', status: '완료', approver: '판매본부장', timestamp: new Date().toLocaleTimeString() },\n { id: 5, action: 'PDF 출력', status: '완료', format: 'A4', timestamp: new Date().toLocaleTimeString() },\n { id: 6, action: '이메일 발송', status: '완료', recipient: 'customer@example.com', timestamp: new Date().toLocaleTimeString() },\n ],\n document: {\n docNo: `QT-${today.toISOString().slice(0, 10).replace(/-/g, '')}-001`,\n title: '방화셔터 견적서',\n customer: selectedCustomer?.name || '삼성물산(주)',\n amount: prev.calculatedResult?.grandTotal || 52800000,\n createdAt: today.toISOString().slice(0, 10),\n createdBy: '김판매',\n status: '결재완료',\n },\n approvalLine: [\n { step: 1, role: '작성', name: '김판매', dept: '판매팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n { step: 2, role: '검토', name: '이팀장', dept: '판매팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n { step: 3, role: '승인', name: '박본부장', dept: '판매본부', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n ],\n printOptions: {\n paperSize: 'A4',\n orientation: 'portrait',\n copies: 2,\n includeLogo: true,\n includeStamp: true,\n },\n summary: { total: 6, success: 6, fail: 0 },\n }\n }));\n }\n\n // 수주확인서 출력 테스트\n if (scenario === 'doc-order-confirm') {\n setWorkflowData(prev => ({\n ...prev,\n documentTest: {\n docType: '수주확인서',\n docTypeCode: 'OC',\n menuPath: '판매관리 > 수주관리',\n testCases: [\n { id: 1, action: '수주확인서 생성', status: '완료', timestamp: new Date().toLocaleTimeString() },\n { id: 2, action: '견적 연동 확인', status: '완료', linkedDoc: 'KD-PR-250109-01', timestamp: new Date().toLocaleTimeString() },\n { id: 3, action: '결재 상신', status: '완료', timestamp: new Date().toLocaleTimeString() },\n { id: 4, action: '생산팀 확인', status: '완료', approver: '생산팀장', timestamp: new Date().toLocaleTimeString() },\n { id: 5, action: '최종 승인', status: '완료', approver: '공장장', timestamp: new Date().toLocaleTimeString() },\n { id: 6, action: 'PDF 출력', status: '완료', format: 'A4', timestamp: new Date().toLocaleTimeString() },\n ],\n document: {\n docNo: `OC-${today.toISOString().slice(0, 10).replace(/-/g, '')}-001`,\n title: '수주확인서',\n customer: selectedCustomer?.name || '삼성물산(주)',\n orderNo: prev.order?.orderNo || 'KD-TS-251209-01',\n amount: prev.order?.totalAmount || 52800000,\n dueDate: prev.order?.dueDate || new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),\n createdAt: today.toISOString().slice(0, 10),\n status: '결재완료',\n },\n approvalLine: [\n { step: 1, role: '작성', name: '김판매', dept: '판매팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n { step: 2, role: '검토', name: '최생산팀장', dept: '생산팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n { step: 3, role: '승인', name: '정공장장', dept: '생산본부', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n ],\n summary: { total: 6, success: 6, fail: 0 },\n }\n }));\n }\n\n // 작업지시서 출력 테스트\n if (scenario === 'doc-work-order') {\n setWorkflowData(prev => ({\n ...prev,\n documentTest: {\n docType: '작업지시서',\n docTypeCode: 'WO',\n menuPath: '생산관리 > 작업지시관리',\n testCases: [\n { id: 1, action: '작업지시서 생성', status: '완료', count: 5, timestamp: new Date().toLocaleTimeString() },\n { id: 2, action: '수주 연동 확인', status: '완료', linkedDoc: 'KD-TS-251209-01', timestamp: new Date().toLocaleTimeString() },\n { id: 3, action: 'BOM 전개 확인', status: '완료', itemCount: 8, timestamp: new Date().toLocaleTimeString() },\n { id: 4, action: '결재 상신', status: '완료', timestamp: new Date().toLocaleTimeString() },\n { id: 5, action: '생산팀장 승인', status: '완료', approver: '생산팀장', timestamp: new Date().toLocaleTimeString() },\n { id: 6, action: '현장 배포', status: '완료', target: '생산현장', timestamp: new Date().toLocaleTimeString() },\n ],\n document: {\n docNo: `WO-${today.toISOString().slice(0, 10).replace(/-/g, '')}-001`,\n title: '작업지시서',\n orderNo: prev.order?.orderNo || 'KD-TS-251209-01',\n processes: ['스크린', '절곡', '샤프트', '조립', '전장'],\n qty: prev.inputs?.QTY || 2,\n startDate: today.toISOString().slice(0, 10),\n dueDate: prev.order?.dueDate || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),\n status: '결재완료',\n },\n approvalLine: [\n { step: 1, role: '작성', name: '박생산', dept: '생산관리팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n { step: 2, role: '승인', name: '최생산팀장', dept: '생산팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n ],\n summary: { total: 6, success: 6, fail: 0 },\n }\n }));\n }\n\n // 검사성적서 출력 테스트\n if (scenario === 'doc-inspection') {\n setWorkflowData(prev => ({\n ...prev,\n documentTest: {\n docType: '검사성적서',\n docTypeCode: 'IR',\n menuPath: '품질관리 > 검사관리',\n testCases: [\n { id: 1, action: '수입검사성적서(IQC) 생성', status: '완료', result: '합격', timestamp: new Date().toLocaleTimeString() },\n { id: 2, action: '중간검사성적서(PQC) 생성', status: '완료', result: '합격', count: 5, timestamp: new Date().toLocaleTimeString() },\n { id: 3, action: '제품검사성적서(FQC) 생성', status: '완료', result: '합격', timestamp: new Date().toLocaleTimeString() },\n { id: 4, action: 'QC팀장 검토', status: '완료', approver: 'QC팀장', timestamp: new Date().toLocaleTimeString() },\n { id: 5, action: '품질책임자 승인', status: '완료', approver: '품질책임자', timestamp: new Date().toLocaleTimeString() },\n { id: 6, action: 'PDF 출력 (고객제출용)', status: '완료', format: 'A4', timestamp: new Date().toLocaleTimeString() },\n ],\n document: {\n docNo: `IR-${today.toISOString().slice(0, 10).replace(/-/g, '')}-001`,\n title: '검사성적서',\n productionLot: prev.order?.productionLot || 'KD-WE-251209-01',\n inspectionTypes: ['IQC', 'PQC', 'FQC'],\n inspectionItems: [\n { name: '외관검사', spec: '스크래치, 변형 없음', result: '합격', value: 'OK' },\n { name: '치수검사', spec: '±3mm 이내', result: '합격', value: '2502x3148mm' },\n { name: '작동검사', spec: '정상작동', result: '합격', value: '5회 정상' },\n { name: '내화시험', spec: 'KS F 2268-1 합격', result: '합격', value: '60분 차염' },\n ],\n overallResult: '합격',\n createdAt: today.toISOString().slice(0, 10),\n status: '결재완료',\n },\n approvalLine: [\n { step: 1, role: '검사', name: '홍검사', dept: 'QC팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n { step: 2, role: '검토', name: '강QC팀장', dept: 'QC팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n { step: 3, role: '승인', name: '윤품질책임자', dept: '품질본부', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n ],\n summary: { total: 7, success: 7, fail: 0 },\n }\n }));\n }\n\n // 출하지시서 출력 테스트\n if (scenario === 'doc-shipment') {\n setWorkflowData(prev => ({\n ...prev,\n documentTest: {\n docType: '출하지시서',\n docTypeCode: 'SD',\n menuPath: '물류관리 > 출하관리',\n testCases: [\n { id: 1, action: '출하지시서 생성', status: '완료', timestamp: new Date().toLocaleTimeString() },\n { id: 2, action: '수주/생산 완료 확인', status: '완료', linkedDoc: 'KD-TS-251209-01', timestamp: new Date().toLocaleTimeString() },\n { id: 3, action: '검사성적서 첨부', status: '완료', attachment: 'IR-20251209-001', timestamp: new Date().toLocaleTimeString() },\n { id: 4, action: '물류팀장 승인', status: '완료', approver: '물류팀장', timestamp: new Date().toLocaleTimeString() },\n { id: 5, action: '거래명세서 자동생성', status: '완료', linkedDoc: 'IV-20251209-001', timestamp: new Date().toLocaleTimeString() },\n { id: 6, action: '출하 PDF 출력', status: '완료', format: 'A4', timestamp: new Date().toLocaleTimeString() },\n ],\n document: {\n docNo: `SD-${today.toISOString().slice(0, 10).replace(/-/g, '')}-001`,\n title: '출하지시서',\n orderNo: prev.order?.orderNo || 'KD-TS-251209-01',\n customer: selectedCustomer?.name || '삼성물산(주)',\n site: selectedSite?.name || '강남 현장',\n shipmentDate: today.toISOString().slice(0, 10),\n carrier: '자체배송',\n driverName: '배송기사A',\n vehicleNo: '서울12가3456',\n items: [\n { name: '방화스크린 KS60', spec: '2500x3000mm', qty: 2, unit: 'SET' },\n ],\n status: '결재완료',\n },\n approvalLine: [\n { step: 1, role: '작성', name: '조물류', dept: '물류팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n { step: 2, role: '승인', name: '한물류팀장', dept: '물류팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n ],\n summary: { total: 6, success: 6, fail: 0 },\n }\n }));\n }\n\n // 거래명세서 출력 테스트\n if (scenario === 'doc-invoice') {\n setWorkflowData(prev => ({\n ...prev,\n documentTest: {\n docType: '거래명세서',\n docTypeCode: 'IV',\n menuPath: '판매관리 > 출하관리',\n testCases: [\n { id: 1, action: '거래명세서 생성', status: '완료', timestamp: new Date().toLocaleTimeString() },\n { id: 2, action: '출하 연동 확인', status: '완료', linkedDoc: 'SD-20251209-001', timestamp: new Date().toLocaleTimeString() },\n { id: 3, action: '금액 검증', status: '완료', amount: '52,800,000원', timestamp: new Date().toLocaleTimeString() },\n { id: 4, action: '경리팀 확인', status: '완료', approver: '경리담당', timestamp: new Date().toLocaleTimeString() },\n { id: 5, action: '인감 날인', status: '완료', stampType: '법인인감', timestamp: new Date().toLocaleTimeString() },\n { id: 6, action: 'PDF 출력', status: '완료', copies: 3, timestamp: new Date().toLocaleTimeString() },\n ],\n document: {\n docNo: `IV-${today.toISOString().slice(0, 10).replace(/-/g, '')}-001`,\n title: '거래명세서',\n customer: selectedCustomer?.name || '삼성물산(주)',\n businessNo: '123-45-67890',\n representative: '대표이사',\n address: '서울시 강남구 테헤란로 123',\n supplyAmount: Math.round((prev.order?.totalAmount || 52800000) / 1.1),\n vatAmount: Math.round((prev.order?.totalAmount || 52800000) / 1.1 * 0.1),\n totalAmount: prev.order?.totalAmount || 52800000,\n items: [\n { name: '방화스크린 KS60', spec: '2500x3000mm', qty: 2, unitPrice: 24000000, amount: 48000000 },\n ],\n issueDate: today.toISOString().slice(0, 10),\n status: '발급완료',\n },\n approvalLine: [\n { step: 1, role: '작성', name: '이경리', dept: '경리팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n { step: 2, role: '확인', name: '박경리팀장', dept: '경리팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n ],\n summary: { total: 6, success: 6, fail: 0 },\n }\n }));\n }\n\n // 세금계산서 발행 테스트\n if (scenario === 'doc-tax') {\n setWorkflowData(prev => ({\n ...prev,\n documentTest: {\n docType: '세금계산서',\n docTypeCode: 'TX',\n menuPath: '회계관리 > 매출관리',\n testCases: [\n { id: 1, action: '세금계산서 초안 생성', status: '완료', timestamp: new Date().toLocaleTimeString() },\n { id: 2, action: '매출 데이터 연동', status: '완료', linkedDoc: 'KD-SH-251209-01', timestamp: new Date().toLocaleTimeString() },\n { id: 3, action: '사업자정보 검증', status: '완료', businessNo: '123-45-67890', timestamp: new Date().toLocaleTimeString() },\n { id: 4, action: '경리팀장 승인', status: '완료', approver: '경리팀장', timestamp: new Date().toLocaleTimeString() },\n { id: 5, action: '전자서명', status: '완료', signType: '공인인증', timestamp: new Date().toLocaleTimeString() },\n { id: 6, action: '국세청 전송', status: '완료', ntsSendResult: '전송성공', timestamp: new Date().toLocaleTimeString() },\n { id: 7, action: '승인번호 수신', status: '완료', approvalNo: '20251209-12345678-00001234', timestamp: new Date().toLocaleTimeString() },\n ],\n document: {\n docNo: `TX-${today.toISOString().slice(0, 10).replace(/-/g, '')}-001`,\n title: '전자세금계산서',\n taxType: '세금계산서',\n issueType: '전자발급',\n supplier: {\n businessNo: '111-22-33333',\n companyName: '(주)방화산업',\n representative: '대표이사',\n address: '서울시 금천구 가산디지털로 123',\n },\n buyer: {\n businessNo: '123-45-67890',\n companyName: selectedCustomer?.name || '삼성물산(주)',\n representative: '대표이사',\n },\n supplyAmount: Math.round((prev.order?.totalAmount || 52800000) / 1.1),\n vatAmount: Math.round((prev.order?.totalAmount || 52800000) / 1.1 * 0.1),\n totalAmount: prev.order?.totalAmount || 52800000,\n issueDate: today.toISOString().slice(0, 10),\n ntsApprovalNo: '20251209-12345678-00001234',\n status: '국세청 전송완료',\n },\n approvalLine: [\n { step: 1, role: '작성', name: '이경리', dept: '경리팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n { step: 2, role: '승인', name: '박경리팀장', dept: '경리팀', status: '완료', date: today.toISOString().slice(0, 10), signature: '✓' },\n { step: 3, role: '전자서명', name: '대표이사', dept: '경영진', status: '완료', date: today.toISOString().slice(0, 10), signature: '공인인증' },\n ],\n summary: { total: 7, success: 7, fail: 0 },\n }\n }));\n }\n\n // ═══════════════════════════════════════════════════════════════════════════\n // 결재 프로세스 테스트 시나리오\n // ═══════════════════════════════════════════════════════════════════════════\n\n // 정상 결재 프로세스 테스트\n if (scenario === 'approval-normal') {\n setWorkflowData(prev => ({\n ...prev,\n approvalTest: {\n testType: '정상 결재',\n menuPath: '전체 결재 프로세스',\n description: '작성 → 검토 → 승인 정상 흐름 테스트',\n testCases: [\n { id: 1, action: '문서 작성', status: '완료', actor: '김작성', role: '작성자', timestamp: new Date().toLocaleTimeString() },\n { id: 2, action: '결재 상신', status: '완료', actor: '김작성', nextApprover: '이검토', timestamp: new Date().toLocaleTimeString() },\n { id: 3, action: '검토자 확인', status: '완료', actor: '이검토', role: '검토자', action: '승인', timestamp: new Date().toLocaleTimeString() },\n { id: 4, action: '승인권자 확인', status: '완료', actor: '박승인', role: '승인권자', action: '최종승인', timestamp: new Date().toLocaleTimeString() },\n { id: 5, action: '결재 완료 알림', status: '완료', recipient: '김작성', notifyType: '이메일+푸시', timestamp: new Date().toLocaleTimeString() },\n { id: 6, action: '문서 상태 변경', status: '완료', beforeStatus: '결재중', afterStatus: '결재완료', timestamp: new Date().toLocaleTimeString() },\n ],\n approvalFlow: {\n docType: '견적서',\n docNo: 'KD-PR-251209-01',\n startTime: new Date(Date.now() - 3600000).toLocaleTimeString(),\n endTime: new Date().toLocaleTimeString(),\n totalDuration: '1시간 0분',\n steps: [\n { order: 1, role: '작성', name: '김작성', dept: '판매팀', status: '완료', timestamp: new Date(Date.now() - 3600000).toLocaleTimeString(), comment: '' },\n { order: 2, role: '검토', name: '이검토', dept: '판매팀', status: '완료', timestamp: new Date(Date.now() - 1800000).toLocaleTimeString(), comment: '내용 확인했습니다.' },\n { order: 3, role: '승인', name: '박승인', dept: '판매본부', status: '완료', timestamp: new Date().toLocaleTimeString(), comment: '승인합니다.' },\n ],\n },\n notifications: [\n { type: '상신알림', recipient: '이검토', sentAt: new Date(Date.now() - 3600000).toLocaleTimeString(), status: '발송완료' },\n { type: '검토완료알림', recipient: '박승인', sentAt: new Date(Date.now() - 1800000).toLocaleTimeString(), status: '발송완료' },\n { type: '결재완료알림', recipient: '김작성', sentAt: new Date().toLocaleTimeString(), status: '발송완료' },\n ],\n summary: { total: 6, success: 6, fail: 0 },\n }\n }));\n }\n\n // 결재 반려 테스트\n if (scenario === 'approval-reject') {\n setWorkflowData(prev => ({\n ...prev,\n approvalTest: {\n testType: '결재 반려',\n menuPath: '결재 프로세스',\n description: '검토자 반려 → 수정 → 재상신 테스트',\n testCases: [\n { id: 1, action: '문서 작성', status: '완료', actor: '김작성', timestamp: new Date().toLocaleTimeString() },\n { id: 2, action: '결재 상신', status: '완료', actor: '김작성', timestamp: new Date().toLocaleTimeString() },\n { id: 3, action: '검토자 반려', status: '완료', actor: '이검토', reason: '견적금액 재검토 필요', timestamp: new Date().toLocaleTimeString() },\n { id: 4, action: '반려 알림 수신', status: '완료', recipient: '김작성', timestamp: new Date().toLocaleTimeString() },\n { id: 5, action: '문서 수정', status: '완료', actor: '김작성', changes: ['금액 5% 조정'], timestamp: new Date().toLocaleTimeString() },\n { id: 6, action: '재상신', status: '완료', actor: '김작성', version: 'V2', timestamp: new Date().toLocaleTimeString() },\n { id: 7, action: '검토자 승인', status: '완료', actor: '이검토', comment: '수정 내용 확인', timestamp: new Date().toLocaleTimeString() },\n { id: 8, action: '최종 승인', status: '완료', actor: '박승인', timestamp: new Date().toLocaleTimeString() },\n ],\n approvalFlow: {\n docType: '견적서',\n docNo: 'KD-PR-251209-02',\n version: 'V2',\n rejectionHistory: [\n {\n version: 'V1',\n rejectedBy: '이검토',\n rejectReason: '견적금액 재검토 필요',\n rejectDate: today.toISOString().slice(0, 10),\n modifiedFields: ['totalAmount', 'discountRate'],\n }\n ],\n steps: [\n { order: 1, role: '작성', name: '김작성', status: '완료', version: 'V1→V2' },\n { order: 2, role: '검토', name: '이검토', status: '완료', action: '반려→승인' },\n { order: 3, role: '승인', name: '박승인', status: '완료' },\n ],\n },\n summary: { total: 8, success: 8, fail: 0 },\n }\n }));\n }\n\n // 결재 대결 테스트\n if (scenario === 'approval-delegate') {\n setWorkflowData(prev => ({\n ...prev,\n approvalTest: {\n testType: '결재 대결',\n menuPath: '결재 프로세스',\n description: '부재 시 대결처리 테스트',\n testCases: [\n { id: 1, action: '문서 작성', status: '완료', actor: '김작성', timestamp: new Date().toLocaleTimeString() },\n { id: 2, action: '결재 상신', status: '완료', actor: '김작성', timestamp: new Date().toLocaleTimeString() },\n { id: 3, action: '검토자 부재 확인', status: '완료', absentee: '이검토', reason: '출장', period: '12/9~12/11', timestamp: new Date().toLocaleTimeString() },\n { id: 4, action: '대결자 자동 지정', status: '완료', delegate: '최대결', originalApprover: '이검토', timestamp: new Date().toLocaleTimeString() },\n { id: 5, action: '대결 처리', status: '완료', actor: '최대결', comment: '이검토 대결', timestamp: new Date().toLocaleTimeString() },\n { id: 6, action: '최종 승인', status: '완료', actor: '박승인', timestamp: new Date().toLocaleTimeString() },\n { id: 7, action: '대결 이력 기록', status: '완료', logged: true, timestamp: new Date().toLocaleTimeString() },\n ],\n approvalFlow: {\n docType: '작업지시서',\n docNo: 'KD-WO-251209-01',\n delegateInfo: {\n originalApprover: '이검토',\n delegate: '최대결',\n delegateReason: '출장',\n delegatePeriod: '2025-12-09 ~ 2025-12-11',\n delegateRegisteredAt: '2025-12-08',\n },\n steps: [\n { order: 1, role: '작성', name: '김작성', status: '완료' },\n { order: 2, role: '검토', name: '최대결', status: '완료', note: '이검토 대결', originalApprover: '이검토' },\n { order: 3, role: '승인', name: '박승인', status: '완료' },\n ],\n },\n summary: { total: 7, success: 7, fail: 0 },\n }\n }));\n }\n\n // 긴급 결재 테스트\n if (scenario === 'approval-urgent') {\n setWorkflowData(prev => ({\n ...prev,\n approvalTest: {\n testType: '긴급 결재',\n menuPath: '결재 프로세스',\n description: '긴급 결재선 단축 처리 테스트',\n testCases: [\n { id: 1, action: '긴급 문서 생성', status: '완료', urgentLevel: '긴급', timestamp: new Date().toLocaleTimeString() },\n { id: 2, action: '긴급 결재 요청', status: '완료', actor: '김작성', reason: '납기 긴급', timestamp: new Date().toLocaleTimeString() },\n { id: 3, action: '결재선 자동 단축', status: '완료', originalSteps: 3, reducedSteps: 2, timestamp: new Date().toLocaleTimeString() },\n { id: 4, action: '긴급 알림 발송', status: '완료', notifyType: 'SMS+카카오톡+푸시', timestamp: new Date().toLocaleTimeString() },\n { id: 5, action: '승인권자 즉시 확인', status: '완료', actor: '박승인', responseTime: '5분', timestamp: new Date().toLocaleTimeString() },\n { id: 6, action: '긴급 결재 완료', status: '완료', totalTime: '15분', timestamp: new Date().toLocaleTimeString() },\n ],\n approvalFlow: {\n docType: '출하지시서',\n docNo: 'SD-20251209-001',\n urgentInfo: {\n isUrgent: true,\n urgentLevel: '긴급',\n urgentReason: '고객 긴급 요청 - 오늘 출하 필요',\n originalApprovalLine: ['작성→검토→승인'],\n reducedApprovalLine: ['작성→승인'],\n skippedSteps: ['검토'],\n notificationChannels: ['SMS', '카카오톡', '푸시알림', '이메일'],\n },\n steps: [\n { order: 1, role: '작성', name: '김작성', status: '완료', timestamp: new Date(Date.now() - 900000).toLocaleTimeString() },\n { order: 2, role: '승인', name: '박승인', status: '완료', timestamp: new Date().toLocaleTimeString(), note: '긴급결재' },\n ],\n totalProcessTime: '15분',\n normalProcessTime: '평균 2시간',\n timeSaved: '1시간 45분',\n },\n summary: { total: 6, success: 6, fail: 0 },\n }\n }));\n }\n\n // 문서 전체흐름 테스트 (모든 문서 + 결재 통합)\n if (scenario === 'doc-full-flow') {\n setWorkflowData(prev => ({\n ...prev,\n fullDocumentFlow: {\n testType: '문서 전체흐름 테스트',\n description: '견적 → 수주 → 생산 → 검사 → 출하 → 회계 전 과정의 문서출력 및 결재 통합 테스트',\n testPhases: [\n {\n phase: '판매단계',\n documents: [\n { docType: '견적서', docNo: 'KD-PR-251209-01', status: '결재완료', approvalTime: '30분' },\n { docType: '수주확인서', docNo: 'OC-20251209-001', status: '결재완료', approvalTime: '45분' },\n ],\n approvalCount: 2,\n totalApprovals: 6,\n },\n {\n phase: '생산단계',\n documents: [\n { docType: '작업지시서', docNo: 'KD-WO-251209-01', status: '결재완료', approvalTime: '20분', count: 5 },\n { docType: '생산일보', docNo: 'PR-20251209-001', status: '작성완료', count: 5 },\n ],\n approvalCount: 1,\n totalApprovals: 2,\n },\n {\n phase: '품질단계',\n documents: [\n { docType: '수입검사성적서', docNo: 'IQC-20251209-001', status: '결재완료', approvalTime: '15분' },\n { docType: '중간검사성적서', docNo: 'PQC-20251209-001', status: '결재완료', approvalTime: '15분', count: 5 },\n { docType: '제품검사성적서', docNo: 'FQC-20251209-001', status: '결재완료', approvalTime: '20분' },\n ],\n approvalCount: 3,\n totalApprovals: 9,\n },\n {\n phase: '물류단계',\n documents: [\n { docType: '출하지시서', docNo: 'SD-20251209-001', status: '결재완료', approvalTime: '15분' },\n { docType: '거래명세서', docNo: 'IV-20251209-001', status: '발급완료', approvalTime: '10분' },\n ],\n approvalCount: 2,\n totalApprovals: 4,\n },\n {\n phase: '회계단계',\n documents: [\n { docType: '매출전표', docNo: 'KD-SH-251209-01', status: '등록완료' },\n { docType: '세금계산서', docNo: 'TX-20251209-001', status: '국세청전송완료', approvalTime: '30분' },\n ],\n approvalCount: 1,\n totalApprovals: 3,\n },\n ],\n testSummary: {\n totalDocuments: 14,\n totalApprovals: 27,\n totalApprovalTime: '3시간 30분',\n avgApprovalTime: '15분/건',\n documentTypes: ['견적서', '수주확인서', '작업지시서', '생산일보', '검사성적서(4종)', '출하지시서', '거래명세서', '매출전표', '세금계산서'],\n approvalRoles: ['작성자', '검토자', '승인권자', '품질책임자', '경리팀장', '대표이사'],\n },\n testCases: [\n { id: 1, phase: '판매', action: '견적서 결재', status: '완료' },\n { id: 2, phase: '판매', action: '수주확인서 결재', status: '완료' },\n { id: 3, phase: '생산', action: '작업지시서 결재', status: '완료' },\n { id: 4, phase: '품질', action: '검사성적서 결재 (4건)', status: '완료' },\n { id: 5, phase: '물류', action: '출하지시서 결재', status: '완료' },\n { id: 6, phase: '물류', action: '거래명세서 발급', status: '완료' },\n { id: 7, phase: '회계', action: '세금계산서 발행', status: '완료' },\n { id: 8, phase: '통합', action: '전체 문서 PDF 생성', status: '완료' },\n ],\n summary: { total: 8, success: 8, fail: 0 },\n }\n }));\n }\n\n // ═══════════════════════════════════════════════════════════════════════════\n // MVP 핵심 테스트 시나리오 (수주→작업지시→자재투입→작업실적→검사→출하)\n // ═══════════════════════════════════════════════════════════════════════════\n\n // MVP 핵심흐름 테스트\n if (scenario === 'mvp-core') {\n const yymmdd = `${String(today.getFullYear()).slice(-2)}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`;\n setWorkflowData(prev => ({\n ...prev,\n mvpCoreTest: {\n testType: 'MVP 핵심흐름 테스트',\n description: '수주 → 작업지시 → 자재투입 → 작업실적 → 검사 → 출하 MVP 핵심 프로세스',\n menuPath: 'MVP 핵심 메뉴: 품목관리, 생산현황판, 작업지시관리, 작업실적, 작업자화면',\n testSteps: [\n { step: 1, name: '수주 등록', menu: '판매관리 > 수주관리', status: '완료', orderNo: `KD-TS-${yymmdd}-01` },\n { step: 2, name: '작업지시 생성', menu: '생산관리 > 작업지시관리', status: '완료', woCount: 5 },\n { step: 3, name: '자재투입', menu: '생산관리 > 작업지시관리 > 자재투입', status: '완료', materials: 8 },\n { step: 4, name: '작업실적 입력', menu: '생산관리 > 작업실적', status: '완료', resultCount: 5 },\n { step: 5, name: '품질검사', menu: '품질관리 > 검사관리', status: '완료', inspCount: 6 },\n { step: 6, name: '출하처리', menu: '판매관리 > 출하관리', status: '완료', shipQty: prev.inputs?.QTY || 2 },\n ],\n workOrders: [\n { woNo: `KD-TS-${yymmdd}-01-W01`, process: '스크린', status: '완료', qty: prev.inputs?.QTY || 2 },\n { woNo: `KD-TS-${yymmdd}-01-W02`, process: '절곡', status: '완료', qty: prev.inputs?.QTY || 2 },\n { woNo: `KD-TS-${yymmdd}-01-W03`, process: '샤프트', status: '완료', qty: prev.inputs?.QTY || 2 },\n { woNo: `KD-TS-${yymmdd}-01-W04`, process: '조립', status: '완료', qty: prev.inputs?.QTY || 2 },\n { woNo: `KD-TS-${yymmdd}-01-W05`, process: '전장', status: '완료', qty: prev.inputs?.QTY || 2 },\n ],\n summary: { total: 6, success: 6, fail: 0 },\n testStatus: 'MVP 핵심흐름 테스트 통과',\n }\n }));\n }\n\n // MVP 자재투입 테스트\n if (scenario === 'mvp-material-input') {\n const yymmdd = `${String(today.getFullYear()).slice(-2)}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`;\n setWorkflowData(prev => ({\n ...prev,\n mvpMaterialInput: {\n testType: 'MVP 자재투입 테스트',\n description: '작업지시에서 BOM 기반 자재 자동투입 및 수동조정',\n menuPath: '생산관리 > 작업지시관리 > 자재투입',\n testCases: [\n { id: 1, action: 'BOM 전개', data: { orderNo: `KD-TS-${yymmdd}-01`, items: 8 }, result: '성공' },\n { id: 2, action: '자재 자동투입', data: { items: 8, totalQty: 16 }, result: '성공' },\n { id: 3, action: '재고 확인', data: { available: 8, shortage: 0 }, result: '성공' },\n { id: 4, action: '투입량 조정', data: { item: 'SL-001', before: 2, after: 3 }, result: '성공' },\n { id: 5, action: '투입 확정', data: { totalItems: 8, status: '투입완료' }, result: '성공' },\n ],\n materials: [\n { code: 'SL-001', name: '슬랫', unit: 'EA', required: 4, input: 4, status: '투입완료' },\n { code: 'SC-001', name: '스크린', unit: 'M', required: 10, input: 10, status: '투입완료' },\n { code: 'SH-001', name: '샤프트', unit: 'EA', required: 2, input: 2, status: '투입완료' },\n { code: 'MT-001', name: '모터', unit: 'EA', required: 2, input: 2, status: '투입완료' },\n { code: 'CB-001', name: '제어반', unit: 'EA', required: 2, input: 2, status: '투입완료' },\n { code: 'BR-001', name: '브라켓', unit: 'EA', required: 8, input: 8, status: '투입완료' },\n { code: 'BT-001', name: '볼트세트', unit: 'SET', required: 4, input: 4, status: '투입완료' },\n { code: 'LB-001', name: '라벨', unit: 'EA', required: 4, input: 4, status: '투입완료' },\n ],\n summary: { total: 5, success: 5, fail: 0 },\n testStatus: 'MVP 자재투입 테스트 통과',\n }\n }));\n }\n\n // MVP 작업실적 테스트\n if (scenario === 'mvp-work-result') {\n const yymmdd = `${String(today.getFullYear()).slice(-2)}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`;\n setWorkflowData(prev => ({\n ...prev,\n mvpWorkResult: {\n testType: 'MVP 작업실적 테스트',\n description: '공정별 작업실적 입력 및 완료처리',\n menuPath: '생산관리 > 작업실적',\n testCases: [\n { id: 1, action: '스크린공정 실적입력', process: '스크린', qty: 2, goodQty: 2, defectQty: 0, result: '성공' },\n { id: 2, action: '절곡공정 실적입력', process: '절곡', qty: 2, goodQty: 2, defectQty: 0, result: '성공' },\n { id: 3, action: '샤프트공정 실적입력', process: '샤프트', qty: 2, goodQty: 2, defectQty: 0, result: '성공' },\n { id: 4, action: '조립공정 실적입력', process: '조립', qty: 2, goodQty: 2, defectQty: 0, result: '성공' },\n { id: 5, action: '전장공정 실적입력', process: '전장', qty: 2, goodQty: 2, defectQty: 0, result: '성공' },\n { id: 6, action: '생산완료 처리', data: { totalQty: 2, status: '생산완료' }, result: '성공' },\n ],\n workResults: [\n { woNo: `KD-TS-${yymmdd}-01-W01`, process: '스크린', worker: '김작업', startTime: '09:00', endTime: '11:00', qty: 2, goodQty: 2, status: '완료' },\n { woNo: `KD-TS-${yymmdd}-01-W02`, process: '절곡', worker: '이작업', startTime: '11:00', endTime: '13:00', qty: 2, goodQty: 2, status: '완료' },\n { woNo: `KD-TS-${yymmdd}-01-W03`, process: '샤프트', worker: '박작업', startTime: '14:00', endTime: '15:30', qty: 2, goodQty: 2, status: '완료' },\n { woNo: `KD-TS-${yymmdd}-01-W04`, process: '조립', worker: '최작업', startTime: '15:30', endTime: '17:00', qty: 2, goodQty: 2, status: '완료' },\n { woNo: `KD-TS-${yymmdd}-01-W05`, process: '전장', worker: '정작업', startTime: '17:00', endTime: '18:00', qty: 2, goodQty: 2, status: '완료' },\n ],\n summary: { total: 6, success: 6, fail: 0 },\n testStatus: 'MVP 작업실적 테스트 통과',\n }\n }));\n }\n\n // MVP 작업자화면 테스트\n if (scenario === 'mvp-worker-screen') {\n const yymmdd = `${String(today.getFullYear()).slice(-2)}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`;\n setWorkflowData(prev => ({\n ...prev,\n mvpWorkerScreen: {\n testType: 'MVP 작업자화면 테스트',\n description: '현장 작업자용 간편 입력 화면 테스트',\n menuPath: '생산관리 > 작업자화면',\n testCases: [\n { id: 1, action: '작업자 로그인', data: { workerId: 'W001', name: '김작업' }, result: '성공' },\n { id: 2, action: '할당작업 조회', data: { assignedWO: 3, status: '대기' }, result: '성공' },\n { id: 3, action: '작업시작 처리', data: { woNo: `KD-TS-${yymmdd}-01-W01`, startTime: '09:00' }, result: '성공' },\n { id: 4, action: '진행상황 업데이트', data: { progress: 50, currentQty: 1 }, result: '성공' },\n { id: 5, action: '불량등록', data: { defectType: '외관불량', qty: 0 }, result: '성공' },\n { id: 6, action: '작업완료 처리', data: { endTime: '11:00', goodQty: 2 }, result: '성공' },\n { id: 7, action: '다음작업 할당', data: { nextWO: `KD-TS-${yymmdd}-01-W02` }, result: '성공' },\n ],\n workerInfo: {\n workerId: 'W001',\n name: '김작업',\n department: '생산1팀',\n assignedProcess: '스크린',\n todayTasks: 3,\n completedTasks: 1,\n performance: '100%',\n },\n summary: { total: 7, success: 7, fail: 0 },\n testStatus: 'MVP 작업자화면 테스트 통과',\n }\n }));\n }\n\n // MVP 생산현황판 테스트\n if (scenario === 'mvp-production-board') {\n setWorkflowData(prev => ({\n ...prev,\n mvpProductionBoard: {\n testType: 'MVP 생산현황판 테스트',\n description: '실시간 생산현황 모니터링 대시보드',\n menuPath: '생산관리 > 생산현황판',\n testCases: [\n { id: 1, action: '전체현황 조회', data: { totalWO: 15, inProgress: 5, completed: 8 }, result: '성공' },\n { id: 2, action: '공정별 현황', data: { processes: 5, bottleneck: '없음' }, result: '성공' },\n { id: 3, action: '실시간 갱신', data: { refreshRate: '10초', lastUpdate: new Date().toLocaleTimeString() }, result: '성공' },\n { id: 4, action: '납기지연 알림', data: { delayedOrders: 0, warningOrders: 1 }, result: '성공' },\n { id: 5, action: '작업자별 현황', data: { activeWorkers: 5, idleWorkers: 0 }, result: '성공' },\n { id: 6, action: '일일 생산실적', data: { planned: 10, actual: 8, achievement: '80%' }, result: '성공' },\n ],\n dashboardData: {\n todayOrders: 5,\n inProgressWO: 3,\n completedWO: 12,\n delayedWO: 0,\n processStatus: [\n { process: '스크린', queue: 2, inProgress: 1, completed: 5 },\n { process: '절곡', queue: 3, inProgress: 1, completed: 4 },\n { process: '샤프트', queue: 2, inProgress: 1, completed: 5 },\n { process: '조립', queue: 1, inProgress: 1, completed: 6 },\n { process: '전장', queue: 2, inProgress: 0, completed: 6 },\n ],\n alerts: [\n { type: 'warning', message: '주문 KD-TS-251210-01 납기 D-3', time: new Date().toLocaleTimeString() },\n ],\n },\n summary: { total: 6, success: 6, fail: 0 },\n testStatus: 'MVP 생산현황판 테스트 통과',\n }\n }));\n }\n\n // MVP 재고현황 테스트\n if (scenario === 'mvp-stock-status') {\n setWorkflowData(prev => ({\n ...prev,\n mvpStockStatus: {\n testType: 'MVP 재고현황 테스트',\n description: '자재/제품 재고현황 조회',\n menuPath: '자재관리 > 재고현황',\n testCases: [\n { id: 1, action: '전체재고 조회', data: { totalItems: 50, totalQty: 1250 }, result: '성공' },\n { id: 2, action: '품목별 조회', data: { itemCode: 'SL-001', currentQty: 100, unit: 'EA' }, result: '성공' },\n { id: 3, action: '안전재고 확인', data: { belowSafety: 3, warningItems: ['MT-001', 'CB-001', 'BR-001'] }, result: '성공' },\n { id: 4, action: '창고별 조회', data: { warehouses: 2, mainStock: 1000, subStock: 250 }, result: '성공' },\n { id: 5, action: '재고이력 조회', data: { item: 'SL-001', period: '최근30일', movements: 25 }, result: '성공' },\n ],\n stockData: [\n { code: 'SL-001', name: '슬랫', category: '원자재', qty: 100, safetyStock: 50, status: '정상' },\n { code: 'SC-001', name: '스크린', category: '원자재', qty: 200, safetyStock: 100, status: '정상' },\n { code: 'MT-001', name: '모터', category: '부품', qty: 15, safetyStock: 20, status: '부족' },\n { code: 'CB-001', name: '제어반', category: '부품', qty: 10, safetyStock: 15, status: '부족' },\n { code: 'FG-001', name: '완제품', category: '제품', qty: 5, safetyStock: 0, status: '정상' },\n ],\n summary: { total: 5, success: 5, fail: 0 },\n testStatus: 'MVP 재고현황 테스트 통과',\n }\n }));\n }\n\n // MVP 입고관리 테스트\n if (scenario === 'mvp-inbound') {\n setWorkflowData(prev => ({\n ...prev,\n mvpInbound: {\n testType: 'MVP 입고관리 테스트',\n description: '자재입고 등록 및 처리',\n menuPath: '자재관리 > 입고관리',\n testCases: [\n { id: 1, action: '입고예정 조회', data: { expectedItems: 5, supplier: '대한알루미늄' }, result: '성공' },\n { id: 2, action: '입고등록', data: { poNo: 'KD-SO-251209-01', items: 3, totalQty: 150 }, result: '성공' },\n { id: 3, action: '검수확인', data: { inspResult: '합격', passQty: 150, failQty: 0 }, result: '성공' },\n { id: 4, action: '입고확정', data: { inboundNo: 'KD-IN-251209-01', warehouse: '본창고' }, result: '성공' },\n { id: 5, action: '재고반영', data: { updatedItems: 3, totalIncreased: 150 }, result: '성공' },\n { id: 6, action: '입고이력 조회', data: { period: '오늘', totalInbound: 2 }, result: '성공' },\n ],\n inboundData: [\n {\n inboundNo: 'KD-IN-251209-01', poNo: 'KD-SO-251209-01', supplier: '대한알루미늄',\n items: [\n { code: 'SL-001', name: '슬랫', qty: 50, unit: 'EA', status: '입고완료' },\n { code: 'SC-001', name: '스크린', qty: 50, unit: 'M', status: '입고완료' },\n { code: 'BR-001', name: '브라켓', qty: 50, unit: 'EA', status: '입고완료' },\n ],\n inboundDate: today.toISOString().slice(0, 10),\n status: '입고완료',\n }\n ],\n summary: { total: 6, success: 6, fail: 0 },\n testStatus: 'MVP 입고관리 테스트 통과',\n }\n }));\n }\n\n // MVP 전체흐름 테스트\n if (scenario === 'mvp-full-flow') {\n const yymmdd = `${String(today.getFullYear()).slice(-2)}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`;\n setWorkflowData(prev => ({\n ...prev,\n mvpFullFlow: {\n testType: 'MVP 전체흐름 통합 테스트',\n description: 'MVP 전 과정(수주→작업지시→자재투입→실적→검사→출하) 통합 테스트',\n menuPath: 'MVP 메뉴 전체',\n mvpMenus: {\n production: ['품목관리', '생산현황판', '작업지시관리', '작업실적', '작업자화면'],\n inventory: ['재고현황', '입고관리'],\n quality: ['검사기준', '검사실적'],\n sales: ['견적관리', '수주관리', '출하관리'],\n },\n testPhases: [\n {\n phase: '수주단계',\n tests: [\n { name: '견적등록', status: '✅ 통과', time: '0.5s' },\n { name: '수주전환', status: '✅ 통과', time: '0.3s' },\n ],\n },\n {\n phase: '생산준비',\n tests: [\n { name: '작업지시생성', status: '✅ 통과', time: '0.4s' },\n { name: 'BOM전개', status: '✅ 통과', time: '0.3s' },\n { name: '자재투입', status: '✅ 통과', time: '0.5s' },\n ],\n },\n {\n phase: '생산실행',\n tests: [\n { name: '작업자할당', status: '✅ 통과', time: '0.2s' },\n { name: '공정별실적', status: '✅ 통과', time: '0.8s' },\n { name: '현황판갱신', status: '✅ 통과', time: '0.2s' },\n ],\n },\n {\n phase: '품질검사',\n tests: [\n { name: '중간검사', status: '✅ 통과', time: '0.4s' },\n { name: '제품검사', status: '✅ 통과', time: '0.3s' },\n ],\n },\n {\n phase: '출하/완료',\n tests: [\n { name: '출하처리', status: '✅ 통과', time: '0.3s' },\n { name: '재고차감', status: '✅ 통과', time: '0.2s' },\n { name: '완료처리', status: '✅ 통과', time: '0.2s' },\n ],\n },\n ],\n orderInfo: {\n orderNo: `KD-TS-${yymmdd}-01`,\n customer: selectedCustomer?.name || '삼성물산(주)',\n product: '방화셔터 KS60',\n qty: prev.inputs?.QTY || 2,\n status: '출하완료',\n },\n testSummary: {\n totalTests: 13,\n passed: 13,\n failed: 0,\n passRate: '100%',\n totalTime: '4.1s',\n coverage: 'MVP 핵심기능 100%',\n },\n summary: { total: 13, success: 13, fail: 0 },\n testStatus: 'MVP 전체흐름 통합 테스트 통과',\n }\n }));\n }\n\n setCurrentStep(8);\n };\n\n return (\n
\n {/* 시나리오 선택 */}\n
\n
\n \n 테스트 시나리오 선택\n
\n
\n {testScenarios.map(sc => (\n
\n ))}\n
\n
\n\n {/* 진행 단계 표시 */}\n
\n
\n
\n \n 전체 업무 흐름 테스트\n {selectedScenario !== 'normal' && (\n \n {testScenarios.find(s => s.id === selectedScenario)?.icon} {testScenarios.find(s => s.id === selectedScenario)?.label}\n \n )}\n
\n
\n \n \n
\n
\n\n {/* 전체 사이클 테스트 결과 - 개선된 대시보드 */}\n {selectedScenario === 'full-cycle' && testResults.length > 0 && (\n
\n {/* 테스트 요약 대시보드 */}\n {testSummary && (\n
\n
\n
\n 🎯 전체 테스트 완료\n
\n {testSummary.completedAt}\n \n
\n
\n
{testSummary.totalScenarios}
\n
전체 시나리오
\n
\n
\n
{testSummary.passed}
\n
통과
\n
\n
\n
{testSummary.failed}
\n
실패
\n
\n
\n
{testSummary.passRate}%
\n
통과율
\n
\n
\n
{testSummary.totalDuration}
\n
총 소요시간
\n
\n
\n {/* 진행률 바 */}\n
\n
\n 테스트 진행률\n {testSummary.passRate}% 통과\n
\n
\n
\n
\n )}\n\n {/* 개별 시나리오 결과 목록 */}\n
\n
\n 📋 시나리오별 테스트 결과\n \n ({testResults.filter(r => r.passed).length} / {testResults.length} 완료)\n \n
\n
\n {testResults.map((result, idx) => (\n
\n
\n {result.icon}\n {result.passed === true ? :\n result.passed === false ? :\n }\n
\n
{result.scenario}
\n
\n {result.startTime}\n {result.duration && {result.duration}}\n
\n
\n ))}\n
\n
\n\n {/* 카테고리별 테스트 결과 분류 */}\n {testSummary && (\n
\n
📊 카테고리별 테스트 현황
\n
\n {/* 업무 프로세스 */}\n
\n
📦 업무 프로세스
\n
\n {['normal', 'quote-cancel', 'quote-expire', 'rush-order', 'size-change'].map(id => {\n const result = allTestResults[id];\n return result ? (\n
\n {result.label}\n \n {result.passed ? '✓' : '✗'}\n \n
\n ) : null;\n })}\n
\n
\n {/* 품질/검사 */}\n
\n
🔍 품질/검사
\n
\n {['quality-fail', 'quality-conditional', 'multi-inspection'].map(id => {\n const result = allTestResults[id];\n return result ? (\n
\n {result.label}\n \n {result.passed ? '✓' : '✗'}\n \n
\n ) : null;\n })}\n
\n
\n {/* 기준정보 */}\n
\n
⚙️ 기준정보
\n
\n {['new-item', 'numbering-test', 'code-reference'].map(id => {\n const result = allTestResults[id];\n return result ? (\n
\n {result.label}\n \n {result.passed ? '✓' : '✗'}\n \n
\n ) : null;\n })}\n
\n
\n {/* 회계관리 */}\n
\n
💰 회계관리
\n
\n {['payment-delay', 'partial-shipment', 'accounting-sales', 'accounting-purchase', 'accounting-cashbook', 'accounting-collection', 'accounting-cost'].map(id => {\n const result = allTestResults[id];\n return result ? (\n
\n {result.label}\n \n {result.passed ? '✓' : '✗'}\n \n
\n ) : null;\n })}\n
\n
\n
\n
\n )}\n
\n )}\n\n {/* 단계 진행 표시 */}\n
\n {steps.map((step, idx) => (\n
\n \n \n {step.label}\n {idx < currentStep && }\n
\n {idx < steps.length - 1 && (\n \n )}\n \n ))}\n
\n
\n\n
\n {/* 왼쪽: 입력/현재 단계 정보 */}\n
\n {/* Step 0: 견적 입력 */}\n {currentStep === 0 && (\n
\n
\n \n Step 1: 견적 입력\n
\n\n {/* 거래처/현장 선택 */}\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n {/* 제품 유형 */}\n
\n
\n
\n {['스크린', '철재'].map(type => (\n \n ))}\n
\n
\n\n {/* 오픈사이즈 */}\n
\n\n {/* 옵션 설정 */}\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n
\n
\n )}\n\n {/* Step 1: 자동 산출 결과 */}\n {currentStep >= 1 && workflowData.calculatedResult && (\n
\n
\n \n 자동 산출 결과\n
\n\n {/* 요약 정보 */}\n
\n
\n
제작사이즈
\n
{workflowData.calculatedResult.summary.productionSize}
\n
\n
\n
면적
\n
{workflowData.calculatedResult.summary.area} ㎡
\n
\n
\n
중량
\n
{workflowData.calculatedResult.summary.weight} kg
\n
\n
\n
모터
\n
{workflowData.calculatedResult.summary.motorCapacity}
\n
\n
\n\n {/* 품목 리스트 */}\n
\n
\n \n \n | 품목 | \n 규격 | \n 수량 | \n 금액 | \n
\n \n \n {workflowData.calculatedResult.items.map(item => (\n \n | {item.itemName} | \n {item.spec} | \n {item.qty} | \n {item.amount.toLocaleString()} | \n
\n ))}\n \n
\n
\n\n {/* 합계 */}\n
\n
\n 총 견적금액 (VAT 포함)\n {workflowData.calculatedResult.grandTotal.toLocaleString()}원\n
\n
\n
\n )}\n\n {/* 다음 단계 실행 버튼 */}\n {currentStep < 8 && (\n
\n )}\n\n {currentStep === 8 && (\n
\n
\n
전체 프로세스 완료!
\n
견적 → 수주 → 생산 → 품질 → 출하 → 회계 전 과정이 완료되었습니다.
\n
\n
\n )}\n
\n\n {/* 오른쪽: 생성된 데이터 표시 */}\n
\n {/* 견적 정보 */}\n {workflowData.quote && (\n
\n
\n \n 견적 정보\n
\n
\n
번호: {workflowData.quote.quoteNo}
\n
상태: {workflowData.quote.status}
\n
거래처: {workflowData.quote.customerName}
\n
현장: {workflowData.quote.siteName}
\n
금액: {formatCurrency(workflowData.quote.finalAmount)}
\n
\n
\n )}\n\n {/* 수주 정보 */}\n {workflowData.order && (\n
\n
\n \n 수주 정보\n
\n
\n
수주번호: {workflowData.order.orderNo}
\n
상태: {workflowData.order.status}
\n
납기일: {workflowData.order.dueDate}
\n
금액: {formatCurrency(workflowData.order.totalAmount)}
\n
\n
\n )}\n\n {/* 생산 정보 */}\n {workflowData.workOrders.length > 0 && (\n
\n
\n \n 생산 지시 ({workflowData.workOrders.length}건)\n
\n
\n {workflowData.workOrders.map(wo => (\n
\n {wo.workOrderNo}\n {wo.processName}\n {wo.status}\n
\n ))}\n
\n
\n )}\n\n {/* 품질 검사 정보 */}\n {workflowData.qualityInspections.length > 0 && (\n
\n
\n \n 품질 검사 ({workflowData.qualityInspections.length}건)\n
\n
\n {workflowData.qualityInspections.slice(0, 3).map(qi => (\n
\n {qi.inspectionNo}\n {qi.inspectionType}\n {qi.result}\n
\n ))}\n {workflowData.qualityInspections.length > 3 && (\n
... 외 {workflowData.qualityInspections.length - 3}건
\n )}\n
\n
\n )}\n\n {/* 출하 정보 */}\n {workflowData.shipment && (\n
\n
\n \n 출하 정보\n
\n
\n
출하번호: {workflowData.shipment.shipmentNo}
\n
상태: {workflowData.shipment.status}
\n
출하일: {workflowData.shipment.shipmentDate}
\n
운송: {workflowData.shipment.carrier}
\n
\n
\n )}\n\n {/* 회계 정보 */}\n {workflowData.sales && (\n
\n
\n \n 회계 정보\n
\n
\n
\n
매출번호: {workflowData.sales.salesNo}
\n
상태: {workflowData.sales.paymentStatus}
\n
공급가: {formatCurrency(workflowData.sales.supplyAmount)}
\n
VAT: {formatCurrency(workflowData.sales.vatAmount)}
\n
\n
\n 합계: {formatCurrency(workflowData.sales.totalAmount)}\n
\n\n {workflowData.collection && (\n
\n
💰 수금 완료
\n
\n {workflowData.collection.collectionNo} | {workflowData.collection.paymentMethod} | {workflowData.collection.collectionDate}\n
\n
\n )}\n
\n
\n )}\n\n {/* 신규 품목 등록 결과 */}\n {workflowData.newItem && (\n
\n
\n 📋 신규 품목 등록\n
\n
\n
\n
품목코드: {workflowData.newItem.itemCode}
\n
품목명: {workflowData.newItem.itemName}
\n
분류: {workflowData.newItem.category}
\n
유형: {workflowData.newItem.itemType}
\n
\n
\n 📍 메뉴 경로: {workflowData.newItem.menuPath}\n
\n
\n
\n )}\n\n {/* 검사유형 전환 결과 */}\n {workflowData.inspectionFlow && (\n
\n
\n 🔍 검사유형 통합 테스트\n
\n
\n {['iqc', 'pqc', 'fqc'].map(key => {\n const insp = workflowData.inspectionFlow[key];\n return (\n
\n {insp.typeCode}\n {insp.inspectionType}\n {insp.result}\n
\n );\n })}\n
\n 📍 메뉴 경로: {workflowData.inspectionFlow.menuPath}\n
\n
\n
\n )}\n\n {/* 채번 테스트 결과 */}\n {workflowData.numberingResults && (\n
\n
\n 🔢 채번관리 테스트\n
\n
\n {workflowData.numberingResults.rules.map((rule, idx) => (\n
\n
\n {rule.ruleName}\n {rule.pattern}\n
\n
{rule.example}
\n
\n ))}\n
\n 📍 메뉴 경로: {workflowData.numberingResults.menuPath}\n
\n
\n
\n )}\n\n {/* 공통코드 참조 결과 */}\n {workflowData.codeReferences && (\n
\n
\n 📑 공통코드관리 테스트\n
\n
\n {workflowData.codeReferences.codeGroups.map((group, idx) => (\n
\n
\n {group.groupName}\n {group.groupCode}\n
\n
\n {group.codes.map((code, cidx) => (\n \n {code.code}: {code.name}\n \n ))}\n
\n
\n 사용: {group.usedIn.join(', ')}\n
\n
\n ))}\n
\n 📍 메뉴 경로: {workflowData.codeReferences.menuPath}\n
\n
\n
\n )}\n\n {/* 회계관리 - 매출관리 결과 */}\n {workflowData.accountingSales && (\n
\n
\n 📊 매출관리 테스트\n
\n
\n {workflowData.accountingSales.testCases.map((tc, idx) => (\n
\n {tc.action}\n {tc.result}\n
\n ))}\n
\n 📍 {workflowData.accountingSales.menuPath}\n ✓ {workflowData.accountingSales.summary.success}/{workflowData.accountingSales.summary.total} 성공\n
\n
\n
\n )}\n\n {/* 회계관리 - 매입관리 결과 */}\n {workflowData.accountingPurchase && (\n
\n
\n 🛒 매입관리 테스트\n
\n
\n {workflowData.accountingPurchase.testCases.map((tc, idx) => (\n
\n {tc.action}\n {tc.result}\n
\n ))}\n
\n 📍 {workflowData.accountingPurchase.menuPath}\n ✓ {workflowData.accountingPurchase.summary.success}/{workflowData.accountingPurchase.summary.total} 성공\n
\n
\n
\n )}\n\n {/* 회계관리 - 금전출납부 결과 */}\n {workflowData.accountingCashbook && (\n
\n
\n 💵 금전출납부 테스트\n
\n
\n {workflowData.accountingCashbook.testCases.map((tc, idx) => (\n
\n {tc.action}\n {tc.result}\n
\n ))}\n
\n 📍 {workflowData.accountingCashbook.menuPath}\n ✓ {workflowData.accountingCashbook.summary.success}/{workflowData.accountingCashbook.summary.total} 성공\n
\n
\n
\n )}\n\n {/* 회계관리 - 수금관리 결과 */}\n {workflowData.accountingCollection && (\n
\n
\n 💳 수금관리 테스트\n
\n
\n {workflowData.accountingCollection.testCases.map((tc, idx) => (\n
\n {tc.action}\n {tc.result}\n
\n ))}\n
\n 📍 {workflowData.accountingCollection.menuPath}\n ✓ {workflowData.accountingCollection.summary.success}/{workflowData.accountingCollection.summary.total} 성공\n
\n
\n
\n )}\n\n {/* 회계관리 - 원가관리 결과 */}\n {workflowData.accountingCost && (\n
\n
\n 🧮 원가관리 테스트\n
\n
\n {workflowData.accountingCost.testCases.map((tc, idx) => (\n
\n {tc.action}\n {tc.result}\n
\n ))}\n
\n 📍 {workflowData.accountingCost.menuPath}\n ✓ {workflowData.accountingCost.summary.success}/{workflowData.accountingCost.summary.total} 성공\n
\n
\n
\n )}\n\n {/* ═══════════════════════════════════════════════════════════════════════════ */}\n {/* 문서 출력 테스트 결과 */}\n {/* ═══════════════════════════════════════════════════════════════════════════ */}\n {workflowData.documentTest && (\n
\n
\n 📄 {workflowData.documentTest.docType} 출력 테스트\n
\n\n {/* 문서 정보 */}\n
\n
📋 문서 정보
\n
\n
문서번호: {workflowData.documentTest.document?.docNo}
\n
상태: {workflowData.documentTest.document?.status}
\n {workflowData.documentTest.document?.customer && (\n
거래처: {workflowData.documentTest.document.customer}
\n )}\n {workflowData.documentTest.document?.amount && (\n
금액: {workflowData.documentTest.document.amount.toLocaleString()}원
\n )}\n
\n
\n\n {/* 결재선 */}\n {workflowData.documentTest.approvalLine && (\n
\n
✍️ 결재선
\n
\n {workflowData.documentTest.approvalLine.map((approver, idx) => (\n
\n \n
{approver.role}
\n
{approver.name}
\n
{approver.dept}
\n
{approver.signature}
\n
\n {idx < workflowData.documentTest.approvalLine.length - 1 && (\n \n )}\n \n ))}\n
\n
\n )}\n\n {/* 테스트 케이스 */}\n
\n {workflowData.documentTest.testCases?.map((tc, idx) => (\n
\n
{tc.action}\n
\n {tc.approver && {tc.approver}}\n {tc.status}\n
\n
\n ))}\n
\n\n {/* 요약 */}\n
\n 📍 {workflowData.documentTest.menuPath}\n ✓ {workflowData.documentTest.summary?.success}/{workflowData.documentTest.summary?.total} 성공\n
\n
\n )}\n\n {/* ═══════════════════════════════════════════════════════════════════════════ */}\n {/* 결재 프로세스 테스트 결과 */}\n {/* ═══════════════════════════════════════════════════════════════════════════ */}\n {workflowData.approvalTest && (\n
\n
\n ✍️ {workflowData.approvalTest.testType} 테스트\n
\n
{workflowData.approvalTest.description}
\n\n {/* 결재 흐름 정보 */}\n {workflowData.approvalTest.approvalFlow && (\n
\n
📋 결재 흐름
\n
\n
문서유형: {workflowData.approvalTest.approvalFlow.docType}
\n
문서번호: {workflowData.approvalTest.approvalFlow.docNo}
\n {workflowData.approvalTest.approvalFlow.totalDuration && (\n
소요시간: {workflowData.approvalTest.approvalFlow.totalDuration}
\n )}\n {workflowData.approvalTest.approvalFlow.version && (\n
버전: {workflowData.approvalTest.approvalFlow.version}
\n )}\n
\n\n {/* 결재 단계 표시 */}\n
\n {workflowData.approvalTest.approvalFlow.steps?.map((step, idx) => (\n
\n \n
{step.role}
\n
{step.name}
\n {step.dept &&
{step.dept}
}\n {step.note &&
{step.note}
}\n {step.action &&
{step.action}
}\n
✓ {step.status}
\n
\n {idx < workflowData.approvalTest.approvalFlow.steps.length - 1 && (\n \n )}\n \n ))}\n
\n
\n )}\n\n {/* 대결 정보 */}\n {workflowData.approvalTest.approvalFlow?.delegateInfo && (\n
\n
👥 대결 정보
\n
\n
원결재자: {workflowData.approvalTest.approvalFlow.delegateInfo.originalApprover}
\n
대결자: {workflowData.approvalTest.approvalFlow.delegateInfo.delegate}
\n
사유: {workflowData.approvalTest.approvalFlow.delegateInfo.delegateReason}
\n
기간: {workflowData.approvalTest.approvalFlow.delegateInfo.delegatePeriod}
\n
\n
\n )}\n\n {/* 긴급 결재 정보 */}\n {workflowData.approvalTest.approvalFlow?.urgentInfo && (\n
\n
⚡ 긴급 결재 정보
\n
\n
긴급등급: {workflowData.approvalTest.approvalFlow.urgentInfo.urgentLevel}
\n
사유: {workflowData.approvalTest.approvalFlow.urgentInfo.urgentReason}
\n
원결재선: {workflowData.approvalTest.approvalFlow.urgentInfo.originalApprovalLine?.join(' → ')}
\n
단축결재선: {workflowData.approvalTest.approvalFlow.urgentInfo.reducedApprovalLine?.join(' → ')}
\n
처리시간: {workflowData.approvalTest.approvalFlow.totalProcessTime}
\n
절감시간: {workflowData.approvalTest.approvalFlow.timeSaved}
\n
\n
\n )}\n\n {/* 반려 이력 */}\n {workflowData.approvalTest.approvalFlow?.rejectionHistory && (\n
\n
↩️ 반려 이력
\n {workflowData.approvalTest.approvalFlow.rejectionHistory.map((rejection, idx) => (\n
\n
버전: {rejection.version}
\n
반려자: {rejection.rejectedBy}
\n
사유: {rejection.rejectReason}
\n
수정항목: {rejection.modifiedFields?.join(', ')}
\n
\n ))}\n
\n )}\n\n {/* 알림 내역 */}\n {workflowData.approvalTest.notifications && (\n
\n
🔔 알림 내역
\n
\n {workflowData.approvalTest.notifications.map((notif, idx) => (\n
\n {notif.type}\n {notif.recipient}\n {notif.sentAt}\n {notif.status}\n
\n ))}\n
\n
\n )}\n\n {/* 테스트 케이스 */}\n
\n {workflowData.approvalTest.testCases?.map((tc, idx) => (\n
\n
{tc.action}\n
\n {tc.actor && {tc.actor}}\n {tc.reason && {tc.reason}}\n {tc.status}\n
\n
\n ))}\n
\n\n {/* 요약 */}\n
\n 📍 {workflowData.approvalTest.menuPath}\n ✓ {workflowData.approvalTest.summary?.success}/{workflowData.approvalTest.summary?.total} 성공\n
\n
\n )}\n\n {/* ═══════════════════════════════════════════════════════════════════════════ */}\n {/* 문서 전체흐름 테스트 결과 */}\n {/* ═══════════════════════════════════════════════════════════════════════════ */}\n {workflowData.fullDocumentFlow && (\n
\n
\n 📚 문서 전체흐름 테스트\n
\n
{workflowData.fullDocumentFlow.description}
\n\n {/* 단계별 문서 현황 */}\n
\n {workflowData.fullDocumentFlow.testPhases?.map((phase, idx) => (\n
\n
\n
{phase.phase}
\n 결재 {phase.approvalCount}건 / {phase.totalApprovals}단계\n \n
\n {phase.documents.map((doc, dIdx) => (\n
\n {doc.docType} {doc.count ? `(${doc.count}건)` : ''}\n {doc.status}\n
\n ))}\n
\n
\n ))}\n
\n\n {/* 통계 요약 */}\n {workflowData.fullDocumentFlow.testSummary && (\n
\n
📊 통합 통계
\n
\n
\n
{workflowData.fullDocumentFlow.testSummary.totalDocuments}
\n
총 문서
\n
\n
\n
{workflowData.fullDocumentFlow.testSummary.totalApprovals}
\n
총 결재
\n
\n
\n
{workflowData.fullDocumentFlow.testSummary.totalApprovalTime}
\n
총 소요시간
\n
\n
\n
{workflowData.fullDocumentFlow.testSummary.avgApprovalTime}
\n
평균 결재시간
\n
\n
\n
\n
\n
문서유형: {workflowData.fullDocumentFlow.testSummary.documentTypes?.join(', ')}
\n
결재역할: {workflowData.fullDocumentFlow.testSummary.approvalRoles?.join(', ')}
\n
\n
\n
\n )}\n\n {/* 테스트 케이스 */}\n
\n {workflowData.fullDocumentFlow.testCases?.map((tc, idx) => (\n
\n
\n {tc.phase}\n {tc.action}\n
\n
{tc.status}\n
\n ))}\n
\n\n {/* 요약 */}\n
\n 📍 전체 업무 프로세스\n ✓ {workflowData.fullDocumentFlow.summary?.success}/{workflowData.fullDocumentFlow.summary?.total} 성공\n
\n
\n )}\n
\n
\n
\n );\n };\n\n // ═══════════════════════════════════════════════════════════════════════════\n // E2E 50건 테스트 탭 - 공정별 세부 단계 포함 전체 프로세스 검증\n // ═══════════════════════════════════════════════════════════════════════════\n const E2ETestTab = () => {\n const [e2eData, setE2eData] = useState(null);\n const [summary, setSummary] = useState(null);\n const [validation, setValidation] = useState(null);\n const [selectedOrder, setSelectedOrder] = useState(null);\n const [viewMode, setViewMode] = useState('overview'); // overview, detail, issues\n\n // E2E 테스트 데이터 생성\n useEffect(() => {\n const data = generateE2ETestData(integratedCustomerMaster, integratedSiteMaster);\n setE2eData(data);\n setSummary(generateTestDataSummary(data));\n setValidation(validateE2EDataIntegrity(data));\n }, []);\n\n if (!e2eData) {\n return
테스트 데이터 생성 중...
;\n }\n\n // 선택된 수주의 전체 흐름 조회\n const getOrderFlow = (orderId) => {\n const order = e2eData.orders.find(o => o.id === orderId);\n const quote = e2eData.quotes.find(q => q.id === order?.quoteId);\n const productionOrder = e2eData.productionOrders.find(po => po.orderId === orderId);\n const workOrders = e2eData.workOrders.filter(wo => wo.orderId === orderId);\n const pqcInspections = e2eData.pqcInspections.filter(pqc => pqc.orderId === orderId);\n const packaging = e2eData.packaging.find(p => p.orderId === orderId);\n const shipment = e2eData.shipments.find(s => s.orderId === orderId);\n\n return { order, quote, productionOrder, workOrders, pqcInspections, packaging, shipment };\n };\n\n // 개요 뷰\n const OverviewView = () => (\n
\n {/* 데이터 요약 카드 */}\n
\n
\n
견적
\n
{summary.totalQuotes}건
\n
\n
\n
수주
\n
{summary.totalOrders}건
\n
\n
\n
작업지시
\n
{summary.totalWorkOrders}건
\n
\n
\n
중간검사(PQC)
\n
{summary.totalPQCInspections}건
\n
\n
\n\n {/* 상태별 분포 */}\n
\n
\n
수주 상태 분포
\n
\n {Object.entries(summary.orderStatusDistribution).map(([status, count]) => (\n
\n {status}\n {count}건\n
\n ))}\n
\n
\n
\n
공정별 작업지시 분포
\n
\n {Object.entries(summary.workOrdersByProcess).map(([process, count]) => (\n
\n {process}\n {count}건\n
\n ))}\n
\n
\n
\n\n {/* PQC 검사 결과 */}\n
\n
중간검사(PQC) 결과 분포
\n
\n {Object.entries(summary.pqcResultDistribution).map(([result, count]) => (\n
\n {result}\n {count}건\n \n ({((count / summary.totalPQCInspections) * 100).toFixed(1)}%)\n \n
\n ))}\n
\n
\n\n {/* 데이터 정합성 검증 */}\n
\n
\n {validation.isValid ? : }\n 데이터 정합성 검증\n
\n {validation.isValid ? (\n
모든 데이터 연결이 정상입니다. (견적→수주→생산지시→작업지시→검사→포장→출하)
\n ) : (\n
\n
{validation.issueCount}건의 정합성 문제 발견
\n {validation.issues.slice(0, 5).map((issue, idx) => (\n
• {issue}
\n ))}\n
\n )}\n
\n\n {/* 수주 목록 (클릭하면 상세 흐름 확인) */}\n
\n
\n
E2E 테스트 수주 목록 (클릭하여 전체 흐름 확인)
\n \n
\n
\n \n \n | 수주번호 | \n 거래처 | \n 제품유형 | \n 수량 | \n 상태 | \n 작업지시 | \n PQC | \n
\n \n \n {e2eData.orders.slice(0, 20).map(order => {\n const woCount = e2eData.workOrders.filter(wo => wo.orderId === order.id).length;\n const pqcCount = e2eData.pqcInspections.filter(pqc => pqc.orderId === order.id).length;\n return (\n { setSelectedOrder(order.id); setViewMode('detail'); }}\n >\n | {order.orderNo} | \n {order.customerName} | \n {order.productType} | \n {order.qty}대 | \n \n {order.status}\n | \n {woCount}건 | \n {pqcCount}건 | \n
\n );\n })}\n \n
\n
\n
\n
\n );\n\n // 상세 흐름 뷰\n const DetailView = () => {\n if (!selectedOrder) {\n return
수주를 선택하세요
;\n }\n\n const flow = getOrderFlow(selectedOrder);\n const workflows = processMasterConfigModule.processWorkflows;\n\n return (\n
\n {/* 뒤로가기 */}\n
\n\n {/* 수주 정보 */}\n
\n
수주 정보
\n
\n
수주번호: {flow.order?.orderNo}
\n
거래처: {flow.order?.customerName}
\n
제품유형: {flow.order?.productType}
\n
상태: {flow.order?.status}
\n
\n
\n\n {/* 견적 → 수주 연결 */}\n
\n
\n
견적
\n
{flow.quote?.quoteNo}
\n
{flow.quote?.quoteDate}
\n
\n
\n
생산지시
\n
{flow.productionOrder?.productionOrderNo}
\n
공정 {flow.productionOrder?.completedProcesses}/{flow.productionOrder?.totalProcesses} 완료
\n
\n
\n\n {/* 공정별 작업지시 및 세부 단계 */}\n
\n
\n
공정별 작업지시 (세부 단계 포함)
\n \n
\n {Object.keys(workflows).map(workflowCode => {\n const workflowWOs = flow.workOrders.filter(wo => wo.workflowCode === workflowCode);\n if (workflowWOs.length === 0) return null;\n\n const workflow = workflows[workflowCode];\n return (\n
\n
\n \n {workflow.name} ({workflowWOs.length}단계)\n
\n
\n {workflowWOs.map(wo => {\n const pqc = flow.pqcInspections.find(p => p.workOrderId === wo.id);\n return (\n
\n \n {wo.stepCode}\n {wo.stepName}\n {wo.status}\n {wo.isQCStep && (\n \n {pqc ? `PQC:${pqc.result}` : 'PQC대기'}\n \n )}\n
\n );\n })}\n
\n
\n );\n })}\n
\n
\n\n {/* 포장 및 출하 */}\n
\n
\n
포장
\n {flow.packaging ? (\n <>\n
{flow.packaging.packagingNo}
\n
{flow.packaging.packedQty}대 / {flow.packaging.packageCount}박스
\n >\n ) : (\n
포장 대기
\n )}\n
\n
\n
출하
\n {flow.shipment ? (\n <>\n
{flow.shipment.shipmentNo}
\n
{flow.shipment.status} (진행률: {flow.shipment.progress}%)
\n >\n ) : (\n
출하 대기
\n )}\n
\n
\n
\n );\n };\n\n // 문제점/보완점 뷰\n const IssuesView = () => (\n
\n
E2E 테스트 분석 결과 - 문제점 및 보완점
\n\n {/* 현재 구현된 사항 */}\n
\n
\n \n 정상 구현된 사항\n
\n
\n - • 견적 → 수주 → 생산지시 연계 정상
\n - • 공정별 세부 단계(SCREEN 8단계, SLAT 4단계, FOLD 5단계, STOCK 3단계) 작업지시 생성
\n - • isQCStep=true 단계에서 PQC 검사 자동 생성
\n - • 품목-공정 매핑 테이블 연계 (bomItemProcessMapping)
\n - • 포장 및 출하 데이터 연계
\n
\n
\n\n {/* 해결된 문제점 */}\n
\n
\n \n 해결된 문제점\n
\n
\n - \n ✅ 1. 공정코드 통일 완료\n
itemProcessMapping을 SCR-001, SLT-001, FLD-001, STK-001 형식으로 통일
\n \n - \n ✅ 2. 자재 LOT 추적 구현\n
materialLotMaster 추가, 자재투입 단계에서 LOT 할당 및 사용이력 추적
\n \n - \n ✅ 3. 포장 합류점(mergePoint) 구현\n
포장 데이터에 mergedProcesses 정보 포함, 모든 공정의 PACK 단계 합류 추적
\n \n - \n ✅ 4. 재작업 프로세스 구현\n
PQC 불합격 시 reworkInfo 생성, 재작업지시→재작업완료→재검사 합격 흐름 구현
\n \n - \n ✅ 5. 작업실적 연계 구현\n
workResults 추가, 작업지시 완료/진행중 시 자동 실적 생성
\n \n
\n
\n\n {/* 추가 보완 가능 항목 */}\n
\n
\n \n 추가 보완 가능 항목\n
\n
\n
\n\n {/* 데이터 흐름도 */}\n
\n
E2E 데이터 흐름도
\n
\n
{`\n견적(Quote) ──┬──→ 수주(Order) ──→ 생산지시(ProductionOrder)\n │ │\n │ ├──→ SCREEN 작업지시 (8단계)\n │ │ ├─ SCR-MAT (자재투입)\n │ │ ├─ SCR-CNT (절단매수확인)\n │ │ ├─ SCR-CUT (원단절단)\n │ │ ├─ SCR-CHK (절단Check) → PQC\n │ │ ├─ SCR-SEW (미싱)\n │ │ ├─ SCR-END (앤드락)\n │ │ ├─ SCR-QC (중간검사) → PQC\n │ │ └─ SCR-PACK (포장)\n │ │\n │ ├──→ SLAT 작업지시 (4단계)\n │ ├──→ FOLD 작업지시 (5단계)\n │ └──→ STOCK 작업지시 (3단계)\n │ │\n │ ▼\n │ ┌─────────┐\n │ │ 포장 │ ← 모든 공정 합류\n │ └────┬────┘\n │ │\n └─────────────────────────────→ 출하(Shipment)\n `}\n
\n
\n
\n );\n\n return (\n
\n {/* 뷰 모드 선택 */}\n
\n \n \n \n
\n\n {/* 뷰 렌더링 */}\n {viewMode === 'overview' &&
}\n {viewMode === 'detail' &&
}\n {viewMode === 'issues' &&
}\n
\n );\n };\n\n // 요약 탭\n const SummaryTab = () => {\n // 견적 상태별 통계\n const quoteStats = {\n total: integratedQuoteMaster.length,\n 견적중: integratedQuoteMaster.filter(q => q.status === '견적중').length,\n 제출완료: integratedQuoteMaster.filter(q => q.status === '제출완료').length,\n 수주전환: integratedQuoteMaster.filter(q => q.status === '수주전환').length,\n 취소: integratedQuoteMaster.filter(q => q.status === '취소').length,\n 만료: integratedQuoteMaster.filter(q => q.status === '만료').length,\n };\n\n // 수주 상태별 통계 (생산지시완료까지만 표시)\n const orderStats = {\n total: integratedOrderMaster.length,\n 수주확정: integratedOrderMaster.filter(o => o.status === '수주확정').length,\n 생산지시: integratedOrderMaster.filter(o => o.status === '생산지시').length,\n 생산중: integratedOrderMaster.filter(o => o.status === '생산중').length,\n 생산완료: integratedOrderMaster.filter(o => o.status === '생산완료').length,\n 생산지시완료: integratedOrderMaster.filter(o => o.status === '생산지시완료').length,\n };\n\n // 생산 상태별 통계\n const workOrderStats = {\n total: integratedWorkOrderMaster.length,\n 대기: integratedWorkOrderMaster.filter(w => w.status === '대기').length,\n 진행중: integratedWorkOrderMaster.filter(w => w.status === '진행중').length,\n 완료: integratedWorkOrderMaster.filter(w => w.status === '완료').length,\n };\n\n // 품질검사 결과별 통계\n const qualityStats = {\n total: integratedQualityInspection.length,\n 합격: integratedQualityInspection.filter(q => q.result === '합격').length,\n 조건부합격: integratedQualityInspection.filter(q => q.result === '조건부합격').length,\n 불합격: integratedQualityInspection.filter(q => q.result === '불합격').length,\n };\n\n // 수금 현황\n const collectionStats = {\n total: integratedDataSummary.totalSalesAmount,\n collected: integratedDataSummary.totalCollectedAmount,\n uncollected: integratedDataSummary.totalSalesAmount - integratedDataSummary.totalCollectedAmount,\n collectionRate: integratedDataSummary.totalSalesAmount > 0\n ? ((integratedDataSummary.totalCollectedAmount / integratedDataSummary.totalSalesAmount) * 100).toFixed(1)\n : 0,\n };\n\n // 신용등급별 거래처\n const creditStats = {\n A: integratedCustomerMaster.filter(c => c.creditGrade === 'A').length,\n B: integratedCustomerMaster.filter(c => c.creditGrade === 'B').length,\n C: integratedCustomerMaster.filter(c => c.creditGrade === 'C').length,\n };\n\n return (\n
\n {/* 핵심 KPI 카드 */}\n
\n
\n
\n \n 거래처\n
\n
{integratedDataSummary.customers}개
\n
\n A:{creditStats.A}\n B:{creditStats.B}\n C:{creditStats.C}\n
\n
\n
\n
\n \n 현장\n
\n
{integratedDataSummary.sites}개
\n
\n 평균 {(integratedDataSummary.sites / integratedDataSummary.customers).toFixed(1)}개/거래처\n
\n
\n
\n
\n \n 견적\n
\n
{integratedDataSummary.quotes}건
\n
{formatCurrency(integratedDataSummary.totalQuoteAmount)}
\n
\n
\n
\n
{integratedDataSummary.orders}건
\n
{formatCurrency(integratedDataSummary.totalOrderAmount)}
\n
\n
\n\n
\n
\n
\n \n 작업지시\n
\n
{integratedDataSummary.workOrders}건
\n
\n
\n
\n \n 품질검사\n
\n
{integratedDataSummary.qualityInspections}건
\n
\n
\n
\n \n 출하\n
\n
{integratedDataSummary.shipments}건
\n
\n
\n
\n \n 매출\n
\n
{integratedDataSummary.sales}건
\n
{formatCurrency(integratedDataSummary.totalSalesAmount)}
\n
\n
\n
\n \n 수금\n
\n
{integratedDataSummary.collections}건
\n
{formatCurrency(integratedDataSummary.totalCollectedAmount)}
\n
\n
\n\n {/* 프로세스 흐름도 */}\n
\n
\n \n 데이터 흐름 요약\n
\n
\n
\n \n 거래처 {integratedDataSummary.customers}\n
\n
\n
\n \n 현장 {integratedDataSummary.sites}\n
\n
\n
\n \n 견적 {integratedDataSummary.quotes}\n
\n
\n
\n
\n
수주 {integratedDataSummary.orders}\n
\n
\n
\n \n 작업 {integratedDataSummary.workOrders}\n
\n
\n
\n \n 검사 {integratedDataSummary.qualityInspections}\n
\n
\n
\n \n 출하 {integratedDataSummary.shipments}\n
\n
\n
\n \n 매출 {integratedDataSummary.sales}\n
\n
\n
\n \n 수금 {integratedDataSummary.collections}\n
\n
\n
\n\n {/* 상세 통계 그리드 */}\n
\n {/* 견적 상태별 분포 */}\n
\n
\n \n 견적 상태 분포\n
\n
\n {Object.entries(quoteStats).filter(([k]) => k !== 'total').map(([status, count]) => (\n
\n ))}\n
\n
\n\n {/* 수주 상태별 분포 */}\n
\n
\n \n 수주 상태 분포\n
\n
\n {Object.entries(orderStats).filter(([k]) => k !== 'total').map(([status, count]) => (\n
\n
{status}\n
\n
\n
0 ? (count / orderStats.total) * 100 : 0}%` }}\n />\n
\n
{count}\n
\n
\n ))}\n
\n
\n\n {/* 생산/품질 현황 */}\n
\n
\n \n 생산 현황\n
\n
\n
\n
\n 진행률\n \n {workOrderStats.total > 0\n ? ((workOrderStats.완료 / workOrderStats.total) * 100).toFixed(0)\n : 0}%\n \n
\n
\n
0 ? (workOrderStats.완료 / workOrderStats.total) * 100 : 0}%` }} />\n
\n
\n
\n
\n
{workOrderStats.대기}
\n
대기
\n
\n
\n
{workOrderStats.진행중}
\n
진행중
\n
\n
\n
{workOrderStats.완료}
\n
완료
\n
\n
\n
\n
\n\n {/* 수금 현황 */}\n
\n
\n \n 수금 현황\n
\n
\n
\n
\n 수금률\n {collectionStats.collectionRate}%\n
\n
\n
\n
\n
\n
{formatCurrency(collectionStats.collected)}
\n
수금완료
\n
\n
\n
{formatCurrency(collectionStats.uncollected)}
\n
미수금
\n
\n
\n
\n
\n
\n\n {/* 품질검사 결과 요약 */}\n
\n
\n \n 품질검사 결과 요약 (총 {qualityStats.total}건)\n
\n
\n
\n
\n
0 ? (qualityStats.합격 / qualityStats.total) * 100 : 0}%` }}\n title={`합격 ${qualityStats.합격}건`} />\n
0 ? (qualityStats.조건부합격 / qualityStats.total) * 100 : 0}%` }}\n title={`조건부합격 ${qualityStats.조건부합격}건`} />\n
0 ? (qualityStats.불합격 / qualityStats.total) * 100 : 0}%` }}\n title={`불합격 ${qualityStats.불합격}건`} />\n
\n
\n
\n
\n
\n
합격 {qualityStats.합격}\n
\n
\n
\n
조건부 {qualityStats.조건부합격}\n
\n
\n
\n
불합격 {qualityStats.불합격}\n
\n
\n
\n
\n
\n );\n };\n\n // 거래처 탭\n const CustomersTab = () => (\n
\n
\n
\n \n \n | 코드 | \n 거래처명 | \n 신용등급 | \n 할인율 | \n 담당자 | \n 현장수 | \n 결제조건 | \n
\n \n \n {integratedCustomerMaster.map(customer => {\n const siteCount = integratedSiteMaster.filter(s => s.customerId === customer.id).length;\n return (\n setSelectedCustomer(customer)}>\n | {customer.code} | \n {customer.name} | \n \n {customer.creditGrade}등급\n | \n {customer.discountRate}% | \n {customer.manager} | \n {siteCount}개 | \n {customer.paymentTerms} | \n
\n );\n })}\n \n
\n
\n
\n );\n\n // 견적 탭\n const QuotesTab = () => {\n const [quoteFilter, setQuoteFilter] = useState('all');\n const [quoteSearch, setQuoteSearch] = useState('');\n const [quotePage, setQuotePage] = useState(1);\n const pageSize = 15;\n\n // 필터링된 견적 목록\n const filteredQuotes = integratedQuoteMaster.filter(quote => {\n const matchStatus = quoteFilter === 'all' || quote.status === quoteFilter;\n const matchSearch = quoteSearch === '' ||\n quote.quoteNo.toLowerCase().includes(quoteSearch.toLowerCase()) ||\n quote.customerName.toLowerCase().includes(quoteSearch.toLowerCase()) ||\n quote.siteName.toLowerCase().includes(quoteSearch.toLowerCase());\n return matchStatus && matchSearch;\n }).sort((a, b) => new Date(b.quoteDate) - new Date(a.quoteDate)); // 최신순 정렬\n\n // 페이지네이션\n const totalPages = Math.ceil(filteredQuotes.length / pageSize);\n const paginatedQuotes = filteredQuotes.slice((quotePage - 1) * pageSize, quotePage * pageSize);\n\n // 목록 선택 훅\n const { selectedIds, handleSelect, handleSelectAll, isAllSelected, hasSelection, isMultiSelect, isSelected } = useListSelection(paginatedQuotes);\n\n // 필터 변경시 페이지 초기화\n const handleFilterChange = (filter) => {\n setQuoteFilter(filter);\n setQuotePage(1);\n };\n\n const handleEdit = (quote) => alert(`견적 수정: ${quote.quoteNo}`);\n const handleDelete = (quote) => window.confirm(`${quote.quoteNo} 견적을 삭제하시겠습니까?`) && alert(`견적 삭제: ${quote.quoteNo}`);\n const handleBulkDelete = () => window.confirm(`선택한 ${selectedIds.length}건을 삭제하시겠습니까?`) && alert(`${selectedIds.length}건 삭제`);\n\n return (\n
\n {/* 필터 및 검색 영역 */}\n
\n
\n \n {['견적중', '제출완료', '수주전환', '취소', '만료'].map(status => {\n const count = integratedQuoteMaster.filter(q => q.status === status).length;\n return (\n \n );\n })}\n
\n
\n
\n {isMultiSelect && (\n
\n )}\n
\n \n { setQuoteSearch(e.target.value); setQuotePage(1); }}\n className=\"pl-9 pr-4 py-1.5 text-sm border rounded-lg w-64 focus:ring-2 focus:ring-blue-300 focus:outline-none\"\n />\n
\n
\n
\n\n {/* 테이블 */}\n
\n\n {/* 페이지네이션 */}\n
\n
\n 총 {filteredQuotes.length}건 중{' '}\n {(quotePage - 1) * pageSize + 1}-\n {Math.min(quotePage * pageSize, filteredQuotes.length)}건 표시\n
\n
\n
\n
\n
\n {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {\n let pageNum;\n if (totalPages <= 5) pageNum = i + 1;\n else if (quotePage <= 3) pageNum = i + 1;\n else if (quotePage >= totalPages - 2) pageNum = totalPages - 4 + i;\n else pageNum = quotePage - 2 + i;\n return (\n \n );\n })}\n
\n
\n
\n
\n
\n
\n );\n };\n\n // 수주 탭\n const OrdersTab = () => {\n const { selectedIds, handleSelect, handleSelectAll, isAllSelected, hasSelection, isMultiSelect, isSelected } = useListSelection(integratedOrderMaster);\n const handleEdit = (order) => alert(`수주 수정: ${order.orderNo}`);\n const handleDelete = (order) => window.confirm(`${order.orderNo} 수주를 삭제하시겠습니까?`) && alert(`수주 삭제: ${order.orderNo}`);\n const handleBulkDelete = () => window.confirm(`선택한 ${selectedIds.length}건을 삭제하시겠습니까?`) && alert(`${selectedIds.length}건 삭제`);\n\n return (\n
\n
\n
\n {['수주확정', '생산지시', '생산중', '생산완료', '생산지시완료'].map(status => (\n \n {status}: {integratedOrderMaster.filter(o => o.status === status).length}\n \n ))}\n
\n {isMultiSelect && (\n
\n )}\n
\n
\n
\n );\n };\n\n // 생산 탭\n const ProductionTab = () => {\n const displayData = integratedWorkOrderMaster.slice(0, 30);\n const { selectedIds, handleSelect, handleSelectAll, isAllSelected, hasSelection, isMultiSelect, isSelected } = useListSelection(displayData);\n const handleEdit = (wo) => alert(`작업지시 수정: ${wo.workOrderNo}`);\n const handleDelete = (wo) => window.confirm(`${wo.workOrderNo} 작업지시를 삭제하시겠습니까?`) && alert(`작업지시 삭제: ${wo.workOrderNo}`);\n const handleBulkDelete = () => window.confirm(`선택한 ${selectedIds.length}건을 삭제하시겠습니까?`) && alert(`${selectedIds.length}건 삭제`);\n\n return (\n
\n
\n
\n {['대기', '진행중', '완료'].map(status => (\n \n {status}: {integratedWorkOrderMaster.filter(w => w.status === status).length}\n \n ))}\n
\n {isMultiSelect && (\n
\n )}\n
\n
\n {integratedWorkOrderMaster.length > 30 && (\n
... 외 {integratedWorkOrderMaster.length - 30}건
\n )}\n
\n );\n };\n\n // 품질 탭\n const QualityTab = () => {\n const displayData = integratedQualityMaster.slice(0, 30);\n const { selectedIds, handleSelect, handleSelectAll, isAllSelected, hasSelection, isMultiSelect, isSelected } = useListSelection(displayData);\n const handleEdit = (insp) => alert(`검사 수정: ${insp.inspectionNo}`);\n const handleDelete = (insp) => window.confirm(`${insp.inspectionNo} 검사를 삭제하시겠습니까?`) && alert(`검사 삭제: ${insp.inspectionNo}`);\n const handleBulkDelete = () => window.confirm(`선택한 ${selectedIds.length}건을 삭제하시겠습니까?`) && alert(`${selectedIds.length}건 삭제`);\n\n return (\n
\n
\n
\n 합격: {integratedQualityMaster.filter(q => q.result === '합격').length}\n 불합격: {integratedQualityMaster.filter(q => q.result === '불합격').length}\n
\n {isMultiSelect && (\n
\n )}\n
\n
\n
\n );\n };\n\n // 출하 탭\n const ShipmentTab = () => {\n const { selectedIds, handleSelect, handleSelectAll, isAllSelected, hasSelection, isMultiSelect, isSelected } = useListSelection(integratedShipmentMaster);\n const handleEdit = (ship) => alert(`출하 수정: ${ship.shipmentNo}`);\n const handleDelete = (ship) => window.confirm(`${ship.shipmentNo} 출하를 삭제하시겠습니까?`) && alert(`출하 삭제: ${ship.shipmentNo}`);\n const handleBulkDelete = () => window.confirm(`선택한 ${selectedIds.length}건을 삭제하시겠습니까?`) && alert(`${selectedIds.length}건 삭제`);\n\n return (\n
\n
\n
\n 출하대기: {integratedShipmentMaster.filter(s => s.status === '출하대기').length}\n 배송중: {integratedShipmentMaster.filter(s => s.status === '배송중').length}\n 배송완료: {integratedShipmentMaster.filter(s => s.status === '배송완료').length}\n
\n {isMultiSelect && (\n
\n )}\n
\n
\n
\n );\n };\n\n // 회계 탭\n const AccountingTab = () => (\n
\n {/* 매출 */}\n
\n
\n \n 매출 ({integratedSalesMaster.length}건)\n
\n
\n
\n \n \n | 매출번호 | \n 거래처 | \n 공급가 | \n VAT | \n 합계 | \n 상태 | \n
\n \n \n {integratedSalesMaster.slice(0, 10).map(sale => (\n \n | {sale.salesNo} | \n {sale.customerName} | \n {formatCurrency(sale.supplyAmount)} | \n {formatCurrency(sale.vatAmount)} | \n {formatCurrency(sale.totalAmount)} | \n \n {sale.paymentStatus}\n | \n
\n ))}\n \n
\n
\n
\n\n {/* 수금 */}\n
\n
\n \n 수금 ({integratedCollectionMaster.length}건)\n
\n
\n
\n \n \n | 수금번호 | \n 매출번호 | \n 거래처 | \n 수금액 | \n 수금일 | \n 수금방법 | \n
\n \n \n {integratedCollectionMaster.slice(0, 10).map(col => (\n \n | {col.collectionNo} | \n {col.salesNo} | \n {col.customerName} | \n {formatCurrency(col.amount)} | \n {col.collectionDate} | \n {col.paymentMethod} | \n
\n ))}\n \n
\n
\n
\n
\n );\n\n // 전체 흐름 탭 (특정 건의 전체 프로세스 추적)\n const FlowTab = () => {\n const [selectedOrderId, setSelectedOrderId] = useState(integratedOrderMaster[0]?.id);\n const selectedOrder = integratedOrderMaster.find(o => o.id === selectedOrderId);\n\n if (!selectedOrder) return
수주 데이터가 없습니다.
;\n\n const relatedQuote = integratedQuoteMaster.find(q => q.id === selectedOrder.quoteId);\n const relatedCustomer = integratedCustomerMaster.find(c => c.id === selectedOrder.customerId);\n const relatedSite = integratedSiteMaster.find(s => s.id === selectedOrder.siteId);\n const relatedWorkOrders = integratedWorkOrderMaster.filter(w => w.orderId === selectedOrder.id);\n const relatedQuality = integratedQualityMaster.filter(q =>\n relatedWorkOrders.some(w => w.id === q.workOrderId)\n );\n const relatedShipment = integratedShipmentMaster.find(s => s.orderId === selectedOrder.id);\n const relatedSales = integratedSalesMaster.find(s => s.orderId === selectedOrder.id);\n const relatedCollection = relatedSales ? integratedCollectionMaster.find(c => c.salesId === relatedSales.id) : null;\n\n return (\n
\n
\n \n \n
\n\n
\n {/* 거래처 정보 */}\n
\n
\n \n 1. 거래처\n
\n {relatedCustomer && (\n
\n 코드: {relatedCustomer.code}\n 명칭: {relatedCustomer.name}\n 등급: {relatedCustomer.creditGrade}\n 할인: {relatedCustomer.discountRate}%\n
\n )}\n
\n\n {/* 현장 정보 */}\n
\n
\n \n 2. 현장\n
\n {relatedSite && (\n
\n 코드: {relatedSite.code}\n 현장명: {relatedSite.name}\n 담당자: {relatedSite.manager}\n 상태: {relatedSite.status}\n
\n )}\n
\n\n {/* 견적 정보 */}\n
\n
\n \n 3. 견적\n
\n {relatedQuote && (\n
\n 번호: {relatedQuote.quoteNo}\n 일자: {relatedQuote.quoteDate}\n 금액: {formatCurrency(relatedQuote.finalAmount)}\n 상태: {relatedQuote.status}\n
\n )}\n
\n\n {/* 수주 정보 */}\n
\n
\n \n 4. 수주\n
\n
\n 번호: {selectedOrder.orderNo}\n 일자: {selectedOrder.orderDate}\n 금액: {formatCurrency(selectedOrder.totalAmount)}\n 상태: {selectedOrder.status}\n
\n
\n\n {/* 생산 정보 */}\n
\n
\n \n 5. 생산 ({relatedWorkOrders.length}건)\n
\n
\n {relatedWorkOrders.map(wo => (\n
\n {wo.workOrderNo}\n {wo.processName}\n {wo.status}\n {wo.producedQty}/{wo.qty}\n
\n ))}\n
\n
\n\n {/* 품질 정보 */}\n
\n
\n \n 6. 품질검사 ({relatedQuality.length}건)\n
\n
\n {relatedQuality.length > 0 ? relatedQuality.map(q => (\n
\n {q.inspectionNo}\n {q.inspectionType}\n {q.result}\n
\n )) :
검사 대기중}\n
\n
\n\n {/* 출하 정보 */}\n
\n
\n \n 7. 출하\n
\n {relatedShipment ? (\n
\n 번호: {relatedShipment.shipmentNo}\n 일자: {relatedShipment.shipmentDate}\n 운송: {relatedShipment.carrier}\n 상태: {relatedShipment.status}\n
\n ) :
출하 대기중}\n
\n\n {/* 매출/수금 정보 */}\n
\n
\n \n 8. 매출/수금\n
\n {relatedSales ? (\n
\n
\n 매출번호: {relatedSales.salesNo}\n 공급가: {formatCurrency(relatedSales.supplyAmount)}\n VAT: {formatCurrency(relatedSales.vatAmount)}\n 합계: {formatCurrency(relatedSales.totalAmount)}\n
\n {relatedCollection && (\n
\n 수금번호: {relatedCollection.collectionNo}\n 수금액: {formatCurrency(relatedCollection.amount)}\n 수금일: {relatedCollection.collectionDate}\n 방법: {relatedCollection.paymentMethod}\n
\n )}\n
\n ) :
매출 미발생}\n
\n
\n
\n );\n };\n\n return (\n
\n
\n
\n \n 통합 테스트 대시보드\n
\n \n 거래처→현장→견적→수주→생산→품질→출하→회계 전 과정 연동\n \n \n\n {/* 탭 네비게이션 */}\n
\n {tabs.map(tab => (\n \n ))}\n
\n\n {/* 탭 콘텐츠 */}\n
\n {activeTab === 'process-integration' &&
}\n {activeTab === 'complete-integration' &&
}\n {activeTab === 'full-integration' &&
}\n {activeTab === 'workflow' &&
}\n {activeTab === 'e2e-test' &&
}\n {activeTab === 'summary' &&
}\n {activeTab === 'customers' &&
}\n {activeTab === 'quotes' &&
}\n {activeTab === 'orders' &&
}\n {activeTab === 'production' &&
}\n {activeTab === 'quality' &&
}\n {activeTab === 'shipment' &&
}\n {activeTab === 'accounting' &&
}\n {activeTab === 'flow' &&
}\n
\n
\n );\n};\n\n// 단가 계산 분류 (카테고리 그룹)\nconst initialPriceClassifications = [\n { id: 1, name: '스크린 제품', description: '스크린 관련 모든 제품', categories: ['스크린', '오픈사이즈', '제작사이즈', '실리카', '와이어'], itemCount: 5 },\n { id: 2, name: '철재 제품', description: '철재 관련 모든 제품', categories: ['철재', '가이드레일', '케이스', '프레임'], itemCount: 4 },\n { id: 3, name: '전기 부품', description: '전기/전자 관련 부품', categories: ['모터', '제어기', '센서', '스위치'], itemCount: 4 },\n { id: 4, name: '기타 부자재', description: '기타 부자재 및 소모품', categories: ['볼트', '너트', '와셔', '접착제', '테이프'], itemCount: 5 },\n];\n\n// 단가 수식 연결\nconst initialPriceFormulas = [\n { id: 1, classificationId: 2, classificationName: '철재 제품', formula: 'W0 * H0 / 1000000', description: '철재 면적 기반 단가', isActive: true, qtyCalc: 'UP' },\n];\n\n// 수식 계산 엔진 (Enhanced)\nconst formulaEngine = {\n // 변수 저장소\n variables: {},\n\n // 룩업 테이블 참조\n lookupTables: {\n motorCapacityTable,\n bracketTable,\n shaftInchTableSteel,\n shaftInchTableScreen,\n shaftLengthTable,\n guideRailLengthTable,\n caseLengthTable,\n bottomBarTable,\n hwanbongQtyTable,\n squarePipeTableScreen,\n squarePipeTableSteel,\n subShaftTable,\n },\n\n // 변수 설정\n setVariable(name, value) {\n this.variables[name] = value;\n },\n\n // 변수 가져오기\n getVariable(name) {\n return this.variables[name] || 0;\n },\n\n // 조건 평가\n evaluateCondition(condition) {\n if (!condition) return true;\n try {\n let evalCondition = condition;\n for (const [key, value] of Object.entries(this.variables)) {\n const regex = new RegExp(`\\\\b${key}\\\\b`, 'g');\n evalCondition = evalCondition.replace(regex, typeof value === 'string' ? `'${value}'` : value);\n }\n return Function('\"use strict\";return (' + evalCondition + ')')();\n } catch (e) {\n console.error('Condition evaluation error:', condition, e);\n return false;\n }\n },\n\n // 수식 평가\n evaluate(formula) {\n if (!formula || formula === '-' || formula === '') return null;\n\n try {\n // 변수 치환\n let evalFormula = formula;\n for (const [key, value] of Object.entries(this.variables)) {\n const regex = new RegExp(`\\\\b${key}\\\\b`, 'g');\n evalFormula = evalFormula.replace(regex, typeof value === 'string' ? `'${value}'` : value);\n }\n\n // 내장 함수 치환\n evalFormula = evalFormula\n .replace(/CEIL\\(/g, 'Math.ceil(')\n .replace(/FLOOR\\(/g, 'Math.floor(')\n .replace(/ROUND\\(/g, 'Math.round(')\n .replace(/IF\\(/g, '((')\n .replace(/SUM\\(/g, '(')\n .replace(/MAX\\(/g, 'Math.max(')\n .replace(/MIN\\(/g, 'Math.min(');\n\n // 평가\n const result = Function('\"use strict\";return (' + evalFormula + ')')();\n return isNaN(result) ? 0 : result;\n } catch (e) {\n console.error('Formula evaluation error:', formula, e);\n return 0;\n }\n },\n\n // 룩업 테이블 평가 (새 테이블 형식 지원)\n evaluateLookup(lookupTable, params) {\n const { W1, K, PC, S, B, G } = params;\n const productType = PC === '스크린' ? 'screen' : 'steel';\n\n switch (lookupTable) {\n case 'motorCapacityTable': {\n // 샤프트 인치 결정\n const shaftInch = this.getShaftInch(W1, K, productType);\n this.setVariable('SHAFT_INCH', shaftInch);\n\n // 모터 용량 결정\n const table = motorCapacityTable[productType]?.[String(shaftInch)];\n if (!table) return { motor: 300, shaftInch };\n\n for (const row of table) {\n if (K >= row.min && K <= row.max) {\n return { motor: row.motor, shaftInch };\n }\n }\n return { motor: table[table.length - 1]?.motor || 300, shaftInch };\n }\n\n case 'bracketTable': {\n const motorKG = this.getVariable('MOTOR_KG') || 300;\n const bracket = bracketTable[productType]?.[String(motorKG)];\n return bracket || bracketTable.screen['150'];\n }\n\n case 'shaftLengthTable': {\n // 인치별 W1 구간에 따라 샤프트 자재 길이 결정\n const shaftInch = this.getVariable('SHAFT_INCH') || '5';\n const inchTable = shaftLengthTable[String(shaftInch)];\n if (inchTable) {\n for (const row of inchTable) {\n if (W1 >= row.w1Min && W1 < row.w1Max) {\n return row.length;\n }\n }\n }\n return 8200; // default max\n }\n\n case 'guideRailLengthTable': {\n // G값에 따라 가이드레일 자재 길이 결정\n for (const row of guideRailLengthTable) {\n if (G >= row.gMin && G < row.gMax) {\n // materials 배열에서 자재 정보 추출\n const materials = row.materials || [{ length: row.length || 3000, qty: 2 }];\n const primaryMat = materials[0];\n return {\n length: primaryMat.length,\n qty: materials.reduce((sum, m) => sum + m.qty, 0),\n materials: materials,\n code: `RC${Math.floor(primaryMat.length / 100)}`\n };\n }\n }\n return { length: 4300, qty: 2, code: 'RC43' };\n }\n\n case 'caseLengthTable': {\n // S값에 따라 케이스 자재 길이/수량 결정\n for (const row of caseLengthTable) {\n if (S >= row.sMin && S < row.sMax) {\n const materials = row.materials || [{ length: row.length, qty: row.qty || 1 }];\n const primaryMat = materials[0];\n return {\n length: primaryMat.length,\n qty: row.qty || materials.reduce((sum, m) => sum + m.qty, 0),\n materials: materials,\n combo: row.combo || null,\n code: `CB${Math.floor(primaryMat.length / 100)}`\n };\n }\n }\n return { length: 1219, qty: 1, code: 'CB12' };\n }\n\n case 'bottomBarLengthTable': {\n // B값에 따라 하장바 자재 길이/수량 결정\n for (const row of bottomBarTable) {\n if (B >= row.bMin && B < row.bMax) {\n const materials = row.materials || [{ length: 3000, qty: 1 }];\n const totalQty = materials.reduce((sum, m) => sum + m.qty, 0);\n return {\n length: materials[0].length,\n materials: materials,\n barQty: totalQty,\n elbarQty: totalQty * 2, // 엘바는 2량1세트\n reinforceQty: totalQty,\n combo: row.combo || null\n };\n }\n }\n return { length: 4000, barQty: 2, elbarQty: 4, reinforceQty: 2 };\n }\n\n case 'hwanbongQtyTable': {\n // W1값에 따라 환봉 수량 결정 (스크린 전용)\n for (const row of hwanbongQtyTable) {\n if (W1 >= row.w1Min && W1 < row.w1Max) {\n return row.qty;\n }\n }\n return 4;\n }\n\n case 'squarePipeTable': {\n // S값에 따라 각파이프 수량 결정 (스크린/철재 구분)\n const table = productType === 'screen' ? squarePipeTableScreen : squarePipeTableSteel;\n for (const row of table) {\n if (S >= row.lMin && S < row.lMax) {\n return { qty3000: row.mat3000, qty6000: row.mat6000 };\n }\n }\n return { qty3000: 3, qty6000: 0 };\n }\n\n default:\n return null;\n }\n },\n\n // 샤프트 인치 결정 (W1, K, 제품유형 기반) - 새 테이블 형식 지원\n getShaftInch(W1, K, productType) {\n // 철재는 W1과 K 두 조건 모두 만족해야 함\n if (productType === 'steel') {\n for (const row of shaftInchTableSteel) {\n if (W1 >= row.w1Min && W1 < row.w1Max && K >= row.kMin && K < row.kMax) {\n return row.inch;\n }\n }\n return '5'; // 철재 기본값\n }\n\n // 스크린은 W1 기준으로만 결정\n for (const row of shaftInchTableScreen) {\n if (W1 >= row.w1Min && W1 < row.w1Max) {\n return row.inch;\n }\n }\n return '5'; // 스크린 기본값\n },\n\n // 동적 품목코드 생성\n generateItemCode(template, params) {\n let code = template;\n for (const [key, value] of Object.entries(params)) {\n code = code.replace(`{${key}}`, value);\n }\n return code;\n },\n\n // 견적 품목 산출 (Enhanced)\n calculateQuoteItems(inputs, formulas, priceList) {\n // 입력값 설정\n this.variables = { ...inputs };\n\n // 기본 변수 설정\n const PC = inputs.PC || '스크린';\n const W0 = inputs.W0 || 2000;\n const H0 = inputs.H0 || 2500;\n const QTY = inputs.QTY || 1;\n const V = inputs.V || '220';\n const WIRE = inputs.WIRE || '유선';\n const CT = inputs.CT || '매립';\n const GT = inputs.GT || '벽면형';\n\n this.setVariable('PC', PC);\n this.setVariable('QTY', QTY);\n this.setVariable('V', V);\n this.setVariable('WIRE', WIRE);\n this.setVariable('CT', CT);\n this.setVariable('GT', GT);\n\n // 제작사이즈 계산\n const W1 = PC === '스크린' ? W0 + 140 : W0 + 110;\n const H1 = H0 + 350;\n this.setVariable('W0', W0);\n this.setVariable('H0', H0);\n this.setVariable('W1', W1);\n this.setVariable('H1', H1);\n\n // 면적 및 중량 계산\n const M = (W1 * H1) / 1000000;\n const K = PC === '스크린' ? (M * 2) + (W0 * 14.17 / 1000) : M * 25;\n this.setVariable('M', M);\n this.setVariable('K', K);\n\n // 주요 제작 사이즈\n const S = W1; // 케이스 제작사이즈\n const B = W0; // 하단마감재 제작사이즈\n const G = H0 + 250; // 가이드레일 제작길이\n this.setVariable('S', S);\n this.setVariable('B', B);\n this.setVariable('G', G);\n\n // 룩업 테이블 평가\n const lookupParams = { W1, K, PC, S, B, G };\n\n // 모터 용량 결정\n const motorResult = this.evaluateLookup('motorCapacityTable', lookupParams);\n const MOTOR_KG = motorResult.motor;\n const SHAFT_INCH = motorResult.shaftInch;\n this.setVariable('MOTOR_KG', MOTOR_KG);\n this.setVariable('SHAFT_INCH', SHAFT_INCH);\n\n // 브라켓/앵글 규격\n const bracketResult = this.evaluateLookup('bracketTable', lookupParams);\n this.setVariable('BRACKET_SPEC', bracketResult.bracket);\n this.setVariable('ANGLE_SPEC', bracketResult.angle);\n this.setVariable('BRACKET_CODE', bracketResult.bracketCode);\n this.setVariable('ANGLE_CODE', bracketResult.angleCode);\n\n // 샤프트 자재 길이\n const shaftLength = this.evaluateLookup('shaftLengthTable', lookupParams);\n this.setVariable('MAIN_SHAFT_LEN', shaftLength);\n\n // 가이드레일 자재 길이\n const grResult = this.evaluateLookup('guideRailLengthTable', lookupParams);\n this.setVariable('GR_MAT_LEN', grResult.length);\n this.setVariable('GR_CODE', grResult.code);\n\n // 케이스 자재 길이/수량\n const caseResult = this.evaluateLookup('caseLengthTable', lookupParams);\n this.setVariable('CASE_MAT_LEN', caseResult.length);\n this.setVariable('CASE_MAT_QTY', caseResult.qty);\n this.setVariable('CASE_CODE', caseResult.code);\n\n // 하장바/엘바/보강평철\n const bottomResult = this.evaluateLookup('bottomBarLengthTable', lookupParams);\n this.setVariable('HJ_MAT_LEN', bottomResult.length);\n this.setVariable('HJ_QTY', bottomResult.barQty);\n this.setVariable('ELBAR_QTY', bottomResult.elbarQty);\n this.setVariable('REINFORCE_QTY', bottomResult.reinforceQty);\n\n // 환봉 수량 (스크린 전용)\n if (PC === '스크린') {\n const hwanbongQty = this.evaluateLookup('hwanbongQtyTable', lookupParams);\n this.setVariable('HWANBONG_QTY', hwanbongQty);\n }\n\n // 각파이프 수량\n const sqpResult = this.evaluateLookup('squarePipeTable', lookupParams);\n this.setVariable('SQP3000_QTY', sqpResult.qty3000);\n this.setVariable('SQP6000_QTY', sqpResult.qty6000);\n\n // 추가 수식 계산\n const TOP_COVER_QTY = Math.ceil(S / 1219);\n const CASE_SMOKE_QTY = Math.ceil(S / 3000) * 2;\n const JOINTBAR_QTY = 2 + Math.floor((W1 - 500) / 1000);\n const WEIGHT_FLAT_QTY = PC === '스크린' ? Math.ceil(B / 2000) * 2 : 0;\n const SUB_SHAFT_LEN = W1 <= 8200 ? 300 : 500;\n\n this.setVariable('TOP_COVER_QTY', TOP_COVER_QTY);\n this.setVariable('CASE_SMOKE_QTY', CASE_SMOKE_QTY);\n this.setVariable('JOINTBAR_QTY', JOINTBAR_QTY);\n this.setVariable('WEIGHT_FLAT_QTY', WEIGHT_FLAT_QTY);\n this.setVariable('SUB_SHAFT_LEN', SUB_SHAFT_LEN);\n\n // 동적 품목 코드 생성\n const MOTOR_CODE = `E-${MOTOR_KG}K${V}V-${WIRE === '유선' ? 'W' : 'R'}`;\n const CTL_CODE = `CTL-${CT === '매립' ? 'EMBED' : 'EXPOSE'}-${WIRE === '유선' ? 'W' : 'R'}`;\n this.setVariable('MOTOR_CODE', MOTOR_CODE);\n this.setVariable('CTL_CODE', CTL_CODE);\n\n // 출력 품목 생성\n const outputItems = [];\n const addItem = (code, name, spec, qty, category, process) => {\n const priceItem = priceList.find(p => p.itemCode === code || p.itemCode.startsWith(code.split('-')[0] + '-'));\n const unitPrice = priceItem?.sellingPrice || 0;\n const itemQty = Math.ceil(qty * QTY);\n if (itemQty > 0) {\n outputItems.push({\n id: Date.now() + outputItems.length + Math.random(),\n itemCode: code,\n itemName: name,\n spec,\n qty: itemQty,\n unit: priceItem?.unit || 'EA',\n unitPrice,\n amount: itemQty * unitPrice,\n category,\n process,\n });\n }\n };\n\n // === 구매부품 ===\n // 전동개폐기\n addItem(MOTOR_CODE, `전동개폐기 ${MOTOR_KG}KG ${V}V ${WIRE}`, `${MOTOR_KG}KG/${V}V`, 1, 'purchased-parts', '조립');\n\n // 연동제어기\n addItem(CTL_CODE, `연동제어기 ${CT}형 ${WIRE}`, `${CT}/${WIRE}`, 1, 'purchased-parts', '조립');\n\n // 브라켓/앵글\n addItem(bracketResult.bracketCode, `브라켓 ${bracketResult.bracket}`, bracketResult.bracket, 1, 'bracket-angle', '조립');\n addItem(bracketResult.angleCode, `받침용앵글 ${bracketResult.angle}`, bracketResult.angle, 1, 'bracket-angle', '조립');\n\n // === 감기샤프트 ===\n // 메인샤프트\n addItem(`SHAFT-${SHAFT_INCH}IN-${shaftLength}`, `감기샤프트(메인) ${SHAFT_INCH}인치`, `${SHAFT_INCH}인치 L:${shaftLength}`, 1, 'shaft', '샤프트');\n\n // 보조샤프트 (스크린 전용)\n if (PC === '스크린') {\n addItem(`SHAFT-SUB-3-${SUB_SHAFT_LEN}`, `감기샤프트(보조) 3인치`, `3인치 L:${SUB_SHAFT_LEN}`, 1, 'shaft', '샤프트');\n }\n\n // === 가이드레일 ===\n addItem(grResult.code, `가이드레일`, `L:${grResult.length}mm`, 2, 'guide-rail', '절곡');\n\n // 하부 BASE\n const baseCode = GT === '벽면형' ? 'BP-BASE-WALL' : 'BP-BASE-CORNER';\n addItem(baseCode, `하부BASE(${GT})`, GT, 1, 'guide-rail', '절곡');\n\n // === 연기차단재 ===\n const smokeQty = PC === '스크린' ? 2 : 1;\n addItem('BP-SMOKE', '연기차단재', `L:${G}mm`, smokeQty, 'smoke-block', '절곡');\n\n // === 셔터박스(케이스) ===\n addItem(caseResult.code, '케이스판재', `L:${caseResult.length}mm`, caseResult.qty, 'shutter-box', '절곡');\n addItem('BP-MARG', '마구리', '1SET=2EA', 1, 'shutter-box', '절곡');\n addItem('BP-TOPCOVER', '상부덮개', 'L:1219mm', TOP_COVER_QTY, 'shutter-box', '절곡');\n addItem('BP-CASE-SMOKE', '케이스용연기차단재', 'L:3000mm', CASE_SMOKE_QTY, 'shutter-box', '절곡');\n\n // === 하단마감재 ===\n const hjCode = bottomResult.length === 3000 ? 'BP-HJ-30' : 'BP-HJ-40';\n addItem(hjCode, `하장바 60×${PC === '스크린' ? '40' : '30'}`, `L:${bottomResult.length}mm`, bottomResult.barQty, 'bottom-finish', '절곡');\n\n if (PC === '스크린') {\n const elCode = bottomResult.length === 3000 ? 'BP-ELBAR-30' : 'BP-ELBAR-40';\n addItem(elCode, '엘바 17×60', `L:${bottomResult.length}mm`, bottomResult.elbarQty, 'bottom-finish', '절곡');\n\n const reCode = bottomResult.length === 3000 ? 'BP-REINFORCE-30' : 'BP-REINFORCE-40';\n addItem(reCode, '보강평철 50mm', `L:${bottomResult.length}mm`, bottomResult.reinforceQty, 'bottom-finish', '절곡');\n\n addItem('BP-WEIGHT-20', '무게평철 50×12T', 'L:2000mm', WEIGHT_FLAT_QTY, 'bottom-finish', '절곡');\n }\n\n // === 부자재 ===\n if (PC === '스크린') {\n addItem('SM-HWANBONG-30', '환봉', 'L:3000mm', this.getVariable('HWANBONG_QTY'), 'sub-material', '스크린');\n }\n addItem('SM-JOINTBAR', '조인트바', '', JOINTBAR_QTY, 'sub-material', '샤프트');\n addItem('SQP-30-30', '각파이프 30×30', 'L:3000mm', sqpResult.qty3000, 'sub-material', '조립');\n addItem('SQP-30-60', '각파이프 30×30', 'L:6000mm', sqpResult.qty6000, 'sub-material', '조립');\n\n // === 검사비 ===\n addItem('INSP-FEE', '검사비', '', 1, 'purchased-parts', '검사');\n\n return {\n variables: { ...this.variables },\n items: outputItems,\n totalAmount: outputItems.reduce((sum, item) => sum + item.amount, 0),\n summary: {\n productType: PC,\n openSize: `${W0} × ${H0}`,\n productionSize: `${W1} × ${H1}`,\n area: M.toFixed(2),\n weight: K.toFixed(2),\n motorCapacity: `${MOTOR_KG}KG`,\n shaftInch: `${SHAFT_INCH}인치`,\n qty: QTY,\n }\n };\n },\n};\n\n// ============ 공정 마스터 데이터 ============\nconst sampleProcesses = [\n {\n id: 1,\n processCode: 'P-001',\n processName: '스크린',\n processType: '생산',\n department: '스크린생산부서',\n equipment: '미싱기 3대, 절단기 1대',\n personnel: 3,\n workSheetType: 'WL-SCR',\n isActive: true,\n description: '방화스크린 원단 가공 및 조립',\n workSteps: ['원단절단', '미싱', '앤드락작업', '중간검사', '포장'],\n assignedWorkers: ['김스크린', '이스크린', '박스크린'], // 배정된 작업자\n createdAt: '2025-01-01',\n updatedAt: '2025-01-15',\n },\n {\n id: 2,\n processCode: 'P-002',\n processName: '절곡',\n processType: '생산',\n department: '절곡생산부서',\n equipment: '절곡기 2대, 용접기 1대',\n personnel: 4,\n workSheetType: 'WL-FLD',\n isActive: true,\n description: '가이드레일, 케이스, 하단마감재 제작',\n workSteps: ['가이드레일 제작', '케이스 제작', '하단마감재 제작', '검사'],\n assignedWorkers: ['김절곡', '이절곡', '박절곡', '최절곡'], // 배정된 작업자\n createdAt: '2025-01-01',\n updatedAt: '2025-01-15',\n },\n {\n id: 3,\n processCode: 'P-003',\n processName: '슬랫',\n processType: '생산',\n department: '슬랫생산부서',\n equipment: '코일절단기 1대, 성형기 2대',\n personnel: 3,\n workSheetType: 'WL-SLT',\n isActive: true,\n description: '슬랫 코일 절단 및 성형',\n workSteps: ['코일절단', '성형', '미미작업', '검사', '포장'],\n assignedWorkers: ['김슬랫', '이슬랫', '박슬랫'], // 배정된 작업자\n createdAt: '2025-01-01',\n updatedAt: '2025-01-15',\n },\n {\n id: 4,\n processCode: 'P-004',\n processName: '재고(포밍)',\n processType: '생산',\n department: '포밍생산부서',\n equipment: '포밍기, 프레스, 바코드스캐너',\n personnel: 2,\n workSheetType: 'WL-STK',\n isActive: true,\n description: '철판을 포밍하여 절곡 부품(반제품) 생산 후 재고 입고',\n workSteps: ['포밍', '검사', '포장'],\n assignedWorkers: ['김포밍', '이포밍'], // 배정된 작업자\n createdAt: '2025-01-01',\n updatedAt: '2025-01-15',\n },\n];\n\n// 샘플 상세 발주서 데이터 (용산고등학교 현장)\nconst sampleDetailedOrderData = {\n // 기본 정보\n orderInfo: {\n lotNo: 'KD-WE-251015-01-(3)',\n productName: '국민방화스크린셔터스테',\n productType: 'KWE01',\n certNo: 'FDS-OTS23-0117-4',\n orderDate: '2025-10-15',\n siteName: '용산고등학교(4층)',\n customerName: '주말건설',\n dueDate: '2025-10-30',\n receiverManager: '오전식반장',\n receiverContact: '010-6414-7929',\n orderManager: '김명현과장',\n deliveryDate: '2025-10-29',\n totalQty: 11, // 셔터총수량\n deliveryMethod: '선배',\n deliveryAddress: '경기도 용인시 처인구 고림동 320-8 용산고등학교',\n },\n\n // 1. 스크린 품목 테이블\n screenItems: [\n { seq: 1, type: '와이어', drawingNo: '4층 FSS1', openWidth: 7260, openHeight: 2600, prodWidth: 7400, prodHeight: 2950, guideRailType: '백면형', guideRailSpec: '120-70', shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감' },\n { seq: 2, type: '와이어', drawingNo: '4층 FSS5', openWidth: 4560, openHeight: 2600, prodWidth: 4700, prodHeight: 2950, guideRailType: '백면형', guideRailSpec: '120-70', shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감' },\n { seq: 3, type: '와이어', drawingNo: 'FSS17A', openWidth: 6650, openHeight: 2600, prodWidth: 6790, prodHeight: 2950, guideRailType: '백면형', guideRailSpec: '120-70', shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감' },\n { seq: 4, type: '와이어', drawingNo: '4층 FSS3', openWidth: 3560, openHeight: 2600, prodWidth: 3700, prodHeight: 2950, guideRailType: '백면형', guideRailSpec: '120-70', shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감' },\n { seq: 5, type: '와이어', drawingNo: '4층 FSS7', openWidth: 5860, openHeight: 2600, prodWidth: 6000, prodHeight: 2950, guideRailType: '백면형', guideRailSpec: '120-70', shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감' },\n { seq: 6, type: '와이어', drawingNo: '4층 FSS16', openWidth: 7160, openHeight: 2600, prodWidth: 7300, prodHeight: 2950, guideRailType: '백면형', guideRailSpec: '120-70', shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감' },\n { seq: 7, type: '와이어', drawingNo: '4층 FSS3-2', openWidth: 3560, openHeight: 2600, prodWidth: 3700, prodHeight: 2950, guideRailType: '백면형', guideRailSpec: '120-70', shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감' },\n { seq: 8, type: '와이어', drawingNo: '4층 FSS6', openWidth: 5760, openHeight: 2600, prodWidth: 5900, prodHeight: 2950, guideRailType: '백면형', guideRailSpec: '120-70', shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감' },\n { seq: 9, type: '와이어', drawingNo: '4층 FSS3-3', openWidth: 3770, openHeight: 2600, prodWidth: 3910, prodHeight: 2950, guideRailType: '백면형', guideRailSpec: '120-70', shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감' },\n { seq: 10, type: '와이어', drawingNo: '4층 FSS17', openWidth: 7260, openHeight: 2600, prodWidth: 7400, prodHeight: 2950, guideRailType: '백면형', guideRailSpec: '120-70', shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감' },\n { seq: 11, type: '와이어', drawingNo: '4층 FSS15', openWidth: 6860, openHeight: 2600, prodWidth: 7000, prodHeight: 2950, guideRailType: '백면형', guideRailSpec: '120-70', shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감' },\n ],\n\n // 2. 모터/전장품\n motorSpec: {\n motors220V: [\n { model: 'KD-150K', qty: 0 },\n { model: 'KD-300K', qty: 0 },\n { model: 'KD-400K', qty: 0 },\n ],\n motors380V: [\n { model: 'KD-150K', qty: 6 },\n { model: 'KD-300K', qty: 5 },\n { model: 'KD-400K', qty: 0 },\n ],\n brackets: [\n { spec: '380-180 [2-4\"]', qty: 0 },\n { spec: '380-180 [2-5\"]', qty: 5 },\n ],\n heatSinks: [\n { spec: '40-60', qty: 44 },\n { spec: 'L-380', qty: 0 },\n ],\n controllers: [\n { type: '매입형', qty: 0 },\n { type: '노출형', qty: 0 },\n { type: '핫라스', qty: 0 },\n ],\n },\n\n // 3. 절곡물 BOM\n bomData: {\n // 3-1. 가이드레일 (EGI 1.55T + 마감재 EGI 1.15T + 별도마감재 SUS 1.15T)\n guideRails: {\n description: '가이드레일 - EGI 1.55T + 마감재 EGI 1.15T + 별도마감재 SUS 1.15T',\n items: [\n // 백면형 (120-70)\n {\n type: '백면형', spec: '120-70', code: 'KSS01', lengths: [\n { length: 4300, qty: 0 },\n { length: 3500, qty: 0 },\n ]\n },\n {\n type: '백면형', spec: '120-70', code: 'KSE01/KWE01', lengths: [\n { length: 3000, qty: 22 },\n { length: 2438, qty: 0 },\n ]\n },\n // 측면형 (120-120)\n {\n type: '측면형', spec: '120-120', code: 'KSS01', lengths: [\n { length: 4300, qty: 0 },\n { length: 4000, qty: 0 },\n { length: 3500, qty: 0 },\n { length: 3000, qty: 0 },\n { length: 2438, qty: 0 },\n ]\n },\n // 하부BASE\n {\n type: '하부BASE', spec: '130-80', code: '', lengths: [\n { length: 0, qty: 22 },\n ]\n },\n ],\n smokeBarrier: { // 연기차단재\n spec: 'W80',\n material: '원 0.8T 화이바글라스크탑직물',\n lengths: [\n { length: 2438, qty: 44 },\n ],\n note: '* 전면부, 린텔부 양쪽에 설치',\n },\n },\n\n // 3-2. 케이스(셔터박스) - EGI 1.55T\n cases: {\n description: '케이스(셔터박스) - EGI 1.55T',\n mainSpec: '500-330 (150,300,400/K용)',\n items: [\n { length: 4150, qty: 1 },\n { length: 4000, qty: 7 },\n { length: 3500, qty: 5 },\n { length: 3000, qty: 3 },\n { length: 2438, qty: 3 },\n { length: 1219, qty: 0 },\n ],\n sideCover: { spec: '500-355', qty: 22 }, // 측면덮개\n topCover: { qty: 3 }, // 상부덮개\n extension: { spec: '1219+무게', qty: 59 },\n smokeBarrier: { spec: 'W80', length: 3000, qty: 47 }, // 연기차단재\n },\n\n // 3-3. 하단마감재\n bottomFinish: {\n description: '하단마감재 - 마단마감재(EGI 1.55T) + 하단보강별바(EGI 1.55T) + 하단 보강횡철(EGI 1.15T) + 하단 부재횡철(50-12T)',\n items: [\n {\n name: '하단마감재', spec: '50-40', lengths: [\n { length: 4000, qty: 11 },\n { length: 3000, qty: 8 },\n ]\n },\n {\n name: '하단보강빔바', spec: '80-17', lengths: [\n { length: 4000, qty: 28 },\n { length: 3000, qty: 12 },\n ]\n },\n {\n name: '하단보강철', spec: '', lengths: [\n { length: 4000, qty: 13 },\n { length: 3000, qty: 0 },\n ]\n },\n {\n name: '하단부재횡철', spec: '50-12T', lengths: [\n { length: 2000, qty: 67 },\n ]\n },\n ],\n },\n },\n};\n\n// 수주 (로트번호 = 수주번호 = 모든 추적의 기준)\n// ⚠️ 배열 순서: 최신 등록일 순 (내림차순) - 가장 최근 수주가 맨 앞에 위치\n// 채번규칙: SO-YYMMDD-## (수주확인서), QT-YYMMDD-## (견적서), WO-YYMMDD-## (작업지시서)\n// 현장코드: S-### (현장), 거래처코드: CUS-### (고객)\nconst initialOrders = [\n /*\n * ============================================================\n * 📋 수주 데이터 - 8개 테스트 시나리오 (최신순 정렬)\n * - customerMasterConfig.sampleCustomers 기준 (거래처 연동)\n * - siteMasterConfig.sampleSites 기준 (현장 연동)\n * - 채번규칙: 스크린=KD-TS, 슬랫=KD-SL, 절곡=KD-BD + YYMMDD-##\n * ============================================================\n * [최신] KD-TS-251210-01 (용산고등학교 - 직접수주) → 생산중\n * [2] KD-TS-251208-01 (현대건설 - 힐스테이트 용인) → 생산중\n * [3] KD-TS-251207-01 (삼성물산 - 래미안 강남 2차) → 재작업중\n * [4] KD-TS-251206-01 (서울인테리어 - 강남 오피스타워) → 분할 진행중\n * [5] KD-TS-251205-01 (삼성물산 추가분, 래미안 강남 1차) → 생산중\n * [6] KD-TS-251204-01 (대우건설 - 푸르지오 일산) → 경리승인 대기\n * [7] KD-TS-251203-01 (현대건설 - 힐스테이트 판교) → 생산완료, 출고보류\n * [8] KD-TS-251201-01 (삼성물산 - 래미안 강남 1차) → 생산지시완료\n * ============================================================\n */\n\n // ========== 시나리오 1: A등급 정상 플로우 (삼성물산 - 래미안 강남 1차) ==========\n {\n id: 1,\n orderNo: 'KD-TS-251201-01',\n lotNo: 'KD-TS-251201-01',\n orderDate: '2025-12-01',\n quoteId: 1,\n quoteNo: 'KD-PR-251201-01',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 1, // 삼성물산(주) - customerMasterConfig.sampleCustomers[0]\n customerCode: 'CUS-001',\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteId: 1, // siteMasterConfig.sampleSites[0]\n siteName: '삼성물산 래미안 강남 1차',\n siteCode: 'PJ-250110-01',\n manager: '이건설',\n contact: '010-1111-1111',\n dueDate: '2025-12-20',\n scheduledShipDate: '2025-12-18', // 출고예정일\n status: '생산지시완료',\n paymentStatus: '전액입금',\n // 회계 상태 (A등급: 자동 진행)\n accountingStatus: '회계확인완료',\n accountingConfirmedBy: '회계팀 박회계',\n accountingConfirmedAt: '2025-02-06 10:00',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n // 거래명세서/세금계산서\n invoiceIssued: true,\n invoiceIssuedAt: '2025-02-05 15:00',\n taxInvoiceIssued: true,\n taxInvoiceIssuedAt: '2025-02-05 15:30',\n // 금액\n totalAmount: 52000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 52000000,\n paidAmount: 52000000,\n remainingAmount: 0,\n deliveryMethod: '상차',\n deliveryAddress: '서울시 강남구 테헤란로 100',\n receiverName: '이건설',\n receiverPhone: '010-1111-1111',\n // 품목 목록 (상세 제작 스펙 포함)\n items: [\n {\n id: 1,\n productCode: 'SCR-001',\n productName: '스크린 셔터 (표준형)',\n floor: '1층',\n location: 'A-01',\n spec: '7660×2550',\n qty: 1,\n unit: 'EA',\n unitPrice: 35000000,\n amount: 35000000,\n // 상세 제작 스펙 (발주서 양식)\n productionSpec: {\n type: '와이어',\n drawingNo: '1층 FSS1',\n openWidth: 7660,\n openHeight: 2550,\n prodWidth: 7800,\n prodHeight: 2950,\n guideRailType: '백면형',\n guideRailSpec: '120-70',\n shaft: 5,\n caseSpec: '500-330',\n motorBracket: '380-180',\n capacity: 300,\n finish: 'SUS마감',\n },\n },\n {\n id: 2,\n productCode: 'SCR-001',\n productName: '스크린 셔터 (표준형)',\n floor: '1층',\n location: 'A-02',\n spec: '6500×2400',\n qty: 1,\n unit: 'EA',\n unitPrice: 17000000,\n amount: 17000000,\n productionSpec: {\n type: '와이어',\n drawingNo: '1층 FSS2',\n openWidth: 6500,\n openHeight: 2400,\n prodWidth: 6640,\n prodHeight: 2950,\n guideRailType: '백면형',\n guideRailSpec: '120-70',\n shaft: 4,\n caseSpec: '500-330',\n motorBracket: '380-180',\n capacity: 160,\n finish: 'SUS마감',\n },\n },\n ],\n // 모터/전장품 스펙\n motorSpec: {\n motors220V: [\n { model: 'KD-150K', qty: 0 },\n { model: 'KD-300K', qty: 0 },\n ],\n motors380V: [\n { model: 'KD-150K', qty: 1 },\n { model: 'KD-300K', qty: 1 },\n ],\n brackets: [\n { spec: '380-180 [2-5\"]', qty: 2 },\n ],\n heatSinks: [\n { spec: '40-60', qty: 4 },\n ],\n controllers: [\n { type: '매입형', qty: 0 },\n { type: '노출형', qty: 0 },\n ],\n },\n // 절곡물 BOM (자동 계산된 자재 소요량)\n bomData: {\n guideRails: {\n items: [\n {\n type: '백면형', spec: '120-70', lengths: [\n { length: 3000, qty: 4 },\n ]\n },\n {\n type: '하부BASE', spec: '130-80', lengths: [\n { length: 0, qty: 4 },\n ]\n },\n ],\n smokeBarrier: { lengths: [{ length: 2950, qty: 4 }] },\n },\n cases: {\n mainSpec: '500-330',\n items: [\n { length: 4000, qty: 2 },\n ],\n sideCover: { spec: '500-355', qty: 4 },\n topCover: { qty: 0 },\n smokeBarrier: { length: 3000, qty: 2 },\n },\n bottomFinish: {\n items: [\n { name: '하단마감재', spec: '50-40', lengths: [{ length: 4000, qty: 2 }] },\n { name: '하단보강빔바', spec: '80-17', lengths: [{ length: 4000, qty: 4 }] },\n ],\n },\n },\n splits: [\n {\n id: 1,\n splitNo: 'KD-TS-251201-01-01',\n lotNo: 'KD-TS-251201-01',\n splitOrder: 1,\n splitType: '일괄',\n itemIds: [1, 2],\n dueDate: '2025-12-15',\n productionStatus: '작업완료',\n shipmentStatus: '출하완료',\n productionOrderNo: 'KD-WO-251201-01',\n totalQty: 2,\n completedQty: 2,\n remainingQty: 0,\n },\n ],\n documentHistory: [\n { id: 1, docType: '거래명세서', sentAt: '2025-12-01 15:00', sentBy: '판매팀', sentMethod: '이메일', recipient: 'procurement@samsung.com', status: '발송완료' },\n { id: 2, docType: '세금계산서', sentAt: '2025-12-01 15:30', sentBy: '회계팀', sentMethod: '전자발행', recipient: '1248100998', status: '발행완료' },\n ],\n changeHistory: [\n { id: 1, changedAt: '2025-12-01 14:00', changeType: '수주등록', description: 'A등급 거래처 - 자동 진행', changedBy: '판매팀 김판매' },\n { id: 2, changedAt: '2025-12-02 10:00', changeType: '회계확인', description: '전액 입금 확인', changedBy: '회계팀 박회계' },\n { id: 3, changedAt: '2025-12-18 16:00', changeType: '출하완료', description: '배송 완료', changedBy: '물류팀 박배송' },\n ],\n createdAt: '2025-12-01',\n createdBy: '판매팀 김판매',\n note: '[시나리오1] A등급 정상 플로우 - 자동 진행 완료',\n },\n\n // ========== 시나리오 2: A등급 생산완료 후 입금대기 (현대건설 - 힐스테이트 판교) ==========\n {\n id: 2,\n orderNo: 'KD-TS-251203-01',\n lotNo: 'KD-TS-251203-01',\n orderDate: '2025-12-03',\n quoteId: 2,\n quoteNo: 'KD-PR-251203-01',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 2, // 현대건설(주) - customerMasterConfig.sampleCustomers[1]\n customerName: '현대건설(주)',\n creditGrade: 'A',\n siteId: 3, // siteMasterConfig.sampleSites[2] - 현대건설 힐스테이트 판교\n siteName: '현대건설 힐스테이트 판교',\n siteCode: 'PJ-250112-01',\n manager: '이현장',\n contact: '010-2345-6789',\n dueDate: '2025-12-25',\n scheduledShipDate: '미정', // 출고예정일 미정\n status: '생산지시완료',\n paymentStatus: '미입금',\n // 회계 상태 (입금확인 후 출고)\n accountingStatus: '세금계산서발행',\n accountingConfirmedBy: null,\n accountingConfirmedAt: null,\n requireApproval: false,\n requirePaymentBeforeShip: true, // 입금 후 출고\n shipmentHold: true, // 출고 보류\n shipmentHoldReason: '입금 미확인',\n // 거래명세서/세금계산서\n invoiceIssued: true,\n invoiceIssuedAt: '2025-02-08 14:00',\n taxInvoiceIssued: true,\n taxInvoiceIssuedAt: '2025-02-08 14:30',\n // 금액\n totalAmount: 28000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 28000000,\n paidAmount: 0,\n remainingAmount: 28000000,\n deliveryMethod: '직접배차',\n deliveryAddress: '경기도 성남시 분당구 판교동 100-1',\n receiverName: '이현장',\n receiverPhone: '010-2345-6789',\n items: [\n { id: 1, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: 'B1', location: 'C-01', spec: '6000×2400', qty: 2, unit: 'EA', unitPrice: 8000000, amount: 16000000 },\n { id: 2, productCode: 'SLT-001', productName: '슬랫 셔터', floor: '1층', location: 'D-01', spec: '4500×2000', qty: 2, unit: 'EA', unitPrice: 6000000, amount: 12000000 },\n ],\n splits: [\n {\n id: 1,\n splitNo: 'KD-TS-251203-01-01',\n lotNo: 'KD-TS-251203-01',\n splitOrder: 1,\n splitType: '층별',\n itemIds: [1],\n dueDate: '2025-12-20',\n productionStatus: '작업완료',\n shipmentStatus: '출고보류', // 입금대기\n productionOrderNo: 'KD-WO-251203-01',\n totalQty: 2,\n completedQty: 2,\n remainingQty: 0,\n },\n {\n id: 2,\n splitNo: 'KD-TS-251203-01-02',\n lotNo: 'KD-TS-251203-01',\n splitOrder: 2,\n splitType: '층별',\n itemIds: [2],\n dueDate: '2025-12-25',\n productionStatus: '작업완료',\n shipmentStatus: '출고보류', // 입금대기\n productionOrderNo: 'KD-WO-251203-02',\n totalQty: 2,\n completedQty: 2,\n remainingQty: 0,\n },\n ],\n documentHistory: [\n { id: 1, docType: '거래명세서', sentAt: '2025-12-03 14:00', sentBy: '판매팀', sentMethod: '이메일', status: '발송완료' },\n { id: 2, docType: '세금계산서', sentAt: '2025-12-03 14:30', sentBy: '회계팀', sentMethod: '전자발행', status: '발행완료' },\n ],\n changeHistory: [\n { id: 1, changedAt: '2025-12-03 13:00', changeType: '수주등록', description: '입금확인 후 출고 조건', changedBy: '판매팀 이판매' },\n { id: 2, changedAt: '2025-12-15 09:00', changeType: '생산완료', description: '전량 생산 완료, 출고 대기', changedBy: '생산팀 박생산' },\n { id: 3, changedAt: '2025-12-16 10:00', changeType: '출고보류', description: '입금 미확인으로 출고 보류', changedBy: '물류팀 김물류' },\n ],\n createdAt: '2025-12-03',\n createdBy: '판매팀 이판매',\n note: '[시나리오2] 생산완료, 입금확인 대기 중 (출고보류)',\n },\n\n // ========== 시나리오 3: 경리승인 후 생산 (대우건설 - 푸르지오 일산) ==========\n {\n id: 3,\n orderNo: 'KD-TS-251204-01',\n lotNo: 'KD-TS-251204-01',\n orderDate: '2025-12-05',\n quoteId: 3,\n quoteNo: 'KD-PR-251204-01',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 3, // 대우건설(주) - customerMasterConfig.sampleCustomers[2]\n customerName: '대우건설(주)',\n creditGrade: 'A',\n siteId: 4, // siteMasterConfig.sampleSites[3] - 대우건설 푸르지오 일산\n siteName: '대우건설 푸르지오 일산',\n siteCode: 'PJ-250118-01',\n manager: '박건설',\n contact: '010-3456-7890',\n dueDate: '2025-12-30',\n scheduledShipDate: '미정', // 출고예정일 미정 (경리승인 대기)\n status: '수주확정', // 아직 생산지시 안됨\n paymentStatus: '선입금대기',\n // 회계 상태 (경리승인 필요)\n accountingStatus: '미확인',\n accountingConfirmedBy: null,\n accountingConfirmedAt: null,\n requireApproval: true, // 경리 승인 필요\n approvalStatus: '승인대기',\n approvalRequestedAt: '2025-02-10 15:00',\n requirePaymentBeforeShip: true, // 입금 후 출고\n productionHold: true, // 생산 보류\n productionHoldReason: '경리 승인 대기',\n // 거래명세서/세금계산서\n invoiceIssued: false,\n taxInvoiceIssued: false,\n // 금액\n totalAmount: 18000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 18000000,\n paidAmount: 0,\n remainingAmount: 18000000,\n depositRequired: 9000000, // 선입금 50%\n deliveryMethod: '화물',\n deliveryAddress: '경기도 고양시 일산동구 마두동 200-5',\n receiverName: '박건설',\n receiverPhone: '010-3456-7890',\n items: [\n { id: 1, productCode: 'SLT-001', productName: '슬랫 셔터', floor: '1층', location: 'G-01', spec: '4000×2000', qty: 6, unit: 'EA', unitPrice: 3000000, amount: 18000000 },\n ],\n splits: [], // 아직 분할 안됨\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-05 14:00', changeType: '수주등록', description: '경리승인 필요', changedBy: '판매팀 박판매' },\n { id: 2, changedAt: '2025-12-05 15:00', changeType: '승인요청', description: '경리팀에 생산 승인 요청', changedBy: '판매팀 박판매' },\n { id: 3, changedAt: '2025-12-06 09:00', changeType: '경리확인', description: '거래처 통화 - 선입금 50% 약속 확인', changedBy: '회계팀 김회계' },\n ],\n createdAt: '2025-12-05',\n createdBy: '판매팀 박판매',\n note: '[시나리오3] 경리승인 대기 중 (생산보류)',\n },\n\n // ========== 시나리오 4: 추가분 수주 (삼성물산 - 래미안 강남 1차 추가분) ==========\n {\n id: 4,\n orderNo: 'KD-TS-251205-01-A',\n lotNo: 'KD-TS-251205-01-A',\n orderDate: '2025-12-08',\n quoteId: 4,\n quoteNo: 'KD-PR-251205-01',\n orderType: 'additional', // 추가분\n parentLotNo: 'KD-TS-251201-01', // 원 로트번호\n customerId: 1, // 삼성물산(주)\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteId: 1, // 래미안 강남 1차\n siteName: '삼성물산 래미안 강남 1차',\n siteCode: 'PJ-250110-01',\n manager: '김건설',\n contact: '010-1234-5678',\n dueDate: '2025-12-28',\n scheduledShipDate: '2025-12-26', // 출고예정일\n status: '생산지시완료',\n paymentStatus: '계약금입금',\n accountingStatus: '입금확인',\n accountingConfirmedBy: '회계팀 박회계',\n accountingConfirmedAt: '2025-12-09 10:00',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n invoiceIssuedAt: '2025-12-08 16:00',\n taxInvoiceIssued: true,\n taxInvoiceIssuedAt: '2025-12-08 16:30',\n totalAmount: 12000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 12000000,\n paidAmount: 6000000,\n remainingAmount: 6000000,\n deliveryMethod: '상차',\n deliveryAddress: '서울특별시 강남구 도곡동 123-45',\n receiverName: '김건설',\n receiverPhone: '010-1234-5678',\n items: [\n { id: 1, productCode: 'SCR-002', productName: '스크린 셔터 (프리미엄)', floor: '3층', location: 'C-01', spec: '8000×2800', qty: 3, unit: 'EA', unitPrice: 4000000, amount: 12000000 },\n ],\n splits: [\n {\n id: 1,\n splitNo: 'KD-TS-251205-01-A-01',\n lotNo: 'KD-TS-251205-01-A',\n splitOrder: 1,\n splitType: '일괄',\n itemIds: [1],\n dueDate: '2025-12-24',\n productionStatus: '작업중',\n shipmentStatus: '대기',\n productionOrderNo: 'KD-WO-251205-01',\n totalQty: 3,\n completedQty: 1,\n remainingQty: 2,\n },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-08 15:00', changeType: '추가분등록', description: '원 수주 KD-TS-251201-01의 3층 추가분', changedBy: '판매팀 김판매' },\n { id: 2, changedAt: '2025-12-10 09:00', changeType: '생산지시', description: '생산지시 완료', changedBy: '판매팀 김판매' },\n ],\n createdAt: '2025-12-08',\n createdBy: '판매팀 김판매',\n note: '[시나리오4] 추가분 수주 - 기존 현장 3층 추가',\n },\n\n // ========== 시나리오 5: 분할 출하 (서울인테리어 - 강남 오피스타워) ==========\n {\n id: 5,\n orderNo: 'KD-TS-251206-01',\n lotNo: 'KD-TS-251206-01',\n orderDate: '2025-12-09',\n quoteId: 5,\n quoteNo: 'KD-PR-251206-01',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 4, // (주)서울인테리어\n customerName: '(주)서울인테리어',\n creditGrade: 'B',\n siteId: 5, // 강남 오피스타워 인테리어\n siteName: '강남 오피스타워 인테리어',\n siteCode: 'PJ-250201-01',\n manager: '김담당',\n contact: '010-4567-8901',\n dueDate: '2025-12-29',\n scheduledShipDate: '2025-12-27', // 출고예정일 (2차)\n status: '생산지시완료',\n paymentStatus: '전액입금',\n accountingStatus: '회계확인완료',\n accountingConfirmedBy: '회계팀 박회계',\n accountingConfirmedAt: '2025-12-10 10:00',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n invoiceIssuedAt: '2025-12-09 15:00',\n taxInvoiceIssued: true,\n taxInvoiceIssuedAt: '2025-12-09 15:30',\n totalAmount: 35000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 35000000,\n paidAmount: 35000000,\n remainingAmount: 0,\n deliveryMethod: '직접배차',\n deliveryAddress: '서울특별시 강남구 역삼동 300-10',\n receiverName: '김담당',\n receiverPhone: '010-4567-8901',\n items: [\n { id: 1, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'E-01', spec: '7000×2500', qty: 3, unit: 'EA', unitPrice: 8000000, amount: 24000000 },\n { id: 2, productCode: 'SLT-001', productName: '슬랫 셔터', floor: '2층', location: 'F-01', spec: '5000×2200', qty: 2, unit: 'EA', unitPrice: 5500000, amount: 11000000 },\n ],\n splits: [\n {\n id: 1,\n splitNo: 'KD-TS-251206-01-01',\n lotNo: 'KD-TS-251206-01',\n splitOrder: 1,\n splitType: '층별',\n itemIds: [1],\n dueDate: '2025-12-22', // 1차 출고일\n productionStatus: '작업완료',\n shipmentStatus: '배송완료',\n productionOrderNo: 'KD-WO-251206-01',\n totalQty: 3,\n completedQty: 3,\n remainingQty: 0,\n },\n {\n id: 2,\n splitNo: 'KD-TS-251206-01-02',\n lotNo: 'KD-TS-251206-01',\n splitOrder: 2,\n splitType: '층별',\n itemIds: [2],\n dueDate: '2025-12-27', // 2차 출고일\n productionStatus: '작업완료',\n shipmentStatus: '출하준비',\n productionOrderNo: 'KD-WO-251206-02',\n totalQty: 2,\n completedQty: 2,\n remainingQty: 0,\n },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-09 14:00', changeType: '수주등록', description: '분할 출하 설정 - 1차 12/22, 2차 12/27', changedBy: '판매팀 최판매' },\n { id: 2, changedAt: '2025-12-22 16:00', changeType: '1차출하', description: '1층 스크린 셔터 출하 완료', changedBy: '물류팀 박배송' },\n ],\n createdAt: '2025-12-09',\n createdBy: '판매팀 최판매',\n note: '[시나리오5] 분할 출하 - 1차 완료, 2차 대기',\n },\n\n // ========== 시나리오 6: 품질 불량 (삼성물산 - 래미안 강남 2차) ==========\n {\n id: 6,\n orderNo: 'KD-TS-251207-01',\n lotNo: 'KD-TS-251207-01',\n orderDate: '2025-12-10',\n quoteId: 6,\n quoteNo: 'KD-PR-251207-01',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 1, // 삼성물산(주)\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteId: 2, // 래미안 강남 2차\n siteName: '삼성물산 래미안 강남 2차',\n siteCode: 'PJ-250115-01',\n manager: '이건설',\n contact: '010-1111-1111',\n dueDate: '2025-12-28',\n scheduledShipDate: '미정', // 출고예정일 미정 (재작업 중)\n status: '생산지시완료',\n paymentStatus: '계약금입금',\n accountingStatus: '입금확인',\n accountingConfirmedBy: '회계팀 박회계',\n accountingConfirmedAt: '2025-12-11 10:00',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n invoiceIssuedAt: '2025-12-10 15:00',\n taxInvoiceIssued: true,\n taxInvoiceIssuedAt: '2025-12-10 15:30',\n totalAmount: 16000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 16000000,\n paidAmount: 8000000,\n remainingAmount: 8000000,\n deliveryMethod: '화물',\n deliveryAddress: '서울시 강남구 테헤란로 100',\n receiverName: '이건설',\n receiverPhone: '010-1111-1111',\n items: [\n { id: 1, productCode: 'SLT-001', productName: '슬랫 셔터', floor: '1층', location: 'H-01', spec: '4500×2000', qty: 4, unit: 'EA', unitPrice: 4000000, amount: 16000000 },\n ],\n splits: [\n {\n id: 1,\n splitNo: 'KD-TS-251207-01-01',\n lotNo: 'KD-TS-251207-01',\n splitOrder: 1,\n splitType: '일괄',\n itemIds: [1],\n dueDate: '2025-12-24',\n productionStatus: '재작업중', // ⚠️ 품질불량으로 재작업\n shipmentStatus: '대기',\n productionOrderNo: 'KD-WO-251207-01',\n totalQty: 4,\n completedQty: 2,\n remainingQty: 2,\n qualityIssue: true,\n qualityIssueNote: '중간검사 불합격 - 치수불량 2EA 재작업',\n },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-10 14:00', changeType: '수주등록', description: 'B동 슬랫 셔터', changedBy: '판매팀 김판매' },\n { id: 2, changedAt: '2025-12-13 14:00', changeType: '품질불량', description: '중간검사 불합격 - 치수불량 2EA', changedBy: '품질팀 이검사' },\n { id: 3, changedAt: '2025-12-14 09:00', changeType: '재작업지시', description: '불량 2EA 재작업 지시', changedBy: '생산팀 박생산' },\n ],\n createdAt: '2025-12-10',\n createdBy: '판매팀 김판매',\n note: '[시나리오6] 품질 불량 - 중간검사 불합격, 재작업 진행 중',\n },\n\n // ========== 시나리오 7: 할인 적용 (현대건설 - 힐스테이트 용인) ==========\n {\n id: 7,\n orderNo: 'KD-TS-251208-01',\n lotNo: 'KD-TS-251208-01',\n orderDate: '2025-12-12',\n quoteId: 7,\n quoteNo: 'KD-PR-251208-01',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 2, // 현대건설(주) - customerMasterConfig.sampleCustomers[1]\n customerName: '현대건설(주)',\n creditGrade: 'A',\n siteId: 7, // siteMasterConfig.sampleSites[6] - 현대건설 힐스테이트 용인\n siteName: '현대건설 힐스테이트 용인',\n siteCode: 'PJ-240915-01',\n manager: '이현장',\n contact: '010-2345-6789',\n dueDate: '2026-01-05',\n scheduledShipDate: '2026-01-03', // 출고예정일\n status: '생산지시완료',\n paymentStatus: '미입금',\n accountingStatus: '세금계산서발행',\n accountingConfirmedBy: null,\n accountingConfirmedAt: null,\n requireApproval: false,\n requirePaymentBeforeShip: true,\n invoiceIssued: true,\n invoiceIssuedAt: '2025-12-12 15:00',\n taxInvoiceIssued: true,\n taxInvoiceIssuedAt: '2025-12-12 15:30',\n // 할인 적용\n totalAmount: 30000000,\n discountRate: 10,\n discountAmount: 3000000,\n finalAmount: 27000000, // 할인 후 금액\n paidAmount: 0,\n remainingAmount: 27000000,\n deliveryMethod: '택배',\n deliveryAddress: '경기도 용인시 수지구 상현동 400-20',\n receiverName: '강현장',\n receiverPhone: '010-7777-8888',\n items: [\n { id: 1, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'I-01', spec: '6000×2400', qty: 6, unit: 'EA', unitPrice: 5000000, amount: 30000000 },\n ],\n splits: [\n {\n id: 1,\n splitNo: 'KD-TS-251208-01-01',\n lotNo: 'KD-TS-251208-01',\n splitOrder: 1,\n splitType: '일괄',\n itemIds: [1],\n dueDate: '2025-12-30',\n productionStatus: '작업중',\n shipmentStatus: '대기',\n productionOrderNo: 'KD-WO-251208-01',\n totalQty: 6,\n completedQty: 2,\n remainingQty: 4,\n },\n ],\n documentHistory: [\n { id: 1, docType: '거래명세서', sentAt: '2025-12-12 15:00', sentBy: '판매팀', sentMethod: '이메일', status: '발송완료', note: '할인 10% 적용' },\n { id: 2, docType: '세금계산서', sentAt: '2025-12-12 15:30', sentBy: '회계팀', sentMethod: '전자발행', status: '발행완료', note: '공급가액 24,545,455원' },\n ],\n changeHistory: [\n { id: 1, changedAt: '2025-12-12 14:00', changeType: '수주등록', description: '대량 주문 10% 할인 적용', changedBy: '판매팀 이판매' },\n { id: 2, changedAt: '2025-12-12 14:30', changeType: '할인적용', description: '할인 10% (3,000,000원) 적용', changedBy: '회계팀 김회계' },\n ],\n createdAt: '2025-12-12',\n createdBy: '판매팀 이판매',\n note: '[시나리오7] 할인 적용 - 대량 주문 10% 할인',\n },\n\n // ========== 시나리오 8: 발주서 양식 테스트 (용산고등학교 - 직접수주) - 가장 최신 ==========\n {\n id: 8,\n orderNo: 'KD-TS-251210-01',\n lotNo: 'KD-TS-251210-01',\n orderDate: '2025-12-15',\n quoteId: null,\n quoteNo: null,\n orderType: 'direct', // 직접수주 (견적 없이 바로 수주)\n parentLotNo: null,\n customerId: null, // 신규 거래처 - 미등록\n customerName: '주말건설',\n creditGrade: 'B',\n siteId: null, // 신규 현장 - 미등록\n siteName: '용산고등학교(4층)',\n siteCode: 'PJ-251215-02',\n manager: '김명현과장',\n contact: '010-6414-7929',\n dueDate: '2026-01-10',\n scheduledShipDate: '미정', // 출고예정일 미정 (신규 직접수주)\n status: '생산지시완료',\n paymentStatus: '미입금',\n accountingStatus: '미확인',\n accountingConfirmedBy: null,\n accountingConfirmedAt: null,\n requireApproval: false,\n requirePaymentBeforeShip: true,\n invoiceIssued: false,\n taxInvoiceIssued: false,\n totalAmount: 88000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 88000000,\n paidAmount: 0,\n remainingAmount: 88000000,\n deliveryMethod: '선배',\n deliveryAddress: '경기도 용인시 처인구 고림동 320-8 용산고등학교',\n receiverName: '오전식반장',\n receiverPhone: '010-6414-7929',\n // ====== 품목 11개 (발주서 이미지와 동일) ======\n items: [\n {\n id: 1, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터',\n floor: '4층', location: 'FSS1', spec: '7260×2600', qty: 1, unit: 'EA',\n unitPrice: 8000000, amount: 8000000,\n productionSpec: {\n type: '와이어', drawingNo: '4층 FSS1',\n openWidth: 7260, openHeight: 2600, prodWidth: 7400, prodHeight: 2950,\n guideRailType: '백면형', guideRailSpec: '120-70',\n shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감',\n },\n },\n {\n id: 2, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터',\n floor: '4층', location: 'FSS5', spec: '4560×2600', qty: 1, unit: 'EA',\n unitPrice: 8000000, amount: 8000000,\n productionSpec: {\n type: '와이어', drawingNo: '4층 FSS5',\n openWidth: 4560, openHeight: 2600, prodWidth: 4700, prodHeight: 2950,\n guideRailType: '백면형', guideRailSpec: '120-70',\n shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감',\n },\n },\n {\n id: 3, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터',\n floor: '4층', location: 'FSS17A', spec: '6650×2600', qty: 1, unit: 'EA',\n unitPrice: 8000000, amount: 8000000,\n productionSpec: {\n type: '와이어', drawingNo: 'FSS17A',\n openWidth: 6650, openHeight: 2600, prodWidth: 6790, prodHeight: 2950,\n guideRailType: '백면형', guideRailSpec: '120-70',\n shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감',\n },\n },\n {\n id: 4, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터',\n floor: '4층', location: 'FSS3', spec: '3560×2600', qty: 1, unit: 'EA',\n unitPrice: 8000000, amount: 8000000,\n productionSpec: {\n type: '와이어', drawingNo: '4층 FSS3',\n openWidth: 3560, openHeight: 2600, prodWidth: 3700, prodHeight: 2950,\n guideRailType: '백면형', guideRailSpec: '120-70',\n shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감',\n },\n },\n {\n id: 5, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터',\n floor: '4층', location: 'FSS7', spec: '5860×2600', qty: 1, unit: 'EA',\n unitPrice: 8000000, amount: 8000000,\n productionSpec: {\n type: '와이어', drawingNo: '4층 FSS7',\n openWidth: 5860, openHeight: 2600, prodWidth: 6000, prodHeight: 2950,\n guideRailType: '백면형', guideRailSpec: '120-70',\n shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감',\n },\n },\n {\n id: 6, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터',\n floor: '4층', location: 'FSS16', spec: '7160×2600', qty: 1, unit: 'EA',\n unitPrice: 8000000, amount: 8000000,\n productionSpec: {\n type: '와이어', drawingNo: '4층 FSS16',\n openWidth: 7160, openHeight: 2600, prodWidth: 7300, prodHeight: 2950,\n guideRailType: '백면형', guideRailSpec: '120-70',\n shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감',\n },\n },\n {\n id: 7, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터',\n floor: '4층', location: 'FSS3-2', spec: '3560×2600', qty: 1, unit: 'EA',\n unitPrice: 8000000, amount: 8000000,\n productionSpec: {\n type: '와이어', drawingNo: '4층 FSS3-2',\n openWidth: 3560, openHeight: 2600, prodWidth: 3700, prodHeight: 2950,\n guideRailType: '백면형', guideRailSpec: '120-70',\n shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감',\n },\n },\n {\n id: 8, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터',\n floor: '4층', location: 'FSS6', spec: '5760×2600', qty: 1, unit: 'EA',\n unitPrice: 8000000, amount: 8000000,\n productionSpec: {\n type: '와이어', drawingNo: '4층 FSS6',\n openWidth: 5760, openHeight: 2600, prodWidth: 5900, prodHeight: 2950,\n guideRailType: '백면형', guideRailSpec: '120-70',\n shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감',\n },\n },\n {\n id: 9, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터',\n floor: '4층', location: 'FSS3-3', spec: '3770×2600', qty: 1, unit: 'EA',\n unitPrice: 8000000, amount: 8000000,\n productionSpec: {\n type: '와이어', drawingNo: '4층 FSS3-3',\n openWidth: 3770, openHeight: 2600, prodWidth: 3910, prodHeight: 2950,\n guideRailType: '백면형', guideRailSpec: '120-70',\n shaft: 4, caseSpec: '500-330', motorBracket: '380-180', capacity: 160, finish: 'SUS마감',\n },\n },\n {\n id: 10, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터',\n floor: '4층', location: 'FSS17', spec: '7260×2600', qty: 1, unit: 'EA',\n unitPrice: 8000000, amount: 8000000,\n productionSpec: {\n type: '와이어', drawingNo: '4층 FSS17',\n openWidth: 7260, openHeight: 2600, prodWidth: 7400, prodHeight: 2950,\n guideRailType: '백면형', guideRailSpec: '120-70',\n shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감',\n },\n },\n {\n id: 11, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터',\n floor: '4층', location: 'FSS15', spec: '6860×2600', qty: 1, unit: 'EA',\n unitPrice: 8000000, amount: 8000000,\n productionSpec: {\n type: '와이어', drawingNo: '4층 FSS15',\n openWidth: 6860, openHeight: 2600, prodWidth: 7000, prodHeight: 2950,\n guideRailType: '백면형', guideRailSpec: '120-70',\n shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감',\n },\n },\n ],\n // ====== 모터/전장품 (발주서 이미지와 동일) ======\n motorSpec: {\n motors220V: [\n { model: 'KD-150K', qty: 0 },\n { model: 'KD-300K', qty: 0 },\n { model: 'KD-400K', qty: 0 },\n ],\n motors380V: [\n { model: 'KD-150K', qty: 6 }, // 160K 모터 (작은 셔터용)\n { model: 'KD-300K', qty: 5 }, // 300K 모터 (큰 셔터용)\n { model: 'KD-400K', qty: 0 },\n ],\n brackets: [\n { spec: '380-180 [2-4\"]', qty: 0 },\n { spec: '380-180 [2-5\"]', qty: 5 },\n ],\n heatSinks: [\n { spec: '40-60', qty: 44 },\n { spec: 'L-380', qty: 0 },\n ],\n controllers: [\n { type: '매입형', qty: 0 },\n { type: '노출형', qty: 0 },\n { type: '핫라스', qty: 0 },\n ],\n },\n // ====== 절곡물 BOM (발주서 이미지와 동일) ======\n bomData: {\n guideRails: {\n description: '가이드레일 - EGI 1.55T + 마감재 EGI 1.15T + 별도마감재 SUS 1.15T',\n items: [\n {\n type: '백면형', spec: '120-70', code: 'KSS01', lengths: [\n { length: 4300, qty: 0 },\n { length: 3500, qty: 0 },\n ]\n },\n {\n type: '백면형', spec: '120-70', code: 'KSE01/KWE01', lengths: [\n { length: 3000, qty: 22 },\n { length: 2438, qty: 0 },\n ]\n },\n {\n type: '측면형', spec: '120-120', code: 'KSS01', lengths: [\n { length: 4300, qty: 0 },\n { length: 4000, qty: 0 },\n { length: 3500, qty: 0 },\n { length: 3000, qty: 0 },\n { length: 2438, qty: 0 },\n ]\n },\n {\n type: '하부BASE', spec: '130-80', code: '', lengths: [\n { length: 0, qty: 22 },\n ]\n },\n ],\n smokeBarrier: {\n spec: 'W80',\n material: '원 0.8T 화이바글라스크탑직물',\n lengths: [{ length: 2438, qty: 44 }],\n note: '* 전면부, 린텔부 양쪽에 설치',\n },\n },\n cases: {\n description: '케이스(셔터박스) - EGI 1.55T',\n mainSpec: '500-330 (150,300,400/K용)',\n items: [\n { length: 4150, qty: 1 },\n { length: 4000, qty: 7 },\n { length: 3500, qty: 5 },\n { length: 3000, qty: 3 },\n { length: 2438, qty: 3 },\n { length: 1219, qty: 0 },\n ],\n sideCover: { spec: '500-355', qty: 22 },\n topCover: { qty: 3 },\n extension: { spec: '1219+무게', qty: 59 },\n smokeBarrier: { spec: 'W80', length: 3000, qty: 47 },\n },\n bottomFinish: {\n description: '하단마감재 - 마단마감재(EGI 1.55T) + 하단보강별바(EGI 1.55T) + 하단 보강횡철(EGI 1.15T) + 하단 부재횡철(50-12T)',\n items: [\n {\n name: '하단마감재', spec: '50-40', lengths: [\n { length: 4000, qty: 11 },\n { length: 3000, qty: 8 },\n ]\n },\n {\n name: '하단보강빔바', spec: '80-17', lengths: [\n { length: 4000, qty: 28 },\n { length: 3000, qty: 12 },\n ]\n },\n {\n name: '하단보강철', spec: '', lengths: [\n { length: 4000, qty: 13 },\n { length: 3000, qty: 0 },\n ]\n },\n {\n name: '하단부재횡철', spec: '50-12T', lengths: [\n { length: 2000, qty: 67 },\n ]\n },\n ],\n },\n },\n splits: [\n {\n id: 1,\n splitNo: 'KD-TS-251210-01-01',\n lotNo: 'KD-TS-251210-01',\n splitOrder: 1,\n splitType: '일괄',\n itemIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],\n dueDate: '2026-01-05',\n productionStatus: '작업대기',\n shipmentStatus: '미출고',\n productionOrderNo: 'KD-WO-251210-01',\n totalQty: 11,\n completedQty: 0,\n remainingQty: 11,\n },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-15 09:00', changeType: '수주등록', description: '용산고등학교 4층 방화셔터 11개소', changedBy: '판매팀 김판매' },\n { id: 2, changedAt: '2025-12-16 10:00', changeType: '생산지시', description: '전체 일괄 생산지시', changedBy: '생산팀 박생산' },\n ],\n createdAt: '2025-12-15',\n createdBy: '판매팀 김판매',\n note: '[시나리오8] 발주서 양식 테스트 - 용산고등학교 4층 방화셔터 11개소 (직접수주, 가장 최신)',\n },\n\n // ═══════════════════════════════════════════════════════════════════════════\n // E2E 테스트용 수주 데이터 (견적 101-105 연결)\n // ═══════════════════════════════════════════════════════════════════════════\n\n // E2E-101: 삼성물산 - 강남 오피스 A동 스크린 (견적101 → 수주)\n {\n id: 101,\n orderNo: 'KD-TS-251216-01',\n lotNo: 'KD-TS-251216-01',\n quoteId: 101,\n quoteNo: 'KD-PR-251216-01',\n orderDate: '2025-12-16',\n customerId: 1,\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteId: 'PJ-E2E-001',\n siteName: '[E2E테스트] 강남 오피스 A동',\n siteAddress: '서울시 강남구 테헤란로 100',\n contact: '010-1234-5678',\n deliveryAddress: '서울시 강남구 테헤란로 100',\n dueDate: '2025-12-30',\n totalAmount: 18500000,\n status: '생산지시완료',\n paymentStatus: '입금완료',\n accountingApproval: '승인완료',\n shipmentStatus: '미출고',\n productionProgress: 33,\n items: [\n { id: 1, floor: '1층', location: 'A-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, spec: '3000×2500', guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, unitPrice: 6166667, amount: 6166667, process: 'screen' },\n { id: 2, floor: '2층', location: 'A-02', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, spec: '3000×2500', guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, unitPrice: 6166667, amount: 6166667, process: 'screen' },\n { id: 3, floor: '3층', location: 'A-03', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, spec: '3000×2500', guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, unitPrice: 6166666, amount: 6166666, process: 'screen' },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251216-01-01', lotNo: 'KD-TS-251216-01', splitOrder: 1, splitType: '일괄', itemIds: [1, 2, 3], dueDate: '2025-12-30', productionStatus: '작업중', shipmentStatus: '미출고', productionOrderNo: 'KD-WO-251216-01', totalQty: 3, completedQty: 1, remainingQty: 2 },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-16 09:00', changeType: '수주등록', description: '견적 KD-PR-251216-01 기반 수주 전환', changedBy: '[E2E] 시스템' },\n { id: 2, changedAt: '2025-12-16 10:00', changeType: '생산지시', description: '전체 일괄 생산지시', changedBy: '생산팀' },\n ],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n note: '[E2E-101] 견적→수주 전환 테스트 - 스크린 3개소',\n },\n\n // E2E-102: 현대건설 - 판교 물류센터 슬랫 (견적102 → 수주)\n {\n id: 102,\n orderNo: 'KD-TS-251216-02',\n lotNo: 'KD-TS-251216-02',\n quoteId: 102,\n quoteNo: 'KD-PR-251216-02',\n orderDate: '2025-12-16',\n customerId: 2,\n customerName: '현대건설(주)',\n creditGrade: 'A',\n siteId: 'PJ-E2E-002',\n siteName: '[E2E테스트] 판교 물류센터',\n siteAddress: '경기도 성남시 분당구 판교로 200',\n contact: '010-2345-6789',\n deliveryAddress: '경기도 성남시 분당구 판교로 200',\n dueDate: '2025-12-28',\n totalAmount: 15000000,\n status: '작업완료',\n paymentStatus: '입금완료',\n accountingApproval: '승인완료',\n shipmentStatus: '출고대기',\n productionProgress: 100,\n items: [\n { id: 1, floor: 'B1', location: 'C-01', category: '슬랫', productName: '철재 슬랫 셔터', width: 4000, height: 3000, spec: '4000×3000', guideType: '코너형', motorPower: '220V', controller: '노출형', qty: 1, unitPrice: 7500000, amount: 7500000, process: 'slat' },\n { id: 2, floor: 'B2', location: 'C-02', category: '슬랫', productName: '철재 슬랫 셔터', width: 4000, height: 3000, spec: '4000×3000', guideType: '코너형', motorPower: '220V', controller: '노출형', qty: 1, unitPrice: 7500000, amount: 7500000, process: 'slat' },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251216-02-01', lotNo: 'KD-TS-251216-02', splitOrder: 1, splitType: '일괄', itemIds: [1, 2], dueDate: '2025-12-28', productionStatus: '작업완료', shipmentStatus: '출고대기', productionOrderNo: 'KD-WO-251216-02', totalQty: 2, completedQty: 2, remainingQty: 0 },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-16 09:30', changeType: '수주등록', description: '견적 KD-PR-251216-02 기반 수주 전환', changedBy: '[E2E] 시스템' },\n { id: 2, changedAt: '2025-12-16 14:00', changeType: '작업완료', description: '슬랫 2개소 작업완료', changedBy: '생산팀' },\n ],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n note: '[E2E-102] 슬랫 공정 테스트 - 작업완료 상태',\n },\n\n // E2E-103: 대우건설 - 송도 아파트 B동 혼합 (견적103 → 수주, 공정분리)\n {\n id: 103,\n orderNo: 'KD-TS-251216-03',\n lotNo: 'KD-TS-251216-03',\n quoteId: 103,\n quoteNo: 'KD-PR-251216-03',\n orderDate: '2025-12-16',\n customerId: 3,\n customerName: '대우건설(주)',\n creditGrade: 'A',\n siteId: 'PJ-E2E-003',\n siteName: '[E2E테스트] 송도 아파트 B동',\n siteAddress: '인천시 연수구 송도동 300',\n contact: '010-3456-7890',\n deliveryAddress: '인천시 연수구 송도동 300',\n dueDate: '2025-12-25',\n totalAmount: 28000000,\n status: '생산지시완료',\n paymentStatus: '입금완료',\n accountingApproval: '승인완료',\n shipmentStatus: '생산완료',\n productionProgress: 100,\n items: [\n { id: 1, floor: '1층', location: 'D-01', category: '스크린', productName: '스크린 셔터 (대형)', width: 6000, height: 4000, spec: '6000×4000', guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, unitPrice: 12000000, amount: 12000000, process: 'screen' },\n { id: 2, floor: '1층', location: 'D-02', category: '슬랫', productName: '철재 슬랫 셔터', width: 3500, height: 2500, spec: '3500×2500', guideType: '벽면형', motorPower: '220V', controller: '노출형', qty: 2, unitPrice: 5000000, amount: 10000000, process: 'slat' },\n { id: 3, floor: '2층', location: 'D-03', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, spec: '3000×2500', guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, unitPrice: 6000000, amount: 6000000, process: 'screen' },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251216-03-01', lotNo: 'KD-TS-251216-03', splitOrder: 1, splitType: '공정분리-스크린', itemIds: [1, 3], dueDate: '2025-12-23', productionStatus: '작업완료', shipmentStatus: '배송완료', productionOrderNo: 'KD-WO-251216-03', totalQty: 2, completedQty: 2, remainingQty: 0 },\n { id: 2, splitNo: 'KD-TS-251216-03-02', lotNo: 'KD-TS-251216-03', splitOrder: 2, splitType: '공정분리-슬랫', itemIds: [2], dueDate: '2025-12-25', productionStatus: '작업완료', shipmentStatus: '배송완료', productionOrderNo: 'KD-WO-251216-04', totalQty: 2, completedQty: 2, remainingQty: 0 },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-16 10:00', changeType: '수주등록', description: '견적 KD-PR-251216-03 기반 수주 전환 (공정분리)', changedBy: '[E2E] 시스템' },\n { id: 2, changedAt: '2025-12-23 16:00', changeType: '출하완료', description: '스크린+슬랫 전체 배송완료', changedBy: '물류팀' },\n ],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n note: '[E2E-103] 공정분리 테스트 - 스크린/슬랫 분리 작업지시 후 배송완료',\n },\n\n // E2E-104: 서울인테리어 - 용산 호텔 대형 스크린 (견적104 → 수주)\n {\n id: 104,\n orderNo: 'KD-TS-251216-04',\n lotNo: 'KD-TS-251216-04',\n quoteId: 104,\n quoteNo: 'KD-PR-251216-04',\n orderDate: '2025-12-16',\n customerId: 4,\n customerName: '(주)서울인테리어',\n creditGrade: 'B',\n siteId: 'PJ-E2E-004',\n siteName: '[E2E테스트] 용산 호텔',\n siteAddress: '서울시 용산구 한강로 400',\n contact: '010-4567-8901',\n deliveryAddress: '서울시 용산구 한강로 400',\n dueDate: '2025-12-31',\n totalAmount: 22000000,\n status: '작업대기',\n paymentStatus: '부분입금',\n accountingApproval: '승인대기',\n shipmentStatus: '미출고',\n productionProgress: 0,\n items: [\n { id: 1, floor: '로비', location: 'E-01', category: '스크린', productName: '스크린 셔터 (대형)', width: 6000, height: 4000, spec: '6000×4000', guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, unitPrice: 11000000, amount: 11000000, process: 'screen' },\n { id: 2, floor: '연회장', location: 'E-02', category: '스크린', productName: '스크린 셔터 (대형)', width: 6000, height: 4000, spec: '6000×4000', guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, unitPrice: 11000000, amount: 11000000, process: 'screen' },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251216-04-01', lotNo: 'KD-TS-251216-04', splitOrder: 1, splitType: '일괄', itemIds: [1, 2], dueDate: '2025-12-31', productionStatus: '작업대기', shipmentStatus: '미출고', productionOrderNo: 'KD-WO-251216-05', totalQty: 2, completedQty: 0, remainingQty: 2 },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-16 11:00', changeType: '수주등록', description: '견적 KD-PR-251216-04 기반 수주 전환', changedBy: '[E2E] 시스템' },\n ],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n note: '[E2E-104] B등급 거래처 테스트 - 부분입금/승인대기 상태',\n },\n\n // E2E-105: 삼성물산 - 삼성타운 종합 (견적105 → 수주, 전 공정 통합)\n {\n id: 105,\n orderNo: 'KD-TS-251216-05',\n lotNo: 'KD-TS-251216-05',\n quoteId: 105,\n quoteNo: 'KD-PR-251216-05',\n orderDate: '2025-12-16',\n customerId: 1,\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteId: 'PJ-E2E-005',\n siteName: '[E2E테스트] 삼성타운 종합',\n siteAddress: '서울시 서초구 서초대로 500',\n contact: '010-5678-9012',\n deliveryAddress: '서울시 서초구 서초대로 500',\n dueDate: '2026-01-15',\n totalAmount: 42750000,\n discountRate: 5,\n discountAmount: 2250000,\n status: '생산지시완료',\n paymentStatus: '입금완료',\n accountingApproval: '승인완료',\n shipmentStatus: '일부출고',\n productionProgress: 67,\n items: [\n { id: 1, floor: '1층', location: 'F-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3500, height: 2800, spec: '3500×2800', guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 2, unitPrice: 7000000, amount: 14000000, process: 'screen' },\n { id: 2, floor: '2층', location: 'F-02', category: '슬랫', productName: '철재 슬랫 셔터', width: 4500, height: 3200, spec: '4500×3200', guideType: '코너형', motorPower: '220V', controller: '노출형', qty: 2, unitPrice: 8000000, amount: 16000000, process: 'slat' },\n { id: 3, floor: '3층', location: 'F-03', category: '스크린', productName: '스크린 셔터 (대형)', width: 5000, height: 3500, spec: '5000×3500', guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 2, unitPrice: 7500000, amount: 15000000, process: 'screen' },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251216-05-01', lotNo: 'KD-TS-251216-05', splitOrder: 1, splitType: '1차출하-스크린', itemIds: [1], dueDate: '2025-12-28', productionStatus: '작업완료', shipmentStatus: '배송완료', productionOrderNo: 'KD-WO-251216-06', totalQty: 2, completedQty: 2, remainingQty: 0 },\n { id: 2, splitNo: 'KD-TS-251216-05-02', lotNo: 'KD-TS-251216-05', splitOrder: 2, splitType: '2차출하-슬랫', itemIds: [2], dueDate: '2026-01-05', productionStatus: '작업완료', shipmentStatus: '출고대기', productionOrderNo: 'KD-WO-251216-07', totalQty: 2, completedQty: 2, remainingQty: 0 },\n { id: 3, splitNo: 'KD-TS-251216-05-03', lotNo: 'KD-TS-251216-05', splitOrder: 3, splitType: '3차출하-대형스크린', itemIds: [3], dueDate: '2026-01-15', productionStatus: '작업중', shipmentStatus: '미출고', productionOrderNo: 'KD-WO-251216-08', totalQty: 2, completedQty: 1, remainingQty: 1 },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-16 14:00', changeType: '수주등록', description: '견적 KD-PR-251216-05 기반 수주 전환 (할인 5% 적용)', changedBy: '[E2E] 시스템' },\n { id: 2, changedAt: '2025-12-28 17:00', changeType: '1차출하완료', description: '1층 스크린 2개소 배송완료', changedBy: '물류팀' },\n ],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n note: '[E2E-105] 전 공정 통합 테스트 - 3차 분할출하 진행중 (1차완료, 2차대기, 3차생산중)',\n },\n\n // ═══════════════════════════════════════════════════════════════════════════\n // 전체 워크플로우 통합 테스트 수주 데이터 10건 (ID: 201-210)\n // 견적(QT-251217-XX) → 수주(SO-251217-XX) → 생산 → 품질 → 출하 연동\n // ═══════════════════════════════════════════════════════════════════════════\n\n // [통합테스트 1] 삼성물산 - 래미안 강남 프레스티지 (A등급, 스크린 3대)\n {\n id: 201,\n orderNo: 'KD-TS-251217-01',\n lotNo: 'KD-TS-251217-01',\n quoteId: 201,\n quoteNo: 'KD-PR-251217-01',\n orderDate: '2025-12-17',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 1,\n customerCode: 'CUS-001',\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteId: 1,\n siteName: '래미안 강남 프레스티지',\n siteCode: 'S-001',\n manager: '김건설',\n contact: '010-1234-5678',\n deliveryAddress: '서울시 강남구 테헤란로 100',\n receiverName: '김건설',\n receiverPhone: '010-1234-5678',\n dueDate: '2026-01-15',\n scheduledShipDate: '2026-01-13',\n status: '생산지시완료',\n paymentStatus: '전액입금',\n accountingStatus: '회계확인완료',\n accountingConfirmedBy: '회계팀 박회계',\n accountingConfirmedAt: '2025-12-18 10:00',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n invoiceIssuedAt: '2025-12-17 15:00',\n taxInvoiceIssued: true,\n taxInvoiceIssuedAt: '2025-12-17 15:30',\n totalAmount: 24000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 24000000,\n paidAmount: 24000000,\n remainingAmount: 0,\n deliveryMethod: '상차',\n items: [\n { id: 1, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'A-01', spec: '3000×2500', qty: 1, unit: 'EA', unitPrice: 8000000, amount: 8000000, productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, prodWidth: 3140, prodHeight: 2850, guideRailType: '벽면형', guideRailSpec: '120-70', shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감' } },\n { id: 2, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '2층', location: 'A-02', spec: '3000×2500', qty: 1, unit: 'EA', unitPrice: 8000000, amount: 8000000, productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, prodWidth: 3140, prodHeight: 2850, guideRailType: '벽면형', guideRailSpec: '120-70', shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감' } },\n { id: 3, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '3층', location: 'A-03', spec: '3000×2500', qty: 1, unit: 'EA', unitPrice: 8000000, amount: 8000000, productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, prodWidth: 3140, prodHeight: 2850, guideRailType: '벽면형', guideRailSpec: '120-70', shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감' } },\n ],\n motorSpec: { motors220V: [{ model: 'KD-300K', qty: 3 }], brackets: [{ spec: '380-180', qty: 3 }], controllers: [{ type: '매립형', qty: 3 }] },\n splits: [\n { id: 1, splitNo: 'KD-TS-251217-01-01', lotNo: 'KD-TS-251217-01', splitOrder: 1, splitType: '일괄', itemIds: [1, 2, 3], dueDate: '2026-01-15', productionStatus: '작업완료', shipmentStatus: '출하준비', productionOrderNo: 'KD-WO-251217-01', totalQty: 3, completedQty: 3, remainingQty: 0 },\n ],\n documentHistory: [\n { id: 1, docType: '거래명세서', sentAt: '2025-12-17 15:00', sentBy: '판매팀', sentMethod: '이메일', status: '발송완료' },\n { id: 2, docType: '세금계산서', sentAt: '2025-12-17 15:30', sentBy: '회계팀', sentMethod: '전자발행', status: '발행완료' },\n ],\n changeHistory: [\n { id: 1, changedAt: '2025-12-17 14:00', changeType: '수주등록', description: '[통합테스트1] A등급 정상 플로우 - 자동 진행', changedBy: '판매1팀 김영업' },\n { id: 2, changedAt: '2025-12-18 10:00', changeType: '회계확인', description: '전액 입금 확인', changedBy: '회계팀 박회계' },\n ],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 김영업',\n note: '[통합테스트1] A등급 정상 플로우 - 스크린 3대 생산완료',\n },\n\n // [통합테스트 2] 현대건설 - 힐스테이트 판교 (A등급, 스크린+슬랫 혼합)\n {\n id: 202,\n orderNo: 'KD-TS-251217-02',\n lotNo: 'KD-TS-251217-02',\n quoteId: 202,\n quoteNo: 'KD-PR-251217-02',\n orderDate: '2025-12-17',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 2,\n customerCode: 'CUS-002',\n customerName: '현대건설(주)',\n creditGrade: 'A',\n siteId: 11,\n siteName: '힐스테이트 판교 더 퍼스트',\n siteCode: 'S-011',\n manager: '박현장',\n contact: '010-2345-6789',\n deliveryAddress: '경기도 성남시 분당구 판교로 200',\n receiverName: '박현장',\n receiverPhone: '010-2345-6789',\n dueDate: '2026-01-20',\n scheduledShipDate: '2026-01-18',\n status: '생산중',\n paymentStatus: '계약금입금',\n accountingStatus: '입금확인',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n taxInvoiceIssued: true,\n totalAmount: 32000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 32000000,\n paidAmount: 16000000,\n remainingAmount: 16000000,\n deliveryMethod: '직접배차',\n items: [\n { id: 1, productCode: 'SH4030', productName: '스크린 셔터 (표준형)', floor: 'B1', location: 'B-01', spec: '4000×3000', qty: 2, unit: 'EA', unitPrice: 9000000, amount: 18000000, productionSpec: { type: '와이어', openWidth: 4000, openHeight: 3000, prodWidth: 4140, prodHeight: 3350, guideRailType: '벽면형', capacity: 300 } },\n { id: 2, productCode: 'ST3525', productName: '철재 슬랫 셔터', floor: '1층', location: 'B-02', spec: '3500×2500', qty: 2, unit: 'EA', unitPrice: 7000000, amount: 14000000, productionSpec: { type: '슬랫', openWidth: 3500, openHeight: 2500 } },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251217-02-01', lotNo: 'KD-TS-251217-02', splitOrder: 1, splitType: '공정분리-스크린', itemIds: [1], dueDate: '2026-01-15', productionStatus: '작업완료', shipmentStatus: '출하준비', productionOrderNo: 'KD-WO-251217-02', totalQty: 2, completedQty: 2, remainingQty: 0 },\n { id: 2, splitNo: 'KD-TS-251217-02-02', lotNo: 'KD-TS-251217-02', splitOrder: 2, splitType: '공정분리-슬랫', itemIds: [2], dueDate: '2026-01-20', productionStatus: '작업중', shipmentStatus: '미출고', productionOrderNo: 'KD-WO-251217-03', totalQty: 2, completedQty: 1, remainingQty: 1 },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-17 14:30', changeType: '수주등록', description: '[통합테스트2] 스크린+슬랫 혼합 공정분리', changedBy: '판매1팀 이영업' },\n ],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 이영업',\n note: '[통합테스트2] 스크린+슬랫 혼합 공정 분리 - 스크린 완료, 슬랫 진행중',\n },\n\n // [통합테스트 3] 대우건설 - 푸르지오 일산 (A등급, 대형 스크린)\n {\n id: 203,\n orderNo: 'KD-TS-251217-03',\n lotNo: 'KD-TS-251217-03',\n quoteId: 203,\n quoteNo: 'KD-PR-251217-03',\n orderDate: '2025-12-17',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 3,\n customerCode: 'CUS-003',\n customerName: '대우건설(주)',\n creditGrade: 'A',\n siteId: 21,\n siteName: '푸르지오 일산 센트럴파크',\n siteCode: 'S-021',\n manager: '최건설',\n contact: '010-3456-7890',\n deliveryAddress: '경기도 고양시 일산동구 마두동 500',\n receiverName: '최건설',\n receiverPhone: '010-3456-7890',\n dueDate: '2026-01-25',\n scheduledShipDate: '2026-01-23',\n status: '생산지시완료',\n paymentStatus: '전액입금',\n accountingStatus: '회계확인완료',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n taxInvoiceIssued: true,\n totalAmount: 28000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 28000000,\n paidAmount: 28000000,\n remainingAmount: 0,\n deliveryMethod: '화물',\n items: [\n { id: 1, productCode: 'SH6040', productName: '스크린 셔터 (대형)', floor: '로비', location: 'C-01', spec: '6000×4000', qty: 1, unit: 'EA', unitPrice: 14000000, amount: 14000000, productionSpec: { type: '와이어', openWidth: 6000, openHeight: 4000, prodWidth: 6140, prodHeight: 4350, guideRailType: '벽면형', capacity: 500 } },\n { id: 2, productCode: 'SH6040', productName: '스크린 셔터 (대형)', floor: '연회장', location: 'C-02', spec: '6000×4000', qty: 1, unit: 'EA', unitPrice: 14000000, amount: 14000000, productionSpec: { type: '와이어', openWidth: 6000, openHeight: 4000, prodWidth: 6140, prodHeight: 4350, guideRailType: '벽면형', capacity: 500 } },\n ],\n motorSpec: { motors380V: [{ model: 'KD-500K', qty: 2 }], brackets: [{ spec: '500-250', qty: 2 }], controllers: [{ type: '매립형', qty: 2 }] },\n splits: [\n { id: 1, splitNo: 'KD-TS-251217-03-01', lotNo: 'KD-TS-251217-03', splitOrder: 1, splitType: '일괄', itemIds: [1, 2], dueDate: '2026-01-25', productionStatus: '작업완료', shipmentStatus: '출하준비', productionOrderNo: 'KD-WO-251217-04', totalQty: 2, completedQty: 2, remainingQty: 0 },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-17 15:00', changeType: '수주등록', description: '[통합테스트3] 대형 스크린 500KG 모터', changedBy: '판매2팀 박영업' },\n ],\n createdAt: '2025-12-17',\n createdBy: '판매2팀 박영업',\n note: '[통합테스트3] 대형 스크린(6m×4m) 모터용량 500KG - 생산완료',\n },\n\n // [통합테스트 4] GS건설 - 자이 위례 (A등급, 슬랫 전용)\n {\n id: 204,\n orderNo: 'KD-TS-251217-04',\n lotNo: 'KD-TS-251217-04',\n quoteId: 204,\n quoteNo: 'KD-PR-251217-04',\n orderDate: '2025-12-17',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 4,\n customerCode: 'CUS-004',\n customerName: 'GS건설(주)',\n creditGrade: 'A',\n siteId: 31,\n siteName: '자이 위례 더 퍼스트',\n siteCode: 'S-031',\n manager: '정건설',\n contact: '010-4567-8901',\n deliveryAddress: '경기도 성남시 수정구 위례동 100',\n receiverName: '정건설',\n receiverPhone: '010-4567-8901',\n dueDate: '2026-01-30',\n scheduledShipDate: '2026-01-28',\n status: '생산중',\n paymentStatus: '계약금입금',\n accountingStatus: '입금확인',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n taxInvoiceIssued: false,\n totalAmount: 35000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 35000000,\n paidAmount: 17500000,\n remainingAmount: 17500000,\n deliveryMethod: '직접배차',\n items: [\n { id: 1, productCode: 'ST4030', productName: '철재 슬랫 셔터', floor: 'B1', location: 'D-01', spec: '4000×3000', qty: 3, unit: 'EA', unitPrice: 7000000, amount: 21000000, productionSpec: { type: '슬랫', openWidth: 4000, openHeight: 3000 } },\n { id: 2, productCode: 'ST4030-C', productName: '철재 슬랫 셔터 (코너형)', floor: '1층', location: 'D-02', spec: '4000×3000', qty: 2, unit: 'EA', unitPrice: 7000000, amount: 14000000, productionSpec: { type: '슬랫', openWidth: 4000, openHeight: 3000, guideRailType: '코너형' } },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251217-04-01', lotNo: 'KD-TS-251217-04', splitOrder: 1, splitType: '일괄-슬랫', itemIds: [1, 2], dueDate: '2026-01-30', productionStatus: '작업중', shipmentStatus: '미출고', productionOrderNo: 'KD-WO-251217-05', totalQty: 5, completedQty: 3, remainingQty: 2 },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-17 15:30', changeType: '수주등록', description: '[통합테스트4] 슬랫 전용 공정', changedBy: '판매2팀 최영업' },\n ],\n createdAt: '2025-12-17',\n createdBy: '판매2팀 최영업',\n note: '[통합테스트4] 슬랫 전용 공정 - 5대 중 3대 완료',\n },\n\n // [통합테스트 5] 포스코건설 - 더샵 송도 (A등급, 코너형 가이드)\n {\n id: 205,\n orderNo: 'KD-TS-251217-05',\n lotNo: 'KD-TS-251217-05',\n quoteId: 205,\n quoteNo: 'KD-PR-251217-05',\n orderDate: '2025-12-17',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 5,\n customerCode: 'CUS-005',\n customerName: '포스코건설(주)',\n creditGrade: 'A',\n siteId: 41,\n siteName: '더샵 송도 센트럴파크',\n siteCode: 'S-041',\n manager: '강건설',\n contact: '010-5678-9012',\n deliveryAddress: '인천시 연수구 송도동 200',\n receiverName: '강건설',\n receiverPhone: '010-5678-9012',\n dueDate: '2026-02-05',\n scheduledShipDate: '2026-02-03',\n status: '생산지시완료',\n paymentStatus: '전액입금',\n accountingStatus: '회계확인완료',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n taxInvoiceIssued: true,\n totalAmount: 27000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 27000000,\n paidAmount: 27000000,\n remainingAmount: 0,\n deliveryMethod: '상차',\n items: [\n { id: 1, productCode: 'SH3530-C', productName: '스크린 셔터 (코너형)', floor: '1층', location: 'E-01', spec: '3500×3000', qty: 1, unit: 'EA', unitPrice: 9000000, amount: 9000000, productionSpec: { type: '와이어', openWidth: 3500, openHeight: 3000, guideRailType: '코너형', capacity: 300 } },\n { id: 2, productCode: 'SH3530-C', productName: '스크린 셔터 (코너형)', floor: '2층', location: 'E-02', spec: '3500×3000', qty: 1, unit: 'EA', unitPrice: 9000000, amount: 9000000, productionSpec: { type: '와이어', openWidth: 3500, openHeight: 3000, guideRailType: '코너형', capacity: 300 } },\n { id: 3, productCode: 'SH3530-C', productName: '스크린 셔터 (코너형)', floor: '3층', location: 'E-03', spec: '3500×3000', qty: 1, unit: 'EA', unitPrice: 9000000, amount: 9000000, productionSpec: { type: '와이어', openWidth: 3500, openHeight: 3000, guideRailType: '코너형', capacity: 300 } },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251217-05-01', lotNo: 'KD-TS-251217-05', splitOrder: 1, splitType: '일괄', itemIds: [1, 2, 3], dueDate: '2026-02-05', productionStatus: '작업완료', shipmentStatus: '출하준비', productionOrderNo: 'KD-WO-251217-06', totalQty: 3, completedQty: 3, remainingQty: 0 },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-17 16:00', changeType: '수주등록', description: '[통합테스트5] 코너형 가이드레일 BOM', changedBy: '판매1팀 김영업' },\n ],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 김영업',\n note: '[통합테스트5] 코너형 가이드레일 BOM 산출 - 생산완료',\n },\n\n // [통합테스트 6] 롯데건설 - 캐슬 잠실 (B등급, 경리승인 필요)\n {\n id: 206,\n orderNo: 'KD-TS-251217-06',\n lotNo: 'KD-TS-251217-06',\n quoteId: 206,\n quoteNo: 'KD-PR-251217-06',\n orderDate: '2025-12-17',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 6,\n customerCode: 'CUS-006',\n customerName: '롯데건설(주)',\n creditGrade: 'B',\n siteId: 51,\n siteName: '캐슬 잠실 파인시티',\n siteCode: 'S-051',\n manager: '윤건설',\n contact: '010-6789-0123',\n deliveryAddress: '서울시 송파구 잠실동 300',\n receiverName: '윤건설',\n receiverPhone: '010-6789-0123',\n dueDate: '2026-02-10',\n scheduledShipDate: '미정',\n status: '수주확정',\n paymentStatus: '미입금',\n accountingStatus: '미확인',\n requireApproval: true,\n approvalStatus: '승인대기',\n approvalRequestedAt: '2025-12-17 17:00',\n requirePaymentBeforeShip: true,\n productionHold: true,\n productionHoldReason: '경리 승인 대기',\n invoiceIssued: false,\n taxInvoiceIssued: false,\n totalAmount: 36000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 36000000,\n paidAmount: 0,\n remainingAmount: 36000000,\n depositRequired: 18000000,\n deliveryMethod: '직접배차',\n items: [\n { id: 1, productCode: 'SH4028', productName: '스크린 셔터 (표준형)', floor: 'B1', location: 'F-01', spec: '4000×2800', qty: 2, unit: 'EA', unitPrice: 9000000, amount: 18000000, productionSpec: { type: '와이어', openWidth: 4000, openHeight: 2800, capacity: 300 } },\n { id: 2, productCode: 'SH4028', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'F-02', spec: '4000×2800', qty: 2, unit: 'EA', unitPrice: 9000000, amount: 18000000, productionSpec: { type: '와이어', openWidth: 4000, openHeight: 2800, capacity: 300 } },\n ],\n splits: [],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-17 17:00', changeType: '수주등록', description: '[통합테스트6] B등급 경리승인 필요', changedBy: '판매2팀 이영업' },\n { id: 2, changedAt: '2025-12-17 17:00', changeType: '승인요청', description: '경리팀에 생산 승인 요청', changedBy: '판매2팀 이영업' },\n ],\n createdAt: '2025-12-17',\n createdBy: '판매2팀 이영업',\n note: '[통합테스트6] B등급 경리승인 대기 - 생산보류',\n },\n\n // [통합테스트 7] 호반건설 - 써밋 광교 (A등급, 분할출하)\n {\n id: 207,\n orderNo: 'KD-TS-251217-07',\n lotNo: 'KD-TS-251217-07',\n quoteId: 207,\n quoteNo: 'KD-PR-251217-07',\n orderDate: '2025-12-17',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 7,\n customerCode: 'CUS-007',\n customerName: '호반건설(주)',\n creditGrade: 'A',\n siteId: 61,\n siteName: '써밋 광교 센트럴시티',\n siteCode: 'S-061',\n manager: '서건설',\n contact: '010-7890-1234',\n deliveryAddress: '경기도 수원시 영통구 광교동 400',\n receiverName: '서건설',\n receiverPhone: '010-7890-1234',\n dueDate: '2026-02-15',\n scheduledShipDate: '2026-02-10',\n status: '생산중',\n paymentStatus: '계약금입금',\n accountingStatus: '입금확인',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n taxInvoiceIssued: true,\n totalAmount: 48000000,\n discountRate: 5,\n discountAmount: 2400000,\n finalAmount: 45600000,\n paidAmount: 22800000,\n remainingAmount: 22800000,\n deliveryMethod: '화물',\n items: [\n { id: 1, productCode: 'SH3528', productName: '스크린 셔터 (표준형)', floor: 'B1', location: 'G-01', spec: '3500×2800', qty: 2, unit: 'EA', unitPrice: 8500000, amount: 17000000, productionSpec: { type: '와이어', openWidth: 3500, openHeight: 2800, capacity: 300 } },\n { id: 2, productCode: 'ST4030', productName: '철재 슬랫 셔터', floor: 'B2', location: 'G-02', spec: '4000×3000', qty: 2, unit: 'EA', unitPrice: 7500000, amount: 15000000, productionSpec: { type: '슬랫', openWidth: 4000, openHeight: 3000 } },\n { id: 3, productCode: 'SH5035', productName: '스크린 셔터 (대형)', floor: '1층', location: 'G-03', spec: '5000×3500', qty: 2, unit: 'EA', unitPrice: 8000000, amount: 16000000, productionSpec: { type: '와이어', openWidth: 5000, openHeight: 3500, capacity: 400 } },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251217-07-01', lotNo: 'KD-TS-251217-07', splitOrder: 1, splitType: '1차분할-스크린', itemIds: [1], dueDate: '2026-02-05', productionStatus: '작업완료', shipmentStatus: '배송완료', productionOrderNo: 'KD-WO-251217-07', totalQty: 2, completedQty: 2, remainingQty: 0 },\n { id: 2, splitNo: 'KD-TS-251217-07-02', lotNo: 'KD-TS-251217-07', splitOrder: 2, splitType: '2차분할-슬랫', itemIds: [2], dueDate: '2026-02-10', productionStatus: '작업완료', shipmentStatus: '출하준비', productionOrderNo: 'KD-WO-251217-08', totalQty: 2, completedQty: 2, remainingQty: 0 },\n { id: 3, splitNo: 'KD-TS-251217-07-03', lotNo: 'KD-TS-251217-07', splitOrder: 3, splitType: '3차분할-대형스크린', itemIds: [3], dueDate: '2026-02-15', productionStatus: '작업중', shipmentStatus: '미출고', productionOrderNo: 'KD-WO-251217-09', totalQty: 2, completedQty: 1, remainingQty: 1 },\n ],\n documentHistory: [\n { id: 1, docType: '거래명세서', sentAt: '2025-12-17 17:30', sentBy: '판매팀', sentMethod: '이메일', status: '발송완료' },\n ],\n changeHistory: [\n { id: 1, changedAt: '2025-12-17 17:30', changeType: '수주등록', description: '[통합테스트7] 분할출하 3회 예정', changedBy: '판매1팀 박영업' },\n { id: 2, changedAt: '2026-02-05 16:00', changeType: '1차출하완료', description: 'B1 스크린 2대 배송완료', changedBy: '물류팀' },\n ],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 박영업',\n note: '[통합테스트7] 분할출하 - 1차완료, 2차준비, 3차생산중',\n },\n\n // [통합테스트 8] 한화건설 - 포레나 수지 (A등급, 추가분 수주)\n {\n id: 208,\n orderNo: 'KD-TS-251217-08',\n lotNo: 'KD-TS-251217-08',\n quoteId: 208,\n quoteNo: 'KD-PR-251217-08',\n orderDate: '2025-12-17',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 8,\n customerCode: 'CUS-008',\n customerName: '한화건설(주)',\n creditGrade: 'A',\n siteId: 71,\n siteName: '포레나 수지 더 센트럴',\n siteCode: 'S-071',\n manager: '한건설',\n contact: '010-8901-2345',\n deliveryAddress: '경기도 용인시 수지구 풍덕천동 500',\n receiverName: '한건설',\n receiverPhone: '010-8901-2345',\n dueDate: '2026-02-20',\n scheduledShipDate: '2026-02-18',\n status: '생산지시완료',\n paymentStatus: '전액입금',\n accountingStatus: '회계확인완료',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n taxInvoiceIssued: true,\n totalAmount: 16000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 16000000,\n paidAmount: 16000000,\n remainingAmount: 0,\n deliveryMethod: '상차',\n items: [\n { id: 1, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '4층', location: 'H-01', spec: '3000×2500', qty: 1, unit: 'EA', unitPrice: 8000000, amount: 8000000, productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, capacity: 300 } },\n { id: 2, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '5층', location: 'H-02', spec: '3000×2500', qty: 1, unit: 'EA', unitPrice: 8000000, amount: 8000000, productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, capacity: 300 } },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251217-08-01', lotNo: 'KD-TS-251217-08', splitOrder: 1, splitType: '일괄', itemIds: [1, 2], dueDate: '2026-02-20', productionStatus: '작업완료', shipmentStatus: '출하준비', productionOrderNo: 'KD-WO-251217-10', totalQty: 2, completedQty: 2, remainingQty: 0 },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-17 18:00', changeType: '수주등록', description: '[통합테스트8] 추가분 수주', changedBy: '판매2팀 최영업' },\n ],\n createdAt: '2025-12-17',\n createdBy: '판매2팀 최영업',\n note: '[통합테스트8] 추가분 수주 - 생산완료',\n },\n\n // [통합테스트 9] 태영건설 - 데시앙 동탄 (A등급, 품질불량→재작업)\n {\n id: 209,\n orderNo: 'KD-TS-251217-09',\n lotNo: 'KD-TS-251217-09',\n quoteId: 209,\n quoteNo: 'KD-PR-251217-09',\n orderDate: '2025-12-17',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 9,\n customerCode: 'CUS-009',\n customerName: '태영건설(주)',\n creditGrade: 'A',\n siteId: 81,\n siteName: '데시앙 동탄 파크뷰',\n siteCode: 'S-081',\n manager: '조건설',\n contact: '010-9012-3456',\n deliveryAddress: '경기도 화성시 동탄동 600',\n receiverName: '조건설',\n receiverPhone: '010-9012-3456',\n dueDate: '2026-02-25',\n scheduledShipDate: '2026-02-23',\n status: '재작업중',\n paymentStatus: '계약금입금',\n accountingStatus: '입금확인',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n taxInvoiceIssued: false,\n totalAmount: 33000000,\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 33000000,\n paidAmount: 16500000,\n remainingAmount: 16500000,\n deliveryMethod: '직접배차',\n items: [\n { id: 1, productCode: 'SH4535', productName: '스크린 셔터 (프리미엄)', floor: '로비', location: 'I-01', spec: '4500×3500', qty: 1, unit: 'EA', unitPrice: 11000000, amount: 11000000, productionSpec: { type: '와이어', openWidth: 4500, openHeight: 3500, capacity: 400 } },\n { id: 2, productCode: 'SH4535', productName: '스크린 셔터 (프리미엄)', floor: '카페', location: 'I-02', spec: '4500×3500', qty: 1, unit: 'EA', unitPrice: 11000000, amount: 11000000, productionSpec: { type: '와이어', openWidth: 4500, openHeight: 3500, capacity: 400 } },\n { id: 3, productCode: 'SH4535', productName: '스크린 셔터 (프리미엄)', floor: '헬스장', location: 'I-03', spec: '4500×3500', qty: 1, unit: 'EA', unitPrice: 11000000, amount: 11000000, productionSpec: { type: '와이어', openWidth: 4500, openHeight: 3500, capacity: 400 } },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251217-09-01', lotNo: 'KD-TS-251217-09', splitOrder: 1, splitType: '일괄', itemIds: [1, 2, 3], dueDate: '2026-02-25', productionStatus: '재작업중', shipmentStatus: '미출고', productionOrderNo: 'KD-WO-251217-11', totalQty: 3, completedQty: 2, remainingQty: 1, hasQualityIssue: true, qualityIssueDescription: '1대 품질불량 발생 - 원단 불량으로 재작업' },\n ],\n documentHistory: [],\n changeHistory: [\n { id: 1, changedAt: '2025-12-17 18:30', changeType: '수주등록', description: '[통합테스트9] 프리미엄 스크린', changedBy: '판매1팀 김영업' },\n { id: 2, changedAt: '2026-02-15 14:00', changeType: '품질불량', description: '로비 1대 원단 불량 발생, 재작업 진행', changedBy: '품질팀 이품질' },\n ],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 김영업',\n note: '[통합테스트9] 품질불량 → 재작업 플로우 - 1대 재작업중',\n },\n\n // [통합테스트 10] 두산건설 - 위브 청라 (A등급, 전체 완료 시나리오)\n {\n id: 210,\n orderNo: 'KD-TS-251217-10',\n lotNo: 'KD-TS-251217-10',\n quoteId: 210,\n quoteNo: 'KD-PR-251217-10',\n orderDate: '2025-12-17',\n orderType: 'initial',\n parentLotNo: null,\n customerId: 10,\n customerCode: 'CUS-010',\n customerName: '두산건설(주)',\n creditGrade: 'A',\n siteId: 91,\n siteName: '위브 청라 더 퍼스트',\n siteCode: 'S-091',\n manager: '임건설',\n contact: '010-0123-4567',\n deliveryAddress: '인천시 서구 청라동 700',\n receiverName: '임건설',\n receiverPhone: '010-0123-4567',\n dueDate: '2026-03-01',\n scheduledShipDate: '2026-02-27',\n status: '출하완료',\n paymentStatus: '전액입금',\n accountingStatus: '회계확인완료',\n requireApproval: false,\n requirePaymentBeforeShip: false,\n invoiceIssued: true,\n invoiceIssuedAt: '2025-12-17 19:00',\n taxInvoiceIssued: true,\n taxInvoiceIssuedAt: '2025-12-17 19:30',\n totalAmount: 40000000,\n discountRate: 3,\n discountAmount: 1200000,\n finalAmount: 38800000,\n paidAmount: 38800000,\n remainingAmount: 0,\n deliveryMethod: '상차',\n items: [\n { id: 1, productCode: 'SH3528', productName: '스크린 셔터 (표준형)', floor: 'B1', location: 'J-01', spec: '3500×2800', qty: 2, unit: 'EA', unitPrice: 8500000, amount: 17000000, productionSpec: { type: '와이어', openWidth: 3500, openHeight: 2800, capacity: 300 } },\n { id: 2, productCode: 'ST4030', productName: '철재 슬랫 셔터', floor: '1층', location: 'J-02', spec: '4000×3000', qty: 2, unit: 'EA', unitPrice: 7000000, amount: 14000000, productionSpec: { type: '슬랫', openWidth: 4000, openHeight: 3000 } },\n { id: 3, productCode: 'SH5035', productName: '스크린 셔터 (대형)', floor: '2층', location: 'J-03', spec: '5000×3500', qty: 1, unit: 'EA', unitPrice: 9000000, amount: 9000000, productionSpec: { type: '와이어', openWidth: 5000, openHeight: 3500, capacity: 400 } },\n ],\n splits: [\n { id: 1, splitNo: 'KD-TS-251217-10-01', lotNo: 'KD-TS-251217-10', splitOrder: 1, splitType: '일괄', itemIds: [1, 2, 3], dueDate: '2026-03-01', productionStatus: '작업완료', shipmentStatus: '배송완료', productionOrderNo: 'KD-WO-251217-12', totalQty: 5, completedQty: 5, remainingQty: 0 },\n ],\n documentHistory: [\n { id: 1, docType: '거래명세서', sentAt: '2025-12-17 19:00', sentBy: '판매팀', sentMethod: '이메일', status: '발송완료' },\n { id: 2, docType: '세금계산서', sentAt: '2025-12-17 19:30', sentBy: '회계팀', sentMethod: '전자발행', status: '발행완료' },\n ],\n changeHistory: [\n { id: 1, changedAt: '2025-12-17 19:00', changeType: '수주등록', description: '[통합테스트10] 전체 완료 시나리오', changedBy: '판매2팀 이영업' },\n { id: 2, changedAt: '2025-12-18 09:00', changeType: '회계확인', description: '전액 입금 확인', changedBy: '회계팀 박회계' },\n { id: 3, changedAt: '2026-02-25 10:00', changeType: '생산완료', description: '전량 생산 완료', changedBy: '생산팀' },\n { id: 4, changedAt: '2026-02-27 16:00', changeType: '출하완료', description: '전량 배송 완료', changedBy: '물류팀' },\n { id: 5, changedAt: '2026-02-28 10:00', changeType: '수금완료', description: '잔금 수금 완료', changedBy: '회계팀' },\n ],\n createdAt: '2025-12-17',\n createdBy: '판매2팀 이영업',\n note: '[통합테스트10] 전체 완료 시나리오 - 견적→수주→생산→품질→출하→회계 완료',\n },\n];\n\n// 작업지시\nconst initialWorkOrders = [\n /*\n * ============================================================\n * 📋 생산 데이터 - 8개 테스트 시나리오 연동 (새 번호 체계)\n * ============================================================\n * [1] KD-WO-2502-001 ← KD-TS-2502-001-01 (시나리오1: 삼성물산 래미안 강남 1차) → 작업완료 ✅\n * [2] KD-WO-2502-002 ← KD-TS-2502-002-01 (시나리오2: 현대건설 힐스테이트 판교 스크린) → 작업완료\n * [3] KD-WO-2502-003 ← KD-TS-2502-002-02 (시나리오2: 현대건설 힐스테이트 판교 슬랫) → 작업완료\n * [4] KD-WO-2502-004 ← KD-TS-2502-001-A-01 (시나리오4: 삼성물산 래미안 강남 1차 추가분) → 작업중\n * [5] KD-WO-2502-005 ← KD-TS-2502-004-01 (시나리오5: 서울인테리어 오피스타워 1차) → 작업완료 ✅\n * [6] KD-WO-2502-006 ← KD-TS-2502-004-02 (시나리오5: 서울인테리어 오피스타워 2차) → 작업완료\n * [7] KD-WO-2502-007 ← KD-TS-2502-005-01 (시나리오6: 삼성물산 래미안 강남 2차 품질불량) → 재작업중 ⚠️\n * [8] KD-WO-2502-008 ← KD-TS-2502-006-01 (시나리오7: 현대건설 힐스테이트 용인 할인적용) → 작업중\n * [9] WO-2510-001 ← SO-2510-001-01 (시나리오8: 용산고등학교 직접수주) → 작업대기\n *\n * ※ 시나리오3 (대우건설 푸르지오 일산)은 경리승인 대기로 생산 미시작\n * ============================================================\n */\n\n // ========== 시나리오 1: A등급 정상 플로우 (삼성물산) - 작업완료 ==========\n {\n id: 1,\n workOrderNo: 'KD-PL-250207-01',\n orderNo: 'KD-SO-250205-01',\n splitNo: 'KD-SO-250205-01-01',\n lotNo: 'KD-TS-251201-01', // 수주 로트번호\n shipRequestDate: '2025-12-18', // 출고예정일\n orderDate: '2025-02-07',\n processType: '스크린',\n customerName: '삼성물산(주)',\n siteName: '강남 타워 신축현장',\n productName: '스크린 셔터 (표준형)',\n workPriority: 1,\n workSequence: 1, // 당일 작업 순서\n instruction: '1층 A구역 우선 작업, 절단 시 원단 방향 주의', // 작업 지시사항\n dueDate: '2025-03-01',\n status: '작업완료',\n priority: '긴급',\n assignee: '김스크린',\n assignedWorkers: ['김스크린', '이스크린'], // 스크린 공정 작업자 2명 배정\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '원단절단': { status: '완료', worker: '김스크린', completedAt: '2025-02-08 10:00' },\n '미싱': { status: '완료', worker: '이스크린', completedAt: '2025-02-09 14:00' },\n '앤드락작업': { status: '완료', worker: '김스크린', completedAt: '2025-02-10 11:00' },\n '중간검사': {\n status: '완료',\n inspector: '품질팀 이검사',\n result: '합격',\n completedAt: '2025-02-10 15:00',\n approvedBy: '품질팀장 최품질',\n inspectionLot: 'KD-SC-250210-01-(4)'\n },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-02-11 09:00', labelInfo: '1층 A-01,A-02' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-02-11 10:00',\n items: [\n { id: 1, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'A-01', spec: '7660×2550', qty: 1, lotNo: 'KD-TS-250211-01-01', materialLotNo: '250201-01' },\n { id: 2, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'A-02', spec: '6500×2400', qty: 1, lotNo: 'KD-TS-250211-01-02', materialLotNo: '250201-01' },\n ],\n issues: [],\n createdAt: '2025-02-07',\n createdBy: '판매팀 김판매',\n // 결재라인\n approval: {\n drafter: { name: '김판매', date: '2025-02-07', dept: '판매팀' },\n approver: { name: '박생산', date: '2025-02-07', dept: '생산관리' },\n cc: ['회계팀'],\n },\n approvalStatus: '승인완료', // 승인대기, 승인완료, 반려\n },\n\n // ========== 시나리오 2: B등급 입금 후 출고 (현대건설) - 스크린 작업완료 ==========\n {\n id: 2,\n workOrderNo: 'KD-PL-250210-01',\n orderNo: 'KD-SO-250208-01',\n splitNo: 'KD-SO-250208-01-01',\n lotNo: 'KD-TS-251208-01', // 수주 로트번호\n shipRequestDate: '2026-01-03', // 출고예정일\n orderDate: '2025-02-10',\n processType: '스크린',\n customerName: '현대건설(주)',\n siteName: '송도 오피스빌딩',\n productName: '스크린 셔터 (고급형)',\n workPriority: 2,\n workSequence: 2,\n instruction: '고급형 마감 처리, 미싱 라인 정밀 확인',\n dueDate: '2025-03-15',\n status: '작업완료',\n priority: '일반',\n assignee: '박스크린',\n assignedWorkers: ['박스크린'], // 스크린 공정 작업자 1명 배정\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '원단절단': { status: '완료', worker: '박스크린', completedAt: '2025-02-11 10:00' },\n '미싱': { status: '완료', worker: '박스크린', completedAt: '2025-02-12 14:00' },\n '앤드락작업': { status: '완료', worker: '박스크린', completedAt: '2025-02-13 11:00' },\n '중간검사': {\n status: '완료',\n inspector: '품질팀 이검사',\n result: '합격',\n completedAt: '2025-02-13 15:00',\n approvedBy: '품질팀장 최품질',\n inspectionLot: 'KD-SC-250213-01-(4)'\n },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-02-14 09:00', labelInfo: 'B1 C-01' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-02-14 10:00',\n items: [\n { id: 1, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: 'B1', location: 'C-01', spec: '6000×2400', qty: 2, lotNo: 'KD-TS-250214-01-01', materialLotNo: '250205-01' },\n ],\n issues: [],\n createdAt: '2025-02-10',\n createdBy: '판매팀 이판매',\n approval: {\n drafter: { name: '이판매', date: '2025-02-10', dept: '판매팀' },\n approver: { name: '박생산', date: '2025-02-10', dept: '생산관리' },\n cc: ['회계팀'],\n },\n approvalStatus: '승인완료',\n },\n\n // ========== 시나리오 2: B등급 입금 후 출고 (현대건설) - 슬랫 작업완료 ==========\n {\n id: 3,\n workOrderNo: 'KD-PL-250212-01',\n orderNo: 'KD-SO-250208-01',\n splitNo: 'KD-SO-250208-01-02',\n lotNo: 'KD-TS-251208-01', // 수주 로트번호\n shipRequestDate: '2026-01-03', // 출고예정일\n orderDate: '2025-02-12',\n processType: '슬랫',\n customerName: '현대건설(주)',\n siteName: '송도 오피스빌딩',\n productName: '슬랫 셔터',\n workPriority: 3,\n workSequence: 3,\n instruction: '코일 색상 확인 필수, B1층 설치용',\n dueDate: '2025-03-20',\n status: '작업완료',\n priority: '일반',\n assignee: '김슬랫',\n assignedWorkers: ['김슬랫', '이슬랫', '박슬랫'], // 슬랫 공정 팀 배정 (3명)\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '코일절단': { status: '완료', worker: '김슬랫', completedAt: '2025-02-13 10:00' },\n '중간검사': {\n status: '완료',\n inspector: '품질팀 이검사',\n result: '합격',\n completedAt: '2025-02-13 14:00',\n approvedBy: '품질팀장 최품질',\n inspectionLot: 'KD-SL-250213-01-(2)'\n },\n '미미작업': { status: '완료', worker: '이슬랫', completedAt: '2025-02-14 11:00' },\n '포장': { status: '완료', worker: '박슬랫', completedAt: '2025-02-14 15:00', labelInfo: '1층 D-01' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-02-14 16:00',\n items: [\n { id: 1, productCode: 'SLT-001', productName: '슬랫 셔터', floor: '1층', location: 'D-01', spec: '4500×2000', qty: 2, lotNo: 'KD-TS-250214-02-01', materialLotNo: '250203-01' },\n ],\n issues: [],\n createdAt: '2025-02-12',\n createdBy: '판매팀 이판매',\n approval: {\n drafter: { name: '이판매', date: '2025-02-12', dept: '판매팀' },\n approver: { name: '박생산', date: '2025-02-12', dept: '생산관리' },\n cc: ['회계팀'],\n },\n approvalStatus: '승인완료',\n },\n\n // ========== 시나리오 4: 추가분 수주 (삼성물산) - 작업중 ==========\n {\n id: 4,\n workOrderNo: 'KD-PL-250222-01',\n orderNo: 'KD-SO-250205-01-A',\n splitNo: 'KD-SO-250205-01-A-01',\n lotNo: 'KD-TS-251201-02', // 수주 로트번호 (추가분)\n shipRequestDate: '2025-03-25', // 출고예정일\n orderDate: '2025-02-22',\n processType: '스크린',\n customerName: '삼성물산(주)',\n siteName: '강남 타워 신축현장',\n productName: '스크린 셔터 (표준형) - 추가',\n workPriority: 4,\n workSequence: 1, // 오늘 첫번째 작업\n instruction: '추가분 주문, 기존 납품분과 동일 사양 유지',\n dueDate: '2025-03-25',\n status: '작업중',\n priority: '일반',\n assignee: '김스크린',\n assignedWorkers: ['김스크린', '이스크린', '박스크린'], // 스크린 공정 팀 배정 (3명)\n totalQty: 3,\n completedQty: 1,\n currentStep: '미싱',\n stepStatus: {\n '원단절단': { status: '완료', worker: '김스크린', completedAt: '2025-02-23 10:00' },\n '미싱': { status: '진행중', worker: '이스크린', startedAt: '2025-02-23 14:00' },\n '앤드락작업': { status: '대기' },\n '중간검사': { status: '대기' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'SCR-002', productName: '스크린 셔터 (프리미엄)', floor: '3층', location: 'C-01', spec: '8000×2800', qty: 3, lotNo: null, materialLotNo: '250220-01' },\n ],\n issues: [],\n createdAt: '2025-02-22',\n createdBy: '전진팀 최전진',\n approval: {\n drafter: { name: '최전진', date: '2025-02-22', dept: '전진팀' },\n approver: { name: '박생산', date: '2025-02-22', dept: '생산관리' },\n cc: ['회계팀'],\n },\n approvalStatus: '승인완료',\n },\n\n // ========== 시나리오 5: 분할 출하 (서울인테리어) - 1차분 작업완료 ==========\n {\n id: 5,\n workOrderNo: 'KD-PL-250215-01',\n orderNo: 'KD-SO-250212-01',\n splitNo: 'KD-SO-250212-01-01',\n lotNo: 'KD-TS-251212-01', // 수주 로트번호\n shipRequestDate: '2025-03-15', // 출고예정일\n orderDate: '2025-02-15',\n processType: '스크린',\n customerName: '(주)서울인테리어',\n siteName: '해운대 타워',\n productName: '스크린 셔터 (고급형)',\n workPriority: 5,\n workSequence: 4,\n instruction: '1차 분할 출하, 포장 시 출하 구분 라벨 부착',\n dueDate: '2025-03-15',\n status: '작업완료',\n priority: '긴급',\n assignee: '김스크린',\n assignedWorkers: ['김스크린', '이스크린'], // 스크린 공정 작업자 2명 배정\n totalQty: 3,\n completedQty: 3,\n currentStep: '포장',\n stepStatus: {\n '원단절단': { status: '완료', worker: '김스크린', completedAt: '2025-02-16 10:00' },\n '미싱': { status: '완료', worker: '이스크린', completedAt: '2025-02-17 14:00' },\n '앤드락작업': { status: '완료', worker: '김스크린', completedAt: '2025-02-18 11:00' },\n '중간검사': {\n status: '완료',\n inspector: '품질팀 이검사',\n result: '합격',\n completedAt: '2025-02-18 15:00',\n approvedBy: '품질팀장 최품질',\n inspectionLot: 'KD-SC-250218-01-(4)'\n },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-02-19 09:00', labelInfo: '1층 E-01' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-02-19 10:00',\n items: [\n { id: 1, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'E-01', spec: '7000×2500', qty: 3, lotNo: 'KD-TS-250219-01-01', materialLotNo: '250210-01' },\n ],\n issues: [],\n createdAt: '2025-02-15',\n createdBy: '판매팀 김판매',\n approval: {\n drafter: { name: '김판매', date: '2025-02-15', dept: '판매팀' },\n approver: { name: '박생산', date: '2025-02-15', dept: '생산관리' },\n cc: ['회계팀'],\n },\n approvalStatus: '승인완료',\n },\n\n // ========== 시나리오 5: 분할 출하 (서울인테리어) - 2차분 작업완료 ==========\n {\n id: 6,\n workOrderNo: 'KD-PL-250217-01',\n orderNo: 'KD-SO-250212-01',\n splitNo: 'KD-SO-250212-01-02',\n lotNo: 'KD-TS-251212-01', // 수주 로트번호 (동일 수주)\n shipRequestDate: '2025-03-25', // 출고예정일\n orderDate: '2025-02-17',\n processType: '슬랫',\n customerName: '(주)서울인테리어',\n siteName: '해운대 타워',\n productName: '슬랫 셔터 (고급형)',\n workPriority: 6,\n workSequence: 5,\n instruction: '2차 분할 출하, 1차와 동일 품질 기준 적용',\n dueDate: '2025-03-25',\n status: '작업완료',\n priority: '일반',\n assignee: '박슬랫',\n assignedWorkers: ['박슬랫', '이슬랫'], // 슬랫 공정 작업자 2명 배정\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '코일절단': { status: '완료', worker: '박슬랫', completedAt: '2025-02-18 10:00' },\n '중간검사': {\n status: '완료',\n inspector: '품질팀 이검사',\n result: '합격',\n completedAt: '2025-02-18 14:00',\n approvedBy: '품질팀장 최품질',\n inspectionLot: 'KD-SL-250218-02-(2)'\n },\n '미미작업': { status: '완료', worker: '이슬랫', completedAt: '2025-02-19 11:00' },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-02-19 15:00', labelInfo: '2층 F-01' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-02-19 16:00',\n items: [\n { id: 1, productCode: 'SLT-001', productName: '슬랫 셔터', floor: '2층', location: 'F-01', spec: '5000×2200', qty: 2, lotNo: 'KD-TS-250219-02-01', materialLotNo: '250210-02' },\n ],\n issues: [],\n createdAt: '2025-02-17',\n createdBy: '판매팀 김판매',\n approval: {\n drafter: { name: '김판매', date: '2025-02-17', dept: '판매팀' },\n approver: { name: '박생산', date: '2025-02-17', dept: '생산관리' },\n cc: ['회계팀'],\n },\n approvalStatus: '승인완료',\n },\n\n // ========== 시나리오 6: 품질 불량 (삼성물산) - 재작업중 ⚠️ ==========\n {\n id: 7,\n workOrderNo: 'KD-PL-250218-01',\n orderNo: 'KD-SO-250215-01',\n splitNo: 'KD-SO-250215-01-01',\n lotNo: 'KD-TS-251215-01', // 수주 로트번호\n shipRequestDate: '2025-03-15', // 출고예정일\n orderDate: '2025-02-18',\n processType: '슬랫',\n customerName: '삼성물산(주)',\n siteName: '강남 타워 신축현장 (B동)',\n productName: '슬랫 셔터 (특수)',\n workPriority: 1,\n workSequence: 2, // 긴급이라 우선순위 높음\n instruction: '⚠️ 재작업 건, 치수 ±5mm 준수 필수, 품질팀 입회 검사',\n dueDate: '2025-03-15',\n status: '재작업중',\n priority: '긴급',\n assignee: '김슬랫',\n assignedWorkers: ['김슬랫', '이슬랫'], // 슬랫 공정 작업자 2명 배정\n totalQty: 4,\n completedQty: 2,\n currentStep: '코일절단',\n stepStatus: {\n '코일절단': {\n status: '재작업',\n worker: '김슬랫',\n completedAt: '2025-02-19 10:00',\n reworkStartedAt: '2025-02-20 09:00',\n reworkReason: '치수불량 2EA 재작업'\n },\n '중간검사': {\n status: '불합격',\n inspector: '품질팀 이검사',\n result: '불합격',\n completedAt: '2025-02-19 14:00',\n failReason: '치수불량 - 4500mm 기준 ±5mm 초과 (2EA)',\n inspectionLot: 'KD-SL-250219-01-(2)',\n reworkRequired: true,\n reworkQty: 2\n },\n '미미작업': { status: '대기' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'SLT-001', productName: '슬랫 셔터', floor: '1층', location: 'H-01', spec: '4500×2000', qty: 4, lotNo: null, materialLotNo: '250215-01' },\n ],\n issues: [\n {\n id: 1,\n issueType: '불량품발생',\n description: '중간검사 불합격 - 치수불량 2EA (4500mm 기준 ±10mm 초과)',\n reportedBy: '품질팀 이검사',\n reportedAt: '2025-02-19 14:30',\n status: '처리중',\n resolvedBy: null,\n resolvedAt: null,\n resolution: null,\n actionTaken: '재작업 지시 - 코일 재절단 진행'\n },\n ],\n qualityIssue: {\n hasIssue: true,\n inspectionLot: 'KD-SL-250219-01-(2)',\n failedQty: 2,\n passedQty: 2,\n failReason: '치수불량',\n reworkStatus: '재작업중',\n reworkStartDate: '2025-02-20',\n expectedCompletionDate: '2025-02-22'\n },\n createdAt: '2025-02-18',\n createdBy: '판매팀 이판매',\n approval: {\n drafter: { name: '이판매', date: '2025-02-18', dept: '판매팀' },\n approver: { name: '박생산', date: '2025-02-18', dept: '생산관리' },\n cc: ['회계팀'],\n },\n approvalStatus: '승인완료',\n },\n\n // ========== 시나리오 7: 할인 적용 (현대건설) - 작업중 ==========\n {\n id: 8,\n workOrderNo: 'KD-PL-250220-01',\n orderNo: 'KD-SO-250218-01',\n splitNo: 'KD-SO-250218-01-01',\n lotNo: 'KD-TS-251218-01', // 수주 로트번호\n shipRequestDate: '2025-04-05', // 출고예정일\n orderDate: '2025-02-20',\n processType: '스크린',\n customerName: '현대건설(주)',\n siteName: '연수 오피스텔',\n productName: '스크린 셔터 (표준형)',\n workPriority: 2,\n workSequence: 3,\n instruction: '할인 적용 건, 표준 공정 준수',\n dueDate: '2025-04-05',\n status: '작업중',\n priority: '일반',\n assignee: '박스크린',\n assignedWorkers: ['박스크린', '이스크린'], // 스크린 공정 작업자 2명 배정\n totalQty: 6,\n completedQty: 2,\n currentStep: '앤드락작업',\n stepStatus: {\n '원단절단': { status: '완료', worker: '박스크린', completedAt: '2025-02-21 10:00' },\n '미싱': { status: '완료', worker: '이스크린', completedAt: '2025-02-22 14:00' },\n '앤드락작업': { status: '진행중', worker: '박스크린', startedAt: '2025-02-23 09:00' },\n '중간검사': { status: '대기' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'I-01', spec: '6000×2400', qty: 6, lotNo: null, materialLotNo: '250218-01' },\n ],\n issues: [],\n createdAt: '2025-02-20',\n createdBy: '판매팀 이판매',\n approval: {\n drafter: { name: '이판매', date: '2025-02-20', dept: '판매팀' },\n approver: { name: '박생산', date: '2025-02-20', dept: '생산관리' },\n cc: ['회계팀'],\n },\n approvalStatus: '승인완료',\n },\n\n // ========== 시나리오 8: 발주서 양식 테스트 (용산고등학교) - 작업대기 ==========\n {\n id: 9,\n workOrderNo: 'KD-PL-251016-01',\n orderNo: 'KD-WE-251015-01',\n splitNo: 'KD-WE-251015-01-01',\n lotNo: 'KD-TS-251015-01', // 수주 로트번호\n shipRequestDate: '2025-10-30', // 출고예정일\n orderDate: '2025-10-16',\n processType: '스크린',\n customerName: '주말건설',\n siteName: '용산고등학교(4층)',\n productName: '스크린 셔터 (학교용)',\n workPriority: 3,\n workSequence: 6,\n instruction: '학교 시설용, 안전 기준 강화, 대량 작업 - 팀 전원 투입',\n dueDate: '2025-10-30',\n status: '작업대기',\n priority: '일반',\n assignee: '김스크린',\n assignedWorkers: ['김스크린', '이스크린', '박스크린'], // 스크린 공정 팀 배정 (3명) - 대량 작업\n totalQty: 11,\n completedQty: 0,\n currentStep: null,\n stepStatus: {\n '원단절단': { status: '대기' },\n '미싱': { status: '대기' },\n '앤드락작업': { status: '대기' },\n '중간검사': { status: '대기' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터', floor: '4층', location: 'FSS1', spec: '7260×2600', qty: 1 },\n { id: 2, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터', floor: '4층', location: 'FSS5', spec: '4560×2600', qty: 1 },\n { id: 3, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터', floor: '4층', location: 'FSS17A', spec: '6650×2600', qty: 1 },\n { id: 4, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터', floor: '4층', location: 'FSS3', spec: '3560×2600', qty: 1 },\n { id: 5, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터', floor: '4층', location: 'FSS7', spec: '5860×2600', qty: 1 },\n { id: 6, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터', floor: '4층', location: 'FSS16', spec: '7160×2600', qty: 1 },\n { id: 7, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터', floor: '4층', location: 'FSS3-2', spec: '3560×2600', qty: 1 },\n { id: 8, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터', floor: '4층', location: 'FSS6', spec: '5760×2600', qty: 1 },\n { id: 9, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터', floor: '4층', location: 'FSS3-3', spec: '3770×2600', qty: 1 },\n { id: 10, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터', floor: '4층', location: 'FSS17', spec: '7260×2600', qty: 1 },\n { id: 11, productCode: 'SCR-WE-001', productName: '국민방화스크린셔터', floor: '4층', location: 'FSS15', spec: '6860×2600', qty: 1 },\n ],\n issues: [],\n createdAt: '2025-10-16',\n createdBy: '전진팀 최전진',\n approval: {\n drafter: { name: '최전진', date: '2025-10-16', dept: '전진팀' },\n approver: null, // 승인 대기\n cc: ['회계팀'],\n },\n approvalStatus: '승인대기', // 이 건은 아직 승인 대기 상태\n },\n\n // ═══════════════════════════════════════════════════════════════════════════\n // E2E 테스트용 작업지시 데이터 (수주 101-105 연결)\n // ═══════════════════════════════════════════════════════════════════════════\n\n // WO-E2E-01: 강남 오피스 A동 스크린 (수주101 → 작업지시) - 작업중\n {\n id: 101,\n workOrderNo: 'KD-WO-251216-01',\n orderNo: 'KD-TS-251216-01',\n splitNo: 'KD-TS-251216-01-01',\n lotNo: 'KD-TS-251216-01', // 수주 로트번호\n shipRequestDate: '2025-12-30', // 출고예정일\n orderDate: '2025-12-16',\n processType: '스크린',\n customerName: '삼성물산(주)',\n siteName: '[E2E테스트] 강남 오피스 A동',\n productName: '스크린 셔터 (표준형)',\n workPriority: 1,\n workSequence: 1,\n instruction: 'E2E 테스트 - A동 스크린, 표준 사양',\n dueDate: '2025-12-30',\n status: '작업중',\n priority: '긴급',\n assignee: '김스크린',\n assignedWorkers: ['김스크린', '이스크린'], // 스크린 공정 작업자 2명 배정\n totalQty: 3,\n completedQty: 1,\n currentStep: '미싱',\n stepStatus: {\n '원단절단': { status: '완료', worker: '김스크린', completedAt: '2025-12-17 10:00' },\n '미싱': { status: '진행중', worker: '이스크린', startedAt: '2025-12-17 14:00' },\n '앤드락작업': { status: '대기' },\n '중간검사': { status: '대기' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'SCR-E2E-001', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'A-01', spec: '3000×2500', qty: 1, lotNo: 'KD-TS-E2E-01-01' },\n { id: 2, productCode: 'SCR-E2E-001', productName: '스크린 셔터 (표준형)', floor: '2층', location: 'A-02', spec: '3000×2500', qty: 1, lotNo: null },\n { id: 3, productCode: 'SCR-E2E-001', productName: '스크린 셔터 (표준형)', floor: '3층', location: 'A-03', spec: '3000×2500', qty: 1, lotNo: null },\n ],\n issues: [],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n approval: { drafter: { name: '시스템', date: '2025-12-16', dept: 'E2E' }, approver: { name: '생산팀', date: '2025-12-16', dept: '생산' }, cc: [] },\n approvalStatus: '승인완료',\n },\n\n // WO-E2E-02: 판교 물류센터 슬랫 (수주102 → 작업지시) - 작업완료\n {\n id: 102,\n workOrderNo: 'KD-WO-251216-02',\n orderNo: 'KD-TS-251216-02',\n splitNo: 'KD-TS-251216-02-01',\n lotNo: 'KD-TS-251216-02', // 수주 로트번호\n shipRequestDate: '2025-12-28', // 출고예정일\n orderDate: '2025-12-16',\n processType: '슬랫',\n customerName: '현대건설(주)',\n siteName: '[E2E테스트] 판교 물류센터',\n productName: '철재 슬랫 셔터',\n workPriority: 2,\n dueDate: '2025-12-28',\n status: '작업완료',\n priority: '일반',\n assignee: '김슬랫',\n assignedWorkers: ['김슬랫', '박슬랫'], // 슬랫 공정 작업자 2명 배정\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '코일절단': { status: '완료', worker: '김슬랫', completedAt: '2025-12-17 09:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-17 14:00', inspectionLot: 'KD-SL-E2E-01-(2)' },\n '미미작업': { status: '완료', worker: '박슬랫', completedAt: '2025-12-18 10:00' },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-12-18 14:00', labelInfo: 'B1 C-01, B2 C-02' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-18 15:00',\n items: [\n { id: 1, productCode: 'STL-E2E-001', productName: '철재 슬랫 셔터', floor: 'B1', location: 'C-01', spec: '4000×3000', qty: 1, lotNo: 'KD-TS-E2E-02-01' },\n { id: 2, productCode: 'STL-E2E-001', productName: '철재 슬랫 셔터', floor: 'B2', location: 'C-02', spec: '4000×3000', qty: 1, lotNo: 'KD-TS-E2E-02-02' },\n ],\n issues: [],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n approval: { drafter: { name: '시스템', date: '2025-12-16', dept: 'E2E' }, approver: { name: '생산팀', date: '2025-12-16', dept: '생산' }, cc: [] },\n approvalStatus: '승인완료',\n },\n\n // WO-E2E-03: 송도 아파트 스크린 (수주103-01 → 작업지시) - 작업완료\n {\n id: 103,\n workOrderNo: 'KD-WO-251216-03',\n orderNo: 'KD-TS-251216-03',\n splitNo: 'KD-TS-251216-03-01',\n lotNo: 'KD-TS-251216-03', // 수주 로트번호\n shipRequestDate: '2025-12-23', // 출고예정일\n orderDate: '2025-12-16',\n processType: '스크린',\n customerName: '대우건설(주)',\n siteName: '[E2E테스트] 송도 아파트 B동',\n productName: '스크린 셔터 혼합',\n workPriority: 3,\n dueDate: '2025-12-23',\n status: '작업완료',\n priority: '일반',\n assignee: '이스크린',\n assignedWorkers: ['이스크린'], // 스크린 공정 작업자 1명 배정\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '원단절단': { status: '완료', worker: '이스크린', completedAt: '2025-12-18 10:00' },\n '미싱': { status: '완료', worker: '이스크린', completedAt: '2025-12-19 10:00' },\n '앤드락작업': { status: '완료', worker: '이스크린', completedAt: '2025-12-20 10:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-20 14:00', inspectionLot: 'KD-SC-E2E-02-(2)' },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-12-21 10:00', labelInfo: '1층 D-01, 2층 D-03' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-21 11:00',\n items: [\n { id: 1, productCode: 'SCR-E2E-002', productName: '스크린 셔터 (대형)', floor: '1층', location: 'D-01', spec: '6000×4000', qty: 1, lotNo: 'KD-TS-E2E-03-01' },\n { id: 2, productCode: 'SCR-E2E-002', productName: '스크린 셔터 (표준형)', floor: '2층', location: 'D-03', spec: '3000×2500', qty: 1, lotNo: 'KD-TS-E2E-03-02' },\n ],\n issues: [],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n approval: { drafter: { name: '시스템', date: '2025-12-16', dept: 'E2E' }, approver: { name: '생산팀', date: '2025-12-16', dept: '생산' }, cc: [] },\n approvalStatus: '승인완료',\n },\n\n // WO-E2E-04: 송도 아파트 슬랫 (수주103-02 → 작업지시) - 작업완료\n {\n id: 104,\n workOrderNo: 'KD-WO-251216-04',\n orderNo: 'KD-TS-251216-03',\n splitNo: 'KD-TS-251216-03-02',\n lotNo: 'KD-TS-251216-03', // 수주 로트번호 (동일 수주)\n shipRequestDate: '2025-12-25', // 출고예정일\n orderDate: '2025-12-16',\n processType: '슬랫',\n customerName: '대우건설(주)',\n siteName: '[E2E테스트] 송도 아파트 B동',\n productName: '철재 슬랫 셔터',\n workPriority: 4,\n dueDate: '2025-12-25',\n status: '작업완료',\n priority: '일반',\n assignee: '이슬랫',\n assignedWorkers: ['이슬랫', '박슬랫'], // 슬랫 공정 작업자 2명 배정\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '코일절단': { status: '완료', worker: '이슬랫', completedAt: '2025-12-19 09:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-19 14:00', inspectionLot: 'KD-SL-E2E-02-(2)' },\n '미미작업': { status: '완료', worker: '박슬랫', completedAt: '2025-12-20 10:00' },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-12-20 14:00', labelInfo: '1층 D-02' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-20 15:00',\n items: [\n { id: 1, productCode: 'STL-E2E-002', productName: '철재 슬랫 셔터', floor: '1층', location: 'D-02', spec: '3500×2500', qty: 2, lotNo: 'KD-TS-E2E-04-01' },\n ],\n issues: [],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n approval: { drafter: { name: '시스템', date: '2025-12-16', dept: 'E2E' }, approver: { name: '생산팀', date: '2025-12-16', dept: '생산' }, cc: [] },\n approvalStatus: '승인완료',\n },\n\n // WO-E2E-05: 용산 호텔 대형 스크린 (수주104 → 작업지시) - 작업대기 (승인대기)\n {\n id: 105,\n workOrderNo: 'KD-WO-251216-05',\n orderNo: 'KD-TS-251216-04',\n splitNo: 'KD-TS-251216-04-01',\n lotNo: 'KD-TS-251216-04', // 수주 로트번호\n shipRequestDate: '2025-12-31', // 출고예정일\n orderDate: '2025-12-16',\n processType: '스크린',\n customerName: '(주)서울인테리어',\n siteName: '[E2E테스트] 용산 호텔',\n productName: '스크린 셔터 (대형)',\n workPriority: 5,\n dueDate: '2025-12-31',\n status: '작업대기',\n priority: '일반',\n assignee: null,\n assignedWorkers: [], // 승인대기로 작업자 미배정\n totalQty: 2,\n completedQty: 0,\n currentStep: null,\n stepStatus: {\n '원단절단': { status: '대기' },\n '미싱': { status: '대기' },\n '앤드락작업': { status: '대기' },\n '중간검사': { status: '대기' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'SCR-E2E-003', productName: '스크린 셔터 (대형)', floor: '로비', location: 'E-01', spec: '6000×4000', qty: 1 },\n { id: 2, productCode: 'SCR-E2E-003', productName: '스크린 셔터 (대형)', floor: '연회장', location: 'E-02', spec: '6000×4000', qty: 1 },\n ],\n issues: [{ id: 1, type: '일정지연', description: 'B등급 거래처 - 경리승인 대기중', reportedBy: '시스템', reportedAt: '2025-12-16', resolved: false }],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n approval: { drafter: { name: '시스템', date: '2025-12-16', dept: 'E2E' }, approver: null, cc: ['회계팀'] },\n approvalStatus: '승인대기',\n },\n\n // WO-E2E-06: 삼성타운 1차 스크린 (수주105-01 → 작업지시) - 작업완료\n {\n id: 106,\n workOrderNo: 'KD-WO-251216-06',\n orderNo: 'KD-TS-251216-05',\n splitNo: 'KD-TS-251216-05-01',\n lotNo: 'KD-TS-251216-05', // 수주 로트번호\n shipRequestDate: '2025-12-28', // 출고예정일\n orderDate: '2025-12-16',\n processType: '스크린',\n customerName: '삼성물산(주)',\n siteName: '[E2E테스트] 삼성타운 종합',\n productName: '스크린 셔터 (표준형)',\n workPriority: 6,\n dueDate: '2025-12-28',\n status: '작업완료',\n priority: '일반',\n assignee: '박스크린',\n assignedWorkers: ['박스크린'], // 스크린 공정 작업자 1명 배정\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '원단절단': { status: '완료', worker: '박스크린', completedAt: '2025-12-19 10:00' },\n '미싱': { status: '완료', worker: '박스크린', completedAt: '2025-12-20 10:00' },\n '앤드락작업': { status: '완료', worker: '박스크린', completedAt: '2025-12-21 10:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-21 14:00', inspectionLot: 'KD-SC-E2E-03-(2)' },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-12-22 10:00', labelInfo: '1층 F-01' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-22 11:00',\n items: [\n { id: 1, productCode: 'SCR-E2E-004', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'F-01', spec: '3500×2800', qty: 2, lotNo: 'KD-TS-E2E-06-01' },\n ],\n issues: [],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n approval: { drafter: { name: '시스템', date: '2025-12-16', dept: 'E2E' }, approver: { name: '생산팀', date: '2025-12-16', dept: '생산' }, cc: [] },\n approvalStatus: '승인완료',\n },\n\n // WO-E2E-07: 삼성타운 2차 슬랫 (수주105-02 → 작업지시) - 작업완료\n {\n id: 107,\n workOrderNo: 'KD-WO-251216-07',\n orderNo: 'KD-TS-251216-05',\n splitNo: 'KD-TS-251216-05-02',\n lotNo: 'KD-TS-251216-05', // 수주 로트번호 (동일 수주)\n shipRequestDate: '2026-01-05', // 출고예정일\n orderDate: '2025-12-16',\n processType: '슬랫',\n customerName: '삼성물산(주)',\n siteName: '[E2E테스트] 삼성타운 종합',\n productName: '철재 슬랫 셔터',\n workPriority: 7,\n dueDate: '2026-01-05',\n status: '작업완료',\n priority: '일반',\n assignee: '김슬랫',\n assignedWorkers: ['김슬랫', '이슬랫', '박슬랫'], // 슬랫 공정 팀 배정 (3명)\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '코일절단': { status: '완료', worker: '김슬랫', completedAt: '2025-12-23 09:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-23 14:00', inspectionLot: 'KD-SL-E2E-03-(2)' },\n '미미작업': { status: '완료', worker: '이슬랫', completedAt: '2025-12-24 10:00' },\n '포장': { status: '완료', worker: '박슬랫', completedAt: '2025-12-24 14:00', labelInfo: '2층 F-02' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-24 15:00',\n items: [\n { id: 1, productCode: 'STL-E2E-003', productName: '철재 슬랫 셔터', floor: '2층', location: 'F-02', spec: '4500×3200', qty: 2, lotNo: 'KD-TS-E2E-07-01' },\n ],\n issues: [],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n approval: { drafter: { name: '시스템', date: '2025-12-16', dept: 'E2E' }, approver: { name: '생산팀', date: '2025-12-16', dept: '생산' }, cc: [] },\n approvalStatus: '승인완료',\n },\n\n // WO-E2E-08: 삼성타운 3차 대형스크린 (수주105-03 → 작업지시) - 작업중\n {\n id: 108,\n workOrderNo: 'KD-WO-251216-08',\n orderNo: 'KD-TS-251216-05',\n splitNo: 'KD-TS-251216-05-03',\n lotNo: 'KD-TS-251216-05', // 수주 로트번호 (동일 수주)\n shipRequestDate: '2026-01-15', // 출고예정일\n orderDate: '2025-12-16',\n processType: '스크린',\n customerName: '삼성물산(주)',\n siteName: '[E2E테스트] 삼성타운 종합',\n productName: '스크린 셔터 (대형)',\n workPriority: 8,\n dueDate: '2026-01-15',\n status: '작업중',\n priority: '일반',\n assignee: '김스크린',\n assignedWorkers: ['김스크린', '박스크린'], // 스크린 공정 작업자 2명 배정\n totalQty: 2,\n completedQty: 1,\n currentStep: '앤드락작업',\n stepStatus: {\n '원단절단': { status: '완료', worker: '김스크린', completedAt: '2025-12-26 10:00' },\n '미싱': { status: '완료', worker: '박스크린', completedAt: '2025-12-27 10:00' },\n '앤드락작업': { status: '진행중', worker: '김스크린', startedAt: '2025-12-28 09:00' },\n '중간검사': { status: '대기' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'SCR-E2E-005', productName: '스크린 셔터 (대형)', floor: '3층', location: 'F-03', spec: '5000×3500', qty: 2, lotNo: 'KD-TS-E2E-08-01' },\n ],\n issues: [],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n approval: { drafter: { name: '시스템', date: '2025-12-16', dept: 'E2E' }, approver: { name: '생산팀', date: '2025-12-16', dept: '생산' }, cc: [] },\n approvalStatus: '승인완료',\n },\n\n // ═══════════════════════════════════════════════════════════════════════════\n // 절곡 공정 작업지시 데이터 (전개도 포함)\n // ═══════════════════════════════════════════════════════════════════════════\n\n // WO-FLD-01: 강남 타워 절곡 (작업완료)\n {\n id: 201,\n workOrderNo: 'KD-WO-FLD-251210-01',\n orderNo: 'KD-TS-251210-01',\n splitNo: 'KD-TS-251210-01-01',\n lotNo: 'KD-TS-251210-01',\n shipRequestDate: '2025-12-25',\n orderDate: '2025-12-10',\n processType: '절곡',\n customerName: '삼성물산(주)',\n siteName: '강남 타워 신축현장',\n productName: '방화셔터 절곡 부품',\n workPriority: 1,\n dueDate: '2025-12-25',\n status: '작업완료',\n priority: '긴급',\n assignee: '최절곡',\n assignedWorkers: ['최절곡', '김절곡'], // 절곡 공정 작업자 2명 배정\n totalQty: 1,\n completedQty: 1,\n currentStep: '포장',\n stepStatus: {\n '절단': { status: '완료', worker: '최절곡', completedAt: '2025-12-11 10:00' },\n '절곡': { status: '완료', worker: '김절곡', completedAt: '2025-12-12 14:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-12 16:00', inspectionLot: 'KD-FLD-251212-01-(1)' },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-12-13 10:00', labelInfo: '1층 G-01' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-13 11:00',\n items: [\n { id: 1, productCode: 'FLD-001', productName: '방화셔터 절곡 부품 SET', floor: '1층', location: 'G-01', spec: '3000×4000', qty: 1, lotNo: 'KD-TS-FLD-01-01' },\n ],\n // 전개도 상세정보 (절곡 공정 전용)\n developedParts: [\n { itemCode: 'SD30', itemName: '엘바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 4, weight: 0.9, dimensions: '75', note: '' },\n { itemCode: 'SD31', itemName: '하장바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 1.158, dimensions: '67→126→165→178→193', note: '' },\n { itemCode: 'SD32', itemName: '짜부가스켓', material: 'E.G.I 0.8T', totalWidth: 500, length: 3000, qty: 4, weight: 0.576, dimensions: '48', note: '80*4,50*8' },\n { itemCode: 'SD33', itemName: '가이드레일', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 2.304, dimensions: '40→80→42.5→25', note: '' },\n { itemCode: 'SD34', itemName: '바텀바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.44, dimensions: '30→95→30', note: '' },\n { itemCode: 'SD35', itemName: '바텀커버', material: 'E.G.I 1.2T', totalWidth: 500, length: 3000, qty: 1, weight: 0.864, dimensions: '65→50→16.5→25', note: '' },\n { itemCode: 'SD36', itemName: '전면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.92, dimensions: '25→210→140→30→50→28', note: '' },\n { itemCode: 'SD37', itemName: '후면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.728, dimensions: '25→165→90→155', note: '' },\n { itemCode: 'SD38', itemName: '측면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 0.96, dimensions: '35→50→35', note: '' },\n { itemCode: 'SD39', itemName: '상부덮개', material: 'E.G.I 1.2T', totalWidth: 500, length: 3000, qty: 1, weight: 0.648, dimensions: '25→140→330→25', note: '' },\n ],\n issues: [],\n createdAt: '2025-12-10',\n createdBy: '전진팀 최전진',\n approval: { drafter: { name: '최전진', date: '2025-12-10', dept: '전진팀' }, approver: { name: '박생산', date: '2025-12-10', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // WO-FLD-02: 판교 물류센터 절곡 (작업중)\n {\n id: 202,\n workOrderNo: 'KD-WO-FLD-251212-01',\n orderNo: 'KD-TS-251212-01',\n splitNo: 'KD-TS-251212-01-01',\n lotNo: 'KD-TS-251212-01',\n shipRequestDate: '2025-12-28',\n orderDate: '2025-12-12',\n processType: '절곡',\n customerName: '현대건설(주)',\n siteName: '판교 물류센터',\n productName: '방연셔터 절곡 부품',\n workPriority: 2,\n dueDate: '2025-12-28',\n status: '작업중',\n priority: '일반',\n assignee: '이절곡',\n assignedWorkers: ['이절곡', '박절곡', '최절곡'], // 절곡 공정 팀 배정 (3명)\n totalQty: 2,\n completedQty: 1,\n currentStep: '절곡',\n stepStatus: {\n '절단': { status: '완료', worker: '이절곡', completedAt: '2025-12-13 10:00' },\n '절곡': { status: '진행중', worker: '박절곡', startedAt: '2025-12-14 09:00' },\n '중간검사': { status: '대기' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'FLD-002', productName: '방연셔터 절곡 부품 SET', floor: 'B1', location: 'H-01', spec: '4000×3500', qty: 2, lotNo: null },\n ],\n // 전개도 상세정보 (절곡 공정 전용)\n developedParts: [\n { itemCode: 'SD40', itemName: '벽면형 마감재', material: 'E.G.I 1.15T', totalWidth: 450, length: 4000, qty: 4, weight: 1.2, dimensions: '30→60→30', note: '' },\n { itemCode: 'SD41', itemName: '가이드레일(벽면형)', material: 'E.G.I 1.6T', totalWidth: 500, length: 4000, qty: 2, weight: 3.072, dimensions: '40→80→42.5→25', note: '' },\n { itemCode: 'SD42', itemName: 'C형 레일', material: 'E.G.I 1.55T', totalWidth: 400, length: 4000, qty: 2, weight: 2.4, dimensions: '35→50→35', note: '' },\n { itemCode: 'SD43', itemName: 'D형 레일', material: 'E.G.I 1.55T', totalWidth: 380, length: 4000, qty: 2, weight: 2.28, dimensions: '40→45→40', note: '' },\n { itemCode: 'SD44', itemName: '하부BASE', material: 'E.G.I 1.55T', totalWidth: 280, length: 4000, qty: 2, weight: 1.68, dimensions: '130→80', note: '벽면형 하부' },\n { itemCode: 'SD45', itemName: '하단마감재', material: 'E.G.I 1.55T', totalWidth: 250, length: 3500, qty: 1, weight: 1.05, dimensions: '60→40', note: '' },\n { itemCode: 'SD46', itemName: '하단보강엘바', material: 'E.G.I 1.55T', totalWidth: 150, length: 3500, qty: 2, weight: 0.63, dimensions: '50→30', note: '' },\n { itemCode: 'SD47', itemName: '케이스 전면부', material: 'E.G.I 1.55T', totalWidth: 600, length: 4000, qty: 1, weight: 3.6, dimensions: '25→210→140→30→50→28', note: '500*330 케이스' },\n { itemCode: 'SD48', itemName: '케이스 린텔부', material: 'E.G.I 1.55T', totalWidth: 450, length: 4000, qty: 1, weight: 2.7, dimensions: '150→100→150', note: '' },\n { itemCode: 'SD49', itemName: '케이스 측면부', material: 'E.G.I 1.55T', totalWidth: 350, length: 500, qty: 2, weight: 0.42, dimensions: '120→100→80', note: '마구리' },\n ],\n issues: [],\n createdAt: '2025-12-12',\n createdBy: '판매팀 이판매',\n approval: { drafter: { name: '이판매', date: '2025-12-12', dept: '판매팀' }, approver: { name: '박생산', date: '2025-12-12', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // WO-FLD-03: 송도 아파트 절곡 (작업대기)\n {\n id: 203,\n workOrderNo: 'KD-WO-FLD-251215-01',\n orderNo: 'KD-TS-251215-01',\n splitNo: 'KD-TS-251215-01-01',\n lotNo: 'KD-TS-251215-01',\n shipRequestDate: '2025-12-30',\n orderDate: '2025-12-15',\n processType: '절곡',\n customerName: '대우건설(주)',\n siteName: '송도 아파트 B동',\n productName: '방화문 절곡 부품',\n workPriority: 3,\n dueDate: '2025-12-30',\n status: '작업대기',\n priority: '일반',\n assignee: null,\n assignedWorkers: ['김절곡', '이절곡', '박절곡', '최절곡'], // 절곡 공정 전체 팀 배정 (4명) - 대량 작업\n totalQty: 3,\n completedQty: 0,\n currentStep: null,\n stepStatus: {\n '절단': { status: '대기' },\n '절곡': { status: '대기' },\n '중간검사': { status: '대기' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'FLD-003', productName: '방화문 절곡 부품 SET', floor: '1층', location: 'I-01', spec: '3500×2800', qty: 3, lotNo: null },\n ],\n // 전개도 상세정보 (절곡 공정 전용)\n developedParts: [\n { itemCode: 'SD50', itemName: '문틀 상부', material: 'E.G.I 1.6T', totalWidth: 300, length: 3500, qty: 3, weight: 1.26, dimensions: '50→100→50', note: '' },\n { itemCode: 'SD51', itemName: '문틀 좌측', material: 'E.G.I 1.6T', totalWidth: 300, length: 2800, qty: 3, weight: 1.008, dimensions: '50→100→50', note: '' },\n { itemCode: 'SD52', itemName: '문틀 우측', material: 'E.G.I 1.6T', totalWidth: 300, length: 2800, qty: 3, weight: 1.008, dimensions: '50→100→50', note: '' },\n { itemCode: 'SD53', itemName: '문짝 외판', material: 'E.G.I 1.2T', totalWidth: 850, length: 2700, qty: 3, weight: 2.187, dimensions: 'FLAT', note: '' },\n { itemCode: 'SD54', itemName: '문짝 내판', material: 'E.G.I 1.2T', totalWidth: 850, length: 2700, qty: 3, weight: 2.187, dimensions: 'FLAT', note: '' },\n { itemCode: 'SD55', itemName: '문짝 보강재', material: 'E.G.I 1.6T', totalWidth: 200, length: 2700, qty: 6, weight: 2.592, dimensions: '40→60→40', note: '' },\n { itemCode: 'SD56', itemName: '경첩 보강판', material: 'E.G.I 2.0T', totalWidth: 150, length: 300, qty: 9, weight: 0.648, dimensions: 'FLAT', note: '3개/문짝' },\n { itemCode: 'SD57', itemName: '잠금장치 보강판', material: 'E.G.I 2.0T', totalWidth: 200, length: 400, qty: 3, weight: 0.384, dimensions: 'FLAT', note: '' },\n ],\n issues: [],\n createdAt: '2025-12-15',\n createdBy: '판매팀 김판매',\n approval: { drafter: { name: '김판매', date: '2025-12-15', dept: '판매팀' }, approver: null, cc: ['회계팀'] },\n approvalStatus: '승인대기',\n },\n\n // WO-FLD-04: 해운대 타워 절곡 (재작업중)\n {\n id: 204,\n workOrderNo: 'KD-WO-FLD-251208-01',\n orderNo: 'KD-TS-251208-01',\n splitNo: 'KD-TS-251208-01-01',\n lotNo: 'KD-TS-251208-01',\n shipRequestDate: '2025-12-22',\n orderDate: '2025-12-08',\n processType: '절곡',\n customerName: '(주)서울인테리어',\n siteName: '해운대 타워',\n productName: '스틸셔터 절곡 부품',\n workPriority: 1,\n dueDate: '2025-12-22',\n status: '재작업중',\n priority: '긴급',\n assignee: '최절곡',\n totalQty: 2,\n completedQty: 1,\n currentStep: '절곡',\n stepStatus: {\n '절단': { status: '완료', worker: '최절곡', completedAt: '2025-12-09 10:00' },\n '절곡': {\n status: '재작업',\n worker: '최절곡',\n completedAt: '2025-12-10 14:00',\n reworkStartedAt: '2025-12-11 09:00',\n reworkReason: '절곡 각도 불량 1EA 재작업'\n },\n '중간검사': {\n status: '불합격',\n inspector: '품질팀 이검사',\n result: '불합격',\n completedAt: '2025-12-10 16:00',\n failReason: '절곡 각도 불량 - 90° 기준 ±2° 초과 (1EA)',\n inspectionLot: 'KD-FLD-251210-01-(1)',\n reworkRequired: true,\n reworkQty: 1\n },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'FLD-004', productName: '스틸셔터 절곡 부품 SET', floor: '1층', location: 'J-01', spec: '3200×3000', qty: 2, lotNo: null },\n ],\n // 전개도 상세정보 (절곡 공정 전용)\n developedParts: [\n { itemCode: 'SD60', itemName: '스틸 가이드레일', material: 'SPCC 1.6T', totalWidth: 500, length: 3200, qty: 4, weight: 2.048, dimensions: '40→80→42.5→25', note: '' },\n { itemCode: 'SD61', itemName: '스틸 바텀바', material: 'SPCC 1.6T', totalWidth: 300, length: 3000, qty: 2, weight: 1.152, dimensions: '30→95→30', note: '' },\n { itemCode: 'SD62', itemName: '스틸 전면판', material: 'SPCC 1.6T', totalWidth: 600, length: 3200, qty: 2, weight: 2.4576, dimensions: '25→210→140→30→50→28', note: '' },\n { itemCode: 'SD63', itemName: '스틸 후면판', material: 'SPCC 1.6T', totalWidth: 500, length: 3200, qty: 2, weight: 2.048, dimensions: '25→165→90→155', note: '' },\n { itemCode: 'SD64', itemName: '스틸 측면판', material: 'SPCC 1.6T', totalWidth: 350, length: 400, qty: 4, weight: 0.3584, dimensions: '120→100→80', note: '' },\n { itemCode: 'SD65', itemName: '스틸 상부덮개', material: 'SPCC 1.2T', totalWidth: 600, length: 3200, qty: 2, weight: 1.8432, dimensions: '25→140→330→25', note: '' },\n ],\n issues: [\n {\n id: 1,\n issueType: '불량품발생',\n description: '중간검사 불합격 - 절곡 각도 불량 1EA (90° 기준 ±2° 초과)',\n reportedBy: '품질팀 이검사',\n reportedAt: '2025-12-10 16:30',\n status: '처리중',\n resolvedBy: null,\n resolvedAt: null,\n resolution: null,\n actionTaken: '재작업 지시 - 절곡 재가공 진행'\n },\n ],\n qualityIssue: {\n hasIssue: true,\n inspectionLot: 'KD-FLD-251210-01-(1)',\n failedQty: 1,\n passedQty: 1,\n failReason: '절곡 각도 불량',\n reworkStatus: '재작업중',\n reworkStartDate: '2025-12-11',\n expectedCompletionDate: '2025-12-13'\n },\n createdAt: '2025-12-08',\n createdBy: '전진팀 최전진',\n approval: { drafter: { name: '최전진', date: '2025-12-08', dept: '전진팀' }, approver: { name: '박생산', date: '2025-12-08', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // WO-FLD-05: E2E 테스트 절곡 (작업완료)\n {\n id: 205,\n workOrderNo: 'KD-WO-FLD-251216-01',\n orderNo: 'KD-TS-251216-06',\n splitNo: 'KD-TS-251216-06-01',\n lotNo: 'KD-TS-251216-06',\n shipRequestDate: '2025-12-28',\n orderDate: '2025-12-16',\n processType: '절곡',\n customerName: '삼성물산(주)',\n siteName: '[E2E테스트] 절곡 전용 현장',\n productName: '방화셔터 절곡 부품 (E2E)',\n workPriority: 1,\n dueDate: '2025-12-28',\n status: '작업완료',\n priority: '긴급',\n assignee: '최절곡',\n totalQty: 1,\n completedQty: 1,\n currentStep: '포장',\n stepStatus: {\n '절단': { status: '완료', worker: '최절곡', completedAt: '2025-12-17 10:00' },\n '절곡': { status: '완료', worker: '최절곡', completedAt: '2025-12-18 14:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-18 16:00', inspectionLot: 'KD-FLD-E2E-01-(1)' },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-12-19 10:00', labelInfo: 'E2E-G-01' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-19 11:00',\n items: [\n { id: 1, productCode: 'FLD-E2E-001', productName: '방화셔터 절곡 부품 SET (E2E)', floor: '테스트층', location: 'E2E-G-01', spec: '3000×4000', qty: 1, lotNo: 'KD-TS-FLD-E2E-01' },\n ],\n // 전개도 상세정보 (절곡 공정 전용) - E2E 테스트용\n developedParts: [\n { itemCode: 'E2E-01', itemName: '테스트 엘바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 4, weight: 0.9, dimensions: '75', note: 'E2E 테스트' },\n { itemCode: 'E2E-02', itemName: '테스트 하장바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 1.158, dimensions: '67→126→165→178→193', note: 'E2E 테스트' },\n { itemCode: 'E2E-03', itemName: '테스트 가이드레일', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 2.304, dimensions: '40→80→42.5→25', note: 'E2E 테스트' },\n { itemCode: 'E2E-04', itemName: '테스트 바텀바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.44, dimensions: '30→95→30', note: 'E2E 테스트' },\n { itemCode: 'E2E-05', itemName: '테스트 전면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.92, dimensions: '25→210→140→30→50→28', note: 'E2E 테스트' },\n { itemCode: 'E2E-06', itemName: '테스트 후면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.728, dimensions: '25→165→90→155', note: 'E2E 테스트' },\n ],\n issues: [],\n createdAt: '2025-12-16',\n createdBy: '[E2E] 시스템',\n approval: { drafter: { name: '시스템', date: '2025-12-16', dept: 'E2E' }, approver: { name: '생산팀', date: '2025-12-16', dept: '생산' }, cc: [] },\n approvalStatus: '승인완료',\n },\n\n // ═══════════════════════════════════════════════════════════════════════════\n // 전체 워크플로우 통합 테스트 작업지시 데이터 12건 (ID: 301-312)\n // 수주(SO-251217-XX) → 작업지시(WO-251217-XX) → 품질검사 → 출하 연동\n // ═══════════════════════════════════════════════════════════════════════════\n\n // [통합테스트 1] WO-251217-01: 삼성물산 - 래미안 강남 프레스티지 (스크린 3대) - 작업완료\n {\n id: 301,\n workOrderNo: 'KD-WO-251217-01',\n orderNo: 'KD-TS-251217-01',\n splitNo: 'KD-TS-251217-01-01',\n lotNo: 'KD-TS-251217-01',\n shipRequestDate: '2026-01-13',\n orderDate: '2025-12-17',\n processType: '스크린',\n customerName: '삼성물산(주)',\n siteName: '래미안 강남 프레스티지',\n productName: '스크린 셔터 (표준형)',\n workPriority: 1,\n dueDate: '2026-01-15',\n status: '작업완료',\n priority: '긴급',\n assignee: '김스크린',\n assignedWorkers: ['김스크린', '이스크린'],\n totalQty: 3,\n completedQty: 3,\n currentStep: '포장',\n stepStatus: {\n '원단절단': { status: '완료', worker: '김스크린', completedAt: '2025-12-18 10:00' },\n '미싱': { status: '완료', worker: '이스크린', completedAt: '2025-12-19 14:00' },\n '앤드락작업': { status: '완료', worker: '김스크린', completedAt: '2025-12-20 11:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-20 15:00', approvedBy: '품질팀장 최품질', inspectionLot: 'IQC-251220-01' },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-12-21 09:00', labelInfo: '1층 A-01, 2층 A-02, 3층 A-03' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-21 10:00',\n items: [\n { id: 1, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'A-01', spec: '3000×2500', qty: 1, lotNo: 'LOT-20251218-001', materialLotNo: 'MAT-251217-01', productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, prodWidth: 3140, prodHeight: 2850, guideRailType: '벽면형', guideRailSpec: '120-70', shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감' } },\n { id: 2, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '2층', location: 'A-02', spec: '3000×2500', qty: 1, lotNo: 'LOT-20251218-002', materialLotNo: 'MAT-251217-01', productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, prodWidth: 3140, prodHeight: 2850, guideRailType: '벽면형', guideRailSpec: '120-70', shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감' } },\n { id: 3, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '3층', location: 'A-03', spec: '3000×2500', qty: 1, lotNo: 'LOT-20251218-003', materialLotNo: 'MAT-251217-01', productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, prodWidth: 3140, prodHeight: 2850, guideRailType: '벽면형', guideRailSpec: '120-70', shaft: 5, caseSpec: '500-330', motorBracket: '380-180', capacity: 300, finish: 'SUS마감' } },\n ],\n bomCalculated: {\n guideRail: { itemCode: 'GR12070', spec: '120-70', length: 2850, qty: 6, unitPrice: 45000 },\n case: { itemCode: 'RC500330', spec: '500-330', length: 3140, qty: 3, unitPrice: 120000 },\n smokeBarrier: { itemCode: 'G-I-4C17-53', spec: 'W50×3000', length: 3140, qty: 3, unitPrice: 35000 },\n shaft: { itemCode: 'SHAFT-5', spec: '5각', length: 3140, qty: 3, unitPrice: 28000 },\n motor: { itemCode: 'E-220V-300KG', spec: '220V-300KG', qty: 3, unitPrice: 450000 },\n motorBracket: { itemCode: 'HB380180', spec: '380-180', qty: 3, unitPrice: 25000 },\n },\n issues: [],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 김영업',\n approval: { drafter: { name: '김영업', date: '2025-12-17', dept: '판매1팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // [통합테스트 2-1] WO-251217-02: 현대건설 - 힐스테이트 판교 (스크린 2대) - 작업완료\n {\n id: 302,\n workOrderNo: 'KD-WO-251217-02',\n orderNo: 'KD-TS-251217-02',\n splitNo: 'KD-TS-251217-02-01',\n lotNo: 'KD-TS-251217-02',\n shipRequestDate: '2026-01-18',\n orderDate: '2025-12-17',\n processType: '스크린',\n customerName: '현대건설(주)',\n siteName: '힐스테이트 판교 더 퍼스트',\n productName: '스크린 셔터 (표준형)',\n workPriority: 2,\n dueDate: '2026-01-15',\n status: '작업완료',\n priority: '일반',\n assignee: '박스크린',\n assignedWorkers: ['박스크린', '최스크린'],\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '원단절단': { status: '완료', worker: '박스크린', completedAt: '2025-12-18 14:00' },\n '미싱': { status: '완료', worker: '최스크린', completedAt: '2025-12-19 16:00' },\n '앤드락작업': { status: '완료', worker: '박스크린', completedAt: '2025-12-20 14:00' },\n '중간검사': { status: '완료', inspector: '품질팀 박검사', result: '합격', completedAt: '2025-12-20 17:00', approvedBy: '품질팀장 최품질', inspectionLot: 'IQC-251220-02' },\n '포장': { status: '완료', worker: '김포장', completedAt: '2025-12-21 11:00', labelInfo: 'B1 B-01, B1 B-02' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-21 12:00',\n items: [\n { id: 1, productCode: 'SH4030', productName: '스크린 셔터 (표준형)', floor: 'B1', location: 'B-01', spec: '4000×3000', qty: 1, lotNo: 'LOT-20251218-004', materialLotNo: 'MAT-251217-02', productionSpec: { type: '와이어', openWidth: 4000, openHeight: 3000, prodWidth: 4140, prodHeight: 3350, guideRailType: '벽면형', capacity: 300 } },\n { id: 2, productCode: 'SH4030', productName: '스크린 셔터 (표준형)', floor: 'B1', location: 'B-02', spec: '4000×3000', qty: 1, lotNo: 'LOT-20251218-005', materialLotNo: 'MAT-251217-02', productionSpec: { type: '와이어', openWidth: 4000, openHeight: 3000, prodWidth: 4140, prodHeight: 3350, guideRailType: '벽면형', capacity: 300 } },\n ],\n bomCalculated: {\n guideRail: { itemCode: 'GR12070', spec: '120-70', length: 3350, qty: 4, unitPrice: 45000 },\n case: { itemCode: 'RC500330', spec: '500-330', length: 4140, qty: 2, unitPrice: 120000 },\n smokeBarrier: { itemCode: 'G-I-4C17-53', spec: 'W50×4000', length: 4140, qty: 2, unitPrice: 45000 },\n shaft: { itemCode: 'SHAFT-5', spec: '5각', length: 4140, qty: 2, unitPrice: 35000 },\n motor: { itemCode: 'E-220V-300KG', spec: '220V-300KG', qty: 2, unitPrice: 450000 },\n },\n issues: [],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 이영업',\n approval: { drafter: { name: '이영업', date: '2025-12-17', dept: '판매1팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // [통합테스트 2-2] WO-251217-03: 현대건설 - 힐스테이트 판교 (슬랫 2대) - 작업중\n {\n id: 303,\n workOrderNo: 'KD-WO-251217-03',\n orderNo: 'KD-TS-251217-02',\n splitNo: 'KD-TS-251217-02-02',\n lotNo: 'KD-TS-251217-02',\n shipRequestDate: '2026-01-18',\n orderDate: '2025-12-17',\n processType: '슬랫',\n customerName: '현대건설(주)',\n siteName: '힐스테이트 판교 더 퍼스트',\n productName: '철재 슬랫 셔터',\n workPriority: 3,\n dueDate: '2026-01-20',\n status: '작업중',\n priority: '일반',\n assignee: '김슬랫',\n assignedWorkers: ['김슬랫', '이슬랫'],\n totalQty: 2,\n completedQty: 1,\n currentStep: '미미작업',\n stepStatus: {\n '코일절단': { status: '완료', worker: '김슬랫', completedAt: '2025-12-19 10:00' },\n '중간검사': { status: '완료', inspector: '품질팀 박검사', result: '합격', completedAt: '2025-12-19 14:00', inspectionLot: 'PQC-251219-01' },\n '미미작업': { status: '진행중', worker: '이슬랫', startedAt: '2025-12-20 09:00' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'ST3525', productName: '철재 슬랫 셔터', floor: '1층', location: 'B-03', spec: '3500×2500', qty: 1, lotNo: 'LOT-20251219-001', materialLotNo: 'MAT-251218-01', productionSpec: { type: '슬랫', openWidth: 3500, openHeight: 2500 } },\n { id: 2, productCode: 'ST3525', productName: '철재 슬랫 셔터', floor: '1층', location: 'B-04', spec: '3500×2500', qty: 1, lotNo: null, materialLotNo: 'MAT-251218-01', productionSpec: { type: '슬랫', openWidth: 3500, openHeight: 2500 } },\n ],\n issues: [],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 이영업',\n approval: { drafter: { name: '이영업', date: '2025-12-17', dept: '판매1팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // [통합테스트 3] WO-251217-04: 대우건설 - 푸르지오 일산 (대형 스크린 2대, 500KG모터) - 작업완료\n {\n id: 304,\n workOrderNo: 'KD-WO-251217-04',\n orderNo: 'KD-TS-251217-03',\n splitNo: 'KD-TS-251217-03-01',\n lotNo: 'KD-TS-251217-03',\n shipRequestDate: '2026-01-23',\n orderDate: '2025-12-17',\n processType: '스크린',\n customerName: '대우건설(주)',\n siteName: '푸르지오 일산 센트럴파크',\n productName: '스크린 셔터 (대형)',\n workPriority: 4,\n dueDate: '2026-01-25',\n status: '작업완료',\n priority: '긴급',\n assignee: '김스크린',\n assignedWorkers: ['김스크린', '이스크린', '박스크린'],\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '원단절단': { status: '완료', worker: '김스크린', completedAt: '2025-12-19 10:00' },\n '미싱': { status: '완료', worker: '이스크린', completedAt: '2025-12-20 16:00' },\n '앤드락작업': { status: '완료', worker: '박스크린', completedAt: '2025-12-21 14:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-21 17:00', approvedBy: '품질팀장 최품질', inspectionLot: 'IQC-251221-01' },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-12-22 10:00', labelInfo: '로비 C-01, 연회장 C-02' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-22 11:00',\n items: [\n { id: 1, productCode: 'SH6040', productName: '스크린 셔터 (대형)', floor: '로비', location: 'C-01', spec: '6000×4000', qty: 1, lotNo: 'LOT-20251219-002', materialLotNo: 'MAT-251218-02', productionSpec: { type: '와이어', openWidth: 6000, openHeight: 4000, prodWidth: 6140, prodHeight: 4350, guideRailType: '벽면형', capacity: 500 } },\n { id: 2, productCode: 'SH6040', productName: '스크린 셔터 (대형)', floor: '연회장', location: 'C-02', spec: '6000×4000', qty: 1, lotNo: 'LOT-20251219-003', materialLotNo: 'MAT-251218-02', productionSpec: { type: '와이어', openWidth: 6000, openHeight: 4000, prodWidth: 6140, prodHeight: 4350, guideRailType: '벽면형', capacity: 500 } },\n ],\n bomCalculated: {\n guideRail: { itemCode: 'GR15080', spec: '150-80', length: 4350, qty: 4, unitPrice: 65000 },\n case: { itemCode: 'RC600400', spec: '600-400', length: 6140, qty: 2, unitPrice: 180000 },\n smokeBarrier: { itemCode: 'G-I-4C17-53', spec: 'W50×6000', length: 6140, qty: 2, unitPrice: 65000 },\n shaft: { itemCode: 'SHAFT-6', spec: '6각', length: 6140, qty: 2, unitPrice: 55000 },\n motor: { itemCode: 'E-380V-500KG', spec: '380V-500KG', qty: 2, unitPrice: 850000 },\n motorBracket: { itemCode: 'HB500250', spec: '500-250', qty: 2, unitPrice: 45000 },\n },\n issues: [],\n createdAt: '2025-12-17',\n createdBy: '판매2팀 박영업',\n approval: { drafter: { name: '박영업', date: '2025-12-17', dept: '판매2팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // [통합테스트 4] WO-251217-05: GS건설 - 자이 위례 (슬랫 전용 4대) - 작업완료\n {\n id: 305,\n workOrderNo: 'KD-WO-251217-05',\n orderNo: 'KD-TS-251217-04',\n splitNo: 'KD-TS-251217-04-01',\n lotNo: 'KD-TS-251217-04',\n shipRequestDate: '2026-01-28',\n orderDate: '2025-12-17',\n processType: '슬랫',\n customerName: 'GS건설(주)',\n siteName: '자이 위례 더 퍼스트',\n productName: '철재 슬랫 셔터',\n workPriority: 5,\n dueDate: '2026-01-30',\n status: '작업완료',\n priority: '일반',\n assignee: '박슬랫',\n assignedWorkers: ['박슬랫', '김슬랫', '이슬랫'],\n totalQty: 4,\n completedQty: 4,\n currentStep: '포장',\n stepStatus: {\n '코일절단': { status: '완료', worker: '박슬랫', completedAt: '2025-12-19 16:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-20 10:00', inspectionLot: 'PQC-251220-01' },\n '미미작업': { status: '완료', worker: '김슬랫', completedAt: '2025-12-21 14:00' },\n '포장': { status: '완료', worker: '이슬랫', completedAt: '2025-12-22 10:00', labelInfo: 'B2 D-01~D-04' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-22 11:00',\n items: [\n { id: 1, productCode: 'ST3020', productName: '철재 슬랫 셔터', floor: 'B2', location: 'D-01', spec: '3000×2000', qty: 1, lotNo: 'LOT-20251219-004', materialLotNo: 'MAT-251218-03', productionSpec: { type: '슬랫', openWidth: 3000, openHeight: 2000 } },\n { id: 2, productCode: 'ST3020', productName: '철재 슬랫 셔터', floor: 'B2', location: 'D-02', spec: '3000×2000', qty: 1, lotNo: 'LOT-20251219-005', materialLotNo: 'MAT-251218-03', productionSpec: { type: '슬랫', openWidth: 3000, openHeight: 2000 } },\n { id: 3, productCode: 'ST3020', productName: '철재 슬랫 셔터', floor: 'B2', location: 'D-03', spec: '3000×2000', qty: 1, lotNo: 'LOT-20251219-006', materialLotNo: 'MAT-251218-03', productionSpec: { type: '슬랫', openWidth: 3000, openHeight: 2000 } },\n { id: 4, productCode: 'ST3020', productName: '철재 슬랫 셔터', floor: 'B2', location: 'D-04', spec: '3000×2000', qty: 1, lotNo: 'LOT-20251219-007', materialLotNo: 'MAT-251218-03', productionSpec: { type: '슬랫', openWidth: 3000, openHeight: 2000 } },\n ],\n issues: [],\n createdAt: '2025-12-17',\n createdBy: '판매2팀 최영업',\n approval: { drafter: { name: '최영업', date: '2025-12-17', dept: '판매2팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // [통합테스트 5] WO-251217-06: 포스코건설 - 더샵 송도 (코너형 가이드레일 2대) - 작업완료\n {\n id: 306,\n workOrderNo: 'KD-WO-251217-06',\n orderNo: 'KD-TS-251217-05',\n splitNo: 'KD-TS-251217-05-01',\n lotNo: 'KD-TS-251217-05',\n shipRequestDate: '2026-01-28',\n orderDate: '2025-12-17',\n processType: '스크린',\n customerName: '포스코건설(주)',\n siteName: '더샵 송도 마린베이',\n productName: '스크린 셔터 (코너형)',\n workPriority: 6,\n dueDate: '2026-01-30',\n status: '작업완료',\n priority: '일반',\n assignee: '최스크린',\n assignedWorkers: ['최스크린', '김스크린'],\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '원단절단': { status: '완료', worker: '최스크린', completedAt: '2025-12-20 10:00' },\n '미싱': { status: '완료', worker: '김스크린', completedAt: '2025-12-21 14:00' },\n '앤드락작업': { status: '완료', worker: '최스크린', completedAt: '2025-12-22 11:00' },\n '중간검사': { status: '완료', inspector: '품질팀 박검사', result: '합격', completedAt: '2025-12-22 15:00', approvedBy: '품질팀장 최품질', inspectionLot: 'IQC-251222-01' },\n '포장': { status: '완료', worker: '김포장', completedAt: '2025-12-23 09:00', labelInfo: '1층 E-01, E-02 (코너형)' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-23 10:00',\n items: [\n { id: 1, productCode: 'SH3525C', productName: '스크린 셔터 (코너형)', floor: '1층', location: 'E-01', spec: '3500×2500', qty: 1, lotNo: 'LOT-20251220-001', materialLotNo: 'MAT-251219-01', productionSpec: { type: '와이어', openWidth: 3500, openHeight: 2500, prodWidth: 3640, prodHeight: 2850, guideRailType: '코너형', guideRailSpec: '130-80', capacity: 300 } },\n { id: 2, productCode: 'SH3525C', productName: '스크린 셔터 (코너형)', floor: '1층', location: 'E-02', spec: '3500×2500', qty: 1, lotNo: 'LOT-20251220-002', materialLotNo: 'MAT-251219-01', productionSpec: { type: '와이어', openWidth: 3500, openHeight: 2500, prodWidth: 3640, prodHeight: 2850, guideRailType: '코너형', guideRailSpec: '130-80', capacity: 300 } },\n ],\n bomCalculated: {\n guideRail: { itemCode: 'GR13080-C', spec: '130-80 코너형', length: 2850, qty: 4, unitPrice: 75000 },\n case: { itemCode: 'RC500330', spec: '500-330', length: 3640, qty: 2, unitPrice: 130000 },\n smokeBarrier: { itemCode: 'G-I-4C17-53', spec: 'W50×3500', length: 3640, qty: 2, unitPrice: 40000 },\n cornerBracket: { itemCode: 'CB-90', spec: '90도 코너 브라켓', qty: 4, unitPrice: 35000 },\n motor: { itemCode: 'E-220V-300KG', spec: '220V-300KG', qty: 2, unitPrice: 450000 },\n },\n issues: [],\n createdAt: '2025-12-17',\n createdBy: '판매3팀 정영업',\n approval: { drafter: { name: '정영업', date: '2025-12-17', dept: '판매3팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // [통합테스트 7-1] WO-251217-07: 호반건설 - 써밋 광교 1차분할 (스크린 2대) - 작업완료\n {\n id: 307,\n workOrderNo: 'KD-WO-251217-07',\n orderNo: 'KD-TS-251217-07',\n splitNo: 'KD-TS-251217-07-01',\n lotNo: 'KD-TS-251217-07',\n shipRequestDate: '2026-01-08',\n orderDate: '2025-12-17',\n processType: '스크린',\n customerName: '호반건설(주)',\n siteName: '써밋 광교 리버파크',\n productName: '스크린 셔터 (표준형)',\n workPriority: 7,\n dueDate: '2026-01-10',\n status: '작업완료',\n priority: '긴급',\n assignee: '이스크린',\n assignedWorkers: ['이스크린'],\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '원단절단': { status: '완료', worker: '이스크린', completedAt: '2025-12-18 10:00' },\n '미싱': { status: '완료', worker: '이스크린', completedAt: '2025-12-19 10:00' },\n '앤드락작업': { status: '완료', worker: '이스크린', completedAt: '2025-12-19 16:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-20 10:00', inspectionLot: 'IQC-251220-03' },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-12-20 14:00', labelInfo: '1층 F-01, F-02' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-20 15:00',\n items: [\n { id: 1, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'F-01', spec: '3000×2500', qty: 1, lotNo: 'LOT-20251218-006', materialLotNo: 'MAT-251217-03', productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, prodWidth: 3140, prodHeight: 2850, guideRailType: '벽면형', capacity: 300 } },\n { id: 2, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'F-02', spec: '3000×2500', qty: 1, lotNo: 'LOT-20251218-007', materialLotNo: 'MAT-251217-03', productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, prodWidth: 3140, prodHeight: 2850, guideRailType: '벽면형', capacity: 300 } },\n ],\n issues: [],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 윤영업',\n approval: { drafter: { name: '윤영업', date: '2025-12-17', dept: '판매1팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // [통합테스트 7-2] WO-251217-08: 호반건설 - 써밋 광교 2차분할 (스크린 2대) - 작업완료\n {\n id: 308,\n workOrderNo: 'KD-WO-251217-08',\n orderNo: 'KD-TS-251217-07',\n splitNo: 'KD-TS-251217-07-02',\n lotNo: 'KD-TS-251217-07',\n shipRequestDate: '2026-01-18',\n orderDate: '2025-12-17',\n processType: '스크린',\n customerName: '호반건설(주)',\n siteName: '써밋 광교 리버파크',\n productName: '스크린 셔터 (표준형)',\n workPriority: 8,\n dueDate: '2026-01-20',\n status: '작업완료',\n priority: '일반',\n assignee: '박스크린',\n assignedWorkers: ['박스크린'],\n totalQty: 2,\n completedQty: 2,\n currentStep: '포장',\n stepStatus: {\n '원단절단': { status: '완료', worker: '박스크린', completedAt: '2025-12-21 10:00' },\n '미싱': { status: '완료', worker: '박스크린', completedAt: '2025-12-22 10:00' },\n '앤드락작업': { status: '완료', worker: '박스크린', completedAt: '2025-12-22 16:00' },\n '중간검사': { status: '완료', inspector: '품질팀 박검사', result: '합격', completedAt: '2025-12-23 10:00', inspectionLot: 'IQC-251223-01' },\n '포장': { status: '완료', worker: '김포장', completedAt: '2025-12-23 14:00', labelInfo: '2층 F-03, F-04' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-23 15:00',\n items: [\n { id: 1, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '2층', location: 'F-03', spec: '3000×2500', qty: 1, lotNo: 'LOT-20251221-001', materialLotNo: 'MAT-251220-01', productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, prodWidth: 3140, prodHeight: 2850, guideRailType: '벽면형', capacity: 300 } },\n { id: 2, productCode: 'SH3025', productName: '스크린 셔터 (표준형)', floor: '2층', location: 'F-04', spec: '3000×2500', qty: 1, lotNo: 'LOT-20251221-002', materialLotNo: 'MAT-251220-01', productionSpec: { type: '와이어', openWidth: 3000, openHeight: 2500, prodWidth: 3140, prodHeight: 2850, guideRailType: '벽면형', capacity: 300 } },\n ],\n issues: [],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 윤영업',\n approval: { drafter: { name: '윤영업', date: '2025-12-17', dept: '판매1팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // [통합테스트 7-3] WO-251217-09: 호반건설 - 써밋 광교 3차분할 (슬랫 1대) - 작업중\n {\n id: 309,\n workOrderNo: 'KD-WO-251217-09',\n orderNo: 'KD-TS-251217-07',\n splitNo: 'KD-TS-251217-07-03',\n lotNo: 'KD-TS-251217-07',\n shipRequestDate: '2026-01-28',\n orderDate: '2025-12-17',\n processType: '슬랫',\n customerName: '호반건설(주)',\n siteName: '써밋 광교 리버파크',\n productName: '철재 슬랫 셔터',\n workPriority: 9,\n dueDate: '2026-01-30',\n status: '작업중',\n priority: '일반',\n assignee: '이슬랫',\n assignedWorkers: ['이슬랫'],\n totalQty: 1,\n completedQty: 0,\n currentStep: '코일절단',\n stepStatus: {\n '코일절단': { status: '진행중', worker: '이슬랫', startedAt: '2025-12-23 10:00' },\n '중간검사': { status: '대기' },\n '미미작업': { status: '대기' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'ST3525', productName: '철재 슬랫 셔터', floor: '3층', location: 'F-05', spec: '3500×2500', qty: 1, lotNo: null, materialLotNo: 'MAT-251222-01', productionSpec: { type: '슬랫', openWidth: 3500, openHeight: 2500 } },\n ],\n issues: [],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 윤영업',\n approval: { drafter: { name: '윤영업', date: '2025-12-17', dept: '판매1팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // [통합테스트 8] WO-251217-10: 한화건설 - 포레나 수지 추가분 (스크린 2대) - 작업대기\n {\n id: 310,\n workOrderNo: 'KD-WO-251217-10',\n orderNo: 'KD-TS-251217-08',\n splitNo: 'KD-TS-251217-08-01',\n lotNo: 'KD-TS-251217-08',\n shipRequestDate: '2026-02-13',\n orderDate: '2025-12-17',\n processType: '스크린',\n customerName: '한화건설(주)',\n siteName: '포레나 수지 더 퍼스트',\n productName: '스크린 셔터 (표준형) - 추가',\n workPriority: 7,\n dueDate: '2026-02-15',\n status: '작업대기',\n priority: '일반',\n assignee: null,\n assignedWorkers: [],\n totalQty: 2,\n completedQty: 0,\n currentStep: null,\n stepStatus: {\n '원단절단': { status: '대기' },\n '미싱': { status: '대기' },\n '앤드락작업': { status: '대기' },\n '중간검사': { status: '대기' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'SH3525', productName: '스크린 셔터 (표준형) 추가', floor: '4층', location: 'G-01', spec: '3500×2500', qty: 1, lotNo: null, materialLotNo: null, productionSpec: { type: '와이어', openWidth: 3500, openHeight: 2500, prodWidth: 3640, prodHeight: 2850, guideRailType: '벽면형', capacity: 300 } },\n { id: 2, productCode: 'SH3525', productName: '스크린 셔터 (표준형) 추가', floor: '5층', location: 'G-02', spec: '3500×2500', qty: 1, lotNo: null, materialLotNo: null, productionSpec: { type: '와이어', openWidth: 3500, openHeight: 2500, prodWidth: 3640, prodHeight: 2850, guideRailType: '벽면형', capacity: 300 } },\n ],\n issues: [],\n createdAt: '2025-12-17',\n createdBy: '판매2팀 강영업',\n approval: { drafter: { name: '강영업', date: '2025-12-17', dept: '판매2팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n\n // [통합테스트 9] WO-251217-11: 태영건설 - 데시앙 동탄 (품질불량 → 재작업) - 재작업중\n {\n id: 311,\n workOrderNo: 'KD-WO-251217-11',\n orderNo: 'KD-TS-251217-09',\n splitNo: 'KD-TS-251217-09-01',\n lotNo: 'KD-TS-251217-09',\n shipRequestDate: '2026-02-08',\n orderDate: '2025-12-17',\n processType: '스크린',\n customerName: '태영건설(주)',\n siteName: '데시앙 동탄 레이크파크',\n productName: '스크린 셔터 (표준형)',\n workPriority: 8,\n dueDate: '2026-02-10',\n status: '재작업중',\n priority: '긴급',\n assignee: '김스크린',\n assignedWorkers: ['김스크린', '이스크린'],\n totalQty: 2,\n completedQty: 0,\n currentStep: '앤드락작업',\n stepStatus: {\n '원단절단': { status: '완료', worker: '김스크린', completedAt: '2025-12-19 10:00' },\n '미싱': { status: '완료', worker: '이스크린', completedAt: '2025-12-20 14:00' },\n '앤드락작업': { status: '재작업', worker: '김스크린', startedAt: '2025-12-22 09:00', reworkReason: '앤드락 접착불량으로 재작업' },\n '중간검사': { status: '불합격', inspector: '품질팀 이검사', result: '불합격', completedAt: '2025-12-21 15:00', defectType: '앤드락 접착불량', inspectionLot: 'IQC-251221-02', ncrNo: 'NCR-251221-01' },\n '포장': { status: '대기' },\n },\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items: [\n { id: 1, productCode: 'SH4030', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'H-01', spec: '4000×3000', qty: 1, lotNo: 'LOT-20251219-008', materialLotNo: 'MAT-251218-04', productionSpec: { type: '와이어', openWidth: 4000, openHeight: 3000, prodWidth: 4140, prodHeight: 3350, guideRailType: '벽면형', capacity: 300 }, reworkStatus: '재작업중' },\n { id: 2, productCode: 'SH4030', productName: '스크린 셔터 (표준형)', floor: '2층', location: 'H-02', spec: '4000×3000', qty: 1, lotNo: 'LOT-20251219-009', materialLotNo: 'MAT-251218-04', productionSpec: { type: '와이어', openWidth: 4000, openHeight: 3000, prodWidth: 4140, prodHeight: 3350, guideRailType: '벽면형', capacity: 300 }, reworkStatus: '재작업중' },\n ],\n issues: [\n { id: 1, issueType: '불량품발생', description: '앤드락 접착불량 - 전체 재작업 필요', reportedAt: '2025-12-21 15:30', reportedBy: '품질팀 이검사', status: '처리중', ncrNo: 'NCR-251221-01' },\n ],\n ncrInfo: {\n ncrNo: 'NCR-251221-01',\n defectType: '앤드락 접착불량',\n defectQty: 2,\n rootCause: '접착제 경화시간 미준수',\n correctiveAction: '접착제 경화시간 표준 재교육, 작업표준서 개정',\n preventiveAction: '공정 체크리스트에 경화시간 확인 항목 추가',\n reworkStartDate: '2025-12-22',\n expectedCompletionDate: '2025-12-25',\n },\n createdAt: '2025-12-17',\n createdBy: '판매3팀 임영업',\n approval: { drafter: { name: '임영업', date: '2025-12-17', dept: '판매3팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀', '품질팀'] },\n approvalStatus: '승인완료',\n },\n\n // [통합테스트 10] WO-251217-12: 두산건설 - 위브 청라 (전체 완료) - 출하완료\n {\n id: 312,\n workOrderNo: 'KD-WO-251217-12',\n orderNo: 'KD-TS-251217-10',\n splitNo: 'KD-TS-251217-10-01',\n lotNo: 'KD-TS-251217-10',\n shipRequestDate: '2025-12-28',\n orderDate: '2025-12-17',\n processType: '스크린',\n customerName: '두산건설(주)',\n siteName: '위브 청라 센트럴파크',\n productName: '스크린 셔터 (표준형)',\n workPriority: 9,\n dueDate: '2025-12-30',\n status: '출하완료',\n priority: '긴급',\n assignee: '최스크린',\n assignedWorkers: ['최스크린', '김스크린'],\n totalQty: 2,\n completedQty: 2,\n currentStep: '출하완료',\n stepStatus: {\n '원단절단': { status: '완료', worker: '최스크린', completedAt: '2025-12-18 09:00' },\n '미싱': { status: '완료', worker: '김스크린', completedAt: '2025-12-18 16:00' },\n '앤드락작업': { status: '완료', worker: '최스크린', completedAt: '2025-12-19 11:00' },\n '중간검사': { status: '완료', inspector: '품질팀 이검사', result: '합격', completedAt: '2025-12-19 14:00', approvedBy: '품질팀장 최품질', inspectionLot: 'IQC-251219-01' },\n '포장': { status: '완료', worker: '박포장', completedAt: '2025-12-19 17:00', labelInfo: '1층 I-01, I-02' },\n },\n movedToShippingArea: true,\n movedToShippingAreaAt: '2025-12-20 09:00',\n shippedAt: '2025-12-20 14:00',\n shipmentNo: 'SL-251220-01',\n items: [\n { id: 1, productCode: 'SH3525', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'I-01', spec: '3500×2500', qty: 1, lotNo: 'LOT-20251218-008', materialLotNo: 'MAT-251217-04', productionSpec: { type: '와이어', openWidth: 3500, openHeight: 2500, prodWidth: 3640, prodHeight: 2850, guideRailType: '벽면형', capacity: 300 }, shipmentStatus: '출하완료' },\n { id: 2, productCode: 'SH3525', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'I-02', spec: '3500×2500', qty: 1, lotNo: 'LOT-20251218-009', materialLotNo: 'MAT-251217-04', productionSpec: { type: '와이어', openWidth: 3500, openHeight: 2500, prodWidth: 3640, prodHeight: 2850, guideRailType: '벽면형', capacity: 300 }, shipmentStatus: '출하완료' },\n ],\n bomCalculated: {\n guideRail: { itemCode: 'GR12070', spec: '120-70', length: 2850, qty: 4, unitPrice: 45000 },\n case: { itemCode: 'RC500330', spec: '500-330', length: 3640, qty: 2, unitPrice: 125000 },\n smokeBarrier: { itemCode: 'G-I-4C17-53', spec: 'W50×3500', length: 3640, qty: 2, unitPrice: 38000 },\n shaft: { itemCode: 'SHAFT-5', spec: '5각', length: 3640, qty: 2, unitPrice: 30000 },\n motor: { itemCode: 'E-220V-300KG', spec: '220V-300KG', qty: 2, unitPrice: 450000 },\n },\n issues: [],\n createdAt: '2025-12-17',\n createdBy: '판매1팀 조영업',\n approval: { drafter: { name: '조영업', date: '2025-12-17', dept: '판매1팀' }, approver: { name: '박생산', date: '2025-12-17', dept: '생산관리' }, cc: ['회계팀'] },\n approvalStatus: '승인완료',\n },\n];\n\n// 이슈 유형\nconst issueTypes = ['불량품발생', '재고없음', '일정지연', '설비고장', '기타'];\n\n// 작업실적 (LOT별)\nconst initialWorkResults = [\n {\n id: 1,\n lotNo: 'KD-TS-250210-01-01', // 완제품LOT: KD-공장코드-YYMMDD-순번-분할순번\n workOrderNo: 'KD-PL-250120-01',\n workDate: '2025-02-10',\n processType: '스크린',\n productCode: 'SCR-001',\n productName: '스크린 셔터 (표준형)',\n spec: '7660×2550',\n productionQty: 1,\n goodQty: 1,\n defectQty: 0,\n defectType: null,\n inspectionCompleted: true,\n inspectionLot: 'KD-SC-250209-01-(3)', // 중간검사LOT\n packingCompleted: true,\n worker: '김생산',\n note: '',\n createdAt: '2025-02-10 09:30',\n },\n {\n id: 2,\n lotNo: 'KD-TS-250210-01-02',\n workOrderNo: 'KD-PL-250120-01',\n workDate: '2025-02-10',\n processType: '스크린',\n productCode: 'SCR-001',\n productName: '스크린 셔터 (표준형)',\n spec: '6500×2400',\n productionQty: 1,\n goodQty: 1,\n defectQty: 0,\n defectType: null,\n inspectionCompleted: true,\n inspectionLot: 'KD-SC-250209-01-(3)',\n packingCompleted: true,\n worker: '김생산',\n note: '',\n createdAt: '2025-02-10 14:20',\n },\n {\n id: 3,\n lotNo: 'KD-TS-250212-01-01',\n workOrderNo: 'KD-PL-250122-01',\n workDate: '2025-02-12',\n processType: '스크린',\n productCode: 'SCR-002',\n productName: '스크린 셔터 (프리미엄)',\n spec: '8000×2800',\n productionQty: 1,\n goodQty: 0,\n defectQty: 1,\n defectType: '치수불량',\n inspectionCompleted: true,\n inspectionLot: 'KD-SC-250212-01-(3)',\n packingCompleted: false,\n worker: '이생산',\n note: '재작업 필요',\n createdAt: '2025-02-12 11:00',\n },\n];\n\n// 출하 데이터 (출고번호 채번규칙: 날짜-P-순번, 예: 251217-P-01)\nconst initialShipments = [\n /*\n * ============================================================\n * 📋 출하 데이터 (수주 분할과 연동)\n * ============================================================\n * [1] KD-TS-250115-01-01 (서울 1차분) → 배송완료 ✓\n * [2] KD-TS-250115-01-02 (서울 2차분) → 출고지시 (생산중)\n * → 서울건축 출하진행률: 50% (1/2 완료)\n * \n * [3] KD-TS-250118-01-01 (인천 1차분) → 배송완료 ✓\n * [4] KD-TS-250118-01-02 (인천 2차분) → 배송완료 ✓\n * → 인천건설 출하진행률: 100% (2/2 완료)\n * \n * [5] KD-TS-250115-01-A-01 (서울 추가분) → 출하준비\n * → 서울건축 추가분 출하진행률: 0% (0/1 완료)\n * ============================================================\n */\n {\n id: 1,\n releaseNo: '250215-P-01', // 출고번호 (채번규칙: 날짜-P-순번)\n shipmentDate: '2025-02-15',\n lotNo: 'KD-TS-250115-01', // 로트번호 (추적 기준)\n splitNo: 'KD-TS-250115-01-01', // 분할번호 (수주 연동)\n customerName: '서울건축',\n siteName: '강남 오피스텔 A동',\n deliveryAddress: '서울시 강남구 테헤란로 123',\n receiverName: '김현장',\n receiverPhone: '010-9999-8888',\n status: '배송완료', // 테스트: 배송완료로 변경 → 50% 표시\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n // 출고순위\n shipmentPriority: 1,\n isSplitShipment: true,\n splitOrder: 1, // 분할 순서 (1차, 2차...)\n // 배차 정보\n dispatchType: '상차',\n logisticsCompany: '한진물류',\n vehicleType: '5톤',\n vehicleNo: '12가 3456',\n driverName: '홍운전',\n driverPhone: '010-1111-2222',\n scheduledArrival: '2025-02-15 09:00',\n confirmedArrival: '2025-02-15 09:30',\n shippingCost: 150000,\n // 상차 정보\n loadingCompleted: false,\n loadingCompletedAt: null,\n loadingWorker: null,\n // 품목 - 완제품LOT: KD-공장코드-YYMMDD-순번-분할순번\n items: [\n { id: 1, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'A-01', spec: '7660×2550', qty: 1, lotNo: 'KD-TS-250210-01-01', arrivedAtLoadingArea: true, loadingChecked: false, accessoryLotNo: '' },\n { id: 2, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'A-02', spec: '6500×2400', qty: 1, lotNo: 'KD-TS-250210-01-02', arrivedAtLoadingArea: true, loadingChecked: false, accessoryLotNo: '' },\n ],\n // 변경이력\n changeHistory: [\n { id: 1, changedAt: '2025-02-14 10:30', changeType: '출하일 변경', beforeValue: '2025-02-14', afterValue: '2025-02-15', reason: '고객사 현장 일정 변경', changedBy: '판매1팀 김판매' },\n ],\n // 미출고사유\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-02-10',\n createdBy: '판매1팀 김판매',\n note: '',\n },\n {\n id: 2,\n releaseNo: '250218-P-01', // 출고번호 (채번규칙: 날짜-P-순번)\n shipmentDate: '2025-02-18',\n lotNo: 'KD-TS-250115-01',\n splitNo: 'KD-TS-250115-01-02',\n customerName: '서울건축',\n siteName: '강남 오피스텔 A동',\n deliveryAddress: '서울시 강남구 테헤란로 123',\n receiverName: '김현장',\n receiverPhone: '010-9999-8888',\n status: '출고지시',\n paymentConfirmed: false,\n taxInvoiceIssued: false,\n shipmentPriority: 2,\n isSplitShipment: true,\n splitOrder: 2,\n dispatchType: '직접배차',\n logisticsCompany: null,\n vehicleType: null,\n vehicleNo: null,\n driverName: null,\n driverPhone: null,\n scheduledArrival: '2025-02-18 10:00',\n confirmedArrival: null,\n shippingCost: 0,\n loadingCompleted: false,\n loadingCompletedAt: null,\n loadingWorker: null,\n items: [\n { id: 1, productCode: 'SCR-002', productName: '스크린 셔터 (프리미엄)', floor: '2층', location: 'B-01', spec: '8000×2800', qty: 1, lotNo: null, arrivedAtLoadingArea: false, loadingChecked: false, accessoryLotNo: '' },\n { id: 2, productCode: 'SLT-001', productName: '슬랫 셔터', floor: '2층', location: 'B-02', spec: '5000×2200', qty: 2, lotNo: null, arrivedAtLoadingArea: false, loadingChecked: false, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-02-12',\n createdBy: '판매1팀 김판매',\n note: '2차 분할 출고 예정 - 생산 진행중',\n },\n // KD-TS-250118-01 출하 (100% 완료)\n {\n id: 3,\n releaseNo: '250220-P-01', // 출고번호 (채번규칙: 날짜-P-순번)\n shipmentDate: '2025-02-20',\n lotNo: 'KD-TS-250118-01',\n splitNo: 'KD-TS-250118-01-01',\n customerName: '인천건설',\n siteName: '송도 오피스텔 B동',\n deliveryAddress: '인천시 연수구 송도동 45',\n receiverName: '박현장',\n receiverPhone: '010-8888-7777',\n status: '배송완료',\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n shipmentPriority: 1,\n isSplitShipment: true,\n splitOrder: 1,\n dispatchType: '직접배차',\n logisticsCompany: null,\n vehicleType: '5톤',\n vehicleNo: '34나 5678',\n driverName: '이운전',\n driverPhone: '010-3333-4444',\n scheduledArrival: '2025-02-20 10:00',\n confirmedArrival: '2025-02-20 10:15',\n shippingCost: 0,\n loadingCompleted: true,\n loadingCompletedAt: '2025-02-20 08:30',\n loadingWorker: '김상차',\n items: [\n { id: 1, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: 'B1', location: 'C-01', spec: '6000×2400', qty: 2, lotNo: 'KD-TS-250215-01-01', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-02-18',\n createdBy: '판매1팀 이판매',\n completedAt: '2025-02-20 14:00',\n note: '1차 분할 배송완료',\n },\n {\n id: 4,\n releaseNo: '250301-P-01', // 출고번호 (채번규칙: 날짜-P-순번)\n shipmentDate: '2025-03-01',\n lotNo: 'KD-TS-250118-01',\n splitNo: 'KD-TS-250118-01-02',\n customerName: '인천건설',\n siteName: '송도 오피스텔 B동',\n deliveryAddress: '인천시 연수구 송도동 45',\n receiverName: '박현장',\n receiverPhone: '010-8888-7777',\n status: '배송완료',\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n shipmentPriority: 1,\n isSplitShipment: true,\n splitOrder: 2,\n dispatchType: '직접배차',\n logisticsCompany: null,\n vehicleType: '5톤',\n vehicleNo: '34나 5678',\n driverName: '이운전',\n driverPhone: '010-3333-4444',\n scheduledArrival: '2025-03-01 10:00',\n confirmedArrival: '2025-03-01 10:30',\n shippingCost: 0,\n loadingCompleted: true,\n loadingCompletedAt: '2025-03-01 08:00',\n loadingWorker: '김상차',\n items: [\n { id: 1, productCode: 'SLT-001', productName: '슬랫 셔터', floor: '1층', location: 'D-01', spec: '4500×2000', qty: 2, lotNo: 'KD-TS-250225-01-01', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-02-25',\n createdBy: '판매1팀 이판매',\n completedAt: '2025-03-01 15:00',\n note: '2차 분할 배송완료 - 최종 출하',\n },\n // KD-TS-250115-01-A 출하 (0% - 출하준비)\n {\n id: 5,\n releaseNo: '250320-P-01', // 출고번호 (채번규칙: 날짜-P-순번)\n shipmentDate: '2025-03-20',\n lotNo: 'KD-TS-250115-01-A',\n splitNo: 'KD-TS-250115-01-A-01',\n customerName: '서울건축',\n siteName: '강남 오피스텔 A동',\n deliveryAddress: '서울시 강남구 테헤란로 123',\n receiverName: '김현장',\n receiverPhone: '010-9999-8888',\n status: '출하준비', // 아직 배송완료 아님 → 0%\n paymentConfirmed: false,\n taxInvoiceIssued: false,\n shipmentPriority: 1,\n isSplitShipment: true,\n splitOrder: 1,\n dispatchType: '상차',\n logisticsCompany: null,\n vehicleType: null,\n vehicleNo: null,\n driverName: null,\n driverPhone: null,\n scheduledArrival: '2025-03-20 09:00',\n confirmedArrival: null,\n shippingCost: 0,\n loadingCompleted: false,\n loadingCompletedAt: null,\n loadingWorker: null,\n items: [\n { id: 1, productCode: 'SCR-002', productName: '스크린 셔터 (프리미엄)', floor: '3층', location: 'C-01', spec: '8000×2800', qty: 3, lotNo: null, arrivedAtLoadingArea: false, loadingChecked: false, accessoryLotNo: '' },\n { id: 2, productCode: 'MTR-001', productName: '모터 세트', floor: '-', location: '-', spec: '-', qty: 2, lotNo: null, arrivedAtLoadingArea: false, loadingChecked: false, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-03-10',\n createdBy: '판매1팀 박판매',\n note: '추가분 출하 예정',\n },\n\n // ═══════════════════════════════════════════════════════════════════════════\n // E2E 테스트용 출하 데이터 (수주 101-105 연결)\n // ═══════════════════════════════════════════════════════════════════════════\n\n // SHP-E2E-01: 판교 물류센터 슬랫 (수주102 → 출하대기)\n {\n id: 101,\n releaseNo: '251228-P-01', // 출고번호 (채번규칙: 날짜-P-순번)\n shipmentDate: '2025-12-28',\n lotNo: 'KD-TS-251216-02',\n splitNo: 'KD-TS-251216-02-01',\n customerName: '현대건설(주)',\n siteName: '[E2E테스트] 판교 물류센터',\n deliveryAddress: '경기도 성남시 분당구 판교로 200',\n receiverName: '이현장',\n receiverPhone: '010-2345-6789',\n status: '출고대기',\n paymentConfirmed: true,\n taxInvoiceIssued: false,\n shipmentPriority: 1,\n isSplitShipment: false,\n splitOrder: 1,\n dispatchType: '상차',\n logisticsCompany: '경동로지스',\n vehicleType: '5톤',\n vehicleNo: null,\n driverName: null,\n driverPhone: null,\n scheduledArrival: '2025-12-28 14:00',\n confirmedArrival: null,\n shippingCost: 150000,\n loadingCompleted: false,\n loadingCompletedAt: null,\n loadingWorker: null,\n items: [\n { id: 1, productCode: 'STL-E2E-001', productName: '철재 슬랫 셔터', floor: 'B1', location: 'C-01', spec: '4000×3000', qty: 1, lotNo: 'KD-TS-E2E-02-01', arrivedAtLoadingArea: true, loadingChecked: false, accessoryLotNo: '' },\n { id: 2, productCode: 'STL-E2E-001', productName: '철재 슬랫 셔터', floor: 'B2', location: 'C-02', spec: '4000×3000', qty: 1, lotNo: 'KD-TS-E2E-02-02', arrivedAtLoadingArea: true, loadingChecked: false, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-18',\n createdBy: '[E2E] 시스템',\n note: '[E2E-102] 슬랫 공정 완료 → 출고대기 상태',\n },\n\n // SHP-E2E-02: 송도 아파트 스크린 (수주103-01 → 배송완료)\n {\n id: 102,\n releaseNo: '251223-P-01', // 출고번호 (채번규칙: 날짜-P-순번)\n shipmentDate: '2025-12-23',\n lotNo: 'KD-TS-251216-03',\n splitNo: 'KD-TS-251216-03-01',\n customerName: '대우건설(주)',\n siteName: '[E2E테스트] 송도 아파트 B동',\n deliveryAddress: '인천시 연수구 송도동 300',\n receiverName: '박현장',\n receiverPhone: '010-3456-7890',\n status: '배송완료',\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n shipmentPriority: 1,\n isSplitShipment: true,\n splitOrder: 1,\n dispatchType: '납품',\n logisticsCompany: '경동로지스',\n vehicleType: '5톤',\n vehicleNo: '서울 12가 3456',\n driverName: '김운송',\n driverPhone: '010-7777-8888',\n scheduledArrival: '2025-12-23 10:00',\n confirmedArrival: '2025-12-23 10:15',\n shippingCost: 200000,\n loadingCompleted: true,\n loadingCompletedAt: '2025-12-23 08:30',\n loadingWorker: '물류팀 이적재',\n items: [\n { id: 1, productCode: 'SCR-E2E-002', productName: '스크린 셔터 (대형)', floor: '1층', location: 'D-01', spec: '6000×4000', qty: 1, lotNo: 'KD-TS-E2E-03-01', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: 'ACC-E2E-01' },\n { id: 2, productCode: 'SCR-E2E-002', productName: '스크린 셔터 (표준형)', floor: '2층', location: 'D-03', spec: '3000×2500', qty: 1, lotNo: 'KD-TS-E2E-03-02', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: 'ACC-E2E-02' },\n ],\n changeHistory: [\n { id: 1, changedAt: '2025-12-23 10:15', changeType: '배송완료', description: '정상 배송완료', changedBy: '김운송' },\n ],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-21',\n createdBy: '[E2E] 시스템',\n note: '[E2E-103] 공정분리 스크린 1차 → 배송완료',\n },\n\n // SHP-E2E-03: 송도 아파트 슬랫 (수주103-02 → 배송완료)\n {\n id: 103,\n releaseNo: '251225-P-01', // 출고번호 (채번규칙: 날짜-P-순번)\n shipmentDate: '2025-12-25',\n lotNo: 'KD-TS-251216-03',\n splitNo: 'KD-TS-251216-03-02',\n customerName: '대우건설(주)',\n siteName: '[E2E테스트] 송도 아파트 B동',\n deliveryAddress: '인천시 연수구 송도동 300',\n receiverName: '박현장',\n receiverPhone: '010-3456-7890',\n status: '배송완료',\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n shipmentPriority: 2,\n isSplitShipment: true,\n splitOrder: 2,\n dispatchType: '납품',\n logisticsCompany: '경동로지스',\n vehicleType: '2.5톤',\n vehicleNo: '서울 34나 5678',\n driverName: '박운송',\n driverPhone: '010-6666-7777',\n scheduledArrival: '2025-12-25 14:00',\n confirmedArrival: '2025-12-25 14:30',\n shippingCost: 120000,\n loadingCompleted: true,\n loadingCompletedAt: '2025-12-25 12:00',\n loadingWorker: '물류팀 이적재',\n items: [\n { id: 1, productCode: 'STL-E2E-002', productName: '철재 슬랫 셔터', floor: '1층', location: 'D-02', spec: '3500×2500', qty: 2, lotNo: 'KD-TS-E2E-04-01', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: 'ACC-E2E-03' },\n ],\n changeHistory: [\n { id: 1, changedAt: '2025-12-25 14:30', changeType: '배송완료', description: '슬랫 2차분 배송완료', changedBy: '박운송' },\n ],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-20',\n createdBy: '[E2E] 시스템',\n note: '[E2E-103] 공정분리 슬랫 2차 → 배송완료',\n },\n\n // SHP-E2E-04: 삼성타운 1차 스크린 (수주105-01 → 배송완료)\n {\n id: 104,\n releaseNo: '251228-P-02', // 출고번호 (채번규칙: 날짜-P-순번)\n shipmentDate: '2025-12-28',\n lotNo: 'KD-TS-251216-05',\n splitNo: 'KD-TS-251216-05-01',\n customerName: '삼성물산(주)',\n siteName: '[E2E테스트] 삼성타운 종합',\n deliveryAddress: '서울시 서초구 서초대로 500',\n receiverName: '정현장',\n receiverPhone: '010-5678-9012',\n status: '배송완료',\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n shipmentPriority: 1,\n isSplitShipment: true,\n splitOrder: 1,\n dispatchType: '납품',\n logisticsCompany: '삼성물류',\n vehicleType: '5톤',\n vehicleNo: '서울 56다 7890',\n driverName: '최운송',\n driverPhone: '010-5555-6666',\n scheduledArrival: '2025-12-28 09:00',\n confirmedArrival: '2025-12-28 09:10',\n shippingCost: 180000,\n loadingCompleted: true,\n loadingCompletedAt: '2025-12-28 07:30',\n loadingWorker: '물류팀 박적재',\n items: [\n { id: 1, productCode: 'SCR-E2E-004', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'F-01', spec: '3500×2800', qty: 2, lotNo: 'KD-TS-E2E-06-01', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: 'ACC-E2E-04' },\n ],\n changeHistory: [\n { id: 1, changedAt: '2025-12-28 09:10', changeType: '배송완료', description: '1차 스크린 배송완료', changedBy: '최운송' },\n ],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-22',\n createdBy: '[E2E] 시스템',\n note: '[E2E-105] 삼성타운 3차분할 中 1차 스크린 → 배송완료',\n },\n\n // SHP-E2E-05: 삼성타운 2차 슬랫 (수주105-02 → 출고대기)\n {\n id: 105,\n releaseNo: '260105-P-01', // 출고번호 (채번규칙: 날짜-P-순번)\n shipmentDate: '2026-01-05',\n lotNo: 'KD-TS-251216-05',\n splitNo: 'KD-TS-251216-05-02',\n customerName: '삼성물산(주)',\n siteName: '[E2E테스트] 삼성타운 종합',\n deliveryAddress: '서울시 서초구 서초대로 500',\n receiverName: '정현장',\n receiverPhone: '010-5678-9012',\n status: '출고대기',\n paymentConfirmed: true,\n taxInvoiceIssued: false,\n shipmentPriority: 2,\n isSplitShipment: true,\n splitOrder: 2,\n dispatchType: '상차',\n logisticsCompany: '삼성물류',\n vehicleType: '5톤',\n vehicleNo: null,\n driverName: null,\n driverPhone: null,\n scheduledArrival: '2026-01-05 10:00',\n confirmedArrival: null,\n shippingCost: 180000,\n loadingCompleted: false,\n loadingCompletedAt: null,\n loadingWorker: null,\n items: [\n { id: 1, productCode: 'STL-E2E-003', productName: '철재 슬랫 셔터', floor: '2층', location: 'F-02', spec: '4500×3200', qty: 2, lotNo: 'KD-TS-E2E-07-01', arrivedAtLoadingArea: true, loadingChecked: false, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-24',\n createdBy: '[E2E] 시스템',\n note: '[E2E-105] 삼성타운 3차분할 中 2차 슬랫 → 출고대기 (작업완료, 배차대기)',\n },\n\n // ═══════════════════════════════════════════════════════════════════════════\n // 전체 워크플로우 통합 테스트 출하 데이터 10건 (ID: 301-310)\n // 품질검사(IQC/PQC/FQC-YYMMDD-XX) → 출하(SL-YYMMDD-XX) → 회계 연동\n // ═══════════════════════════════════════════════════════════════════════════\n\n // [통합테스트 1] SL-251221-01: 삼성물산 - 래미안 강남 프레스티지 (스크린 3대) - 배송완료\n {\n id: 301,\n releaseNo: 'SL-251221-01',\n shipmentDate: '2025-12-21',\n lotNo: 'KD-TS-251217-01',\n splitNo: 'KD-TS-251217-01-01',\n orderNo: 'KD-TS-251217-01',\n workOrderNo: 'KD-WO-251217-01',\n inspectionNo: 'IQC-251220-01',\n customerName: '삼성물산(주)',\n siteName: '래미안 강남 프레스티지',\n deliveryAddress: '서울시 강남구 테헤란로 123',\n receiverName: '박현장',\n receiverPhone: '010-1234-5678',\n status: '배송완료',\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n shipmentPriority: 1,\n isSplitShipment: false,\n splitOrder: 1,\n dispatchType: '상차',\n logisticsCompany: '한진물류',\n vehicleType: '5톤',\n vehicleNo: '12가 3456',\n driverName: '홍운전',\n driverPhone: '010-1111-2222',\n scheduledArrival: '2025-12-21 09:00',\n confirmedArrival: '2025-12-21 09:15',\n shippingCost: 180000,\n loadingCompleted: true,\n loadingCompletedAt: '2025-12-21 07:30',\n loadingWorker: '김상차',\n items: [\n { id: 1, productCode: 'SH3025', productName: '스크린 셔터 (와이어)', floor: '1층', location: 'A-01', spec: '3000×2500', qty: 1, lotNo: 'LOT-20251218-001', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n { id: 2, productCode: 'SH3025', productName: '스크린 셔터 (와이어)', floor: '1층', location: 'A-02', spec: '3000×2500', qty: 1, lotNo: 'LOT-20251218-002', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n { id: 3, productCode: 'SH2820', productName: '스크린 셔터 (와이어)', floor: '2층', location: 'B-01', spec: '2800×2000', qty: 1, lotNo: 'LOT-20251218-003', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-20',\n createdBy: '판매1팀 김판매',\n completedAt: '2025-12-21 14:00',\n note: '[통합테스트1] 삼성물산 - 스크린 3대 배송완료',\n },\n\n // [통합테스트 2] SL-251221-02: 현대건설 - 힐스테이트 판교 (스크린 2대) - 배송완료\n {\n id: 302,\n releaseNo: 'SL-251221-02',\n shipmentDate: '2025-12-21',\n lotNo: 'KD-TS-251217-02',\n splitNo: 'KD-TS-251217-02-01',\n orderNo: 'KD-TS-251217-02',\n workOrderNo: 'KD-WO-251217-02',\n inspectionNo: 'IQC-251220-02',\n customerName: '현대건설(주)',\n siteName: '힐스테이트 판교',\n deliveryAddress: '경기도 성남시 분당구 판교로 200',\n receiverName: '이현장',\n receiverPhone: '010-2345-6789',\n status: '배송완료',\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n shipmentPriority: 1,\n isSplitShipment: true,\n splitOrder: 1,\n dispatchType: '직접배차',\n logisticsCompany: null,\n vehicleType: '5톤',\n vehicleNo: '34나 5678',\n driverName: '이운전',\n driverPhone: '010-3333-4444',\n scheduledArrival: '2025-12-21 14:00',\n confirmedArrival: '2025-12-21 14:20',\n shippingCost: 0,\n loadingCompleted: true,\n loadingCompletedAt: '2025-12-21 12:00',\n loadingWorker: '박상차',\n items: [\n { id: 1, productCode: 'SH4030', productName: '스크린 셔터 (튜블러)', floor: 'B1', location: 'C-01', spec: '4000×3000', qty: 1, lotNo: 'LOT-20251219-001', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n { id: 2, productCode: 'SH4030', productName: '스크린 셔터 (튜블러)', floor: 'B1', location: 'C-02', spec: '4000×3000', qty: 1, lotNo: 'LOT-20251219-002', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-20',\n createdBy: '판매1팀 이판매',\n completedAt: '2025-12-21 18:00',\n note: '[통합테스트2] 현대건설 - 스크린 1차분 2대 배송완료',\n },\n\n // [통합테스트 3] SL-251223-01: 현대건설 - 힐스테이트 판교 (슬랫 2대) - 출고대기\n {\n id: 303,\n releaseNo: 'SL-251223-01',\n shipmentDate: '2025-12-23',\n lotNo: 'KD-TS-251217-02',\n splitNo: 'KD-TS-251217-02-02',\n orderNo: 'KD-TS-251217-02',\n workOrderNo: 'KD-WO-251217-03',\n inspectionNo: 'PQC-251219-01',\n customerName: '현대건설(주)',\n siteName: '힐스테이트 판교',\n deliveryAddress: '경기도 성남시 분당구 판교로 200',\n receiverName: '이현장',\n receiverPhone: '010-2345-6789',\n status: '출고대기',\n paymentConfirmed: false,\n taxInvoiceIssued: false,\n shipmentPriority: 2,\n isSplitShipment: true,\n splitOrder: 2,\n dispatchType: '상차',\n logisticsCompany: '경동로지스',\n vehicleType: '5톤',\n vehicleNo: null,\n driverName: null,\n driverPhone: null,\n scheduledArrival: '2025-12-23 10:00',\n confirmedArrival: null,\n shippingCost: 150000,\n loadingCompleted: false,\n loadingCompletedAt: null,\n loadingWorker: null,\n items: [\n { id: 1, productCode: 'SL3525', productName: '슬랫 셔터', floor: '1층', location: 'D-01', spec: '3500×2500', qty: 1, lotNo: 'LOT-20251219-003', arrivedAtLoadingArea: true, loadingChecked: false, accessoryLotNo: '' },\n { id: 2, productCode: 'SL3525', productName: '슬랫 셔터', floor: '1층', location: 'D-02', spec: '3500×2500', qty: 1, lotNo: 'LOT-20251219-004', arrivedAtLoadingArea: true, loadingChecked: false, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-22',\n createdBy: '판매1팀 이판매',\n note: '[통합테스트3] 현대건설 - 슬랫 2차분 2대 출고대기 (검사 진행중)',\n },\n\n // [통합테스트 4] SL-251222-01: 대우건설 - 푸르지오 일산 (대형 스크린 2대) - 배송완료\n {\n id: 304,\n releaseNo: 'SL-251222-01',\n shipmentDate: '2025-12-22',\n lotNo: 'KD-TS-251217-03',\n splitNo: 'KD-TS-251217-03-01',\n orderNo: 'KD-TS-251217-03',\n workOrderNo: 'KD-WO-251217-04',\n inspectionNo: 'IQC-251221-01',\n customerName: '대우건설(주)',\n siteName: '푸르지오 일산',\n deliveryAddress: '경기도 고양시 일산동구 중앙로 1000',\n receiverName: '최현장',\n receiverPhone: '010-3456-7890',\n status: '배송완료',\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n shipmentPriority: 1,\n isSplitShipment: false,\n splitOrder: 1,\n dispatchType: '상차',\n logisticsCompany: '대한통운',\n vehicleType: '11톤',\n vehicleNo: '56다 7890',\n driverName: '박운전',\n driverPhone: '010-5555-6666',\n scheduledArrival: '2025-12-22 09:00',\n confirmedArrival: '2025-12-22 09:30',\n shippingCost: 250000,\n loadingCompleted: true,\n loadingCompletedAt: '2025-12-22 07:00',\n loadingWorker: '김상차',\n items: [\n { id: 1, productCode: 'SH6035', productName: '스크린 셔터 (대형)', floor: 'B2', location: 'E-01', spec: '6000×3500', qty: 1, lotNo: 'LOT-20251220-001', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n { id: 2, productCode: 'SH6035', productName: '스크린 셔터 (대형)', floor: 'B2', location: 'E-02', spec: '6000×3500', qty: 1, lotNo: 'LOT-20251220-002', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-21',\n createdBy: '판매1팀 박판매',\n completedAt: '2025-12-22 15:00',\n note: '[통합테스트4] 대우건설 - 대형 스크린 2대 배송완료 (11톤 차량)',\n },\n\n // [통합테스트 5] SL-251222-02: GS건설 - 자이 위례 (슬랫 4대) - 배송완료\n {\n id: 305,\n releaseNo: 'SL-251222-02',\n shipmentDate: '2025-12-22',\n lotNo: 'KD-TS-251217-04',\n splitNo: 'KD-TS-251217-04-01',\n orderNo: 'KD-TS-251217-04',\n workOrderNo: 'KD-WO-251217-05',\n inspectionNo: 'PQC-251220-01',\n customerName: 'GS건설(주)',\n siteName: '자이 위례',\n deliveryAddress: '경기도 성남시 수정구 위례중앙로 50',\n receiverName: '정현장',\n receiverPhone: '010-4567-8901',\n status: '배송완료',\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n shipmentPriority: 1,\n isSplitShipment: false,\n splitOrder: 1,\n dispatchType: '직접배차',\n logisticsCompany: null,\n vehicleType: '5톤',\n vehicleNo: '78라 9012',\n driverName: '최운전',\n driverPhone: '010-7777-8888',\n scheduledArrival: '2025-12-22 14:00',\n confirmedArrival: '2025-12-22 14:10',\n shippingCost: 0,\n loadingCompleted: true,\n loadingCompletedAt: '2025-12-22 12:00',\n loadingWorker: '이상차',\n items: [\n { id: 1, productCode: 'SL2520', productName: '슬랫 셔터', floor: '1층', location: 'F-01', spec: '2500×2000', qty: 1, lotNo: 'LOT-20251219-005', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n { id: 2, productCode: 'SL2520', productName: '슬랫 셔터', floor: '1층', location: 'F-02', spec: '2500×2000', qty: 1, lotNo: 'LOT-20251219-006', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n { id: 3, productCode: 'SL3020', productName: '슬랫 셔터', floor: '2층', location: 'G-01', spec: '3000×2000', qty: 1, lotNo: 'LOT-20251219-007', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n { id: 4, productCode: 'SL3020', productName: '슬랫 셔터', floor: '2층', location: 'G-02', spec: '3000×2000', qty: 1, lotNo: 'LOT-20251219-008', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-21',\n createdBy: '판매1팀 최판매',\n completedAt: '2025-12-22 18:00',\n note: '[통합테스트5] GS건설 - 슬랫 4대 배송완료',\n },\n\n // [통합테스트 6] SL-251223-02: 포스코건설 - 더샵 송도 (코너형 스크린 2대) - 배송중\n {\n id: 306,\n releaseNo: 'SL-251223-02',\n shipmentDate: '2025-12-23',\n lotNo: 'KD-TS-251217-05',\n splitNo: 'KD-TS-251217-05-01',\n orderNo: 'KD-TS-251217-05',\n workOrderNo: 'KD-WO-251217-06',\n inspectionNo: 'IQC-251222-01',\n customerName: '포스코건설(주)',\n siteName: '더샵 송도',\n deliveryAddress: '인천시 연수구 송도동 123',\n receiverName: '한현장',\n receiverPhone: '010-5678-9012',\n status: '배송중',\n paymentConfirmed: false,\n taxInvoiceIssued: false,\n shipmentPriority: 1,\n isSplitShipment: false,\n splitOrder: 1,\n dispatchType: '상차',\n logisticsCompany: '한진물류',\n vehicleType: '5톤',\n vehicleNo: '90마 1234',\n driverName: '김운전',\n driverPhone: '010-9999-0000',\n scheduledArrival: '2025-12-23 11:00',\n confirmedArrival: null,\n shippingCost: 200000,\n loadingCompleted: true,\n loadingCompletedAt: '2025-12-23 08:00',\n loadingWorker: '박상차',\n items: [\n { id: 1, productCode: 'SH3530C', productName: '스크린 셔터 (코너형)', floor: '1층', location: 'H-01', spec: '3500×3000', qty: 1, lotNo: 'LOT-20251221-001', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n { id: 2, productCode: 'SH3530C', productName: '스크린 셔터 (코너형)', floor: '1층', location: 'H-02', spec: '3500×3000', qty: 1, lotNo: 'LOT-20251221-002', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-22',\n createdBy: '판매1팀 한판매',\n note: '[통합테스트6] 포스코건설 - 코너형 스크린 2대 배송중',\n },\n\n // [통합테스트 7] SL-251221-03: 호반건설 - 써밋 광교 1차분 (스크린 2대) - 배송완료\n {\n id: 307,\n releaseNo: 'SL-251221-03',\n shipmentDate: '2025-12-21',\n lotNo: 'KD-TS-251217-06',\n splitNo: 'KD-TS-251217-06-01',\n orderNo: 'KD-TS-251217-06',\n workOrderNo: 'KD-WO-251217-07',\n inspectionNo: 'IQC-251220-03',\n customerName: '호반건설(주)',\n siteName: '써밋 광교',\n deliveryAddress: '경기도 수원시 영통구 광교로 100',\n receiverName: '오현장',\n receiverPhone: '010-6789-0123',\n status: '배송완료',\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n shipmentPriority: 1,\n isSplitShipment: true,\n splitOrder: 1,\n dispatchType: '직접배차',\n logisticsCompany: null,\n vehicleType: '3.5톤',\n vehicleNo: '12바 3456',\n driverName: '이운전',\n driverPhone: '010-1212-3434',\n scheduledArrival: '2025-12-21 10:00',\n confirmedArrival: '2025-12-21 10:30',\n shippingCost: 0,\n loadingCompleted: true,\n loadingCompletedAt: '2025-12-21 08:00',\n loadingWorker: '최상차',\n items: [\n { id: 1, productCode: 'SH3228', productName: '스크린 셔터 (와이어)', floor: 'B1', location: 'I-01', spec: '3200×2800', qty: 1, lotNo: 'LOT-20251218-004', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n { id: 2, productCode: 'SH3228', productName: '스크린 셔터 (와이어)', floor: 'B1', location: 'I-02', spec: '3200×2800', qty: 1, lotNo: 'LOT-20251218-005', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-20',\n createdBy: '판매1팀 오판매',\n completedAt: '2025-12-21 15:00',\n note: '[통합테스트7] 호반건설 - 1차 분할 출하 완료 (3차 분할 중 1차)',\n },\n\n // [통합테스트 8] SL-251224-01: 호반건설 - 써밋 광교 2차분 (스크린 2대) - 출고지시\n {\n id: 308,\n releaseNo: 'SL-251224-01',\n shipmentDate: '2025-12-24',\n lotNo: 'KD-TS-251217-06',\n splitNo: 'KD-TS-251217-06-02',\n orderNo: 'KD-TS-251217-06',\n workOrderNo: 'KD-WO-251217-08',\n inspectionNo: 'IQC-251223-01',\n customerName: '호반건설(주)',\n siteName: '써밋 광교',\n deliveryAddress: '경기도 수원시 영통구 광교로 100',\n receiverName: '오현장',\n receiverPhone: '010-6789-0123',\n status: '출고지시',\n paymentConfirmed: false,\n taxInvoiceIssued: false,\n shipmentPriority: 2,\n isSplitShipment: true,\n splitOrder: 2,\n dispatchType: '상차',\n logisticsCompany: '경동로지스',\n vehicleType: '3.5톤',\n vehicleNo: null,\n driverName: null,\n driverPhone: null,\n scheduledArrival: '2025-12-24 10:00',\n confirmedArrival: null,\n shippingCost: 120000,\n loadingCompleted: false,\n loadingCompletedAt: null,\n loadingWorker: null,\n items: [\n { id: 1, productCode: 'SH3228', productName: '스크린 셔터 (와이어)', floor: 'B1', location: 'I-03', spec: '3200×2800', qty: 1, lotNo: 'LOT-20251222-001', arrivedAtLoadingArea: true, loadingChecked: false, accessoryLotNo: '' },\n { id: 2, productCode: 'SH3228', productName: '스크린 셔터 (와이어)', floor: '1층', location: 'J-01', spec: '3200×2800', qty: 1, lotNo: 'LOT-20251222-002', arrivedAtLoadingArea: true, loadingChecked: false, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-23',\n createdBy: '판매1팀 오판매',\n note: '[통합테스트8] 호반건설 - 2차 분할 출고지시 (3차 분할 중 2차)',\n },\n\n // [통합테스트 9] 태영건설 - 데시앙 동탄 - 품질불량으로 출하 보류\n {\n id: 309,\n releaseNo: 'SL-251222-03',\n shipmentDate: '2025-12-22',\n lotNo: 'KD-TS-251217-09',\n splitNo: 'KD-TS-251217-09-01',\n orderNo: 'KD-TS-251217-09',\n workOrderNo: 'KD-WO-251217-11',\n inspectionNo: 'IQC-251221-02',\n customerName: '태영건설(주)',\n siteName: '데시앙 동탄',\n deliveryAddress: '경기도 화성시 동탄순환대로 100',\n receiverName: '강현장',\n receiverPhone: '010-7890-1234',\n status: '출하보류',\n paymentConfirmed: false,\n taxInvoiceIssued: false,\n shipmentPriority: 1,\n isSplitShipment: false,\n splitOrder: 1,\n dispatchType: '상차',\n logisticsCompany: '한진물류',\n vehicleType: '5톤',\n vehicleNo: null,\n driverName: null,\n driverPhone: null,\n scheduledArrival: '2025-12-22 14:00',\n confirmedArrival: null,\n shippingCost: 0,\n loadingCompleted: false,\n loadingCompletedAt: null,\n loadingWorker: null,\n items: [\n { id: 1, productCode: 'SH2822', productName: '스크린 셔터 (와이어)', floor: 'B1', location: 'K-01', spec: '2800×2200', qty: 1, lotNo: 'LOT-20251220-003', arrivedAtLoadingArea: false, loadingChecked: false, accessoryLotNo: '' },\n { id: 2, productCode: 'SH2822', productName: '스크린 셔터 (와이어)', floor: 'B1', location: 'K-02', spec: '2800×2200', qty: 1, lotNo: 'LOT-20251220-004', arrivedAtLoadingArea: false, loadingChecked: false, accessoryLotNo: '' },\n { id: 3, productCode: 'SH2822', productName: '스크린 셔터 (와이어)', floor: '1층', location: 'L-01', spec: '2800×2200', qty: 1, lotNo: 'LOT-20251220-005', arrivedAtLoadingArea: false, loadingChecked: false, accessoryLotNo: '' },\n ],\n changeHistory: [\n { id: 1, changedAt: '2025-12-21 17:30', changeType: '상태 변경', beforeValue: '출고지시', afterValue: '출하보류', reason: '품질검사 불합격 (NCR-251221-01)', changedBy: '품질팀 이검사' },\n ],\n cancelReason: '품질검사 불합격 - NCR-251221-01 발행, 재작업 후 재검사 필요',\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-20',\n createdBy: '판매1팀 강판매',\n note: '[통합테스트9] 태영건설 - 품질불량으로 출하 보류 (NCR 발행, 재작업 진행중)',\n },\n\n // [통합테스트 10] SL-251220-01: 두산건설 - 위브 청라 (스크린 1대) - 배송완료 (전체 플로우 완료)\n {\n id: 310,\n releaseNo: 'SL-251220-01',\n shipmentDate: '2025-12-20',\n lotNo: 'KD-TS-251217-10',\n splitNo: 'KD-TS-251217-10-01',\n orderNo: 'KD-TS-251217-10',\n workOrderNo: 'KD-WO-251217-12',\n inspectionNo: 'FQC-251219-01',\n customerName: '두산건설(주)',\n siteName: '위브 청라',\n deliveryAddress: '인천시 서구 청라동 456',\n receiverName: '임현장',\n receiverPhone: '010-8901-2345',\n status: '배송완료',\n paymentConfirmed: true,\n taxInvoiceIssued: true,\n shipmentPriority: 1,\n isSplitShipment: false,\n splitOrder: 1,\n dispatchType: '직접배차',\n logisticsCompany: null,\n vehicleType: '3.5톤',\n vehicleNo: '34사 5678',\n driverName: '정운전',\n driverPhone: '010-5656-7878',\n scheduledArrival: '2025-12-20 10:00',\n confirmedArrival: '2025-12-20 10:15',\n shippingCost: 0,\n loadingCompleted: true,\n loadingCompletedAt: '2025-12-20 08:30',\n loadingWorker: '김상차',\n items: [\n { id: 1, productCode: 'SH3225', productName: '스크린 셔터 (와이어)', floor: '1층', location: 'M-01', spec: '3200×2500', qty: 1, lotNo: 'LOT-20251217-001', arrivedAtLoadingArea: true, loadingChecked: true, accessoryLotNo: '' },\n ],\n changeHistory: [],\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: '2025-12-19',\n createdBy: '판매1팀 임판매',\n completedAt: '2025-12-20 14:00',\n note: '[통합테스트10] 두산건설 - 전체 플로우 완료 (견적→수주→생산→품질→출하→회계)',\n },\n];\n\n// 품질검사 (제품검사)\n// 제품검사LOT: KD-SA-YYMMDD-순번 (설치LOT와 동일)\nconst initialProductInspections = [\n {\n id: 1,\n inspectionNo: 'KD-SA-250216-01', // 제품검사LOT: KD-SA-YYMMDD-순번\n requestDate: '2025-02-16',\n requestedBy: '서울건축 김대리',\n status: '검사예정', // 검사신청 → 검사예정 → 검사중 → 합격/불합격 → 결재완료\n // 연결 정보 (로트번호 기준)\n lotNo: 'KD-TS-250115-01', // 로트번호 (추적 기준)\n splitNo: 'KD-TS-250115-01-01', // 분할번호\n customerName: '서울건축',\n siteName: '강남 오피스텔 A동',\n // 세트로트 (여러 출하 로트 묶음) - 설치LOT = 제품검사LOT\n setLotNo: 'KD-SA-250216-01', // 설치LOT는 제품검사LOT와 동일\n lotNos: ['KD-TS-250210-01-01', 'KD-TS-250210-01-02'], // 완제품LOT 목록\n // 검사 정보\n inspector: '품질팀 이검사',\n scheduledDate: '2025-02-20',\n scheduledTime: '10:00',\n // 개소별 검사 (50개 설치 → 50장 검사서)\n totalItems: 2,\n completedItems: 0,\n passedItems: 0,\n failedItems: 0,\n items: [\n { id: 1, floor: '1층', location: 'A-01', productName: '스크린 셔터 (표준형)', orderSpec: '7660×2550', productLot: 'KD-TS-250210-01-01', installedSpec: null, specMatch: null, result: null, inspectedAt: null, note: '' },\n { id: 2, floor: '1층', location: 'A-02', productName: '스크린 셔터 (표준형)', orderSpec: '6500×2400', productLot: 'KD-TS-250210-01-02', installedSpec: null, specMatch: null, result: null, inspectedAt: null, note: '' },\n ],\n // 승인 정보\n approvalStatus: '대기', // 대기 → 결재대기 → 결재완료\n approvedBy: null,\n approvedAt: null,\n // 변경 이력\n scheduleHistory: [],\n createdAt: '2025-02-16',\n createdBy: '품질팀 이검사',\n },\n {\n id: 2,\n inspectionNo: 'KD-SA-250218-01',\n requestDate: '2025-02-18',\n requestedBy: '인천건설 박과장',\n status: '검사신청',\n lotNo: 'KD-TS-250118-01', // 로트번호\n splitNo: null,\n customerName: '인천건설',\n siteName: '송도 오피스텔 B동',\n setLotNo: null,\n lotNos: [],\n inspector: null,\n scheduledDate: null,\n scheduledTime: null,\n totalItems: 4,\n completedItems: 0,\n passedItems: 0,\n failedItems: 0,\n items: [\n { id: 1, floor: 'B1', location: 'C-01', productName: '스크린 셔터 (표준형)', orderSpec: '6000×2400', productLot: null, installedSpec: null, specMatch: null, result: null, inspectedAt: null, note: '' },\n { id: 2, floor: 'B1', location: 'C-01', productName: '스크린 셔터 (표준형)', orderSpec: '6000×2400', productLot: null, installedSpec: null, specMatch: null, result: null, inspectedAt: null, note: '' },\n { id: 3, floor: '1층', location: 'D-01', productName: '슬랫 셔터', orderSpec: '4500×2000', productLot: null, installedSpec: null, specMatch: null, result: null, inspectedAt: null, note: '' },\n { id: 4, floor: '1층', location: 'D-01', productName: '슬랫 셔터', orderSpec: '4500×2000', productLot: null, installedSpec: null, specMatch: null, result: null, inspectedAt: null, note: '' },\n ],\n approvalStatus: '대기',\n approvedBy: null,\n approvedAt: null,\n scheduleHistory: [],\n createdAt: '2025-02-18',\n createdBy: '품질팀 최품질',\n },\n\n // ═══════════════════════════════════════════════════════════════════════════\n // 전체 워크플로우 통합 테스트 품질검사 데이터 10건 (ID: 301-310)\n // 작업지시(WO-251217-XX) → 품질검사(IQC/PQC-YYMMDD-XX) → 출하 연동\n // ═══════════════════════════════════════════════════════════════════════════\n\n // [통합테스트 1] IQC-251220-01: 삼성물산 - 래미안 강남 프레스티지 (스크린 3대) - 합격\n {\n id: 301,\n inspectionNo: 'IQC-251220-01',\n requestDate: '2025-12-20',\n requestedBy: '생산팀 김스크린',\n status: '검사완료',\n inspectionType: '중간검사',\n workOrderNo: 'KD-WO-251217-01',\n orderNo: 'KD-TS-251217-01',\n lotNo: 'KD-TS-251217-01',\n splitNo: 'KD-TS-251217-01-01',\n customerName: '삼성물산(주)',\n siteName: '래미안 강남 프레스티지',\n setLotNo: 'IQC-251220-01',\n lotNos: ['LOT-20251218-001', 'LOT-20251218-002', 'LOT-20251218-003'],\n inspector: '품질팀 이검사',\n scheduledDate: '2025-12-20',\n scheduledTime: '14:00',\n totalItems: 3,\n completedItems: 3,\n passedItems: 3,\n failedItems: 0,\n items: [\n { id: 1, floor: '1층', location: 'A-01', productName: '스크린 셔터 (와이어)', productCode: 'SH3025', orderSpec: '3000×2500', productLot: 'LOT-20251218-001', installedSpec: '3000×2500', specMatch: true, result: '합격', inspectedAt: '2025-12-20 14:30', note: '' },\n { id: 2, floor: '1층', location: 'A-02', productName: '스크린 셔터 (와이어)', productCode: 'SH3025', orderSpec: '3000×2500', productLot: 'LOT-20251218-002', installedSpec: '3000×2500', specMatch: true, result: '합격', inspectedAt: '2025-12-20 15:00', note: '' },\n { id: 3, floor: '2층', location: 'B-01', productName: '스크린 셔터 (와이어)', productCode: 'SH2820', orderSpec: '2800×2000', productLot: 'LOT-20251218-003', installedSpec: '2800×2000', specMatch: true, result: '합격', inspectedAt: '2025-12-20 15:30', note: '' },\n ],\n inspectionItems: [\n { id: 1, itemName: '외관검사', standard: '찍힘, 스크래치 없음', result: '합격', note: '' },\n { id: 2, itemName: '치수검사', standard: '±5mm 이내', result: '합격', note: '모든 규격 공차 이내' },\n { id: 3, itemName: '작동검사', standard: '원활한 개폐 동작', result: '합격', note: '' },\n { id: 4, itemName: '안전장치', standard: '급정지 기능 정상', result: '합격', note: '' },\n ],\n approvalStatus: '결재완료',\n approvedBy: '품질팀장 박품질',\n approvedAt: '2025-12-20 16:00',\n scheduleHistory: [],\n createdAt: '2025-12-20',\n createdBy: '품질팀 이검사',\n },\n\n // [통합테스트 2] IQC-251220-02: 현대건설 - 힐스테이트 판교 (스크린 2대) - 합격\n {\n id: 302,\n inspectionNo: 'IQC-251220-02',\n requestDate: '2025-12-20',\n requestedBy: '생산팀 이스크린',\n status: '검사완료',\n inspectionType: '중간검사',\n workOrderNo: 'KD-WO-251217-02',\n orderNo: 'KD-TS-251217-02',\n lotNo: 'KD-TS-251217-02',\n splitNo: 'KD-TS-251217-02-01',\n customerName: '현대건설(주)',\n siteName: '힐스테이트 판교',\n setLotNo: 'IQC-251220-02',\n lotNos: ['LOT-20251219-001', 'LOT-20251219-002'],\n inspector: '품질팀 이검사',\n scheduledDate: '2025-12-20',\n scheduledTime: '16:00',\n totalItems: 2,\n completedItems: 2,\n passedItems: 2,\n failedItems: 0,\n items: [\n { id: 1, floor: 'B1', location: 'C-01', productName: '스크린 셔터 (튜블러)', productCode: 'SH4030', orderSpec: '4000×3000', productLot: 'LOT-20251219-001', installedSpec: '4000×3000', specMatch: true, result: '합격', inspectedAt: '2025-12-20 16:30', note: '' },\n { id: 2, floor: 'B1', location: 'C-02', productName: '스크린 셔터 (튜블러)', productCode: 'SH4030', orderSpec: '4000×3000', productLot: 'LOT-20251219-002', installedSpec: '4000×3000', specMatch: true, result: '합격', inspectedAt: '2025-12-20 17:00', note: '' },\n ],\n inspectionItems: [\n { id: 1, itemName: '외관검사', standard: '찍힘, 스크래치 없음', result: '합격', note: '' },\n { id: 2, itemName: '치수검사', standard: '±5mm 이내', result: '합격', note: '' },\n { id: 3, itemName: '작동검사', standard: '원활한 개폐 동작', result: '합격', note: '' },\n ],\n approvalStatus: '결재완료',\n approvedBy: '품질팀장 박품질',\n approvedAt: '2025-12-20 17:30',\n scheduleHistory: [],\n createdAt: '2025-12-20',\n createdBy: '품질팀 이검사',\n },\n\n // [통합테스트 3] PQC-251219-01: 현대건설 - 힐스테이트 판교 (슬랫 2대) - 공정검사 진행중\n {\n id: 303,\n inspectionNo: 'PQC-251219-01',\n requestDate: '2025-12-19',\n requestedBy: '생산팀 박슬랫',\n status: '검사중',\n inspectionType: '공정검사',\n workOrderNo: 'KD-WO-251217-03',\n orderNo: 'KD-TS-251217-02',\n lotNo: 'KD-TS-251217-02',\n splitNo: 'KD-TS-251217-02-02',\n customerName: '현대건설(주)',\n siteName: '힐스테이트 판교',\n setLotNo: 'PQC-251219-01',\n lotNos: ['LOT-20251219-003', 'LOT-20251219-004'],\n inspector: '품질팀 최품질',\n scheduledDate: '2025-12-22',\n scheduledTime: '10:00',\n totalItems: 2,\n completedItems: 1,\n passedItems: 1,\n failedItems: 0,\n items: [\n { id: 1, floor: '1층', location: 'D-01', productName: '슬랫 셔터', productCode: 'SL3525', orderSpec: '3500×2500', productLot: 'LOT-20251219-003', installedSpec: '3500×2500', specMatch: true, result: '합격', inspectedAt: '2025-12-22 10:30', note: '' },\n { id: 2, floor: '1층', location: 'D-02', productName: '슬랫 셔터', productCode: 'SL3525', orderSpec: '3500×2500', productLot: 'LOT-20251219-004', installedSpec: null, specMatch: null, result: null, inspectedAt: null, note: '' },\n ],\n inspectionItems: [\n { id: 1, itemName: '외관검사', standard: '도장 상태 양호', result: '합격', note: '' },\n { id: 2, itemName: '치수검사', standard: '±5mm 이내', result: '합격', note: '' },\n { id: 3, itemName: '작동검사', standard: '원활한 개폐 동작', result: null, note: '' },\n ],\n approvalStatus: '대기',\n approvedBy: null,\n approvedAt: null,\n scheduleHistory: [],\n createdAt: '2025-12-19',\n createdBy: '품질팀 최품질',\n },\n\n // [통합테스트 4] IQC-251221-01: 대우건설 - 푸르지오 일산 (스크린 2대) - 합격\n {\n id: 304,\n inspectionNo: 'IQC-251221-01',\n requestDate: '2025-12-21',\n requestedBy: '생산팀 김스크린',\n status: '검사완료',\n inspectionType: '중간검사',\n workOrderNo: 'KD-WO-251217-04',\n orderNo: 'KD-TS-251217-03',\n lotNo: 'KD-TS-251217-03',\n splitNo: 'KD-TS-251217-03-01',\n customerName: '대우건설(주)',\n siteName: '푸르지오 일산',\n setLotNo: 'IQC-251221-01',\n lotNos: ['LOT-20251220-001', 'LOT-20251220-002'],\n inspector: '품질팀 이검사',\n scheduledDate: '2025-12-21',\n scheduledTime: '09:00',\n totalItems: 2,\n completedItems: 2,\n passedItems: 2,\n failedItems: 0,\n items: [\n { id: 1, floor: 'B2', location: 'E-01', productName: '스크린 셔터 (대형)', productCode: 'SH6035', orderSpec: '6000×3500', productLot: 'LOT-20251220-001', installedSpec: '6000×3500', specMatch: true, result: '합격', inspectedAt: '2025-12-21 09:30', note: '대형 규격 특별검사 완료' },\n { id: 2, floor: 'B2', location: 'E-02', productName: '스크린 셔터 (대형)', productCode: 'SH6035', orderSpec: '6000×3500', productLot: 'LOT-20251220-002', installedSpec: '6000×3500', specMatch: true, result: '합격', inspectedAt: '2025-12-21 10:00', note: '' },\n ],\n inspectionItems: [\n { id: 1, itemName: '외관검사', standard: '찍힘, 스크래치 없음', result: '합격', note: '' },\n { id: 2, itemName: '치수검사', standard: '±5mm 이내', result: '합격', note: '대형 규격 공차 확인' },\n { id: 3, itemName: '작동검사', standard: '원활한 개폐 동작', result: '합격', note: '' },\n { id: 4, itemName: '하중검사', standard: '정격하중 견딤', result: '합격', note: '500kg 모터 성능 확인' },\n ],\n approvalStatus: '결재완료',\n approvedBy: '품질팀장 박품질',\n approvedAt: '2025-12-21 11:00',\n scheduleHistory: [],\n createdAt: '2025-12-21',\n createdBy: '품질팀 이검사',\n },\n\n // [통합테스트 5] PQC-251220-01: GS건설 - 자이 위례 (슬랫 4대) - 합격\n {\n id: 305,\n inspectionNo: 'PQC-251220-01',\n requestDate: '2025-12-20',\n requestedBy: '생산팀 박슬랫',\n status: '검사완료',\n inspectionType: '공정검사',\n workOrderNo: 'KD-WO-251217-05',\n orderNo: 'KD-TS-251217-04',\n lotNo: 'KD-TS-251217-04',\n splitNo: 'KD-TS-251217-04-01',\n customerName: 'GS건설(주)',\n siteName: '자이 위례',\n setLotNo: 'PQC-251220-01',\n lotNos: ['LOT-20251219-005', 'LOT-20251219-006', 'LOT-20251219-007', 'LOT-20251219-008'],\n inspector: '품질팀 최품질',\n scheduledDate: '2025-12-21',\n scheduledTime: '14:00',\n totalItems: 4,\n completedItems: 4,\n passedItems: 4,\n failedItems: 0,\n items: [\n { id: 1, floor: '1층', location: 'F-01', productName: '슬랫 셔터', productCode: 'SL2520', orderSpec: '2500×2000', productLot: 'LOT-20251219-005', installedSpec: '2500×2000', specMatch: true, result: '합격', inspectedAt: '2025-12-21 14:30', note: '' },\n { id: 2, floor: '1층', location: 'F-02', productName: '슬랫 셔터', productCode: 'SL2520', orderSpec: '2500×2000', productLot: 'LOT-20251219-006', installedSpec: '2500×2000', specMatch: true, result: '합격', inspectedAt: '2025-12-21 15:00', note: '' },\n { id: 3, floor: '2층', location: 'G-01', productName: '슬랫 셔터', productCode: 'SL3020', orderSpec: '3000×2000', productLot: 'LOT-20251219-007', installedSpec: '3000×2000', specMatch: true, result: '합격', inspectedAt: '2025-12-21 15:30', note: '' },\n { id: 4, floor: '2층', location: 'G-02', productName: '슬랫 셔터', productCode: 'SL3020', orderSpec: '3000×2000', productLot: 'LOT-20251219-008', installedSpec: '3000×2000', specMatch: true, result: '합격', inspectedAt: '2025-12-21 16:00', note: '' },\n ],\n inspectionItems: [\n { id: 1, itemName: '외관검사', standard: '도장 상태 양호', result: '합격', note: '' },\n { id: 2, itemName: '치수검사', standard: '±5mm 이내', result: '합격', note: '' },\n { id: 3, itemName: '작동검사', standard: '원활한 개폐 동작', result: '합격', note: '' },\n ],\n approvalStatus: '결재완료',\n approvedBy: '품질팀장 박품질',\n approvedAt: '2025-12-21 16:30',\n scheduleHistory: [],\n createdAt: '2025-12-20',\n createdBy: '품질팀 최품질',\n },\n\n // [통합테스트 6] IQC-251222-01: 포스코건설 - 더샵 송도 (스크린 코너형 2대) - 합격\n {\n id: 306,\n inspectionNo: 'IQC-251222-01',\n requestDate: '2025-12-22',\n requestedBy: '생산팀 김스크린',\n status: '검사완료',\n inspectionType: '중간검사',\n workOrderNo: 'KD-WO-251217-06',\n orderNo: 'KD-TS-251217-05',\n lotNo: 'KD-TS-251217-05',\n splitNo: 'KD-TS-251217-05-01',\n customerName: '포스코건설(주)',\n siteName: '더샵 송도',\n setLotNo: 'IQC-251222-01',\n lotNos: ['LOT-20251221-001', 'LOT-20251221-002'],\n inspector: '품질팀 이검사',\n scheduledDate: '2025-12-22',\n scheduledTime: '10:00',\n totalItems: 2,\n completedItems: 2,\n passedItems: 2,\n failedItems: 0,\n items: [\n { id: 1, floor: '1층', location: 'H-01', productName: '스크린 셔터 (코너형)', productCode: 'SH3530C', orderSpec: '3500×3000', productLot: 'LOT-20251221-001', installedSpec: '3500×3000', specMatch: true, result: '합격', inspectedAt: '2025-12-22 10:30', note: '코너형 가이드레일 연결부 양호' },\n { id: 2, floor: '1층', location: 'H-02', productName: '스크린 셔터 (코너형)', productCode: 'SH3530C', orderSpec: '3500×3000', productLot: 'LOT-20251221-002', installedSpec: '3500×3000', specMatch: true, result: '합격', inspectedAt: '2025-12-22 11:00', note: '' },\n ],\n inspectionItems: [\n { id: 1, itemName: '외관검사', standard: '찍힘, 스크래치 없음', result: '합격', note: '' },\n { id: 2, itemName: '치수검사', standard: '±5mm 이내', result: '합격', note: '' },\n { id: 3, itemName: '작동검사', standard: '원활한 개폐 동작', result: '합격', note: '' },\n { id: 4, itemName: '코너부 검사', standard: '코너 연결부 밀착', result: '합격', note: '코너 가이드 연결 양호' },\n ],\n approvalStatus: '결재완료',\n approvedBy: '품질팀장 박품질',\n approvedAt: '2025-12-22 11:30',\n scheduleHistory: [],\n createdAt: '2025-12-22',\n createdBy: '품질팀 이검사',\n },\n\n // [통합테스트 7] IQC-251220-03: 호반건설 - 써밋 광교 1차분 (스크린 2대) - 합격\n {\n id: 307,\n inspectionNo: 'IQC-251220-03',\n requestDate: '2025-12-20',\n requestedBy: '생산팀 이스크린',\n status: '검사완료',\n inspectionType: '중간검사',\n workOrderNo: 'KD-WO-251217-07',\n orderNo: 'KD-TS-251217-06',\n lotNo: 'KD-TS-251217-06',\n splitNo: 'KD-TS-251217-06-01',\n customerName: '호반건설(주)',\n siteName: '써밋 광교',\n setLotNo: 'IQC-251220-03',\n lotNos: ['LOT-20251218-004', 'LOT-20251218-005'],\n inspector: '품질팀 이검사',\n scheduledDate: '2025-12-20',\n scheduledTime: '11:00',\n totalItems: 2,\n completedItems: 2,\n passedItems: 2,\n failedItems: 0,\n items: [\n { id: 1, floor: 'B1', location: 'I-01', productName: '스크린 셔터 (와이어)', productCode: 'SH3228', orderSpec: '3200×2800', productLot: 'LOT-20251218-004', installedSpec: '3200×2800', specMatch: true, result: '합격', inspectedAt: '2025-12-20 11:30', note: '' },\n { id: 2, floor: 'B1', location: 'I-02', productName: '스크린 셔터 (와이어)', productCode: 'SH3228', orderSpec: '3200×2800', productLot: 'LOT-20251218-005', installedSpec: '3200×2800', specMatch: true, result: '합격', inspectedAt: '2025-12-20 12:00', note: '' },\n ],\n inspectionItems: [\n { id: 1, itemName: '외관검사', standard: '찍힘, 스크래치 없음', result: '합격', note: '' },\n { id: 2, itemName: '치수검사', standard: '±5mm 이내', result: '합격', note: '' },\n { id: 3, itemName: '작동검사', standard: '원활한 개폐 동작', result: '합격', note: '' },\n ],\n approvalStatus: '결재완료',\n approvedBy: '품질팀장 박품질',\n approvedAt: '2025-12-20 12:30',\n scheduleHistory: [],\n createdAt: '2025-12-20',\n createdBy: '품질팀 이검사',\n },\n\n // [통합테스트 8] IQC-251223-01: 호반건설 - 써밋 광교 2차분 (스크린 2대) - 합격\n {\n id: 308,\n inspectionNo: 'IQC-251223-01',\n requestDate: '2025-12-23',\n requestedBy: '생산팀 이스크린',\n status: '검사완료',\n inspectionType: '중간검사',\n workOrderNo: 'KD-WO-251217-08',\n orderNo: 'KD-TS-251217-06',\n lotNo: 'KD-TS-251217-06',\n splitNo: 'KD-TS-251217-06-02',\n customerName: '호반건설(주)',\n siteName: '써밋 광교',\n setLotNo: 'IQC-251223-01',\n lotNos: ['LOT-20251222-001', 'LOT-20251222-002'],\n inspector: '품질팀 최품질',\n scheduledDate: '2025-12-23',\n scheduledTime: '09:00',\n totalItems: 2,\n completedItems: 2,\n passedItems: 2,\n failedItems: 0,\n items: [\n { id: 1, floor: 'B1', location: 'I-03', productName: '스크린 셔터 (와이어)', productCode: 'SH3228', orderSpec: '3200×2800', productLot: 'LOT-20251222-001', installedSpec: '3200×2800', specMatch: true, result: '합격', inspectedAt: '2025-12-23 09:30', note: '' },\n { id: 2, floor: '1층', location: 'J-01', productName: '스크린 셔터 (와이어)', productCode: 'SH3228', orderSpec: '3200×2800', productLot: 'LOT-20251222-002', installedSpec: '3200×2800', specMatch: true, result: '합격', inspectedAt: '2025-12-23 10:00', note: '' },\n ],\n inspectionItems: [\n { id: 1, itemName: '외관검사', standard: '찍힘, 스크래치 없음', result: '합격', note: '' },\n { id: 2, itemName: '치수검사', standard: '±5mm 이내', result: '합격', note: '' },\n { id: 3, itemName: '작동검사', standard: '원활한 개폐 동작', result: '합격', note: '' },\n ],\n approvalStatus: '결재완료',\n approvedBy: '품질팀장 박품질',\n approvedAt: '2025-12-23 10:30',\n scheduleHistory: [],\n createdAt: '2025-12-23',\n createdBy: '품질팀 최품질',\n },\n\n // [통합테스트 9] IQC-251221-02: 태영건설 - 데시앙 동탄 (스크린 3대) - 불합격 (NCR 발행)\n {\n id: 309,\n inspectionNo: 'IQC-251221-02',\n requestDate: '2025-12-21',\n requestedBy: '생산팀 김스크린',\n status: '불합격',\n inspectionType: '중간검사',\n workOrderNo: 'KD-WO-251217-11',\n orderNo: 'KD-TS-251217-09',\n lotNo: 'KD-TS-251217-09',\n splitNo: 'KD-TS-251217-09-01',\n customerName: '태영건설(주)',\n siteName: '데시앙 동탄',\n setLotNo: 'IQC-251221-02',\n lotNos: ['LOT-20251220-003', 'LOT-20251220-004', 'LOT-20251220-005'],\n inspector: '품질팀 이검사',\n scheduledDate: '2025-12-21',\n scheduledTime: '15:00',\n totalItems: 3,\n completedItems: 3,\n passedItems: 1,\n failedItems: 2,\n items: [\n { id: 1, floor: 'B1', location: 'K-01', productName: '스크린 셔터 (와이어)', productCode: 'SH2822', orderSpec: '2800×2200', productLot: 'LOT-20251220-003', installedSpec: '2800×2200', specMatch: true, result: '합격', inspectedAt: '2025-12-21 15:30', note: '' },\n { id: 2, floor: 'B1', location: 'K-02', productName: '스크린 셔터 (와이어)', productCode: 'SH2822', orderSpec: '2800×2200', productLot: 'LOT-20251220-004', installedSpec: '2795×2200', specMatch: false, result: '불합격', inspectedAt: '2025-12-21 16:00', note: '가이드레일 설치 불량 - 기울어짐 발견' },\n { id: 3, floor: '1층', location: 'L-01', productName: '스크린 셔터 (와이어)', productCode: 'SH2822', orderSpec: '2800×2200', productLot: 'LOT-20251220-005', installedSpec: '2800×2195', specMatch: false, result: '불합격', inspectedAt: '2025-12-21 16:30', note: '스크린 원단 손상 - 재작업 필요' },\n ],\n inspectionItems: [\n { id: 1, itemName: '외관검사', standard: '찍힘, 스크래치 없음', result: '불합격', note: '원단 손상 발견' },\n { id: 2, itemName: '치수검사', standard: '±5mm 이내', result: '불합격', note: '공차 초과' },\n { id: 3, itemName: '작동검사', standard: '원활한 개폐 동작', result: '불합격', note: '개폐 시 걸림 발생' },\n ],\n ncrNo: 'NCR-251221-01',\n ncrStatus: '재작업중',\n ncrNote: '가이드레일 재설치 및 스크린 원단 교체 작업 진행중',\n approvalStatus: '결재완료',\n approvedBy: '품질팀장 박품질',\n approvedAt: '2025-12-21 17:00',\n scheduleHistory: [],\n createdAt: '2025-12-21',\n createdBy: '품질팀 이검사',\n },\n\n // [통합테스트 10] FQC-251219-01: 두산건설 - 위브 청라 (스크린 1대) - 최종검사 합격 (출하완료)\n {\n id: 310,\n inspectionNo: 'FQC-251219-01',\n requestDate: '2025-12-19',\n requestedBy: '생산팀 김스크린',\n status: '검사완료',\n inspectionType: '최종검사',\n workOrderNo: 'KD-WO-251217-12',\n orderNo: 'KD-TS-251217-10',\n lotNo: 'KD-TS-251217-10',\n splitNo: 'KD-TS-251217-10-01',\n customerName: '두산건설(주)',\n siteName: '위브 청라',\n setLotNo: 'FQC-251219-01',\n lotNos: ['LOT-20251217-001'],\n inspector: '품질팀 이검사',\n scheduledDate: '2025-12-19',\n scheduledTime: '09:00',\n totalItems: 1,\n completedItems: 1,\n passedItems: 1,\n failedItems: 0,\n items: [\n { id: 1, floor: '1층', location: 'M-01', productName: '스크린 셔터 (와이어)', productCode: 'SH3225', orderSpec: '3200×2500', productLot: 'LOT-20251217-001', installedSpec: '3200×2500', specMatch: true, result: '합격', inspectedAt: '2025-12-19 09:30', note: '최종검사 합격 - 출하 가능' },\n ],\n inspectionItems: [\n { id: 1, itemName: '외관검사', standard: '찍힘, 스크래치 없음', result: '합격', note: '' },\n { id: 2, itemName: '치수검사', standard: '±5mm 이내', result: '합격', note: '' },\n { id: 3, itemName: '작동검사', standard: '원활한 개폐 동작', result: '합격', note: '' },\n { id: 4, itemName: '안전장치', standard: '급정지 기능 정상', result: '합격', note: '' },\n { id: 5, itemName: '포장상태', standard: '포장 이상 없음', result: '합격', note: '출하 포장 완료' },\n ],\n shipmentNo: 'SL-251220-01',\n shipmentStatus: '출하완료',\n approvalStatus: '결재완료',\n approvedBy: '품질팀장 박품질',\n approvedAt: '2025-12-19 10:00',\n scheduleHistory: [],\n createdAt: '2025-12-19',\n createdBy: '품질팀 이검사',\n },\n];\n\n// ============ 거래처 관리 샘플 데이터 ============\nconst initialCustomers = [\n {\n id: 'CUS-001',\n customerCode: 'CUS-001',\n customerName: '삼성물산(주)',\n ceo: '김삼성',\n businessNo: '123-45-67890',\n businessType: '건설업',\n businessItem: '건축,토목',\n zipCode: '06235',\n address: '서울시 강남구 테헤란로 123',\n addressDetail: '삼성빌딩 15층',\n phone: '02-1234-5678',\n fax: '02-1234-5679',\n email: 'samsung@samsung-const.co.kr',\n customerType: '매출',\n creditGrade: 'A',\n creditNote: '우량 거래처',\n creditLimit: 100000000,\n paymentTerms: '월말 마감 익월 10일 결제',\n paymentMethod: '계좌이체',\n contacts: [\n { id: 1, name: '박현장', position: '현장소장', phone: '010-1234-5678', email: 'park@samsung-const.co.kr', isPrimary: true },\n { id: 2, name: '이구매', position: '구매담당', phone: '010-2345-6789', email: 'lee@samsung-const.co.kr', isPrimary: false },\n ],\n transactions: [\n { date: '2025-02-20', type: '수주', amount: 52000000, balance: 52000000, note: 'KD-SO-250205-01' },\n { date: '2025-02-25', type: '입금', amount: 52000000, balance: 0, note: '전액입금' },\n ],\n receivables: { total: 0, overdue: 0, overdueDays: 0 },\n registeredAt: '2024-01-15',\n registeredBy: '판매팀 김판매',\n modifiedAt: '2025-02-01',\n modifiedBy: '판매팀 김판매',\n },\n {\n id: 'CUS-002',\n customerCode: 'CUS-002',\n customerName: '현대건설(주)',\n ceo: '정현대',\n businessNo: '234-56-78901',\n businessType: '건설업',\n businessItem: '건축,인테리어',\n zipCode: '04520',\n address: '서울시 중구 을지로 234',\n addressDetail: '현대빌딩 8층',\n phone: '02-2345-6789',\n fax: '02-2345-6780',\n email: 'info@hyundai-dev.co.kr',\n customerType: '매출',\n creditGrade: 'B',\n creditNote: '결제 약간 지연 이력 있음',\n creditLimit: 50000000,\n paymentTerms: '월말 마감 익월 말 결제',\n paymentMethod: '계좌이체',\n contacts: [\n { id: 1, name: '최구매', position: '구매팀장', phone: '010-3456-7890', email: 'choi@hyundai-dev.co.kr', isPrimary: true },\n ],\n transactions: [\n { date: '2025-02-15', type: '수주', amount: 28000000, balance: 28000000, note: 'KD-SO-250208-01' },\n ],\n receivables: { total: 28000000, overdue: 0, overdueDays: 0 },\n registeredAt: '2024-03-20',\n registeredBy: '판매팀 이판매',\n modifiedAt: '2025-01-15',\n modifiedBy: '판매팀 이판매',\n },\n {\n id: 'CUS-003',\n customerCode: 'CUS-003',\n customerName: '대우건설(주)',\n ceo: '한화중',\n businessNo: '345-67-89012',\n businessType: '건설업',\n businessItem: '건축,시설공사',\n zipCode: '07321',\n address: '서울시 영등포구 여의대로 345',\n addressDetail: '한화빌딩 3층',\n phone: '02-3456-7890',\n fax: '02-3456-7891',\n email: 'info@hanwha-const.co.kr',\n customerType: '매출',\n creditGrade: 'C',\n creditNote: '입금약속 미이행 이력 있음 - 경리승인 필요',\n creditLimit: 20000000,\n paymentTerms: '선입금 50% 후 잔금',\n paymentMethod: '계좌이체',\n contacts: [\n { id: 1, name: '김담당', position: '공사담당', phone: '010-4567-8901', email: 'kim@hanwha-const.co.kr', isPrimary: true },\n ],\n transactions: [\n { date: '2025-02-18', type: '수주', amount: 18000000, balance: 18000000, note: 'KD-SO-250210-01' },\n ],\n receivables: { total: 18000000, overdue: 5000000, overdueDays: 15 },\n registeredAt: '2024-06-10',\n registeredBy: '판매팀 박판매',\n modifiedAt: '2025-02-10',\n modifiedBy: '경리팀 최경리',\n },\n {\n id: 'CUS-004',\n customerCode: 'CUS-004',\n customerName: '(주)서울인테리어',\n ceo: '박부산',\n businessNo: '456-78-90123',\n businessType: '건설업',\n businessItem: '건축,토목',\n zipCode: '48060',\n address: '부산시 해운대구 센텀중앙로 456',\n addressDetail: '센텀빌딩 12층',\n phone: '051-456-7890',\n fax: '051-456-7891',\n email: 'info@busan-const.co.kr',\n customerType: '매출',\n creditGrade: 'A',\n creditNote: '우량 거래처',\n creditLimit: 80000000,\n paymentTerms: '월말 마감 익월 15일 결제',\n paymentMethod: '계좌이체',\n contacts: [\n { id: 1, name: '이현장', position: '현장소장', phone: '010-5678-9012', email: 'lee@busan-const.co.kr', isPrimary: true },\n ],\n transactions: [\n { date: '2025-02-20', type: '수주', amount: 35000000, balance: 35000000, note: 'KD-SO-250212-01' },\n ],\n receivables: { total: 35000000, overdue: 0, overdueDays: 0 },\n registeredAt: '2024-02-28',\n registeredBy: '판매팀 최판매',\n modifiedAt: '2025-02-20',\n modifiedBy: '판매팀 최판매',\n },\n {\n id: 'SUP-001',\n customerCode: 'SUP-001',\n customerName: '(주)대한알루미늄',\n ceo: '김대한',\n businessNo: '567-89-01234',\n businessType: '제조업',\n businessItem: '알루미늄 가공',\n zipCode: '15850',\n address: '경기도 군포시 산본로 567',\n addressDetail: '대한공장',\n phone: '031-567-8901',\n fax: '031-567-8902',\n email: 'sales@daehan-al.co.kr',\n customerType: '매입',\n supplyType: '자재',\n creditGrade: 'A',\n creditNote: '주요 자재 공급처',\n creditLimit: 0,\n paymentTerms: '월말 마감 익월 10일 지급',\n paymentMethod: '계좌이체',\n contacts: [\n { id: 1, name: '박판매', position: '판매팀장', phone: '010-6789-0123', email: 'park@daehan-al.co.kr', isPrimary: true },\n ],\n transactions: [],\n receivables: { total: 0, overdue: 0, overdueDays: 0 },\n registeredAt: '2023-05-15',\n registeredBy: '구매팀 정구매',\n modifiedAt: '2024-12-01',\n modifiedBy: '구매팀 정구매',\n },\n];\n\n// ============ 현장 관리 샘플 데이터 ============\nconst initialSites = [\n {\n id: 'SITE-001',\n siteCode: 'PJ-250201-01',\n siteName: '강남 더리버 아파트 신축현장',\n customerId: 1, // 삼성물산(주) - customerMasterConfig 기준\n customerName: '삼성물산(주)',\n orderNo: 'KD-SO-250205-01',\n siteAddress: '서울시 강남구 압구정로 123',\n siteContact: '02-1234-9999',\n installManager: '박설치',\n installManagerPhone: '010-9999-1234',\n installScheduledDate: '2025-03-01',\n installCompletedDate: '2025-02-28',\n status: '설치완료',\n progress: 100,\n note: 'B1층 주차장 스크린 셔터 설치 완료',\n orders: [\n { orderNo: 'KD-SO-250205-01', product: '스크린 셔터', qty: 5, amount: 52000000 },\n ],\n shipments: [\n { shipmentNo: 'KD-SH-250215-01', date: '2025-02-15', qty: 5, status: '배송완료' },\n ],\n history: [\n { date: '2025-02-01', action: '현장등록', note: '신규 현장 등록', by: '판매팀 김판매' },\n { date: '2025-02-15', action: '출하완료', note: '전량 출하', by: '물류팀 박물류' },\n { date: '2025-02-28', action: '설치완료', note: '설치 검수 완료', by: '박설치' },\n ],\n registeredAt: '2025-02-01',\n registeredBy: '판매팀 김판매',\n },\n {\n id: 'SITE-002',\n siteCode: 'PJ-250208-01',\n siteName: '중구 을지타워 리모델링현장',\n customerId: 2, // 현대건설(주) - customerMasterConfig 기준\n customerName: '현대건설(주)',\n orderNo: 'KD-SO-250208-01',\n siteAddress: '서울시 중구 을지로 456',\n siteContact: '02-2345-9999',\n installManager: '이설치',\n installManagerPhone: '010-8888-2345',\n installScheduledDate: '2025-03-15',\n installCompletedDate: null,\n status: '생산완료',\n progress: 60,\n note: '입금 확인 후 출고 예정',\n orders: [\n { orderNo: 'KD-SO-250208-01', product: '스크린 셔터', qty: 3, amount: 28000000 },\n ],\n shipments: [],\n history: [\n { date: '2025-02-08', action: '현장등록', note: '신규 현장 등록', by: '판매팀 이판매' },\n { date: '2025-02-15', action: '생산완료', note: '생산 완료, 출고보류', by: '생산팀 김생산' },\n ],\n registeredAt: '2025-02-08',\n registeredBy: '판매팀 이판매',\n },\n {\n id: 'SITE-003',\n siteCode: 'PJ-250212-01',\n siteName: '해운대 마린시티 오피스텔',\n customerId: 4, // (주)서울인테리어 - customerMasterConfig 기준\n customerName: '(주)서울인테리어',\n orderNo: 'KD-SO-250212-01',\n siteAddress: '부산시 해운대구 마린시티 789',\n siteContact: '051-789-9999',\n installManager: '최설치',\n installManagerPhone: '010-7777-3456',\n installScheduledDate: '2025-03-25',\n installCompletedDate: null,\n status: '생산중',\n progress: 40,\n note: '분할 출하 예정 - 1차 3/15, 2차 3/25',\n orders: [\n { orderNo: 'KD-SO-250212-01', product: '슬랫 셔터', qty: 8, amount: 35000000 },\n ],\n shipments: [\n { shipmentNo: 'KD-SH-250315-01', date: '2025-03-15', qty: 4, status: '출하준비' },\n ],\n history: [\n { date: '2025-02-12', action: '현장등록', note: '신규 현장 등록', by: '판매팀 최판매' },\n { date: '2025-02-15', action: '생산시작', note: '분할 생산 시작', by: '생산팀 박생산' },\n ],\n registeredAt: '2025-02-12',\n registeredBy: '판매팀 최판매',\n },\n];\n\n// ============ 단가 관리 샘플 데이터 ============\nconst initialPrices = [\n {\n id: 'PRC-001',\n priceCode: 'PRC-001',\n customerId: 1, // 삼성물산(주) - customerMasterConfig 기준\n customerName: '삼성물산(주)',\n productCode: 'SCR-001',\n productName: '스크린 셔터 (표준형)',\n spec: '기본규격',\n unit: 'EA',\n basePrice: 2500000,\n discountRate: 5,\n finalPrice: 2375000,\n currency: 'KRW',\n validFrom: '2025-01-01',\n validTo: '2025-12-31',\n minQty: 1,\n qtyPricing: [\n { minQty: 1, maxQty: 4, price: 2375000 },\n { minQty: 5, maxQty: 9, price: 2300000 },\n { minQty: 10, maxQty: 999, price: 2200000 },\n ],\n status: '적용중',\n approvedBy: '판매팀장 박판매',\n approvedAt: '2024-12-20',\n history: [\n { date: '2024-12-20', action: '신규등록', oldPrice: null, newPrice: 2375000, by: '판매팀 김판매' },\n ],\n registeredAt: '2024-12-20',\n registeredBy: '판매팀 김판매',\n },\n {\n id: 'PRC-002',\n priceCode: 'PRC-002',\n customerId: 1, // 삼성물산(주) - customerMasterConfig 기준\n customerName: '삼성물산(주)',\n productCode: 'SLT-001',\n productName: '슬랫 셔터 (일반형)',\n spec: '기본규격',\n unit: 'EA',\n basePrice: 3500000,\n discountRate: 5,\n finalPrice: 3325000,\n currency: 'KRW',\n validFrom: '2025-01-01',\n validTo: '2025-12-31',\n minQty: 1,\n qtyPricing: [],\n status: '적용중',\n approvedBy: '판매팀장 박판매',\n approvedAt: '2024-12-20',\n history: [],\n registeredAt: '2024-12-20',\n registeredBy: '판매팀 김판매',\n },\n {\n id: 'PRC-003',\n priceCode: 'PRC-003',\n customerId: 2, // 현대건설(주) - customerMasterConfig 기준\n customerName: '현대건설(주)',\n productCode: 'SCR-001',\n productName: '스크린 셔터 (표준형)',\n spec: '기본규격',\n unit: 'EA',\n basePrice: 2500000,\n discountRate: 3,\n finalPrice: 2425000,\n currency: 'KRW',\n validFrom: '2025-01-01',\n validTo: '2025-12-31',\n minQty: 1,\n qtyPricing: [],\n status: '적용중',\n approvedBy: '판매팀장 박판매',\n approvedAt: '2024-12-25',\n history: [\n { date: '2024-12-25', action: '신규등록', oldPrice: null, newPrice: 2425000, by: '판매팀 이판매' },\n ],\n registeredAt: '2024-12-25',\n registeredBy: '판매팀 이판매',\n },\n {\n id: 'PRC-004',\n priceCode: 'PRC-004',\n customerId: null,\n customerName: '기본단가',\n productCode: 'SCR-001',\n productName: '스크린 셔터 (표준형)',\n spec: '기본규격',\n unit: 'EA',\n basePrice: 2500000,\n discountRate: 0,\n finalPrice: 2500000,\n currency: 'KRW',\n validFrom: '2024-01-01',\n validTo: '2099-12-31',\n minQty: 1,\n qtyPricing: [],\n status: '적용중',\n approvedBy: '대표이사',\n approvedAt: '2024-01-01',\n history: [],\n registeredAt: '2024-01-01',\n registeredBy: '시스템',\n },\n {\n id: 'PRC-005',\n priceCode: 'PRC-005',\n customerId: null,\n customerName: '기본단가',\n productCode: 'SLT-001',\n productName: '슬랫 셔터 (일반형)',\n spec: '기본규격',\n unit: 'EA',\n basePrice: 3500000,\n discountRate: 0,\n finalPrice: 3500000,\n currency: 'KRW',\n validFrom: '2024-01-01',\n validTo: '2099-12-31',\n minQty: 1,\n qtyPricing: [],\n status: '적용중',\n approvedBy: '대표이사',\n approvedAt: '2024-01-01',\n history: [],\n registeredAt: '2024-01-01',\n registeredBy: '시스템',\n },\n];\n\n// ============ 견적 관리 ============\n\n// 견적 샘플 데이터\n// 채번규칙: KD-PR-YYMMDD-## (견적서), 현장코드: PJ-YYMMDD-## (현장)\nconst sampleQuotesData = [\n // ========== 스크린샷에 맞는 샘플 데이터 ==========\n // 1: KD-PR-240115-01 - 최초 작성\n {\n id: 1,\n quoteNo: 'KD-PR-240115-01',\n quoteDate: '2024-01-15',\n status: '최초작성',\n productName: 'SCR-001',\n productCode: 'SCR-001',\n qty: 10,\n totalAmount: 15000000,\n customerId: 1, // 삼성물산(주)\n customerCode: 'CUS-001',\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteName: '강남 오피스텔 현장',\n siteCode: 'PJ-240110-01',\n manager: '김철수',\n contact: '010-1111-1111',\n deliveryAddress: '서울시 강남구 테헤란로 100',\n dueDate: '2024-02-15',\n createdBy: '판매1팀 김판매',\n note: '급하게 진행 필요',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 15000000,\n items: []\n },\n // 2: KD-PR-240116-01 - 2차 수정\n {\n id: 2,\n quoteNo: 'KD-PR-240116-01',\n quoteDate: '2024-01-16',\n status: '2차 수정',\n productName: 'STL-002',\n productCode: 'STL-002',\n qty: 5,\n totalAmount: 8500000,\n customerId: 2, // 현대건설(주)\n customerCode: 'CUS-002',\n customerName: '현대건설(주)',\n creditGrade: 'A',\n siteName: '판교 공장',\n siteCode: 'PJ-240112-01',\n manager: '이영희',\n contact: '010-2222-2222',\n deliveryAddress: '경기도 성남시 분당구 판교로 200',\n dueDate: '2024-02-20',\n createdBy: '판매1팀 이판매',\n note: '-',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 8500000,\n items: []\n },\n // 3: KD-PR-240117-01 - 최종확정\n {\n id: 3,\n quoteNo: 'KD-PR-240117-01',\n quoteDate: '2024-01-17',\n status: '최종확정',\n productName: 'SCR-003',\n productCode: 'SCR-003',\n qty: 20,\n totalAmount: 25000000,\n customerId: 3, // 대우건설(주)\n customerCode: 'CUS-003',\n customerName: '대우건설(주)',\n creditGrade: 'A',\n siteName: '송도 아파트 현장',\n siteCode: 'PJ-240113-01',\n manager: '박민수',\n contact: '010-3333-3333',\n deliveryAddress: '인천시 연수구 송도동 300',\n dueDate: '2024-02-25',\n createdBy: '판매2팀 박판매',\n note: '확정 완료',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 25000000,\n items: []\n },\n // 4: KD-PR-240118-01 - 수주전환\n {\n id: 4,\n quoteNo: 'KD-PR-240118-01',\n quoteDate: '2024-01-18',\n status: '수주전환',\n productName: 'STL-004',\n productCode: 'STL-004',\n qty: 8,\n totalAmount: 12000000,\n customerId: 4, // (주)서울인테리어\n customerCode: 'CUS-004',\n customerName: '(주)서울인테리어',\n creditGrade: 'B',\n siteName: '분당 상가 현장',\n siteCode: 'PJ-240114-01',\n manager: '최지원',\n contact: '010-4444-4444',\n deliveryAddress: '경기도 성남시 분당구 수내동 400',\n dueDate: '2024-03-01',\n createdBy: '판매1팀 최판매',\n note: '수주 전환 완료',\n relatedOrders: ['KD-TS-240123-01'],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 12000000,\n items: []\n },\n // 5: KD-PR-240119-01 - 3차 수정\n {\n id: 5,\n quoteNo: 'KD-PR-240119-01',\n quoteDate: '2024-01-19',\n status: '3차 수정',\n productName: 'SCR-005',\n productCode: 'SCR-005',\n qty: 15,\n totalAmount: 22000000,\n customerId: 1, // 삼성물산(주) - 다른 현장\n customerCode: 'CUS-001',\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteName: '용산 오피스빌딩',\n siteCode: 'PJ-240115-01',\n manager: '정수민',\n contact: '010-5555-5555',\n deliveryAddress: '서울시 용산구 한강로 500',\n dueDate: '2024-03-05',\n createdBy: '판매2팀 정판매',\n note: '3차 수정 중',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 22000000,\n items: []\n },\n // 6: KD-PR-250208-01 - 2차 수정\n {\n id: 6,\n quoteNo: 'KD-PR-250208-01',\n quoteDate: '2025-02-08',\n status: '2차 수정',\n productName: 'FD-002',\n productCode: 'FD-002',\n qty: 120,\n totalAmount: 48000000,\n customerId: 1, // 삼성물산(주)\n customerCode: 'CUS-001',\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteName: '강남 타워 신축현장',\n siteCode: 'PJ-250201-01',\n manager: '김영업',\n contact: '010-1111-1111',\n deliveryAddress: '서울시 강남구 테헤란로 100',\n dueDate: '2025-03-20',\n createdBy: '판매1팀 김판매',\n note: '-',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 48000000,\n items: []\n },\n // 7: KD-PR-250215-01 - 최종확정\n {\n id: 7,\n quoteNo: 'KD-PR-250215-01',\n quoteDate: '2025-02-15',\n status: '최종확정',\n productName: 'ST-001',\n productCode: 'ST-001',\n qty: 80,\n totalAmount: 35000000,\n customerId: 2, // 현대건설(주)\n customerCode: 'CUS-002',\n customerName: '현대건설(주)',\n creditGrade: 'A',\n siteName: '연수 오피스텔',\n siteCode: 'PJ-250210-01',\n manager: '홍길동',\n contact: '010-2222-2222',\n deliveryAddress: '인천시 연수구 동춘동 500',\n dueDate: '2025-04-10',\n createdBy: '판매1팀 이판매',\n note: '오피스텔 신축 공사',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 35000000,\n items: []\n },\n // 8: KD-PR-251029-01 - 최초 작성\n {\n id: 8,\n quoteNo: 'KD-PR-251029-01',\n quoteDate: '2025-10-29',\n status: '최초작성',\n productName: '스크린 셔터-표준형',\n productCode: 'SCR-001',\n qty: 1,\n totalAmount: 3400000,\n customerId: 3, // 대우건설(주)\n customerCode: 'CUS-003',\n customerName: '대우건설(주)',\n creditGrade: 'A',\n siteName: '송도 오피스텔 A동',\n siteCode: 'PJ-251025-01',\n manager: '김영업',\n contact: '010-6666-6666',\n deliveryAddress: '인천시 연수구 송도동 100',\n dueDate: '2025-11-30',\n createdBy: '판매1팀 김영업',\n note: '송도 오피스텔 A동 101호 거실창',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 3400000,\n items: []\n },\n\n // ========== 테스트용: 스크린+슬랫 혼합 견적 ==========\n {\n id: 9,\n quoteNo: 'KD-PR-250301-01',\n quoteDate: '2025-03-01',\n status: '최종확정',\n productName: '스크린/슬랫 셔터',\n productCode: 'MIX-001',\n qty: 5,\n totalAmount: 32000000,\n customerId: 2, // 현대건설(주)\n customerCode: 'CUS-002',\n customerName: '현대건설(주)',\n creditGrade: 'A',\n siteName: '판교 테크노밸리 신축',\n siteCode: 'PJ-250225-01',\n manager: '김테크',\n contact: '010-5555-5555',\n deliveryAddress: '경기도 성남시 분당구 판교로 123',\n dueDate: '2025-04-15',\n createdBy: '판매1팀 박판매',\n note: '[테스트] 스크린+슬랫 혼합 주문 - 공정 분리 테스트',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 32000000,\n items: [\n { id: 1, floor: '1층', location: 'A-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 6000, height: 2500, guideType: '일반', motorPower: '220V', controller: '없음', qty: 2, wingIndex: 50, inspectionFee: 50000, unitPrice: 8000000, amount: 16000000, orderStatus: 'pending', orderId: null },\n { id: 2, floor: 'B1', location: 'B-01', category: '슬랫', productName: '슬랫 셔터', width: 4000, height: 2000, guideType: '일반', motorPower: '220V', controller: '없음', qty: 3, wingIndex: 45, inspectionFee: 50000, unitPrice: 5333333, amount: 16000000, orderStatus: 'pending', orderId: null }\n ]\n },\n\n // ═══════════════════════════════════════════════════════════════════════════\n // E2E 테스트용 견적 데이터 (새 수식 기준 자동산출 결과 포함)\n // ═══════════════════════════════════════════════════════════════════════════\n {\n id: 101,\n quoteNo: 'KD-PR-251216-01',\n quoteDate: '2025-12-16',\n status: '최종확정',\n productName: '스크린 셔터 (E2E 테스트 1)',\n productCode: 'SCR-E2E-001',\n qty: 3,\n totalAmount: 18500000,\n customerId: 1,\n customerCode: 'CUS-001',\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteName: '[E2E테스트] 강남 오피스 A동',\n siteCode: 'PJ-251210-01',\n manager: '김테스트',\n contact: '010-1234-5678',\n deliveryAddress: '서울시 강남구 테헤란로 100',\n dueDate: '2025-12-30',\n createdBy: '[E2E] 시스템',\n note: 'E2E 전체 프로세스 테스트 - 스크린 제품 (W3000×H2500)',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 18500000,\n // 새 수식 기준 자동산출 파라미터\n autoCalcParams: { PC: '스크린', W0: 3000, H0: 2500, QTY: 3, V: '220', WIRE: '유선', CT: '매립', GT: '벽면형' },\n // 자동산출 결과 (formulaEngine 기준)\n autoCalcResult: {\n W1: 3140, H1: 2850, M: 8.95, K: 60.41,\n SHAFT_INCH: '5', MAIN_SHAFT_LEN: 4500, SUB_SHAFT_LEN: 300,\n MOTOR_KG: 300, GR_MAT_LEN: 3000, CASE_MAT_LEN: 4270,\n HJ_MAT_LEN: 3000, HWANBONG_QTY: 2, JOINTBAR_QTY: 4,\n SQP3000_QTY: 2, SQP6000_QTY: 1\n },\n items: [\n { id: 1, floor: '1층', location: 'A-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 50, inspectionFee: 50000, unitPrice: 6166667, amount: 6166667, orderStatus: 'pending', orderId: null, process: 'screen' },\n { id: 2, floor: '2층', location: 'A-02', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 50, inspectionFee: 50000, unitPrice: 6166667, amount: 6166667, orderStatus: 'pending', orderId: null, process: 'screen' },\n { id: 3, floor: '3층', location: 'A-03', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 50, inspectionFee: 50000, unitPrice: 6166667, amount: 6166666, orderStatus: 'pending', orderId: null, process: 'screen' }\n ]\n },\n {\n id: 102,\n quoteNo: 'KD-PR-251216-02',\n quoteDate: '2025-12-16',\n status: '최종확정',\n productName: '철재 슬랫 셔터 (E2E 테스트 2)',\n productCode: 'STL-E2E-001',\n qty: 2,\n totalAmount: 15000000,\n customerId: 2,\n customerName: '현대건설(주)',\n creditGrade: 'A',\n siteName: '[E2E테스트] 판교 물류센터',\n siteCode: 'PJ-251216-02',\n manager: '이테스트',\n contact: '010-2345-6789',\n deliveryAddress: '경기도 성남시 분당구 판교로 200',\n dueDate: '2025-12-28',\n createdBy: '[E2E] 시스템',\n note: 'E2E 전체 프로세스 테스트 - 철재 슬랫 제품 (W4000×H3000)',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 15000000,\n autoCalcParams: { PC: '철재', W0: 4000, H0: 3000, QTY: 2, V: '220', WIRE: '유선', CT: '노출', GT: '코너형' },\n autoCalcResult: {\n W1: 4110, H1: 3350, M: 13.77, K: 344.21,\n SHAFT_INCH: '4', MAIN_SHAFT_LEN: 4500,\n MOTOR_KG: 300, GR_MAT_LEN: 3500, CASE_MAT_LEN: 4270,\n HJ_MAT_LEN: 4000, JOINTBAR_QTY: 5,\n SQP3000_QTY: 5, SQP6000_QTY: 1\n },\n items: [\n { id: 1, floor: 'B1', location: 'C-01', category: '슬랫', productName: '철재 슬랫 셔터', width: 4000, height: 3000, guideType: '코너형', motorPower: '220V', controller: '노출형', qty: 1, wingIndex: 60, inspectionFee: 50000, unitPrice: 7500000, amount: 7500000, orderStatus: 'pending', orderId: null, process: 'slat' },\n { id: 2, floor: 'B2', location: 'C-02', category: '슬랫', productName: '철재 슬랫 셔터', width: 4000, height: 3000, guideType: '코너형', motorPower: '220V', controller: '노출형', qty: 1, wingIndex: 60, inspectionFee: 50000, unitPrice: 7500000, amount: 7500000, orderStatus: 'pending', orderId: null, process: 'slat' }\n ]\n },\n {\n id: 103,\n quoteNo: 'KD-PR-251216-03',\n quoteDate: '2025-12-16',\n status: '최종확정',\n productName: '스크린+슬랫 혼합 (E2E 테스트 3)',\n productCode: 'MIX-E2E-001',\n qty: 4,\n totalAmount: 28000000,\n customerId: 3,\n customerName: '대우건설(주)',\n creditGrade: 'A',\n siteName: '[E2E테스트] 송도 아파트 B동',\n siteCode: 'PJ-251216-03',\n manager: '박테스트',\n contact: '010-3456-7890',\n deliveryAddress: '인천시 연수구 송도동 300',\n dueDate: '2025-12-25',\n createdBy: '[E2E] 시스템',\n note: 'E2E 전체 프로세스 테스트 - 스크린+슬랫 혼합 (공정분리 테스트)',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 28000000,\n items: [\n { id: 1, floor: '1층', location: 'D-01', category: '스크린', productName: '스크린 셔터 (대형)', width: 6000, height: 4000, guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, wingIndex: 80, inspectionFee: 80000, unitPrice: 12000000, amount: 12000000, orderStatus: 'pending', orderId: null, process: 'screen' },\n { id: 2, floor: '1층', location: 'D-02', category: '슬랫', productName: '철재 슬랫 셔터', width: 3500, height: 2500, guideType: '벽면형', motorPower: '220V', controller: '노출형', qty: 2, wingIndex: 45, inspectionFee: 50000, unitPrice: 5000000, amount: 10000000, orderStatus: 'pending', orderId: null, process: 'slat' },\n { id: 3, floor: '2층', location: 'D-03', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 50, inspectionFee: 50000, unitPrice: 6000000, amount: 6000000, orderStatus: 'pending', orderId: null, process: 'screen' }\n ]\n },\n {\n id: 104,\n quoteNo: 'KD-PR-251216-04',\n quoteDate: '2025-12-16',\n status: '최종확정',\n productName: '스크린 대형 (E2E 테스트 4)',\n productCode: 'SCR-E2E-002',\n qty: 2,\n totalAmount: 22000000,\n customerId: 4,\n customerName: '(주)서울인테리어',\n creditGrade: 'B',\n siteName: '[E2E테스트] 용산 호텔',\n siteCode: 'PJ-251216-04',\n manager: '최테스트',\n contact: '010-4567-8901',\n deliveryAddress: '서울시 용산구 한강로 400',\n dueDate: '2025-12-31',\n createdBy: '[E2E] 시스템',\n note: 'E2E 전체 프로세스 테스트 - 대형 스크린 (W6000×H4000) 모터 용량 체크',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 22000000,\n autoCalcParams: { PC: '스크린', W0: 6000, H0: 4000, QTY: 2, V: '380', WIRE: '무선', CT: '매립', GT: '벽면형' },\n autoCalcResult: {\n W1: 6140, H1: 4350, M: 26.71, K: 138.44,\n SHAFT_INCH: '5', MAIN_SHAFT_LEN: 7000, SUB_SHAFT_LEN: 300,\n MOTOR_KG: 500, GR_MAT_LEN: 4500, CASE_MAT_LEN: 6700,\n HJ_MAT_LEN: 3000, HWANBONG_QTY: 3, JOINTBAR_QTY: 7,\n SQP3000_QTY: 4, SQP6000_QTY: 1\n },\n items: [\n { id: 1, floor: '로비', location: 'E-01', category: '스크린', productName: '스크린 셔터 (대형)', width: 6000, height: 4000, guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, wingIndex: 80, inspectionFee: 80000, unitPrice: 11000000, amount: 11000000, orderStatus: 'pending', orderId: null, process: 'screen' },\n { id: 2, floor: '연회장', location: 'E-02', category: '스크린', productName: '스크린 셔터 (대형)', width: 6000, height: 4000, guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, wingIndex: 80, inspectionFee: 80000, unitPrice: 11000000, amount: 11000000, orderStatus: 'pending', orderId: null, process: 'screen' }\n ]\n },\n {\n id: 105,\n quoteNo: 'KD-PR-251216-05',\n quoteDate: '2025-12-16',\n status: '최종확정',\n productName: '전 공정 통합 (E2E 테스트 5)',\n productCode: 'FULL-E2E-001',\n qty: 6,\n totalAmount: 45000000,\n customerId: 1,\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteName: '[E2E테스트] 삼성타운 종합',\n siteCode: 'PJ-251216-05',\n manager: '정테스트',\n contact: '010-5678-9012',\n deliveryAddress: '서울시 서초구 서초대로 500',\n dueDate: '2026-01-15',\n createdBy: '[E2E] 시스템',\n note: 'E2E 전체 프로세스 테스트 - 스크린+슬랫+절곡 전 공정 통합 테스트',\n relatedOrders: [],\n discountRate: 5,\n discountAmount: 2250000,\n finalAmount: 42750000,\n items: [\n { id: 1, floor: '1층', location: 'F-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3500, height: 2800, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 2, wingIndex: 55, inspectionFee: 50000, unitPrice: 7000000, amount: 14000000, orderStatus: 'pending', orderId: null, process: 'screen' },\n { id: 2, floor: '2층', location: 'F-02', category: '슬랫', productName: '철재 슬랫 셔터', width: 4500, height: 3200, guideType: '코너형', motorPower: '220V', controller: '노출형', qty: 2, wingIndex: 65, inspectionFee: 60000, unitPrice: 8000000, amount: 16000000, orderStatus: 'pending', orderId: null, process: 'slat' },\n { id: 3, floor: '3층', location: 'F-03', category: '스크린', productName: '스크린 셔터 (대형)', width: 5000, height: 3500, guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 2, wingIndex: 70, inspectionFee: 70000, unitPrice: 7500000, amount: 15000000, orderStatus: 'pending', orderId: null, process: 'screen' }\n ]\n },\n\n // ═══════════════════════════════════════════════════════════════════════════\n // 전체 워크플로우 통합 테스트 데이터 10건 (ID: 201-210)\n // 견적 → 수주 → 생산 → 품질검사 → 출하 → 회계 전체 연동\n // ═══════════════════════════════════════════════════════════════════════════\n\n // [통합테스트 1] 삼성물산 - 래미안 강남 프레스티지 (A등급, 스크린 3대)\n {\n id: 201,\n quoteNo: 'KD-PR-251217-01',\n quoteDate: '2025-12-17',\n status: '수주전환',\n productName: '스크린 셔터 (표준형)',\n productCode: 'SH3025',\n qty: 3,\n totalAmount: 24000000,\n customerId: 1, // C-001 삼성물산(주)\n customerCode: 'CUS-001',\n customerName: '삼성물산(주)',\n creditGrade: 'A',\n siteId: 1, // S-001\n siteName: '래미안 강남 프레스티지',\n siteCode: 'S-001',\n manager: '김건설',\n contact: '010-1234-5678',\n deliveryAddress: '서울시 강남구 테헤란로 100',\n dueDate: '2026-01-15',\n createdBy: '판매1팀 김영업',\n note: '[통합테스트1] A등급 정상 플로우 - 스크린 3대',\n relatedOrders: ['KD-TS-251217-01'],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 24000000,\n items: [\n { id: 1, floor: '1층', location: 'A-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 50, inspectionFee: 50000, unitPrice: 8000000, amount: 8000000, orderStatus: 'converted', orderId: 201, process: 'screen' },\n { id: 2, floor: '2층', location: 'A-02', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 50, inspectionFee: 50000, unitPrice: 8000000, amount: 8000000, orderStatus: 'converted', orderId: 201, process: 'screen' },\n { id: 3, floor: '3층', location: 'A-03', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 50, inspectionFee: 50000, unitPrice: 8000000, amount: 8000000, orderStatus: 'converted', orderId: 201, process: 'screen' }\n ]\n },\n\n // [통합테스트 2] 현대건설 - 힐스테이트 판교 (A등급, 스크린+슬랫 혼합)\n {\n id: 202,\n quoteNo: 'KD-PR-251217-02',\n quoteDate: '2025-12-17',\n status: '수주전환',\n productName: '스크린/슬랫 셔터 혼합',\n productCode: 'MIX-001',\n qty: 4,\n totalAmount: 32000000,\n customerId: 2, // C-002 현대건설(주)\n customerCode: 'CUS-002',\n customerName: '현대건설(주)',\n creditGrade: 'A',\n siteId: 11, // S-011\n siteName: '힐스테이트 판교 더 퍼스트',\n siteCode: 'S-011',\n manager: '박현장',\n contact: '010-2345-6789',\n deliveryAddress: '경기도 성남시 분당구 판교로 200',\n dueDate: '2026-01-20',\n createdBy: '판매1팀 이영업',\n note: '[통합테스트2] 스크린+슬랫 혼합 공정 분리 테스트',\n relatedOrders: ['KD-TS-251217-02'],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 32000000,\n items: [\n { id: 1, floor: 'B1', location: 'B-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 4000, height: 3000, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 2, wingIndex: 60, inspectionFee: 50000, unitPrice: 9000000, amount: 18000000, orderStatus: 'converted', orderId: 202, process: 'screen' },\n { id: 2, floor: '1층', location: 'B-02', category: '슬랫', productName: '철재 슬랫 셔터', width: 3500, height: 2500, guideType: '벽면형', motorPower: '220V', controller: '노출형', qty: 2, wingIndex: 55, inspectionFee: 50000, unitPrice: 7000000, amount: 14000000, orderStatus: 'converted', orderId: 202, process: 'slat' }\n ]\n },\n\n // [통합테스트 3] 대우건설 - 푸르지오 일산 (A등급, 대형 스크린)\n {\n id: 203,\n quoteNo: 'KD-PR-251217-03',\n quoteDate: '2025-12-17',\n status: '수주전환',\n productName: '스크린 셔터 (대형)',\n productCode: 'SH6040',\n qty: 2,\n totalAmount: 28000000,\n customerId: 3, // C-003 대우건설(주)\n customerCode: 'CUS-003',\n customerName: '대우건설(주)',\n creditGrade: 'A',\n siteId: 21, // S-021\n siteName: '푸르지오 일산 센트럴파크',\n siteCode: 'S-021',\n manager: '최건설',\n contact: '010-3456-7890',\n deliveryAddress: '경기도 고양시 일산동구 마두동 500',\n dueDate: '2026-01-25',\n createdBy: '판매2팀 박영업',\n note: '[통합테스트3] 대형 스크린(6m×4m) 모터용량 500KG 테스트',\n relatedOrders: ['KD-TS-251217-03'],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 28000000,\n items: [\n { id: 1, floor: '로비', location: 'C-01', category: '스크린', productName: '스크린 셔터 (대형)', width: 6000, height: 4000, guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, wingIndex: 80, inspectionFee: 80000, unitPrice: 14000000, amount: 14000000, orderStatus: 'converted', orderId: 203, process: 'screen' },\n { id: 2, floor: '연회장', location: 'C-02', category: '스크린', productName: '스크린 셔터 (대형)', width: 6000, height: 4000, guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, wingIndex: 80, inspectionFee: 80000, unitPrice: 14000000, amount: 14000000, orderStatus: 'converted', orderId: 203, process: 'screen' }\n ]\n },\n\n // [통합테스트 4] GS건설 - 자이 위례 (A등급, 슬랫 전용)\n {\n id: 204,\n quoteNo: 'KD-PR-251217-04',\n quoteDate: '2025-12-17',\n status: '수주전환',\n productName: '철재 슬랫 셔터',\n productCode: 'ST4030',\n qty: 5,\n totalAmount: 35000000,\n customerId: 4, // C-004 GS건설(주)\n customerCode: 'CUS-004',\n customerName: 'GS건설(주)',\n creditGrade: 'A',\n siteId: 31, // S-031\n siteName: '자이 위례 더 퍼스트',\n siteCode: 'S-031',\n manager: '정건설',\n contact: '010-4567-8901',\n deliveryAddress: '경기도 성남시 수정구 위례동 100',\n dueDate: '2026-01-30',\n createdBy: '판매2팀 최영업',\n note: '[통합테스트4] 슬랫 전용 공정 테스트',\n relatedOrders: ['KD-TS-251217-04'],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 35000000,\n items: [\n { id: 1, floor: 'B1', location: 'D-01', category: '슬랫', productName: '철재 슬랫 셔터', width: 4000, height: 3000, guideType: '벽면형', motorPower: '220V', controller: '노출형', qty: 3, wingIndex: 60, inspectionFee: 50000, unitPrice: 7000000, amount: 21000000, orderStatus: 'converted', orderId: 204, process: 'slat' },\n { id: 2, floor: '1층', location: 'D-02', category: '슬랫', productName: '철재 슬랫 셔터', width: 4000, height: 3000, guideType: '코너형', motorPower: '220V', controller: '노출형', qty: 2, wingIndex: 60, inspectionFee: 50000, unitPrice: 7000000, amount: 14000000, orderStatus: 'converted', orderId: 204, process: 'slat' }\n ]\n },\n\n // [통합테스트 5] 포스코건설 - 더샵 송도 (A등급, 코너형 가이드)\n {\n id: 205,\n quoteNo: 'KD-PR-251217-05',\n quoteDate: '2025-12-17',\n status: '수주전환',\n productName: '스크린 셔터 (코너형)',\n productCode: 'SH3530-C',\n qty: 3,\n totalAmount: 27000000,\n customerId: 5, // C-005 포스코건설(주)\n customerCode: 'CUS-005',\n customerName: '포스코건설(주)',\n creditGrade: 'A',\n siteId: 41, // S-041\n siteName: '더샵 송도 센트럴파크',\n siteCode: 'S-041',\n manager: '강건설',\n contact: '010-5678-9012',\n deliveryAddress: '인천시 연수구 송도동 200',\n dueDate: '2026-02-05',\n createdBy: '판매1팀 김영업',\n note: '[통합테스트5] 코너형 가이드레일 BOM 산출 테스트',\n relatedOrders: ['KD-TS-251217-05'],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 27000000,\n items: [\n { id: 1, floor: '1층', location: 'E-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3500, height: 3000, guideType: '코너형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 60, inspectionFee: 50000, unitPrice: 9000000, amount: 9000000, orderStatus: 'converted', orderId: 205, process: 'screen' },\n { id: 2, floor: '2층', location: 'E-02', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3500, height: 3000, guideType: '코너형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 60, inspectionFee: 50000, unitPrice: 9000000, amount: 9000000, orderStatus: 'converted', orderId: 205, process: 'screen' },\n { id: 3, floor: '3층', location: 'E-03', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3500, height: 3000, guideType: '코너형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 60, inspectionFee: 50000, unitPrice: 9000000, amount: 9000000, orderStatus: 'converted', orderId: 205, process: 'screen' }\n ]\n },\n\n // [통합테스트 6] 롯데건설 - 캐슬 잠실 (B등급, 경리승인 필요)\n {\n id: 206,\n quoteNo: 'KD-PR-251217-06',\n quoteDate: '2025-12-17',\n status: '최종확정',\n productName: '스크린 셔터 (표준형)',\n productCode: 'SH4028',\n qty: 4,\n totalAmount: 36000000,\n customerId: 6, // C-006 롯데건설(주)\n customerCode: 'CUS-006',\n customerName: '롯데건설(주)',\n creditGrade: 'B',\n siteId: 51, // S-051\n siteName: '캐슬 잠실 파인시티',\n siteCode: 'S-051',\n manager: '윤건설',\n contact: '010-6789-0123',\n deliveryAddress: '서울시 송파구 잠실동 300',\n dueDate: '2026-02-10',\n createdBy: '판매2팀 이영업',\n note: '[통합테스트6] B등급 경리승인 대기 테스트',\n relatedOrders: [],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 36000000,\n items: [\n { id: 1, floor: 'B1', location: 'F-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 4000, height: 2800, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 2, wingIndex: 55, inspectionFee: 50000, unitPrice: 9000000, amount: 18000000, orderStatus: 'pending', orderId: null, process: 'screen' },\n { id: 2, floor: '1층', location: 'F-02', category: '스크린', productName: '스크린 셔터 (표준형)', width: 4000, height: 2800, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 2, wingIndex: 55, inspectionFee: 50000, unitPrice: 9000000, amount: 18000000, orderStatus: 'pending', orderId: null, process: 'screen' }\n ]\n },\n\n // [통합테스트 7] 호반건설 - 써밋 광교 (A등급, 분할출하)\n {\n id: 207,\n quoteNo: 'KD-PR-251217-07',\n quoteDate: '2025-12-17',\n status: '수주전환',\n productName: '스크린/슬랫 셔터 혼합',\n productCode: 'MIX-002',\n qty: 6,\n totalAmount: 48000000,\n customerId: 7, // C-007 호반건설(주)\n customerCode: 'CUS-007',\n customerName: '호반건설(주)',\n creditGrade: 'A',\n siteId: 61, // S-061\n siteName: '써밋 광교 센트럴시티',\n siteCode: 'S-061',\n manager: '서건설',\n contact: '010-7890-1234',\n deliveryAddress: '경기도 수원시 영통구 광교동 400',\n dueDate: '2026-02-15',\n createdBy: '판매1팀 박영업',\n note: '[통합테스트7] 분할출하(1차/2차) 테스트',\n relatedOrders: ['KD-TS-251217-07'],\n discountRate: 5,\n discountAmount: 2400000,\n finalAmount: 45600000,\n items: [\n { id: 1, floor: 'B1', location: 'G-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3500, height: 2800, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 2, wingIndex: 55, inspectionFee: 50000, unitPrice: 8500000, amount: 17000000, orderStatus: 'converted', orderId: 207, process: 'screen' },\n { id: 2, floor: 'B2', location: 'G-02', category: '슬랫', productName: '철재 슬랫 셔터', width: 4000, height: 3000, guideType: '벽면형', motorPower: '220V', controller: '노출형', qty: 2, wingIndex: 60, inspectionFee: 50000, unitPrice: 7500000, amount: 15000000, orderStatus: 'converted', orderId: 207, process: 'slat' },\n { id: 3, floor: '1층', location: 'G-03', category: '스크린', productName: '스크린 셔터 (대형)', width: 5000, height: 3500, guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 2, wingIndex: 70, inspectionFee: 70000, unitPrice: 8000000, amount: 16000000, orderStatus: 'converted', orderId: 207, process: 'screen' }\n ]\n },\n\n // [통합테스트 8] 한화건설 - 포레나 수지 (A등급, 추가분 수주)\n {\n id: 208,\n quoteNo: 'KD-PR-251217-08',\n quoteDate: '2025-12-17',\n status: '수주전환',\n productName: '스크린 셔터 (표준형)',\n productCode: 'SH3025',\n qty: 2,\n totalAmount: 16000000,\n customerId: 8, // C-008 한화건설(주)\n customerCode: 'CUS-008',\n customerName: '한화건설(주)',\n creditGrade: 'A',\n siteId: 71, // S-071\n siteName: '포레나 수지 더 센트럴',\n siteCode: 'S-071',\n manager: '한건설',\n contact: '010-8901-2345',\n deliveryAddress: '경기도 용인시 수지구 풍덕천동 500',\n dueDate: '2026-02-20',\n createdBy: '판매2팀 최영업',\n note: '[통합테스트8] 추가분 수주(원수주 연결) 테스트',\n relatedOrders: ['KD-TS-251217-08'],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 16000000,\n items: [\n { id: 1, floor: '4층', location: 'H-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 50, inspectionFee: 50000, unitPrice: 8000000, amount: 8000000, orderStatus: 'converted', orderId: 208, process: 'screen' },\n { id: 2, floor: '5층', location: 'H-02', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3000, height: 2500, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 1, wingIndex: 50, inspectionFee: 50000, unitPrice: 8000000, amount: 8000000, orderStatus: 'converted', orderId: 208, process: 'screen' }\n ]\n },\n\n // [통합테스트 9] 태영건설 - 데시앙 동탄 (A등급, 품질불량→재작업)\n {\n id: 209,\n quoteNo: 'KD-PR-251217-09',\n quoteDate: '2025-12-17',\n status: '수주전환',\n productName: '스크린 셔터 (프리미엄)',\n productCode: 'SH4535',\n qty: 3,\n totalAmount: 33000000,\n customerId: 9, // C-009 태영건설(주)\n customerCode: 'CUS-009',\n customerName: '태영건설(주)',\n creditGrade: 'A',\n siteId: 81, // S-081\n siteName: '데시앙 동탄 파크뷰',\n siteCode: 'S-081',\n manager: '조건설',\n contact: '010-9012-3456',\n deliveryAddress: '경기도 화성시 동탄동 600',\n dueDate: '2026-02-25',\n createdBy: '판매1팀 김영업',\n note: '[통합테스트9] 품질불량 → 재작업 플로우 테스트',\n relatedOrders: ['KD-TS-251217-09'],\n discountRate: 0,\n discountAmount: 0,\n finalAmount: 33000000,\n items: [\n { id: 1, floor: '로비', location: 'I-01', category: '스크린', productName: '스크린 셔터 (프리미엄)', width: 4500, height: 3500, guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, wingIndex: 70, inspectionFee: 60000, unitPrice: 11000000, amount: 11000000, orderStatus: 'converted', orderId: 209, process: 'screen' },\n { id: 2, floor: '카페', location: 'I-02', category: '스크린', productName: '스크린 셔터 (프리미엄)', width: 4500, height: 3500, guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, wingIndex: 70, inspectionFee: 60000, unitPrice: 11000000, amount: 11000000, orderStatus: 'converted', orderId: 209, process: 'screen' },\n { id: 3, floor: '헬스장', location: 'I-03', category: '스크린', productName: '스크린 셔터 (프리미엄)', width: 4500, height: 3500, guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, wingIndex: 70, inspectionFee: 60000, unitPrice: 11000000, amount: 11000000, orderStatus: 'converted', orderId: 209, process: 'screen' }\n ]\n },\n\n // [통합테스트 10] 두산건설 - 위브 청라 (A등급, 전체 완료 시나리오)\n {\n id: 210,\n quoteNo: 'KD-PR-251217-10',\n quoteDate: '2025-12-17',\n status: '수주전환',\n productName: '스크린/슬랫 셔터 혼합',\n productCode: 'MIX-003',\n qty: 5,\n totalAmount: 40000000,\n customerId: 10, // C-010 두산건설(주)\n customerCode: 'CUS-010',\n customerName: '두산건설(주)',\n creditGrade: 'A',\n siteId: 91, // S-091\n siteName: '위브 청라 더 퍼스트',\n siteCode: 'S-091',\n manager: '임건설',\n contact: '010-0123-4567',\n deliveryAddress: '인천시 서구 청라동 700',\n dueDate: '2026-03-01',\n createdBy: '판매2팀 이영업',\n note: '[통합테스트10] 전체 완료 시나리오 (견적→수주→생산→품질→출하→회계)',\n relatedOrders: ['KD-TS-251217-10'],\n discountRate: 3,\n discountAmount: 1200000,\n finalAmount: 38800000,\n items: [\n { id: 1, floor: 'B1', location: 'J-01', category: '스크린', productName: '스크린 셔터 (표준형)', width: 3500, height: 2800, guideType: '벽면형', motorPower: '220V', controller: '매립형', qty: 2, wingIndex: 55, inspectionFee: 50000, unitPrice: 8500000, amount: 17000000, orderStatus: 'converted', orderId: 210, process: 'screen' },\n { id: 2, floor: '1층', location: 'J-02', category: '슬랫', productName: '철재 슬랫 셔터', width: 4000, height: 3000, guideType: '벽면형', motorPower: '220V', controller: '노출형', qty: 2, wingIndex: 60, inspectionFee: 50000, unitPrice: 7000000, amount: 14000000, orderStatus: 'converted', orderId: 210, process: 'slat' },\n { id: 3, floor: '2층', location: 'J-03', category: '스크린', productName: '스크린 셔터 (대형)', width: 5000, height: 3500, guideType: '벽면형', motorPower: '380V', controller: '매립형', qty: 1, wingIndex: 70, inspectionFee: 70000, unitPrice: 9000000, amount: 9000000, orderStatus: 'converted', orderId: 210, process: 'screen' }\n ]\n },\n];\n\n// ============ 거래처 관리 ============\n\n// 신용등급 배지 컴포넌트\nconst CreditGradeBadge = ({ grade, showLabel = true }) => {\n const colors = {\n 'A': 'bg-green-100 text-green-700 border-green-200',\n 'B': 'bg-yellow-100 text-yellow-700 border-yellow-200',\n 'C': 'bg-red-100 text-red-700 border-red-200',\n };\n const labels = { 'A': '우량', 'B': '관리', 'C': '위험' };\n return (\n
\n {grade}{showLabel && labels[grade] ? ` (${labels[grade]})` : ''}\n \n );\n};\n\n// ============ 공정관리 ============\nconst ProcessList = ({ processes = sampleProcesses, onNavigate }) => {\n const [search, setSearch] = useState('');\n const [activeTab, setActiveTab] = useState('all');\n const [processList, setProcessList] = useState(processes);\n\n const tabs = [\n { id: 'all', label: '전체', count: processList.length },\n { id: 'active', label: '사용중', count: processList.filter(p => p.isActive).length },\n { id: 'inactive', label: '미사용', count: processList.filter(p => !p.isActive).length },\n ];\n\n const filtered = processList\n .filter(p => activeTab === 'all' || (activeTab === 'active' ? p.isActive : !p.isActive))\n .filter(p =>\n p.processCode.toLowerCase().includes(search.toLowerCase()) ||\n p.processName.toLowerCase().includes(search.toLowerCase()) ||\n p.department.toLowerCase().includes(search.toLowerCase())\n )\n .sort((a, b) => b.id - a.id);\n\n // 목록 선택 훅\n const { selectedIds, handleSelect, handleSelectAll, isAllSelected, hasSelection, isMultiSelect, isSelected } = useListSelection(filtered);\n\n const handleCreate = () => {\n onNavigate('process-create');\n };\n\n const handleEdit = (process) => {\n onNavigate('process-edit', process);\n };\n\n const handleView = (process) => {\n onNavigate('process-detail', process);\n };\n\n const handleDelete = (process) => {\n if (window.confirm(`${process.processName} 공정을 삭제하시겠습니까?`)) {\n setProcessList(prev => prev.filter(p => p.id !== process.id));\n }\n };\n\n const handleBulkDelete = () => {\n if (window.confirm(`선택한 ${selectedIds.length}건을 삭제하시겠습니까?`)) {\n setProcessList(prev => prev.filter(p => !selectedIds.includes(p.id)));\n }\n };\n\n const handleToggleActive = (process) => {\n setProcessList(prev => prev.map(p =>\n p.id === process.id ? { ...p, isActive: !p.isActive, updatedAt: new Date().toISOString().split('T')[0] } : p\n ));\n };\n\n return (\n
\n {/* 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n
\n \n 공정 목록\n
\n
\n {isMultiSelect && (\n
\n )}\n
\n
\n
\n\n {/* 탭 + 검색 */}\n
\n \n
\n {tabs.map((tab) => (\n \n ))}\n
\n
\n
\n\n {/* 목록 테이블 */}\n \n\n {filtered.length === 0 && (\n \n 검색 결과가 없습니다\n
\n )}\n \n
\n );\n};\n\n// 공정 등록/수정 폼\nconst ProcessForm = ({ process, onSave, onBack }) => {\n // 부서 목록\n const departments = [\n { code: 'SCREEN', name: '스크린생산부서' },\n { code: 'FOLD', name: '절곡생산부서' },\n { code: 'SLAT', name: '슬랫생산부서' },\n { code: 'QC', name: '품질관리부서' },\n { code: 'PACK', name: '포장/출하부서' },\n ];\n\n // 작업일지 양식 목록 (문서양식관리에서 가져옴)\n const workSheetTemplates = (documentTemplateConfig.documentTypes || [])\n .filter(doc => doc.code && doc.code.startsWith('WL-') && doc.isActive)\n .map(doc => ({ code: doc.code, name: doc.name, description: doc.description }));\n\n // 자동 분류 규칙용 조건 옵션\n const ruleConditionFields = [\n { value: 'itemName', label: '품목명' },\n { value: 'itemCode', label: '품목코드' },\n { value: 'category', label: '품목분류' },\n { value: 'material', label: '원자재' },\n { value: 'size', label: '규격' },\n ];\n\n const ruleConditionOperators = [\n { value: 'startsWith', label: '~로 시작' },\n { value: 'endsWith', label: '~로 끝남' },\n { value: 'contains', label: '~를 포함' },\n { value: 'equals', label: '정확히 일치' },\n ];\n\n // 등록 방식 (패턴 규칙 vs 개별 품목)\n const registrationMethods = [\n { value: 'pattern', label: '패턴 규칙 (코드/명칭 기반 자동 분류)' },\n { value: 'individual', label: '개별 품목 (특정 품목 직접 지정)' },\n ];\n\n // 규칙 유형 (품목코드 또는 품목명)\n const ruleTypes = [\n { value: 'itemCode', label: '품목코드' },\n { value: 'itemName', label: '품목명' },\n ];\n\n const [formData, setFormData] = useState({\n processName: process?.processName || '',\n processType: process?.processType || '생산',\n department: process?.department || '',\n workSheetType: process?.workSheetType || '',\n isActive: process?.isActive ?? true,\n workSteps: process?.workSteps?.join(', ') || '',\n // 작업 정보\n requiredWorkers: process?.requiredWorkers || '1',\n equipmentInfo: process?.equipmentInfo || '',\n // 자동 분류 규칙\n autoClassifyRules: process?.autoClassifyRules || [],\n // 설명\n description: process?.description || '',\n });\n\n // 규칙 추가 다이얼로그 상태\n const [showRuleModal, setShowRuleModal] = useState(false);\n const [editingRule, setEditingRule] = useState(null);\n const [ruleForm, setRuleForm] = useState({\n registrationMethod: 'pattern', // 'pattern' or 'individual'\n field: 'itemCode',\n operator: 'startsWith',\n value: '',\n priority: 10,\n description: '',\n matchedItems: [], // 패턴 매칭된 품목 리스트\n });\n const [isSearching, setIsSearching] = useState(false);\n\n // 샘플 품목 데이터 (itemMasterConfig에서 가져옴)\n // 품목코드 규칙:\n // - 부자재/원자재/소모품: 품목명-규격\n // - 구매부품: 품목명-규격 (규격 = 전원+용량)\n // - 절곡부품: 품목코드+종류코드+모양&길이코드 (예: RM24)\n // - 제품: 품목명 = 품목코드\n const sampleItemsData = [\n // 부자재 - 규칙: 품목명-규격\n { id: 26, itemCode: '스크린원단-0.3T', itemName: '스크린원단', spec: '0.3T', itemType: '부자재' },\n { id: 27, itemCode: '스크린원단-0.5T', itemName: '스크린원단', spec: '0.5T', itemType: '부자재' },\n { id: 28, itemCode: '슬랫-75mm', itemName: '슬랫', spec: '75mm', itemType: '부자재' },\n { id: 29, itemCode: '슬랫-100mm', itemName: '슬랫', spec: '100mm', itemType: '부자재' },\n { id: 30, itemCode: '가이드레일-60×60', itemName: '가이드레일', spec: '60×60', itemType: '부자재' },\n { id: 31, itemCode: '가이드레일-80×80', itemName: '가이드레일', spec: '80×80', itemType: '부자재' },\n { id: 32, itemCode: '샤프트-4인치', itemName: '샤프트', spec: '4인치', itemType: '부자재' },\n { id: 33, itemCode: '샤프트-6인치', itemName: '샤프트', spec: '6인치', itemType: '부자재' },\n { id: 34, itemCode: '스프링-중형', itemName: '스프링', spec: '중형', itemType: '부자재' },\n { id: 35, itemCode: '체인-#35', itemName: '체인', spec: '#35', itemType: '부자재' },\n // 원자재 - 규칙: 품목명-규격\n { id: 36, itemCode: '철판-1.2T', itemName: '철판', spec: '1.2T', itemType: '원자재' },\n { id: 37, itemCode: '철판-1.6T', itemName: '철판', spec: '1.6T', itemType: '원자재' },\n { id: 38, itemCode: '아연도금강판-1.0T', itemName: '아연도금강판', spec: '1.0T', itemType: '원자재' },\n { id: 39, itemCode: '아연도금강판-1.6T', itemName: '아연도금강판', spec: '1.6T', itemType: '원자재' },\n { id: 40, itemCode: '스테인리스-1.0T', itemName: '스테인리스', spec: '1.0T', itemType: '원자재' },\n { id: 41, itemCode: '스테인리스-1.5T', itemName: '스테인리스', spec: '1.5T', itemType: '원자재' },\n { id: 42, itemCode: '알루미늄판-1.5T', itemName: '알루미늄판', spec: '1.5T', itemType: '원자재' },\n { id: 43, itemCode: '알루미늄판-2.0T', itemName: '알루미늄판', spec: '2.0T', itemType: '원자재' },\n { id: 44, itemCode: '각파이프-40×40', itemName: '각파이프', spec: '40×40', itemType: '원자재' },\n { id: 45, itemCode: '각파이프-50×50', itemName: '각파이프', spec: '50×50', itemType: '원자재' },\n // 구매부품 - 규칙: 품목명-규격 (규격 = 전원+용량, 공백제거)\n { id: 21, itemCode: '전동개폐기-220V300KG', itemName: '전동개폐기', power: '220V', capacity: '300KG', itemType: '부품', subType: '구매 부품' },\n { id: 22, itemCode: '전동개폐기-380V600KG', itemName: '전동개폐기', power: '380V', capacity: '600KG', itemType: '부품', subType: '구매 부품' },\n { id: 23, itemCode: '제어반-220V500KG', itemName: '제어반', power: '220V', capacity: '500KG', itemType: '부품', subType: '구매 부품' },\n { id: 24, itemCode: '연기감지기-220V150KG', itemName: '연기감지기', power: '220V', capacity: '150KG', itemType: '부품', subType: '구매 부품' },\n { id: 25, itemCode: '수동개폐기-220V400KG', itemName: '수동개폐기', power: '220V', capacity: '400KG', itemType: '부품', subType: '구매 부품' },\n // 조립부품 - 규칙: 품목명 설치유형-측면규격(가로)*측면규격(세로)*길이(앞2자리)\n // 예: 가이드레일 벽면형-1211*1111*24\n { id: 11, itemCode: '셔터박스 벽면형-3000*3500*30', itemName: '셔터박스', installType: '벽면형', sideWidth: '3000', sideHeight: '3500', length: '3000', itemType: '부품', subType: '조립 부품' },\n { id: 12, itemCode: '슬랫조립체 벽면형-2500*3000*24', itemName: '슬랫조립체', installType: '벽면형', sideWidth: '2500', sideHeight: '3000', length: '2438', itemType: '부품', subType: '조립 부품' },\n { id: 13, itemCode: '가이드조립체 측면형-1200*1100*30', itemName: '가이드조립체', installType: '측면형', sideWidth: '1200', sideHeight: '1100', length: '3000', itemType: '부품', subType: '조립 부품' },\n { id: 14, itemCode: '개폐기조립체 벽면형-400*350*24', itemName: '개폐기조립체', installType: '벽면형', sideWidth: '400', sideHeight: '350', length: '2438', itemType: '부품', subType: '조립 부품' },\n { id: 15, itemCode: '제어반조립체 벽면형-500*400*35', itemName: '제어반조립체', installType: '벽면형', sideWidth: '500', sideHeight: '400', length: '3500', itemType: '부품', subType: '조립 부품' },\n // 절곡부품 - 규칙: 품목코드+종류코드+모양&길이코드 (예: RM24)\n { id: 16, itemCode: 'RM24', itemName: '가이드레일(벽면형)', partType: '본체', length: '2438', itemType: '부품', subType: '절곡 부품' },\n { id: 17, itemCode: 'SM30', itemName: '가이드레일(측면형)', partType: '본체', length: '3000', itemType: '부품', subType: '절곡 부품' },\n { id: 18, itemCode: 'CB24', itemName: '케이스', partType: '후면코너부', length: '2438', itemType: '부품', subType: '절곡 부품' },\n { id: 19, itemCode: 'BE30', itemName: '하단마감재(스크린)', partType: 'EGI', length: '3000', itemType: '부품', subType: '절곡 부품' },\n { id: 20, itemCode: 'LA24', itemName: 'L-Bar', partType: '스크린용', length: '2438', itemType: '부품', subType: '절곡 부품' },\n // 제품 - 규칙: 품목명 = 품목코드\n { id: 1, itemCode: '방화셔터 W3000×H4000', itemName: '방화셔터 W3000×H4000', itemType: '제품' },\n { id: 2, itemCode: '방화셔터 W2500×H3000', itemName: '방화셔터 W2500×H3000', itemType: '제품' },\n { id: 3, itemCode: '방연셔터 W3500×H4000', itemName: '방연셔터 W3500×H4000', itemType: '제품' },\n { id: 4, itemCode: '스틸셔터 W2500×H3000', itemName: '스틸셔터 W2500×H3000', itemType: '제품' },\n { id: 5, itemCode: '방화셔터 W5000×H5000', itemName: '방화셔터 W5000×H5000', itemType: '제품' },\n ];\n\n // 다른 공정에 이미 배정된 품목 ID 목록 계산 (현재 공정 제외)\n const getAssignedItemIds = () => {\n const assignedIds = new Set();\n // sampleProcesses에서 현재 공정이 아닌 모든 공정의 배정된 품목 수집\n sampleProcesses.forEach(proc => {\n if (proc.id !== process?.id && proc.autoClassifyRules) {\n proc.autoClassifyRules.forEach(rule => {\n if (rule.matchedItems) {\n rule.matchedItems.forEach(item => assignedIds.add(item.id));\n }\n // savedItems도 확인 (선별 저장된 품목)\n if (rule.savedItems) {\n rule.savedItems.forEach(item => assignedIds.add(item.id));\n }\n });\n }\n // 공정에 직접 배정된 품목도 확인\n if (proc.id !== process?.id && proc.assignedItems) {\n proc.assignedItems.forEach(item => assignedIds.add(item.id));\n }\n });\n return assignedIds;\n };\n\n // 선별된 품목 상태 (체크박스로 선택)\n const [selectedItemIds, setSelectedItemIds] = useState(new Set());\n // 이미 다른 공정에 배정된 품목 표시 여부\n const [showAssignedWarning, setShowAssignedWarning] = useState(false);\n // 개별 품목 선택 모드용 검색어\n const [individualSearchTerm, setIndividualSearchTerm] = useState('');\n // 개별 품목 선택 모드용 품목 유형 필터\n const [individualTypeFilter, setIndividualTypeFilter] = useState('all');\n\n // 패턴 매칭으로 품목 검색 (다른 공정에 배정된 품목 제외)\n const searchMatchingItems = () => {\n if (!ruleForm.value.trim()) {\n alert('조건 값을 입력해주세요.');\n return;\n }\n\n setIsSearching(true);\n\n // 다른 공정에 이미 배정된 품목 ID 집합\n const assignedIds = getAssignedItemIds();\n\n // 패턴 매칭 로직\n const searchValue = ruleForm.value.toLowerCase();\n const fieldKey = ruleForm.field === 'itemCode' ? 'itemCode' : 'itemName';\n\n // 전체 매칭된 품목\n const allMatchedItems = sampleItemsData.filter(item => {\n const fieldValue = (item[fieldKey] || '').toLowerCase();\n\n switch (ruleForm.operator) {\n case 'startsWith':\n return fieldValue.startsWith(searchValue);\n case 'endsWith':\n return fieldValue.endsWith(searchValue);\n case 'contains':\n return fieldValue.includes(searchValue);\n case 'equals':\n return fieldValue === searchValue;\n default:\n return false;\n }\n });\n\n // 다른 공정에 배정된 품목은 제외하고, isAssigned 플래그 추가\n const matchedItems = allMatchedItems\n .filter(item => !assignedIds.has(item.id)) // 다른 공정에 배정된 품목 제외\n .map(item => ({\n ...item,\n isAssigned: false,\n }));\n\n // 제외된 품목이 있으면 경고 표시\n const excludedCount = allMatchedItems.length - matchedItems.length;\n if (excludedCount > 0) {\n setShowAssignedWarning(true);\n } else {\n setShowAssignedWarning(false);\n }\n\n // 검색 결과가 있으면 모두 선택 상태로 초기화\n setSelectedItemIds(new Set(matchedItems.map(item => item.id)));\n\n setRuleForm(prev => ({ ...prev, matchedItems, excludedCount }));\n setIsSearching(false);\n };\n\n // 규칙 추가 다이얼로그 열기\n const openRuleModal = (rule = null) => {\n if (rule) {\n setEditingRule(rule);\n setRuleForm({\n registrationMethod: rule.registrationMethod || 'pattern',\n field: rule.field || 'itemCode',\n operator: rule.operator || 'startsWith',\n value: rule.value || '',\n priority: rule.priority || 10,\n description: rule.description || '',\n matchedItems: rule.matchedItems || [],\n });\n // 개별 품목 선택 모드인 경우 기존 선택된 품목 로드\n if (rule.registrationMethod === 'individual' && rule.savedItems) {\n setSelectedItemIds(new Set(rule.savedItems.map(item => item.id)));\n } else if (rule.matchedItems) {\n // 패턴 규칙 모드인 경우 매칭된 품목 로드\n setSelectedItemIds(new Set(rule.savedItems?.map(item => item.id) || rule.matchedItems.map(item => item.id)));\n } else {\n setSelectedItemIds(new Set());\n }\n // 개별 품목 검색/필터 초기화\n setIndividualSearchTerm('');\n setIndividualTypeFilter('all');\n } else {\n setEditingRule(null);\n setRuleForm({\n registrationMethod: 'pattern',\n field: 'itemCode',\n operator: 'startsWith',\n value: '',\n priority: 10,\n description: '',\n matchedItems: [],\n });\n setSelectedItemIds(new Set());\n setIndividualSearchTerm('');\n setIndividualTypeFilter('all');\n }\n setShowRuleModal(true);\n };\n\n // 품목 선택/해제 토글\n const toggleItemSelection = (itemId) => {\n setSelectedItemIds(prev => {\n const newSet = new Set(prev);\n if (newSet.has(itemId)) {\n newSet.delete(itemId);\n } else {\n newSet.add(itemId);\n }\n return newSet;\n });\n };\n\n // 전체 선택/해제\n const toggleSelectAll = () => {\n if (selectedItemIds.size === ruleForm.matchedItems.length) {\n // 전체 해제\n setSelectedItemIds(new Set());\n } else {\n // 전체 선택\n setSelectedItemIds(new Set(ruleForm.matchedItems.map(item => item.id)));\n }\n };\n\n // 개별 품목 선택 모드: 필터링된 품목 리스트\n const getFilteredItemsForIndividual = () => {\n const assignedIds = getAssignedItemIds();\n\n return sampleItemsData\n .filter(item => !assignedIds.has(item.id)) // 다른 공정에 배정된 품목 제외\n .filter(item => {\n // 품목 유형 필터\n if (individualTypeFilter !== 'all' && item.itemType !== individualTypeFilter) {\n return false;\n }\n // 검색어 필터\n if (individualSearchTerm) {\n const searchLower = individualSearchTerm.toLowerCase();\n return (\n item.itemCode.toLowerCase().includes(searchLower) ||\n item.itemName.toLowerCase().includes(searchLower)\n );\n }\n return true;\n });\n };\n\n // 개별 품목 선택 모드: 전체 선택/해제\n const toggleSelectAllIndividual = () => {\n const filteredItems = getFilteredItemsForIndividual();\n if (selectedItemIds.size === filteredItems.length && filteredItems.length > 0) {\n // 전체 해제\n setSelectedItemIds(new Set());\n } else {\n // 전체 선택\n setSelectedItemIds(new Set(filteredItems.map(item => item.id)));\n }\n };\n\n // 품목 유형 목록\n const itemTypes = ['제품', '부품', '부자재', '원자재'];\n\n // 규칙 저장 (선별된 품목만 저장)\n const saveRule = () => {\n // 개별 품목 선택 모드인 경우\n if (ruleForm.registrationMethod === 'individual') {\n if (selectedItemIds.size === 0) {\n alert('최소 1개 이상의 품목을 선택해주세요.');\n return;\n }\n\n // 선택된 품목 정보 가져오기\n const selectedItems = sampleItemsData.filter(item => selectedItemIds.has(item.id));\n\n const ruleData = {\n id: editingRule?.id || Date.now(),\n registrationMethod: 'individual',\n field: 'individual',\n operator: 'manual',\n value: `개별 품목 ${selectedItems.length}개 선택`,\n priority: ruleForm.priority,\n description: ruleForm.description || `직접 선택한 품목 ${selectedItems.length}개`,\n // 선별된 품목 저장\n savedItems: selectedItems.map(item => ({\n id: item.id,\n itemCode: item.itemCode,\n itemName: item.itemName,\n itemType: item.itemType,\n })),\n matchedItems: selectedItems.map(item => ({\n id: item.id,\n itemCode: item.itemCode,\n itemName: item.itemName,\n itemType: item.itemType,\n })),\n savedCount: selectedItems.length,\n matchedCount: selectedItems.length,\n createdAt: editingRule?.createdAt || new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n if (editingRule) {\n setFormData(prev => ({\n ...prev,\n autoClassifyRules: prev.autoClassifyRules.map(r =>\n r.id === editingRule.id ? ruleData : r\n )\n }));\n } else {\n setFormData(prev => ({\n ...prev,\n autoClassifyRules: [...prev.autoClassifyRules, ruleData]\n }));\n }\n setShowRuleModal(false);\n setEditingRule(null);\n setSelectedItemIds(new Set());\n setIndividualSearchTerm('');\n setIndividualTypeFilter('all');\n setRuleForm({\n registrationMethod: 'pattern',\n field: 'itemCode',\n operator: 'startsWith',\n value: '',\n priority: 10,\n description: '',\n matchedItems: [],\n });\n return;\n }\n\n // 패턴 규칙 모드\n if (!ruleForm.value.trim()) {\n alert('조건 값을 입력해주세요.');\n return;\n }\n\n // 선별된 품목만 필터링\n const selectedItems = ruleForm.matchedItems.filter(item => selectedItemIds.has(item.id));\n\n // 패턴 규칙인 경우 선별된 품목이 없으면 경고\n if (selectedItems.length === 0) {\n if (!confirm('선택된 품목이 없습니다. 그래도 규칙을 저장하시겠습니까?')) {\n return;\n }\n }\n\n const ruleData = {\n id: editingRule?.id || Date.now(),\n registrationMethod: ruleForm.registrationMethod,\n field: ruleForm.field,\n operator: ruleForm.operator,\n value: ruleForm.value,\n priority: ruleForm.priority,\n description: ruleForm.description,\n // 선별된 품목만 savedItems로 저장\n savedItems: selectedItems.map(item => ({\n id: item.id,\n itemCode: item.itemCode,\n itemName: item.itemName,\n itemType: item.itemType,\n })),\n // 전체 매칭된 품목도 참조용으로 저장\n matchedItems: ruleForm.matchedItems.map(item => ({\n id: item.id,\n itemCode: item.itemCode,\n itemName: item.itemName,\n itemType: item.itemType,\n })),\n savedCount: selectedItems.length,\n matchedCount: ruleForm.matchedItems.length,\n createdAt: editingRule?.createdAt || new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n\n if (editingRule) {\n // 수정\n setFormData(prev => ({\n ...prev,\n autoClassifyRules: prev.autoClassifyRules.map(r =>\n r.id === editingRule.id ? ruleData : r\n )\n }));\n } else {\n // 추가\n setFormData(prev => ({\n ...prev,\n autoClassifyRules: [...prev.autoClassifyRules, ruleData]\n }));\n }\n setShowRuleModal(false);\n setEditingRule(null);\n setRuleForm({\n registrationMethod: 'pattern',\n field: 'itemCode',\n operator: 'startsWith',\n value: '',\n priority: 10,\n description: '',\n matchedItems: [],\n });\n };\n\n // 규칙 삭제\n const removeRule = (ruleId) => {\n setFormData(prev => ({\n ...prev,\n autoClassifyRules: prev.autoClassifyRules.filter(r => r.id !== ruleId)\n }));\n };\n\n const handleSubmit = () => {\n if (!formData.processName || !formData.department) {\n alert('공정명, 담당부서는 필수입니다.');\n return;\n }\n onSave({\n ...formData,\n workSteps: formData.workSteps.split(',').map(s => s.trim()).filter(s => s),\n });\n };\n\n return (\n
\n {/* 상단 헤더 */}\n
\n
\n
{process ? '공정 수정' : '공정 등록'}
\n
\n \n \n
\n
\n
\n\n {/* 기본 정보 섹션 */}\n
\n
\n
기본 정보
\n \n
\n
\n
\n \n setFormData(prev => ({ ...prev, processName: e.target.value }))}\n className=\"w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500\"\n placeholder=\"예: 스크린\"\n />\n
\n
\n \n \n
\n
\n\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n
\n\n {/* 자동 분류 규칙 섹션 */}\n
\n
\n
\n
자동 분류 규칙
\n
품목이 이 공정에 이동으로 분류되는 규칙을 생성합니다.
\n
\n
\n
\n
\n {formData.autoClassifyRules.length === 0 ? (\n
\n
\n
품목별 규칙이 없습니다
\n
규칙을 추가하면 해당 패턴의 품목이 이 공정으로 분류됩니다
\n
\n ) : (\n
\n {formData.autoClassifyRules.map((rule, index) => (\n
\n
\n
{index + 1}.\n
\n {/* 개별 품목 선택 방식인 경우 */}\n {rule.registrationMethod === 'individual' ? (\n
\n \n 개별 품목 지정\n \n -\n \n {rule.savedCount || rule.matchedCount || 0}개 품목\n \n
\n ) : (\n /* 패턴 규칙 방식인 경우 */\n
\n \n {ruleTypes.find(t => t.value === rule.field)?.label || rule.field}\n \n 이(가)\n \n {rule.value}\n \n \n {ruleConditionOperators.find(op => op.value === rule.operator)?.label || rule.operator}\n \n
\n )}\n {/* 저장된/매칭된 품목 수 표시 */}\n {(rule.savedCount > 0 || rule.matchedCount > 0) && (\n
\n {rule.savedCount > 0 ? (\n \n {rule.savedCount}개 품목 배정됨\n \n ) : rule.matchedCount > 0 ? (\n \n {rule.matchedCount}개 품목 매칭\n \n ) : null}\n \n 우선순위: {rule.priority || 10}\n \n
\n )}\n {rule.description && (\n
{rule.description}
\n )}\n
\n
\n \n \n
\n
\n
\n ))}\n
\n )}\n
\n
\n\n {/* 규칙 추가/수정 다이얼로그 */}\n {showRuleModal && (\n
\n
\n
\n
\n {editingRule ? '규칙 수정' : '규칙 추가'}\n
\n \n \n
\n {/* 등록 방식 선택 */}\n
\n
\n
\n {registrationMethods.map(method => (\n \n ))}\n
\n
\n\n {/* 패턴 규칙 모드일 때만 표시 */}\n {ruleForm.registrationMethod === 'pattern' && (\n <>\n {/* 규칙 유형 선택 */}\n
\n \n \n
\n\n {/* 매칭 방식 선택 */}\n
\n \n \n
\n\n {/* 조건 값 입력 */}\n
\n
\n
\n setRuleForm(prev => ({ ...prev, value: e.target.value, matchedItems: [] }))}\n onKeyDown={(e) => e.key === 'Enter' && searchMatchingItems()}\n className=\"flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500\"\n placeholder={ruleForm.field === 'itemCode' ? '예: SCR-, E-, STEEL-' : '예: 스크린, 개폐기'}\n />\n \n
\n
Enter 키를 누르거나 검색 버튼을 클릭하세요
\n
\n\n {/* 우선순위 */}\n
\n
\n
setRuleForm(prev => ({ ...prev, priority: parseInt(e.target.value) || 10 }))}\n className=\"w-32 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500\"\n min=\"1\"\n max=\"100\"\n />\n
낮을수록 먼저 적용됩니다
\n
\n\n {/* 설명 */}\n
\n \n setRuleForm(prev => ({ ...prev, description: e.target.value }))}\n className=\"w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500\"\n placeholder=\"규칙에 대한 설명\"\n />\n
\n\n {/* 활성 상태 */}\n
\n 활성 상태\n \n
\n\n {/* 다른 공정 배정 품목 제외 경고 */}\n {showAssignedWarning && ruleForm.excludedCount > 0 && (\n
\n
\n
\n
\n {ruleForm.excludedCount}개 품목이 다른 공정에 이미 배정되어 제외되었습니다\n
\n
\n 하나의 품목은 하나의 공정에만 배정할 수 있습니다.\n
\n
\n
\n )}\n\n {/* 매칭된 품목 리스트 (선별 가능) */}\n {ruleForm.matchedItems.length > 0 && (\n
\n
\n
\n \n \n 매칭된 품목 ({ruleForm.matchedItems.length}개) | 선택됨 ({selectedItemIds.size}개)\n \n
\n
\n \n |\n \n
\n
\n
\n
\n 체크박스로 이 공정에 배정할 품목을 선별하세요. 선택된 품목만 저장됩니다.\n
\n
\n )}\n\n {/* 검색 후 결과 없음 */}\n {ruleForm.value && ruleForm.matchedItems.length === 0 && (\n
\n
\n
매칭된 품목이 없습니다
\n
검색 버튼을 클릭하여 품목을 검색하세요
\n
\n )}\n >\n )}\n\n {/* 개별 품목 선택 모드 */}\n {ruleForm.registrationMethod === 'individual' && (\n <>\n {/* 설명 */}\n
\n \n setRuleForm(prev => ({ ...prev, description: e.target.value }))}\n className=\"w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500\"\n placeholder=\"이 품목 그룹에 대한 설명\"\n />\n
\n\n {/* 품목 검색 및 필터 */}\n
\n
\n
\n
\n \n setIndividualSearchTerm(e.target.value)}\n className=\"w-full border border-gray-300 rounded-lg pl-9 pr-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500\"\n placeholder=\"품목코드 또는 품목명으로 검색...\"\n />\n
\n
\n
\n \n \n
\n
\n\n {/* 품목 리스트 */}\n
\n
\n
\n
\n
\n 품목 목록 ({getFilteredItemsForIndividual().length}개) | 선택됨 ({selectedItemIds.size}개)\n \n
\n
\n \n |\n \n
\n
\n
\n
\n 이 공정에 배정할 품목을 선택하세요. 다른 공정에 이미 배정된 품목은 표시되지 않습니다.\n
\n
\n\n {/* 선택된 품목 요약 */}\n {selectedItemIds.size > 0 && (\n
\n
\n \n \n 선택된 품목 ({selectedItemIds.size}개)\n \n
\n
\n {Array.from(selectedItemIds).slice(0, 10).map(id => {\n const item = sampleItemsData.find(i => i.id === id);\n return item ? (\n \n {item.itemCode}\n \n \n ) : null;\n })}\n {selectedItemIds.size > 10 && (\n \n +{selectedItemIds.size - 10}개 더\n \n )}\n
\n
\n )}\n >\n )}\n
\n
\n \n \n
\n
\n
\n )}\n\n {/* 작업 정보 섹션 */}\n
\n\n {/* 설명 섹션 */}\n
\n
\n
설명
\n \n
\n
\n \n
\n
\n setFormData(prev => ({ ...prev, isActive: e.target.checked }))}\n className=\"w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500\"\n />\n \n
\n
\n
\n
\n );\n};\n\n// 문서양식 미리보기 모달 컴포넌트\nconst DocumentTemplatePreviewModal = ({ docCode, onClose }) => {\n // 문서양식 템플릿 가져오기\n const template = documentTemplateConfig.documentTemplates?.[docCode];\n const docType = (documentTemplateConfig.documentTypes || []).find(d => d.code === docCode);\n\n if (!template && !docType) {\n return (\n
\n
\n
\n
양식을 찾을 수 없습니다
\n
문서코드: {docCode}
\n
\n
\n
\n );\n }\n\n // 블록 정보 가져오기 헬퍼\n const getBlockInfo = (blockId) => {\n const allBlocks = [\n ...(documentTemplateConfig.blockLibrary?.headers || []),\n ...(documentTemplateConfig.blockLibrary?.sections || []),\n ...(documentTemplateConfig.blockLibrary?.tables || []),\n ...(documentTemplateConfig.blockLibrary?.approvals || []),\n ...(documentTemplateConfig.blockLibrary?.footers || []),\n ];\n return allBlocks.find(b => b.id === blockId);\n };\n\n // 샘플 데이터 생성\n const getSampleData = (code) => {\n const samples = {\n 'WL-SCR': {\n orderDate: '2025-12-17',\n siteName: '송도 오피스텔 A동',\n customerName: '(주)인천건설',\n workDate: '2025-12-17',\n managerName: '김담당',\n lotNo: 'KD-TS-251217-01-01',\n productCode: 'SH3040',\n productName: '방화셔터 W3000×H4000',\n finishType: '스크린',\n finishSpec: '그레이',\n },\n 'WL-FLD': {\n orderDate: '2025-12-17',\n siteName: '강남 주상복합 B동',\n customerName: '(주)서울건설',\n workDate: '2025-12-17',\n managerName: '박담당',\n lotNo: 'KD-BD-251217-01-01',\n productCode: 'CB2430',\n productName: '커버박스 W2400×H3000',\n finishType: '절곡',\n finishSpec: 'SUS',\n },\n 'WL-SLT': {\n orderDate: '2025-12-17',\n siteName: '대전 물류센터',\n customerName: '(주)중부건설',\n workDate: '2025-12-17',\n managerName: '이담당',\n lotNo: 'KD-SL-251217-01-01',\n productCode: 'SL3040',\n productName: '슬랫조립체 W3000×H4000',\n finishType: '슬랫',\n finishSpec: '백색',\n },\n 'TS': {\n requestDate: '2025-12-17',\n requestCompany: '(주)인천건설',\n requestManager: '김담당',\n requestPhone: '010-1234-5678',\n siteName: '송도 오피스텔 A동',\n siteCode: 'S-001',\n deliveryDate: '2025-12-18',\n deliveryAddress: '인천시 연수구 송도동 123-45',\n deliveryPhone: '032-123-4567',\n productName: '방화셔터 W3000×H4000',\n productCode: 'SH3040',\n quantity: 5,\n },\n 'IQC': {\n itemName: '전기 아연도금 강판\\n(KS D 3528, SECC) \"EGI 절곡판\"',\n itemSpec: '1.55 * 1218 * 480',\n materialNo: 'PE02RB',\n lotSize: 200,\n supplier: '지오TNS\\n(KG스틸)',\n lotNo: '250715-02',\n inspectionDate: '2025-07-15',\n inspector: '노완호',\n inboundDate: '2025-07-15',\n inspectionItems: [\n {\n no: 1,\n name: '겉모양',\n standard: '사용상 해로울\\n결함이 없을 것',\n method: '육안검사',\n frequency: '',\n measurements: [{ result: 'OK' }, { result: 'OK' }, { result: 'OK' }],\n judgment: '적'\n },\n {\n no: 2,\n name: '치수',\n subItems: [\n {\n name: '두께', value: '1.55', ranges: [\n { range: '0.8 이상 ~ 1.0 미만', tolerance: '± 0.07', checked: false },\n { range: '1.0 이상 ~ 1.25 미만', tolerance: '± 0.08', checked: false },\n { range: '1.25 이상 ~ 1.6 미만', tolerance: '± 0.10', checked: true },\n { range: '1.6 이상 ~ 2.0 미만', tolerance: '± 0.12', checked: false },\n ], measurements: ['1.528', '1.533', '1.521']\n },\n {\n name: '너비', value: '1219', ranges: [\n { range: '1250 미만', tolerance: '+7/-0', checked: true },\n ], measurements: ['1222', '1222', '1222']\n },\n {\n name: '길이', value: '480', ranges: [\n { range: '1250 미만', tolerance: '+10/-0', checked: false },\n { range: '2000 이상 ~ 4000 미만', tolerance: '+15/-0', checked: false },\n { range: '4000 이상 ~ 6000 미만', tolerance: '+20/-0', checked: false },\n ], measurements: ['480', '480', '480']\n },\n ],\n method: '체크검사',\n frequency: 'n=3\\nc=0',\n judgment: '적'\n },\n {\n no: 3,\n name: '인장강도 (N/mm²)',\n standard: '270 이상',\n method: '',\n frequency: '',\n measurements: ['313.8', '', ''],\n judgment: '적'\n },\n {\n no: 4,\n name: '연신율\\n%',\n subItems: [\n { range: '두께 0.6 이상 ~ 1.0 미만', value: '36 이상', checked: false },\n { range: '두께 1.0 이상 ~ 1.6 미만', value: '37 이상', checked: true },\n { range: '두께 1.6 이상 ~ 2.3 미만', value: '38 이상', checked: false },\n ],\n method: '공급업체\\n밀시트',\n frequency: '입고시',\n measurements: ['46.5', '', ''],\n judgment: '적'\n },\n {\n no: 5,\n name: '아연의 최소\\n부착량 (g/m²)',\n standard: '한면 17 이상',\n method: '',\n frequency: '',\n measurements: ['17.21 / 17.17', '', ''],\n judgment: '적'\n },\n ],\n notes: [\n '1.55mm의 경우 KS F 4510에 따른 MIN 15의 기준에 따름',\n '두께의 경우 너비 1000 이상 ~ 1250 미만 기준에 따름',\n ],\n finalJudgment: '합격',\n },\n 'PQC-SCR': {\n productName: '방화스크린(60분)',\n productSpec: 'W3000 * H4000',\n siteName: '인천 송도 오피스텔 신축공사',\n workOrderNo: 'WO-SCR-251216-01',\n deliveryDate: '2025-12-20',\n inspectionDate: '2025-12-18',\n inspector: '노완호',\n inspectionItems: [\n // 겉모양 검사\n {\n category: '겉모양',\n items: [\n { name: '가공상태', standard: '사용상 해로운 결함이 없을 것', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n { name: '재봉상태', standard: '내화실에 의해 견고하게 접합되어야 함', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n { name: '조립상태', standard: '앤드락이 견고하게 조립되어야 함', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n { name: '연기차단재', standard: '연기차단재 설치여부\\n(케이스 W80, 가이드레일 W50(양측설치))', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n { name: '하단마감재', standard: '내부 무게평철 설치 유무', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n ]\n },\n // 모터 검사\n {\n category: '모터',\n items: [\n { name: '모터', standard: '인정제품과 동일사양', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n ]\n },\n // 재질 검사\n {\n category: '재질',\n items: [\n { name: '재질', standard: 'WY-SC780 인쇄상태 확인', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n ]\n },\n // 치수 검사 (오픈사이즈)\n {\n category: '치수\\n(오픈사이즈)',\n items: [\n { name: '가로', standard: 'W ± 10', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['3002', '', '', '', ''], judgment: '적', checked: true },\n { name: '세로', standard: 'H ± 10', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['4005', '', '', '', ''], judgment: '적', checked: true },\n ]\n },\n // 치수 검사 (케이스)\n {\n category: '치수\\n(케이스)',\n items: [\n { name: '좌우길이', standard: 'W ± 3', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['3001', '', '', '', ''], judgment: '적', checked: true },\n { name: '폭', standard: '250 ± 5', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['252', '', '', '', ''], judgment: '적', checked: true },\n { name: '높이', standard: '350 ± 5', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['348', '', '', '', ''], judgment: '적', checked: true },\n ]\n },\n // 치수 검사 (가이드레일)\n {\n category: '치수\\n(가이드레일)',\n items: [\n { name: '길이', standard: 'H ± 5', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['4003', '', '', '', ''], judgment: '적', checked: true },\n { name: '기능단차', standard: '10 ~ 15', method: '버니어', frequency: 'n=1\\nc=0', measurements: ['12', '', '', '', ''], judgment: '적' },\n ]\n },\n // 치수 검사 (스크린)\n {\n category: '치수\\n(스크린)',\n items: [\n { name: '폭', standard: 'W ± 10', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['2998', '', '', '', ''], judgment: '적', checked: true },\n { name: '높이', standard: 'H + 300 ± 10', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['4295', '', '', '', ''], judgment: '적', checked: true },\n ]\n },\n // 치수 검사 (하단마감재)\n {\n category: '치수\\n(하단마감재)',\n items: [\n { name: '길이', standard: 'W - 3.5 ± 2', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['2996', '', '', '', ''], judgment: '적', checked: true },\n ]\n },\n ],\n defectContent: '',\n finalJudgment: '합격',\n },\n 'PQC-BND': {\n productName: '절곡품 (가이드레일)',\n productSpec: 'H4000 * W50',\n siteName: '인천 송도 오피스텔 신축공사',\n workOrderNo: 'WO-FLD-251216-01',\n deliveryDate: '2025-12-20',\n inspectionDate: '2025-12-18',\n inspector: '노완호',\n inspectionItems: [\n {\n category: '겉모양',\n items: [\n { name: '절곡상태', standard: '사용상 해로운 결함이 없을 것', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n { name: '도장상태', standard: '도장 박리, 긁힘 없을 것', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n ]\n },\n {\n category: '치수\\n(길이)',\n items: [\n { name: '길이', standard: 'H ± 4', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['4002', '', '', '', ''], judgment: '적', checked: true },\n ]\n },\n {\n category: '치수\\n(너비)',\n items: [\n { name: 'W50', standard: '50 ± 5', method: '버니어', frequency: 'n=3\\nc=0', measurements: ['50.2', '49.8', '50.1', '', ''], judgment: '적', checked: true },\n { name: 'W80', standard: '80 ± 5', method: '버니어', frequency: 'n=3\\nc=0', measurements: ['', '', '', '', ''], judgment: '-' },\n ]\n },\n {\n category: '치수\\n(간격)',\n items: [\n { name: '홀 간격', standard: '도면치수 ± 2', method: '버니어', frequency: 'n=3\\nc=0', measurements: ['250.1', '249.9', '250.0', '', ''], judgment: '적', checked: true },\n ]\n },\n ],\n defectContent: '',\n finalJudgment: '합격',\n },\n 'PQC-SLT': {\n productName: '슬랫 커튼',\n productSpec: 'W3000 * H4000',\n siteName: '인천 송도 오피스텔 신축공사',\n workOrderNo: 'WO-SLT-251216-01',\n deliveryDate: '2025-12-20',\n inspectionDate: '2025-12-18',\n inspector: '노완호',\n inspectionItems: [\n // 겉모양 검사\n {\n category: '겉모양',\n items: [\n { name: '가공상태', standard: '사용상 해로운 결함이 없을 것', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n { name: '슬랫 연결', standard: '슬랫 간 결합이 견고할 것', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n { name: '도장상태', standard: '도장 박리, 긁힘 없을 것', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n ]\n },\n // 모터 검사\n {\n category: '모터',\n items: [\n { name: '모터', standard: '인정제품과 동일사양', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n ]\n },\n // 치수 검사 (슬랫)\n {\n category: '치수\\n(슬랫)',\n items: [\n { name: '폭', standard: 'W ± 5', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['3002', '', '', '', ''], judgment: '적', checked: true },\n { name: '슬랫 높이', standard: '110 ± 2', method: '버니어', frequency: 'n=3\\nc=0', measurements: ['110.5', '110.2', '110.3', '', ''], judgment: '적', checked: true },\n { name: '슬랫 매수', standard: '도면규격', method: '계수', frequency: 'n=1\\nc=0', measurements: ['36', '', '', '', ''], judgment: '적' },\n ]\n },\n // 치수 검사 (케이스)\n {\n category: '치수\\n(케이스)',\n items: [\n { name: '좌우길이', standard: 'W ± 3', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['3001', '', '', '', ''], judgment: '적', checked: true },\n { name: '폭', standard: '280 ± 5', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['278', '', '', '', ''], judgment: '적', checked: true },\n { name: '높이', standard: '300 ± 5', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['298', '', '', '', ''], judgment: '적', checked: true },\n ]\n },\n // 치수 검사 (가이드레일)\n {\n category: '치수\\n(가이드레일)',\n items: [\n { name: '길이', standard: 'H ± 5', method: '줄자', frequency: 'n=1\\nc=0', measurements: ['4003', '', '', '', ''], judgment: '적', checked: true },\n { name: '홈 너비', standard: '90 ± 2', method: '버니어', frequency: 'n=1\\nc=0', measurements: ['90.5', '', '', '', ''], judgment: '적', checked: true },\n ]\n },\n // 작동검사\n {\n category: '작동검사',\n items: [\n { name: '개폐동작', standard: '원활한 개폐', method: '실동작', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n { name: '소음', standard: '이상 소음 없음', method: '청음검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n ]\n },\n ],\n defectContent: '',\n finalJudgment: '합격',\n },\n 'PQC-JB': {\n productName: '조인트바',\n productSpec: 'L300 * W30 * T2.0',\n siteName: '인천 송도 오피스텔 신축공사',\n workOrderNo: 'WO-JB-251216-01',\n deliveryDate: '2025-12-20',\n inspectionDate: '2025-12-18',\n inspector: '노완호',\n inspectionItems: [\n // 겉모양 검사\n {\n category: '겉모양',\n items: [\n { name: '가공상태', standard: '사용상 해로운 결함이 없을 것', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n { name: '도금상태', standard: '도금 박리, 녹 발생 없을 것', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n { name: '홀 가공', standard: '버 없이 깨끗한 홀 가공', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n ]\n },\n // 재질 검사\n {\n category: '재질',\n items: [\n { name: '재질', standard: 'EGI (전기아연도금강판)', method: '육안검사', frequency: '전수검사', measurements: ['OK', 'OK', 'OK', 'OK', 'OK'], judgment: '적' },\n ]\n },\n // 치수 검사\n {\n category: '치수',\n items: [\n { name: '길이(L)', standard: '300 ± 2', method: '버니어', frequency: 'n=5\\nc=0', measurements: ['300.2', '299.8', '300.1', '300.0', '299.9'], judgment: '적', checked: true },\n { name: '너비(W)', standard: '30 ± 1', method: '버니어', frequency: 'n=5\\nc=0', measurements: ['30.1', '30.0', '29.9', '30.0', '30.1'], judgment: '적', checked: true },\n { name: '두께(T)', standard: '2.0 ± 0.1', method: '버니어', frequency: 'n=5\\nc=0', measurements: ['2.01', '2.00', '1.99', '2.00', '2.01'], judgment: '적', checked: true },\n ]\n },\n // 홀 검사\n {\n category: '홀 검사',\n items: [\n { name: '홀 직경', standard: 'Φ5.5 ± 0.2', method: '버니어', frequency: 'n=3\\nc=0', measurements: ['5.52', '5.48', '5.50', '', ''], judgment: '적', checked: true },\n { name: '홀 간격', standard: '250 ± 1', method: '버니어', frequency: 'n=3\\nc=0', measurements: ['250.1', '249.9', '250.0', '', ''], judgment: '적', checked: true },\n { name: '홀 위치', standard: '중심선 기준 ± 0.5', method: '버니어', frequency: 'n=3\\nc=0', measurements: ['0.2', '0.1', '0.3', '', ''], judgment: '적', checked: true },\n ]\n },\n // 직진도 검사\n {\n category: '직진도',\n items: [\n { name: '직진도', standard: '1mm/m 이하', method: '직진자', frequency: 'n=5\\nc=0', measurements: ['0.5', '0.3', '0.4', '0.6', '0.4'], judgment: '적' },\n ]\n },\n ],\n defectContent: '',\n finalJudgment: '합격',\n },\n };\n return samples[code] || samples['WL-SCR'];\n };\n\n const sampleData = getSampleData(docCode);\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n \n
{template?.name || docType?.name || docCode} 미리보기
\n ({docCode})\n \n
\n
\n
\n
\n
\n\n {/* 미리보기 본문 */}\n
\n {/* A4 용지 프레임 */}\n
\n {/* 출고증 (TS) 렌더링 */}\n {docCode === 'TS' ? (\n <>\n {/* KD 로고 헤더 - 출고증 */}\n
\n \n \n | \n KD \n 경동기업 \n KYUNGDONG COMPANY \n | \n \n 출 고 증 \n | \n \n 결 \n 재 \n | \n 작성 | \n 검토 | \n 승인 | \n
\n \n | \n 홍길동 \n 12/17 \n | \n | \n | \n
\n \n | \n 출하 관리\n | \n 판매/전진 | \n 출하 | \n 생산관리 | \n
\n \n
\n\n {/* 신청업체/신청내용/납품정보 - 3단 구조 */}\n
\n \n \n | 신 청 업 체 | \n 신 청 내 용 | \n 납 품 정 보 | \n
\n \n | 신청일자 | \n {sampleData.requestDate} | \n 현장명 | \n {sampleData.siteName} | \n 납품일자 | \n {sampleData.deliveryDate} | \n
\n \n | 업체명 | \n {sampleData.requestCompany} | \n 현장코드 | \n {sampleData.siteCode} | \n 납품주소 | \n {sampleData.deliveryAddress} | \n
\n \n | 담당자 | \n {sampleData.requestManager} | \n 제품명 | \n {sampleData.productName} | \n 연락처 | \n {sampleData.deliveryPhone} | \n
\n \n
\n\n {/* 부자재 섹션 */}\n
\n \n \n | 부 자 재 | \n
\n \n | 순번 | \n 품명 | \n 규격 | \n 수량 | \n 단위 | \n 비고 | \n
\n \n \n {[1, 2, 3].map(i => (\n \n | {i} | \n {i === 1 ? '가이드레일' : i === 2 ? '브라켓' : '볼트세트'} | \n {i === 1 ? 'W50*H4000' : i === 2 ? 'L형 100mm' : 'M10*30'} | \n {i * 2} | \n EA | \n | \n
\n ))}\n \n
\n\n {/* 모터 섹션 */}\n
\n \n \n | 모 터 | \n
\n \n | 순번 | \n 품명 | \n 규격 | \n 수량 | \n 단위 | \n 비고 | \n
\n \n \n {[1, 2].map(i => (\n \n | {i} | \n {i === 1 ? '전동개폐기' : '연동제어기'} | \n {i === 1 ? '220V-300KG' : '4CH'} | \n {i} | \n EA | \n | \n
\n ))}\n \n
\n\n {/* 슬랫 섹션 */}\n
\n \n \n | 슬 랫 | \n
\n \n | 순번 | \n 품명 | \n 규격 | \n 수량 | \n 단위 | \n 비고 | \n
\n \n \n {[1, 2].map(i => (\n \n | {i} | \n {i === 1 ? '슬랫조립체' : '엔드락'} | \n {i === 1 ? 'W3000*65T' : 'W3000'} | \n {i === 1 ? 60 : 2} | \n EA | \n | \n
\n ))}\n \n
\n\n {/* 절곡품 섹션 + LOT NO 기록란 */}\n
\n \n \n | 절 곡 품 | \n
\n \n | 순번 | \n 품명 | \n 규격 | \n 수량 | \n 단위 | \n LOT NO | \n
\n \n \n {[1, 2, 3].map(i => (\n \n | {i} | \n {i === 1 ? '커버박스' : i === 2 ? '가이드레일' : '바텀바'} | \n {i === 1 ? 'W3000*H300' : i === 2 ? 'W50*H4000' : 'W3000'} | \n {i} | \n EA | \n {`KD-BD-251217-0${i}`} | \n
\n ))}\n \n
\n\n {/* 소모품 섹션 */}\n
\n \n \n | 소 모 품 | \n
\n \n | 순번 | \n 품명 | \n 규격 | \n 수량 | \n 단위 | \n 비고 | \n
\n \n \n {[1, 2].map(i => (\n \n | {i} | \n {i === 1 ? '리벳' : '실리콘'} | \n {i === 1 ? '4.8*12' : '투명'} | \n {i === 1 ? 200 : 3} | \n {i === 1 ? 'EA' : '개'} | \n | \n
\n ))}\n \n
\n >\n ) : docCode === 'IQC' ? (\n <>\n {/* 수입검사성적서 (IQC) 렌더링 */}\n {/* KD 로고 헤더 - 수입검사성적서 */}\n
\n \n \n | \n KD \n 경동기업 \n KYUNGDONG COMPANY \n | \n \n 수 입 검 사 성 적 서 \n | \n 담당 | \n 부서장 | \n
\n \n | \n {sampleData.inspector} \n 07/15 \n | \n | \n
\n \n
\n \n
\n\n {/* 입고일자 */}\n
\n 입고일자 : \n {sampleData.inboundDate}\n
\n\n {/* 품목정보 테이블 */}\n
\n \n \n | 품 명 | \n {sampleData.itemName} | \n 납품업체 제조업체 | \n {sampleData.supplier} | \n
\n \n 규 격 (두께*너비*길이) | \n {sampleData.itemSpec} | \n 로트번호 | \n {sampleData.lotNo} | \n
\n \n | 자재번호 | \n {sampleData.materialNo} | \n 검사일자 | \n {sampleData.inspectionDate} | \n
\n \n | 로트크기 | \n {sampleData.lotSize}매 | \n 검사자 | \n {sampleData.inspector} | \n
\n \n
\n\n {/* 검사항목 테이블 */}\n
\n \n \n | NO | \n 검사항목 | \n 검사기준 | \n 검사방식 | \n 검사주기 | \n 측정치 | \n 판정 (적/부) | \n
\n \n n1 양호/불량 | \n n2 양호/불량 | \n n3 양호/불량 | \n
\n \n \n {/* 1. 겉모양 */}\n \n | 1 | \n 겉모양 | \n 사용상 해로울 결함이 없을 것 | \n 육안검사 | \n | \n \n ☑OK☐NG\n | \n \n ☑OK☐NG\n | \n \n ☑OK☐NG\n | \n 적 | \n
\n {/* 2. 치수 - 두께 */}\n \n | 2 | \n 치수 | \n 두께 1.55 | \n ☐ 0.8 이상 ~ 1.0 미만 | \n ± 0.07 | \n n=3 c=0 | \n 1.528 | \n 1.533 | \n 1.521 | \n 적 | \n
\n \n | ☐ 1.0 이상 ~ 1.25 미만 | \n ± 0.08 | \n
\n \n | ☑ 1.25 이상 ~ 1.6 미만 | \n ± 0.10 | \n
\n \n | ☐ 1.6 이상 ~ 2.0 미만 | \n ± 0.12 | \n
\n {/* 치수 - 너비 */}\n \n 너비 1219 | \n ☑ 1250 미만 | \n +7 -0 | \n 1222 | \n 1222 | \n 1222 | \n 적 | \n
\n \n | ☐ 1250 미만 | \n +10 -0 | \n
\n {/* 치수 - 길이 */}\n \n 길이 480 | \n ☐ 2000 이상 ~ 4000 미만 | \n +15 -0 | \n 480 | \n 480 | \n 480 | \n 적 | \n
\n \n | ☐ 4000 이상 ~ 6000 미만 | \n +20 -0 | \n
\n \n | \n | \n
\n {/* 3. 인장강도 */}\n \n | 3 | \n 인장강도 (N/mm²) | \n 270 이상 | \n | \n | \n 313.8 | \n 적 | \n
\n {/* 4. 연신율 */}\n \n | 4 | \n 연신율 % | \n ☐ 두께 0.6 이상 ~ 1.0 미만 | \n 36 이상 | \n 공급업체 밀시트 | \n 입고시 | \n 46.5 | \n 적 | \n
\n \n | ☑ 두께 1.0 이상 ~ 1.6 미만 | \n 37 이상 | \n
\n \n | ☐ 두께 1.6 이상 ~ 2.3 미만 | \n 38 이상 | \n
\n {/* 5. 아연 부착량 */}\n \n | 5 | \n 아연의 최소 부착량 (g/m²) | \n 한면 17 이상 | \n | \n | \n 17.21 / 17.17 | \n 적 | \n
\n \n
\n\n {/* 하단 참고사항 및 종합판정 */}\n
\n \n \n | \n # 1.55mm의 경우 KS F 4510에 따른 MIN 15의 기준에 따름 \n # 두께의 경우 너비 1000 이상 ~ 1250 미만 기준에 따름 \n [부적합 내용] \n | \n \n 종합판정 \n 합격 \n | \n
\n \n
\n >\n ) : ['PQC-SCR', 'PQC-BND', 'PQC-SLT', 'PQC-JB'].includes(docCode) ? (\n <>\n {/* 통합 중간검사성적서 (PQC-SCR/BND/SLT/JB) 렌더링 */}\n {/* KD 로고 헤더 */}\n
\n \n \n \n \n \n \n \n \n | \n KD \n 경동기업 \n KYUNGDONG \n | \n \n 중 간 검 사 성 적 서 \n \n ({docCode === 'PQC-SCR' ? '스크린' : docCode === 'PQC-BND' ? '절곡품' : docCode === 'PQC-SLT' ? '슬랫' : '조인트바'})\n \n | \n 담당 | \n 부서장 | \n
\n \n | \n {sampleData.inspector} \n 12/18 \n | \n | \n
\n \n
\n\n {/* 품목정보 테이블 */}\n
\n \n \n \n \n \n \n \n \n | 품명 | \n {sampleData.productName} | \n 현장명 | \n {sampleData.siteName} | \n
\n \n | 규격 | \n {sampleData.productSpec} | \n 작업지시번호 | \n {sampleData.workOrderNo} | \n
\n \n | 납품일자 | \n {sampleData.deliveryDate} | \n 검사일자 | \n {sampleData.inspectionDate} | \n
\n \n | 검사자 | \n {sampleData.inspector} | \n
\n \n
\n\n {/* 검사항목 테이블 */}\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n | 항목 | \n 세부 | \n 검사기준 | \n 방법 | \n 주기 | \n n1 | \n n2 | \n n3 | \n n4 | \n n5 | \n 적 | \n
\n \n \n {sampleData.inspectionItems?.map((category, catIdx) => (\n category.items.map((item, itemIdx) => (\n \n {itemIdx === 0 && (\n | \n {category.category}\n | \n )}\n {item.name} | \n \n {item.checked && ☑}\n {!item.checked && item.standard?.includes('±') && ☐}\n {item.standard}\n | \n {item.method} | \n {item.frequency} | \n {item.measurements?.map((m, mIdx) => (\n \n {m === 'OK' ? O : m}\n | \n ))}\n {item.judgment} | \n
\n ))\n ))}\n \n
\n\n {/* 하단 비고 및 종합판정 */}\n
\n \n \n \n \n \n \n | \n [비고 / 부적합 내용] \n {sampleData.defectContent} \n | \n \n 종합판정 \n {sampleData.finalJudgment} \n | \n
\n \n
\n >\n ) : (\n <>\n {/* KD 로고 헤더 - 작업일지 */}\n
\n \n \n | \n KD \n 경동기업 \n KYUNGDONG COMPANY \n | \n \n 작 업 일 지 \n | \n \n 결 \n 재 \n | \n 작성 | \n 검토 | \n 승인 | \n
\n \n | \n 홍길동 \n 12/17 \n | \n | \n | \n
\n \n | \n {docCode === 'WL-SCR' ? '스크린 생산부서' : docCode === 'WL-SLT' ? '슬랫 생산부서' : '절곡 생산부서'}\n | \n 판매/전진 | \n 생산 | \n 품질 | \n
\n \n
\n\n {/* 신청업체/신청내용 */}\n
\n \n \n | 신 청 업 체 | \n 신 청 내 용 | \n
\n \n | 발 주 일 | \n {sampleData.orderDate} | \n 현 장 명 | \n {sampleData.siteName} | \n
\n \n | 업 체 명 | \n {sampleData.customerName} | \n 작업일자 | \n {sampleData.workDate} | \n
\n \n | 담 당 자 | \n {sampleData.managerName} | \n 제품 LOT NO. | \n {sampleData.lotNo} | \n
\n \n | 제품명 | \n \n {sampleData.productCode}\n {sampleData.productName}\n | \n 마감유형 | \n \n {sampleData.finishType}\n {sampleData.finishSpec}\n | \n
\n \n
\n\n {/* 작업내역 테이블 샘플 */}\n
\n \n \n | 순번 | \n 작업항목 | \n 규격 | \n 수량 | \n 단위 | \n 작업자 | \n 비고 | \n
\n \n \n {[1, 2, 3, 4, 5].map(i => (\n \n | {i} | \n {i === 1 ? '원단 재단' : i === 2 ? '미싱 작업' : i === 3 ? '앤드락 조립' : i === 4 ? '검수' : '포장'} | \n W{3000 - i * 100} × H{4000 - i * 100} | \n {10 - i} | \n EA | \n {i % 2 === 0 ? '김작업' : '이작업'} | \n | \n
\n ))}\n \n
\n >\n )}\n\n {/* 블록 구성 정보 */}\n {template?.blocks && (\n
\n
문서 구성 블록
\n
\n {template.blocks.map((block, idx) => {\n const blockInfo = getBlockInfo(block.blockId);\n return (\n \n {blockInfo?.name || block.blockId}\n \n );\n })}\n
\n
\n )}\n
\n
\n
\n
\n );\n};\n\n// ============================================================\n// 데이터 연동 작업일지 프리뷰 모달\n// ============================================================\nconst WorkLogDataPreviewModal = ({ workOrder, workResults = [], order, onClose }) => {\n if (!workOrder) return null;\n\n // 공정 유형에 따른 문서 코드 결정\n const getDocCode = (processType) => {\n if (processType?.includes('스크린') || processType?.includes('방화') || processType?.includes('SCR')) return 'WL-SCR';\n if (processType?.includes('슬랫') || processType?.includes('SLT')) return 'WL-SLT';\n if (processType?.includes('절곡') || processType?.includes('FLD') || processType?.includes('BD')) return 'WL-FLD';\n return 'WL-SCR'; // 기본값\n };\n\n const docCode = getDocCode(workOrder.processType || workOrder.orderNo);\n\n // 색상 결정\n const getProcessColor = () => {\n switch (docCode) {\n case 'WL-SCR': return '#FFFF00'; // 노랑\n case 'WL-SLT': return '#90EE90'; // 연두\n case 'WL-FLD': return '#FFA500'; // 주황\n default: return '#FFFF00';\n }\n };\n\n const getProcessName = () => {\n switch (docCode) {\n case 'WL-SCR': return '스크린 생산부서';\n case 'WL-SLT': return '슬랫 생산부서';\n case 'WL-FLD': return '절곡 생산부서';\n default: return '생산부서';\n }\n };\n\n // 날짜 포맷\n const formatDate = (dateStr) => {\n if (!dateStr) return new Date().toISOString().slice(0, 10);\n const date = new Date(dateStr);\n return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;\n };\n\n const getShortDate = (dateStr) => {\n if (!dateStr) return '';\n const date = new Date(dateStr);\n return `${date.getMonth() + 1}/${date.getDate()}`;\n };\n\n // 작업 품목 목록 (workOrder.items에서 가져옴)\n const workItems = workOrder.items || [];\n\n // 절곡 공정 전개도 데이터\n const bendingParts = [\n { itemCode: 'SD30', itemName: '엘바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 4, weight: 0.9, dimensions: '75', svgType: 'rect' },\n { itemCode: 'SD31', itemName: '하장바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 1.158, dimensions: '67→126→165→178→193', svgType: 'hajang' },\n { itemCode: 'SD32', itemName: '짜부가스켓', material: 'E.G.I 0.8T', totalWidth: 500, length: 3000, qty: 4, weight: 0.576, dimensions: '48', svgType: 'thin' },\n { itemCode: 'SD33', itemName: '50평철', material: 'E.G.I 1.2T', totalWidth: 500, length: 3000, qty: 2, weight: 0.3, dimensions: '50', svgType: 'flat' },\n { itemCode: 'SD34', itemName: '전면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.764, dimensions: '120→499→553→588', svgType: 'front' },\n { itemCode: 'SD35', itemName: '린텔박스', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 0.504, dimensions: '84→138→168', svgType: 'lintel' },\n ];\n\n // SVG 전개도 렌더링\n const renderBendingSvg = (svgType) => {\n switch (svgType) {\n case 'rect':\n return <>
>;\n case 'hajang':\n return
;\n case 'thin':\n return
;\n case 'flat':\n return
;\n case 'front':\n return
;\n case 'lintel':\n return
;\n default:\n return
;\n }\n };\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n \n
작업일지
\n {getProcessName()}\n ({workOrder.workOrderNo})\n \n
\n
\n\n {/* 미리보기 본문 */}\n
\n {/* A4 용지 프레임 */}\n
\n {/* KD 로고 헤더 */}\n
\n \n \n | \n KD \n 경동기업 \n | \n \n 작 업 일 지 \n {docCode} \n | \n \n 결 재 \n | \n 작성 | \n 검토 | \n 승인 | \n
\n \n | \n {workOrder.assignee || '-'} \n | \n | \n | \n
\n \n | \n {getProcessName()}\n | \n 판매 | \n 생산 | \n 품질 | \n
\n \n
\n\n {/* 기본정보 */}\n
\n \n \n | 발 주 처 | \n {workOrder.customerName || '-'} | \n 현 장 명 | \n {workOrder.siteName || '-'} | \n
\n \n | 작업일자 | \n {formatDate(workOrder.instructionDate)} | \n LOT NO. | \n {workOrder.lotNo || workOrder.workOrderNo} | \n
\n \n | 납 기 일 | \n {formatDate(workOrder.dueDate)} | \n 규 격 | \n {workOrder.spec || `W${workOrder.width || '-'} × H${workOrder.height || '-'}`} | \n
\n \n
\n\n {/* 작업품목 목록 */}\n {workItems.length > 0 && (\n
\n \n \n | No | \n 품목명 | \n 층/부호 | \n 규격 | \n 수량 | \n 상태 | \n
\n \n \n {workItems.slice(0, 8).map((item, idx) => (\n \n | {idx + 1} | \n {item.productName} | \n {item.floor}/{item.location} | \n {item.spec} | \n {item.qty} | \n \n \n {item.itemStatus || '대기'}\n \n | \n
\n ))}\n \n
\n )}\n\n {/* 절곡 공정: 전개도 상세 */}\n {docCode === 'WL-FLD' && (\n <>\n
전개도 상세정보
\n
\n \n \n | 품목코드 | \n 품목명 | \n 재질 | \n 전개폭 | \n 길이 | \n 수량 | \n 중량 | \n 전개치수 | \n 전개도 | \n
\n \n \n {bendingParts.map((part, idx) => (\n \n | {part.itemCode} | \n {part.itemName} | \n {part.material} | \n {part.totalWidth} | \n {part.length} | \n {part.qty} | \n {part.weight}kg | \n {part.dimensions} | \n \n \n | \n
\n ))}\n \n
\n\n {/* 절곡 상세 입력값 */}\n
절곡 작업 상세
\n
\n \n \n | 가이드레일 (벽면형) | \n 120·70 / 수량: {workOrder.guideRailQty || 4}개 | \n 가이드레일 (측면형) | \n 80·70 / 수량: {workOrder.sideGuideQty || 2}개 | \n
\n \n | 하단마감재 (스크린) | \n SUS: {workOrder.bottomSusQty || 2}EA / EGI: {workOrder.bottomEgiQty || 2}EA | \n 하단마감재 (철재) | \n SUS: {workOrder.steelSusQty || 0}EA / EGI: {workOrder.steelEgiQty || 2}EA | \n
\n \n | 케이스 | \n 수량: {workOrder.caseQty || 1}EA / 규격: W{workOrder.caseWidth || 3000} | \n 연기차단재 | \n 수량: {workOrder.smokeBlockQty || 4}EA | \n
\n \n | 생산량 합계 | \n \n SUS: {workOrder.totalSus || 15}kg\n |\n EGI: {workOrder.totalEgi || 28}kg\n |\n 총계: {(workOrder.totalSus || 15) + (workOrder.totalEgi || 28)}kg\n | \n
\n \n
\n >\n )}\n\n {/* 스크린 공정: 작업내역 */}\n {docCode === 'WL-SCR' && (\n <>\n
스크린 작업내역
\n
\n \n \n | 원단 유형 | \n {workOrder.fabricType || '방화 스크린'} | \n 원단 폭 | \n {workOrder.fabricWidth || 1016}mm | \n
\n \n | 원단절단 | \n {workOrder.cuttingQty || workOrder.totalQty || 0} EA | \n 미싱 | \n {workOrder.sewingQty || workOrder.totalQty || 0} EA | \n
\n \n | 앤드락 작업 | \n {workOrder.endlockQty || workOrder.totalQty || 0} EA | \n 포장 | \n {workOrder.packingQty || workOrder.completedQty || 0} EA | \n
\n \n
\n >\n )}\n\n {/* 슬랫 공정: 작업내역 */}\n {docCode === 'WL-SLT' && (\n <>\n
슬랫 작업내역
\n
\n \n \n | 코일 유형 | \n {workOrder.coilType || '슬랫 코일'} | \n 색상 | \n {workOrder.coilColor || '백색'} | \n
\n \n | 코일절단 | \n {workOrder.coilCuttingQty || workOrder.totalQty || 0} EA | \n 미미작업 | \n {workOrder.mimiQty || workOrder.totalQty || 0} EA | \n
\n \n | 총 중량 | \n {workOrder.totalWeight || 0} kg | \n 양품 | \n {workOrder.goodQty || workOrder.completedQty || 0} EA | \n
\n \n
\n >\n )}\n\n {/* 하단 요약 */}\n
\n \n \n | 지시수량 | \n {workOrder.totalQty || workItems.length} EA | \n 완료수량 | \n {workOrder.completedQty || 0} EA | \n 진행률 | \n \n {workOrder.totalQty > 0 ? Math.round((workOrder.completedQty || 0) / workOrder.totalQty * 100) : 0}%\n | \n
\n \n
\n\n {/* 특이사항 */}\n
\n \n \n | 특이사항 | \n {workOrder.note || workOrder.remarks || '-'} | \n
\n \n
\n\n {/* 투입자재 정보 */}\n {(workOrder.materials?.length > 0 || workOrder.materialInputs?.length > 0) && (\n
\n \n \n | 투입자재 | \n
\n \n | 자재코드 | \n 자재명 | \n LOT번호 | \n 수량 | \n 단위 | \n
\n \n \n {(workOrder.materials || workOrder.materialInputs || []).map((mat, idx) => (\n \n | {mat.materialCode || mat.code || '-'} | \n {mat.materialName || mat.name} | \n {mat.lotNo || '-'} | \n {mat.usedQty || mat.qty} | \n {mat.unit || 'EA'} | \n
\n ))}\n \n
\n )}\n
\n
\n
\n
\n );\n};\n\n// 공정 상세\nconst ProcessDetail = ({ process, workOrders = [], onNavigate, onBack }) => {\n // 문서양식 미리보기 모달 상태\n const [showTemplatePreview, setShowTemplatePreview] = useState(false);\n\n if (!process) return null;\n\n // 작업일지 양식 코드 → 이름 매핑 (문서양식관리에서 동적 생성)\n const workSheetNameMap = (documentTemplateConfig.documentTypes || [])\n .filter(doc => doc.code && doc.code.startsWith('WL-'))\n .reduce((acc, doc) => {\n acc[doc.code] = doc.name;\n return acc;\n }, {});\n\n return (\n
\n {/* 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n
\n \n 공정 상세\n
\n
\n \n \n
\n
\n\n {/* 기본 정보 */}\n
\n \n
\n
\n
{process.processCode}
\n
\n
\n
\n
{process.processName}
\n
\n
\n
\n
\n
{process.department}
\n
\n
\n
\n
\n
{workSheetNameMap[process.workSheetType] || process.workSheetType || '-'}
\n {process.workSheetType && (\n
\n )}\n
\n
\n
\n \n\n {/* 등록 정보 */}\n
\n \n
\n
\n
{process.createdAt}
\n
\n
\n
\n
{process.updatedAt}
\n
\n
\n \n\n {/* 자동 분류 규칙 */}\n
\n 자동 분류 규칙\n {(process.autoClassifyRules?.length || 0) > 0 && (\n \n {process.autoClassifyRules.length}개\n \n )}\n \n }>\n {process.autoClassifyRules && process.autoClassifyRules.length > 0 ? (\n
\n {process.autoClassifyRules.map((rule, idx) => {\n const fieldNames = {\n itemName: '품목명',\n itemCode: '품목코드',\n category: '품목분류',\n material: '원자재',\n size: '규격',\n };\n const operatorNames = {\n contains: '포함',\n equals: '같음',\n startsWith: '시작',\n endsWith: '끝남',\n };\n return (\n
\n {idx + 1}\n \n {fieldNames[rule.field] || rule.field}\n 이(가) \n \"{rule.value}\"\n 을(를) \n {operatorNames[rule.operator] || rule.operator}\n \n
\n );\n })}\n
\n 위 조건을 만족하는 품목은 자동으로 이 공정에 분류됩니다.\n
\n
\n ) : (\n
\n
\n
등록된 자동 분류 규칙이 없습니다
\n
\n )}\n \n\n {/* 세부 작업단계 */}\n
\n {process.workSteps && process.workSteps.length > 0 ? (\n \n {process.workSteps.map((step, idx) => (\n
\n \n {idx + 1}\n \n {step}\n {step === '중간검사' && }\n {idx < process.workSteps.length - 1 && }\n
\n ))}\n
\n ) : (\n 등록된 작업단계가 없습니다
\n )}\n \n\n {/* 작업 정보 */}\n {(process.requiredWorkers || process.equipmentInfo || process.description) && (\n
\n \n {process.requiredWorkers && (\n
\n
\n
{process.requiredWorkers}명
\n
\n )}\n {process.equipmentInfo && (\n
\n
\n
{process.equipmentInfo}
\n
\n )}\n {process.description && (\n
\n
\n
{process.description}
\n
\n )}\n
\n \n )}\n\n {/* 문서양식 미리보기 모달 */}\n {showTemplatePreview && process.workSheetType && (\n
setShowTemplatePreview(false)}\n />\n )}\n \n );\n};\n\n// ============ 품질기준관리 ============\n\n// 품질기준 샘플 데이터\nconst initialInspectionStandards = [\n // 입고검사 기준\n {\n id: 1,\n code: 'QS-INC-001',\n name: '스크린 원단 입고검사',\n inspectionType: '입고검사',\n targetType: '자재',\n targetCode: 'SCR-MAT-001',\n targetName: '스크린 원단 (백색)',\n inspectionItems: [\n { id: 1, item: '외관검사', method: '육안검사', standard: '오염, 찢어짐, 변색 없을 것', unit: '-', min: null, max: null, critical: true },\n { id: 2, item: '폭 치수', method: '줄자 측정', standard: '규격 ±5mm 이내', unit: 'mm', min: 2395, max: 2405, critical: true },\n { id: 3, item: '두께', method: '두께게이지', standard: '0.3±0.02mm', unit: 'mm', min: 0.28, max: 0.32, critical: false },\n { id: 4, item: '인장강도', method: '인장시험기', standard: '≥15 N/cm', unit: 'N/cm', min: 15, max: null, critical: true },\n ],\n samplingRule: 'AQL 2.5, 일반검사 Level II',\n samplingQty: '로트당 5개 샘플',\n frequency: '입고시 전수',\n responsibleTeam: '품질팀',\n status: '사용',\n createdAt: '2025-01-15',\n updatedAt: '2025-02-01',\n },\n {\n id: 2,\n code: 'QS-INC-002',\n name: '슬랫 코일 입고검사',\n inspectionType: '입고검사',\n targetType: '자재',\n targetCode: 'SLT-MAT-001',\n targetName: '슬랫 코일',\n inspectionItems: [\n { id: 1, item: '외관검사', method: '육안검사', standard: '스크래치, 녹, 변형 없을 것', unit: '-', min: null, max: null, critical: true },\n { id: 2, item: '폭 치수', method: '버니어캘리퍼스', standard: '규격 ±0.5mm 이내', unit: 'mm', min: 79.5, max: 80.5, critical: true },\n { id: 3, item: '두께', method: '마이크로미터', standard: '0.5±0.05mm', unit: 'mm', min: 0.45, max: 0.55, critical: true },\n { id: 4, item: '도장상태', method: '접착테스트', standard: '박리 없을 것', unit: '-', min: null, max: null, critical: false },\n ],\n samplingRule: 'AQL 1.0, 일반검사 Level II',\n samplingQty: '로트당 3개 샘플',\n frequency: '입고시 전수',\n responsibleTeam: '품질팀',\n status: '사용',\n createdAt: '2025-01-15',\n updatedAt: '2025-02-01',\n },\n // 중간검사 기준\n {\n id: 3,\n code: 'QS-PRC-001',\n name: '스크린 공정 중간검사',\n inspectionType: '중간검사',\n targetType: '공정',\n targetCode: 'PROC-SCR',\n targetName: '스크린 공정',\n inspectionItems: [\n { id: 1, item: '재단 치수', method: '줄자 측정', standard: '도면 치수 ±10mm', unit: 'mm', min: -10, max: 10, critical: true },\n { id: 2, item: '미싱 상태', method: '육안검사', standard: '스티치 간격 균일, 풀림 없음', unit: '-', min: null, max: null, critical: true },\n { id: 3, item: '앤드락 결합', method: '손검사', standard: '확실한 체결, 유격 없음', unit: '-', min: null, max: null, critical: true },\n { id: 4, item: '작동검사', method: '수동 작동', standard: '부드러운 승하강', unit: '-', min: null, max: null, critical: true },\n ],\n samplingRule: '전수검사',\n samplingQty: '생산품 전량',\n frequency: '포장 전',\n responsibleTeam: '품질팀',\n status: '사용',\n createdAt: '2025-01-15',\n updatedAt: '2025-02-01',\n },\n {\n id: 4,\n code: 'QS-PRC-002',\n name: '슬랫 공정 중간검사',\n inspectionType: '중간검사',\n targetType: '공정',\n targetCode: 'PROC-SLT',\n targetName: '슬랫 공정',\n inspectionItems: [\n { id: 1, item: '절단 치수', method: '버니어캘리퍼스', standard: '도면 치수 ±1mm', unit: 'mm', min: -1, max: 1, critical: true },\n { id: 2, item: '성형 상태', method: '육안검사', standard: '곡률 균일, 변형 없음', unit: '-', min: null, max: null, critical: true },\n { id: 3, item: '미미 결합', method: '손검사', standard: '확실한 체결', unit: '-', min: null, max: null, critical: true },\n { id: 4, item: '표면 상태', method: '육안검사', standard: '스크래치, 찍힘 없음', unit: '-', min: null, max: null, critical: false },\n ],\n samplingRule: '전수검사',\n samplingQty: '생산품 전량',\n frequency: '포장 전',\n responsibleTeam: '품질팀',\n status: '사용',\n createdAt: '2025-01-15',\n updatedAt: '2025-02-01',\n },\n {\n id: 5,\n code: 'QS-PRC-003',\n name: '절곡 공정 중간검사',\n inspectionType: '중간검사',\n targetType: '공정',\n targetCode: 'PROC-BND',\n targetName: '절곡 공정',\n inspectionItems: [\n { id: 1, item: '절단 치수', method: '줄자/버니어', standard: '도면 치수 ±2mm', unit: 'mm', min: -2, max: 2, critical: true },\n { id: 2, item: '절곡 각도', method: '각도기', standard: '90° ±1°', unit: '°', min: 89, max: 91, critical: true },\n { id: 3, item: '용접 상태', method: '육안검사', standard: '기공, 균열 없음', unit: '-', min: null, max: null, critical: true },\n { id: 4, item: '도장 상태', method: '접착테스트', standard: '박리 없음, 두께 균일', unit: '-', min: null, max: null, critical: false },\n ],\n samplingRule: '전수검사',\n samplingQty: '생산품 전량',\n frequency: '포장 전',\n responsibleTeam: '품질팀',\n status: '사용',\n createdAt: '2025-01-15',\n updatedAt: '2025-02-01',\n },\n // 제품검사 기준\n {\n id: 6,\n code: 'QS-FIN-001',\n name: '스크린 셔터 제품검사',\n inspectionType: '제품검사',\n targetType: '품목',\n targetCode: 'SCR-001',\n targetName: '스크린 셔터 (표준형)',\n inspectionItems: [\n { id: 1, item: '외관검사', method: '육안검사', standard: '오염, 손상, 변형 없음', unit: '-', min: null, max: null, critical: true },\n { id: 2, item: '완제품 치수', method: '줄자 측정', standard: '주문 치수 ±15mm', unit: 'mm', min: -15, max: 15, critical: true },\n { id: 3, item: '작동검사', method: '수동 작동', standard: '승하강 원활, 이음 없음', unit: '-', min: null, max: null, critical: true },\n { id: 4, item: '부속품 확인', method: '체크리스트', standard: '부속품 전량 포함', unit: '-', min: null, max: null, critical: true },\n { id: 5, item: '포장상태', method: '육안검사', standard: '포장 완전, 라벨 부착', unit: '-', min: null, max: null, critical: false },\n ],\n samplingRule: '전수검사',\n samplingQty: '출하품 전량',\n frequency: '출하 전',\n responsibleTeam: '품질팀',\n status: '사용',\n createdAt: '2025-01-15',\n updatedAt: '2025-02-01',\n },\n {\n id: 7,\n code: 'QS-FIN-002',\n name: '슬랫 셔터 제품검사',\n inspectionType: '제품검사',\n targetType: '품목',\n targetCode: 'SLT-001',\n targetName: '슬랫 셔터',\n inspectionItems: [\n { id: 1, item: '외관검사', method: '육안검사', standard: '오염, 손상, 변형 없음', unit: '-', min: null, max: null, critical: true },\n { id: 2, item: '완제품 치수', method: '줄자 측정', standard: '주문 치수 ±10mm', unit: 'mm', min: -10, max: 10, critical: true },\n { id: 3, item: '작동검사', method: '수동 작동', standard: '승하강 원활, 소음 없음', unit: '-', min: null, max: null, critical: true },\n { id: 4, item: '잠금장치', method: '잠금 테스트', standard: '확실한 잠금', unit: '-', min: null, max: null, critical: true },\n { id: 5, item: '부속품 확인', method: '체크리스트', standard: '부속품 전량 포함', unit: '-', min: null, max: null, critical: true },\n ],\n samplingRule: '전수검사',\n samplingQty: '출하품 전량',\n frequency: '출하 전',\n responsibleTeam: '품질팀',\n status: '사용',\n createdAt: '2025-01-15',\n updatedAt: '2025-02-01',\n },\n];\n\n// 품질기준 목록\nconst InspectionStandardList = ({ standards = initialInspectionStandards, onNavigate }) => {\n const [search, setSearch] = useState('');\n const [activeTab, setActiveTab] = useState('all');\n const [showPanel, setShowPanel] = useState(false);\n const [panelMode, setPanelMode] = useState('create');\n const [selectedStandard, setSelectedStandard] = useState(null);\n const [localStandards, setLocalStandards] = useState(standards);\n\n const tabs = [\n { id: 'all', label: '전체', count: localStandards.length },\n { id: '입고검사', label: '입고검사', count: localStandards.filter(s => s.inspectionType === '입고검사').length },\n { id: '중간검사', label: '중간검사', count: localStandards.filter(s => s.inspectionType === '중간검사').length },\n { id: '제품검사', label: '제품검사', count: localStandards.filter(s => s.inspectionType === '제품검사').length },\n ];\n\n const filteredStandards = localStandards.filter(s => {\n const matchesSearch = s.name.includes(search) || s.code.includes(search) || s.targetName.includes(search);\n const matchesTab = activeTab === 'all' || s.inspectionType === activeTab;\n return matchesSearch && matchesTab;\n });\n\n const handleOpenDetail = (standard) => {\n setSelectedStandard(standard);\n setPanelMode('detail');\n setShowPanel(true);\n };\n\n const handleCreate = () => {\n setSelectedStandard(null);\n setPanelMode('create');\n setShowPanel(true);\n };\n\n const getInspectionTypeBadge = (type) => {\n const styles = {\n '입고검사': 'bg-blue-100 text-blue-700',\n '중간검사': 'bg-yellow-100 text-yellow-700',\n '제품검사': 'bg-green-100 text-green-700',\n };\n return styles[type] || 'bg-gray-100 text-gray-700';\n };\n\n return (\n
\n {/* 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n\n {/* 검색 및 필터 */}\n
\n
\n \n setSearch(e.target.value)}\n className=\"w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n
\n
\n\n {/* 탭 */}\n
\n {tabs.map(tab => (\n \n ))}\n
\n\n {/* 목록 */}\n
\n \n
\n \n \n | 기준코드 | \n 기준명 | \n 검사유형 | \n 대상 | \n 검사항목 | \n 샘플링 | \n 상태 | \n
\n \n \n {filteredStandards.map(standard => (\n handleOpenDetail(standard)}\n className=\"hover:bg-gray-50 cursor-pointer\"\n >\n | {standard.code} | \n {standard.name} | \n \n \n {standard.inspectionType}\n \n | \n \n \n {standard.targetName} \n {standard.targetCode} \n \n | \n \n \n {standard.inspectionItems.length}개 항목\n \n | \n {standard.samplingQty} | \n \n \n | \n
\n ))}\n \n
\n
\n \n\n {/* 통계 카드 */}\n
\n
\n \n
\n \n
\n
\n
입고검사 기준
\n
{localStandards.filter(s => s.inspectionType === '입고검사').length}건
\n
\n
\n \n
\n \n
\n \n
\n
\n
중간검사 기준
\n
{localStandards.filter(s => s.inspectionType === '중간검사').length}건
\n
\n
\n \n
\n \n
\n \n
\n
\n
제품검사 기준
\n
{localStandards.filter(s => s.inspectionType === '제품검사').length}건
\n
\n
\n \n
\n \n
\n
\n
치명적 검사항목
\n
\n {localStandards.reduce((sum, s) => sum + s.inspectionItems.filter(i => i.critical).length, 0)}개\n
\n
\n
\n \n
\n\n {/* 상세/등록 패널 */}\n {showPanel && (\n
setShowPanel(false)}\n onSave={(data) => {\n if (panelMode === 'create') {\n setLocalStandards(prev => [...prev, {\n ...data,\n id: Date.now(),\n createdAt: new Date().toISOString().split('T')[0],\n updatedAt: new Date().toISOString().split('T')[0],\n }]);\n } else if (panelMode === 'edit') {\n setLocalStandards(prev => prev.map(s => s.id === data.id ? { ...s, ...data, updatedAt: new Date().toISOString().split('T')[0] } : s));\n }\n setShowPanel(false);\n }}\n onEdit={() => setPanelMode('edit')}\n />\n )}\n \n );\n};\n\n// ============================================================\n// 생산기준관리 컴포넌트 (MasterConfigurationManager 기반)\n// ============================================================\nconst ProductionMasterManagement = ({ onNavigate }) => {\n return
;\n};\n\n// ============================================================\n// 출고기준관리 컴포넌트 (MasterConfigurationManager 기반)\n// ============================================================\nconst OutboundMasterManagement = ({ onNavigate }) => {\n return
;\n};\n\n// ============================================================\n// 생산기준관리 컴포넌트 (구버전 - 레거시 참조용)\n// ============================================================\nconst ProductionMasterManagementLegacy = ({ onNavigate }) => {\n const [activeTab, setActiveTab] = useState('work-order');\n const [searchTerm, setSearchTerm] = useState('');\n\n // 탭 정의\n const tabs = [\n { id: 'work-order', label: '작업지시 기준', icon: ClipboardList },\n { id: 'worker-screen', label: '작업자화면 기준', icon: User },\n { id: 'equipment', label: '설비 기준', icon: Settings },\n { id: 'work-line', label: '라인/작업장 기준', icon: Factory },\n { id: 'bom', label: 'BOM 기준', icon: Layers },\n ];\n\n // 샘플 데이터: 작업지시 유형\n const workOrderTypes = [\n { id: 1, code: 'WO-STD', name: '표준 작업지시', description: '일반적인 생산 작업지시', autoCreate: true, status: '활성' },\n { id: 2, code: 'WO-URG', name: '긴급 작업지시', description: '긴급 생산 요청 시 사용', autoCreate: false, status: '활성' },\n { id: 3, code: 'WO-RPR', name: '재작업 지시', description: '불량 재작업 시 사용', autoCreate: false, status: '활성' },\n { id: 4, code: 'WO-SMP', name: '샘플 작업지시', description: '샘플 제작 시 사용', autoCreate: true, status: '활성' },\n ];\n\n // 샘플 데이터: 작업자화면 기준\n const workerScreenSettings = [\n { id: 1, code: 'WS-001', name: '기본 작업화면', description: '일반 작업자용 기본 화면', showProgress: true, showQuality: true, status: '활성' },\n { id: 2, code: 'WS-002', name: '검사원 화면', description: '검사원용 품질 중심 화면', showProgress: false, showQuality: true, status: '활성' },\n { id: 3, code: 'WS-003', name: '조장 화면', description: '조장용 관리 화면', showProgress: true, showQuality: true, status: '활성' },\n ];\n\n // 샘플 데이터: 설비 기준\n const equipmentMasters = [\n { id: 1, code: 'EQ-001', name: '포밍기 #1', type: '성형설비', location: 'A동 1라인', capacity: '200ea/h', status: '가동중' },\n { id: 2, code: 'EQ-002', name: '포밍기 #2', type: '성형설비', location: 'A동 1라인', capacity: '180ea/h', status: '가동중' },\n { id: 3, code: 'EQ-003', name: '절곡기 #1', type: '절곡설비', location: 'B동 2라인', capacity: '150ea/h', status: '점검중' },\n { id: 4, code: 'EQ-004', name: '용접기 #1', type: '용접설비', location: 'C동 3라인', capacity: '100ea/h', status: '가동중' },\n { id: 5, code: 'EQ-005', name: '도장기 #1', type: '도장설비', location: 'D동 4라인', capacity: '50ea/h', status: '가동중' },\n ];\n\n // 샘플 데이터: 라인/작업장 기준\n const workLineMasters = [\n { id: 1, code: 'LN-001', name: '1라인 (스크린)', type: '생산라인', location: 'A동', capacity: '50ea/일', workers: 5, status: '활성' },\n { id: 2, code: 'LN-002', name: '2라인 (슬랫)', type: '생산라인', location: 'B동', capacity: '40ea/일', workers: 4, status: '활성' },\n { id: 3, code: 'LN-003', name: '3라인 (절곡)', type: '생산라인', location: 'C동', capacity: '60ea/일', workers: 6, status: '활성' },\n { id: 4, code: 'WS-001', name: '조립작업장', type: '작업장', location: 'D동', capacity: '30ea/일', workers: 3, status: '활성' },\n { id: 5, code: 'WS-002', name: '검사작업장', type: '작업장', location: 'D동', capacity: '-', workers: 2, status: '활성' },\n ];\n\n // 샘플 데이터: BOM 기준\n const bomMasters = [\n { id: 1, code: 'BOM-SH3040', name: '방화셔터 3000×4000', productCode: 'SH3040', revision: 'R1.0', parts: 15, status: '승인' },\n { id: 2, code: 'BOM-SH2530', name: '방화셔터 2500×3000', productCode: 'SH2530', revision: 'R1.0', parts: 12, status: '승인' },\n { id: 3, code: 'BOM-DR2010', name: '방화문 2000×1000', productCode: 'DR2010', revision: 'R2.0', parts: 8, status: '승인' },\n { id: 4, code: 'BOM-SS3535', name: '방연셔터 3500×3500', productCode: 'SS3535', revision: 'R1.1', parts: 18, status: '검토중' },\n ];\n\n // 현재 탭에 따른 컨텐츠 렌더링\n const renderContent = () => {\n switch (activeTab) {\n case 'work-order':\n return (\n
\n
\n
작업지시 유형 목록
\n
\n
\n
\n \n \n | 코드 | \n 유형명 | \n 설명 | \n 자동생성 | \n 상태 | \n
\n \n \n {workOrderTypes.map(item => (\n \n | {item.code} | \n {item.name} | \n {item.description} | \n {item.autoCreate ? '예' : '아니오'} | \n \n {item.status}\n | \n
\n ))}\n \n
\n
\n );\n\n case 'worker-screen':\n return (\n
\n
\n
작업자화면 설정 목록
\n
\n
\n
\n \n \n | 코드 | \n 화면명 | \n 설명 | \n 진행률표시 | \n 품질표시 | \n 상태 | \n
\n \n \n {workerScreenSettings.map(item => (\n \n | {item.code} | \n {item.name} | \n {item.description} | \n {item.showProgress ? 'O' : 'X'} | \n {item.showQuality ? 'O' : 'X'} | \n \n {item.status}\n | \n
\n ))}\n \n
\n
\n );\n\n case 'equipment':\n return (\n
\n
\n
설비 기준 목록
\n
\n
\n
\n \n \n | 설비코드 | \n 설비명 | \n 설비유형 | \n 위치 | \n 생산능력 | \n 상태 | \n
\n \n \n {equipmentMasters.map(item => (\n \n | {item.code} | \n {item.name} | \n {item.type} | \n {item.location} | \n {item.capacity} | \n \n {item.status}\n | \n
\n ))}\n \n
\n
\n );\n\n case 'work-line':\n return (\n
\n
\n
라인/작업장 기준 목록
\n
\n
\n
\n \n \n | 코드 | \n 명칭 | \n 유형 | \n 위치 | \n 생산능력 | \n 작업자수 | \n 상태 | \n
\n \n \n {workLineMasters.map(item => (\n \n | {item.code} | \n {item.name} | \n {item.type} | \n {item.location} | \n {item.capacity} | \n {item.workers}명 | \n \n {item.status}\n | \n
\n ))}\n \n
\n
\n );\n\n case 'bom':\n return (\n
\n
\n
BOM 기준 목록
\n
\n
\n
\n \n \n | BOM코드 | \n BOM명 | \n 제품코드 | \n 리비전 | \n 부품수 | \n 상태 | \n
\n \n \n {bomMasters.map(item => (\n \n | {item.code} | \n {item.name} | \n {item.productCode} | \n {item.revision} | \n {item.parts}개 | \n \n {item.status}\n | \n
\n ))}\n \n
\n
\n );\n\n default:\n return null;\n }\n };\n\n return (\n
\n {/* 헤더 */}\n
\n
\n \n 생산기준관리\n
\n
\n \n
\n
\n\n {/* 탭 네비게이션 */}\n
\n {tabs.map(tab => (\n \n ))}\n
\n\n {/* 검색 */}\n
\n \n setSearchTerm(e.target.value)}\n className=\"w-full pl-10 pr-4 py-2 border rounded-lg\"\n />\n
\n\n {/* 컨텐츠 */}\n
\n {renderContent()}\n
\n
\n );\n};\n\n// ============================================================\n// 번호기준 관리 컴포넌트\n// ============================================================\nconst NumberRuleManagement = ({ onNavigate }) => {\n const [showHelp, setShowHelp] = useState(false);\n const [searchTerm, setSearchTerm] = useState('');\n\n // 번호기준 목록 - localStorage에서 불러오기 (없으면 기본값 사용)\n const [numberRules, setNumberRules] = useState(() => loadNumberRules());\n\n // 규칙 변경 시 localStorage에 자동 저장\n useEffect(() => {\n saveNumberRules(numberRules);\n }, [numberRules]);\n\n const [showModal, setShowModal] = useState(false);\n const [editingRule, setEditingRule] = useState(null);\n const [formData, setFormData] = useState({\n name: '',\n target: '',\n prefix: 'KD-',\n dateFormat: 'YYMMDD',\n seqDigits: 2,\n separator: '-',\n resetCycle: 'daily',\n status: '활성',\n description: '',\n });\n\n // 적용대상 목록 및 템플릿 - numberRuleConfig에서 import\n const targetTemplates = numberTargetTemplates;\n\n // 날짜 형식 옵션 - numberRuleConfig에서 import\n const dateFormats = numberDateFormats;\n\n // 순번 자릿수 옵션\n const seqDigitOptions = [\n { value: 2, label: '2자리 (01~99)' },\n { value: 3, label: '3자리 (001~999)' },\n { value: 4, label: '4자리 (0001~9999)' },\n ];\n\n // 리셋 주기 옵션 - numberRuleConfig에서 import한 것 사용\n // (resetCycleOptions는 이미 import됨)\n\n // 구분자 옵션\n const separatorOptions = [\n { value: '-', label: '하이픈 (-)' },\n { value: '_', label: '언더스코어 (_)' },\n { value: '', label: '없음' },\n ];\n\n // 번호 미리보기 생성 - formatDateCode 함수 사용\n const generatePreview = (data) => {\n const dateStr = formatDateCode(new Date(), data.dateFormat);\n const seq = '01'.padStart(data.seqDigits, '0');\n const sep = data.separator;\n\n if (data.dateFormat === 'NONE' || !dateStr) {\n return data.prefix ? `${data.prefix}${sep}${seq}` : seq;\n }\n return data.prefix ? `${data.prefix}${sep}${dateStr}${sep}${seq}` : `${dateStr}${sep}${seq}`;\n };\n\n // 적용대상 선택 시 템플릿 자동 적용\n const handleTargetChange = (target) => {\n const template = targetTemplates.find(t => t.value === target);\n if (template) {\n setFormData(prev => ({\n ...prev,\n target,\n name: `${target} 기준`,\n prefix: template.prefix,\n }));\n }\n };\n\n // 신규 등록 모달 열기\n const openCreateModal = () => {\n setEditingRule(null);\n setFormData({\n name: '',\n target: '',\n prefix: 'KD-',\n dateFormat: 'YYMMDD',\n seqDigits: 2,\n separator: '-',\n resetCycle: 'daily',\n status: '활성',\n description: '',\n });\n setShowModal(true);\n };\n\n // 수정 모달 열기\n const openEditModal = (rule) => {\n setEditingRule(rule);\n setFormData({\n name: rule.name,\n target: rule.target,\n prefix: rule.prefix,\n dateFormat: rule.dateFormat,\n seqDigits: rule.seqDigits,\n separator: rule.separator,\n resetCycle: rule.resetCycle,\n status: rule.status,\n description: rule.description || '',\n });\n setShowModal(true);\n };\n\n // 저장\n const handleSave = () => {\n const example = generatePreview(formData);\n if (editingRule) {\n setNumberRules(prev => prev.map(r =>\n r.id === editingRule.id ? { ...r, ...formData, example } : r\n ));\n } else {\n setNumberRules(prev => [...prev, {\n id: Math.max(...prev.map(r => r.id)) + 1,\n ...formData,\n example,\n }]);\n }\n setShowModal(false);\n };\n\n // 삭제\n const handleDelete = (id) => {\n if (confirm('이 번호기준을 삭제하시겠습니까?')) {\n setNumberRules(prev => prev.filter(r => r.id !== id));\n }\n };\n\n // 상태 토글\n const toggleStatus = (id) => {\n setNumberRules(prev => prev.map(r =>\n r.id === id ? { ...r, status: r.status === '활성' ? '비활성' : '활성' } : r\n ));\n };\n\n // 검색 필터링 및 정렬 (ID 내림차순)\n const filteredRules = [...numberRules]\n .sort((a, b) => b.id - a.id)\n .filter(rule => {\n if (!searchTerm) return true;\n const term = searchTerm.toLowerCase();\n return (\n rule.name.toLowerCase().includes(term) ||\n rule.target.toLowerCase().includes(term) ||\n rule.prefix.toLowerCase().includes(term) ||\n rule.example.toLowerCase().includes(term)\n );\n });\n\n // 체크박스 선택 관리\n const { selectedIds, handleSelect, handleSelectAll, clearSelection, isAllSelected, hasSelection, isMultiSelect, isSelected } = useListSelection(filteredRules);\n\n // 리포트 카드 데이터\n const activeCount = numberRules.filter(r => r.status === '활성').length;\n const docTypeCount = numberRules.filter(r => ['견적번호', '수주번호(스크린)', '수주번호(슬랫)', '수주번호(절곡)', '발주번호'].includes(r.target)).length;\n const lotTypeCount = numberRules.filter(r => r.target.includes('LOT')).length;\n\n return (\n
\n {/* 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n
\n \n 채번 목록\n
\n
\n
\n
\n
\n
\n\n {/* 리포트 카드 4개 */}\n
\n
\n
전체 채번규칙
\n
{numberRules.length}개
\n
\n
\n
활성 규칙
\n
{activeCount}개
\n
\n
\n
문서번호 규칙
\n
{docTypeCount}개
\n
\n
\n
LOT번호 규칙
\n
{lotTypeCount}개
\n
\n
\n\n {/* 검색바 */}\n
\n
\n \n setSearchTerm(e.target.value)}\n />\n
\n
\n\n {/* 도움말 모달 */}\n {showHelp && (\n
\n
\n
\n
번호기준 vs 코드기준 차이점
\n \n \n
\n
\n
\n
\n 🔢 번호기준 (문서용)\n
\n
\n 문서에 부여하는 일련번호\n
\n
\n - • 견적번호, 수주번호
\n - • 생산지시번호, 출하번호
\n - • 발주번호
\n
\n
\n
\n
\n 🏷️ 코드기준 (실물용)\n
\n
\n 실물/마스터에 부여하는 식별코드\n
\n
\n - • 품목코드 (RC30, CB24 등)
\n - • 입고LOT, 생산LOT
\n - • 거래처코드, 현장코드
\n
\n
\n
\n
\n 요약: 번호기준은 견적서/수주서 등 업무 문서를 식별하고,\n 코드기준은 품목/거래처 등 마스터 데이터를 식별합니다.\n
\n
\n
\n \n
\n
\n
\n )}\n\n {/* 선택 삭제 버튼 (다중 선택 시) */}\n {isMultiSelect && (\n
\n \n
\n )}\n\n {/* 번호기준 목록 */}\n
\n\n {/* 등록/수정 모달 */}\n {showModal && (\n
\n
\n {/* 모달 헤더 */}\n
\n
\n {editingRule ? '번호기준 수정' : '번호기준 등록'}\n
\n \n \n\n {/* 모달 내용 */}\n
\n {/* 기본 정보 */}\n
\n
\n 기본 정보\n
\n\n {/* 적용 대상 선택 (템플릿) */}\n
\n
\n
\n {targetTemplates.map(template => (\n
\n ))}\n
\n
\n\n {/* 번호기준 이름 */}\n
\n \n setFormData(prev => ({ ...prev, name: e.target.value }))}\n className=\"w-full border rounded-lg px-3 py-2 text-sm\"\n placeholder=\"예: 견적번호 기준\"\n />\n
\n
\n\n {/* 번호 구성 */}\n
\n
\n 번호 구성\n
\n\n
\n {/* 접두사 */}\n
\n
\n
setFormData(prev => ({ ...prev, prefix: e.target.value }))}\n className=\"w-full border rounded-lg px-3 py-2 text-sm font-mono\"\n placeholder=\"예: KD-PR\"\n />\n
번호 앞에 붙을 고정 문자열
\n
\n\n {/* 구분자 */}\n
\n
\n
\n
각 요소 사이에 들어갈 구분 문자
\n
\n
\n\n
\n {/* 날짜 형식 */}\n
\n \n \n
\n\n {/* 순번 자릿수 */}\n
\n \n \n
\n
\n\n {/* 리셋 주기 */}\n
\n
\n
\n {resetCycleOptions.map(opt => (\n \n ))}\n
\n
\n
\n\n {/* 번호 미리보기 */}\n
\n
\n 생성 번호 미리보기\n
\n\n {/* 시각적 분해 */}\n
\n
\n {formData.prefix}\n
\n {formData.separator && formData.dateFormat !== 'NONE' && (\n
{formData.separator}
\n )}\n {formData.dateFormat !== 'NONE' && (\n
\n {dateFormats.find(d => d.value === formData.dateFormat)?.example}\n
\n )}\n {formData.separator && (\n
{formData.separator}
\n )}\n
\n {'0'.repeat(formData.seqDigits - 1)}1\n
\n
\n\n {/* 구성 설명 */}\n
\n \n 접두사\n \n {formData.dateFormat !== 'NONE' && (\n \n 날짜 ({formData.dateFormat})\n \n )}\n \n 순번 ({formData.seqDigits}자리)\n \n
\n\n {/* 최종 결과 */}\n
\n 생성될 번호: \n \n {generatePreview(formData)}\n \n
\n
\n\n {/* 상태 */}\n
\n
\n
\n \n \n
\n
비활성 시 번호 생성에 사용되지 않음
\n
\n
\n\n {/* 모달 푸터 */}\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// ============================================================\n// 통합 마스터 기준관리 시스템 (MasterConfigurationManager)\n// ============================================================\n\n// 공통 입력 타입 옵션\nconst commonInputTypeOptions = [\n { value: 'text', label: '텍스트박스' },\n { value: 'textarea', label: '텍스트영역' },\n { value: 'number', label: '숫자' },\n { value: 'select', label: '드롭다운' },\n { value: 'checkbox', label: '체크박스' },\n { value: 'radio', label: '라디오' },\n { value: 'date', label: '날짜' },\n { value: 'datetime', label: '날짜시간' },\n { value: 'file', label: '파일' },\n { value: 'table', label: '테이블' },\n];\n\n// 공통 조건 타입 옵션\nconst commonConditionTypeOptions = [\n { value: 'equals', label: '같음' },\n { value: 'not_equals', label: '같지 않음' },\n { value: 'contains', label: '포함' },\n { value: 'not_contains', label: '포함하지 않음' },\n { value: 'is_empty', label: '비어있음' },\n { value: 'is_not_empty', label: '비어있지 않음' },\n];\n\n// ============================================================\n// 통합 마스터 기준관리 컴포넌트\n// ============================================================\n// Config는 ./configs/index.js에서 import됨\n\n// 개별 config 참조 (하위 호환성)\nconst itemMasterConfig = masterConfigs['item'];\nconst processMasterConfig = masterConfigs['process'];\nconst inspectionMasterConfig = masterConfigs['inspection'];\nconst customerMasterConfig = masterConfigs['customer'];\nconst siteMasterConfig = masterConfigs['site'];\n\n// 기존 config 정의는 ./configs/ 폴더로 이동됨\n// ============================================================\n\nconst MasterConfigurationManager = ({ configId, onNavigate }) => {\n const config = masterConfigs[configId] || itemMasterConfig;\n\n // 탭 상태\n const [activeTab, setActiveTab] = useState('hierarchy');\n\n // 데이터 상태 (config에서 초기값 로드)\n const [entityTypes, setEntityTypes] = useState(config.entityTypes);\n const [categories, setCategories] = useState(config.categories);\n const [masterFields, setMasterFields] = useState(config.masterFields);\n const [masterSections, setMasterSections] = useState(config.masterSections);\n const [pageTemplates, setPageTemplates] = useState(config.pageTemplates);\n\n // 모달 상태\n const [showPageModal, setShowPageModal] = useState(false);\n const [showSectionModal, setShowSectionModal] = useState(false);\n const [showFieldModal, setShowFieldModal] = useState(false);\n const [showCategoryModal, setShowCategoryModal] = useState(false);\n\n // 선택 상태\n const [selectedPage, setSelectedPage] = useState(pageTemplates[0]);\n const [selectedSection, setSelectedSection] = useState(null);\n const [selectedField, setSelectedField] = useState(null);\n const [selectedCategory, setSelectedCategory] = useState(null);\n\n // 폼 상태\n const [pageForm, setPageForm] = useState({ pageName: '', pagePath: '', itemTypes: [] });\n const [sectionForm, setSectionForm] = useState({ sectionName: '', sectionKey: '', sectionType: 'general', description: '', useMaster: false });\n const [fieldForm, setFieldForm] = useState({ fieldName: '', fieldKey: '', inputType: 'text', isRequired: false, description: '', useMaster: false });\n const [categoryForm, setCategoryForm] = useState({ code: '', name: '', parentId: null });\n\n // config 변경 시 데이터 리셋\n useEffect(() => {\n setEntityTypes(config.entityTypes);\n setCategories(config.categories);\n setMasterFields(config.masterFields);\n setMasterSections(config.masterSections);\n setPageTemplates(config.pageTemplates);\n setSelectedPage(config.pageTemplates[0]);\n setActiveTab('hierarchy');\n }, [configId]);\n\n // 아이콘 맵핑\n const iconMap = {\n 'GitBranch': GitBranch,\n 'Layers': Layers,\n 'ClipboardList': ClipboardList,\n 'Settings': Settings,\n 'Package': Package,\n 'Factory': Factory,\n 'ShieldCheck': ShieldCheck,\n 'Users': Users,\n 'MapPin': MapPin,\n 'FileText': FileText,\n 'BarChart3': BarChart3,\n };\n\n const getIcon = (iconName) => iconMap[iconName] || Settings;\n const HeaderIcon = getIcon(config.icon);\n\n // 카테고리 트리 빌드\n const buildCategoryTree = (items, parentId = null) => {\n return items\n .filter(item => item.parentId === parentId)\n .sort((a, b) => a.sortOrder - b.sortOrder)\n .map(item => ({\n ...item,\n children: buildCategoryTree(items, item.id)\n }));\n };\n\n const categoryTree = buildCategoryTree(categories);\n\n // 페이지 추가\n const handleAddPage = () => {\n setPageForm({ pageName: '', pagePath: '', itemTypes: [entityTypes[0]?.code || ''] });\n setShowPageModal(true);\n };\n\n const handleSavePage = () => {\n const newPage = {\n id: pageTemplates.length + 1,\n pageCode: pageForm.pageName.toLowerCase().replace(/\\s+/g, '_'),\n ...pageForm,\n sections: [],\n conditions: []\n };\n setPageTemplates([...pageTemplates, newPage]);\n setSelectedPage(newPage);\n setShowPageModal(false);\n };\n\n // 섹션 추가\n const handleAddSection = (useMaster = false) => {\n if (useMaster) {\n setSectionForm({ ...sectionForm, useMaster: true });\n } else {\n setSectionForm({ sectionName: '', sectionKey: '', sectionType: 'general', description: '', useMaster: false });\n }\n setShowSectionModal(true);\n };\n\n const handleSaveSection = () => {\n if (!selectedPage) return;\n\n const newSection = {\n sectionId: Date.now(),\n sectionKey: sectionForm.sectionKey || sectionForm.sectionName.toLowerCase().replace(/\\s+/g, '_'),\n sectionName: sectionForm.sectionName,\n sortOrder: selectedPage.sections.length + 1,\n fields: []\n };\n\n const updatedPage = {\n ...selectedPage,\n sections: [...selectedPage.sections, newSection]\n };\n\n setPageTemplates(pageTemplates.map(p => p.id === selectedPage.id ? updatedPage : p));\n setSelectedPage(updatedPage);\n setShowSectionModal(false);\n };\n\n // 필드 추가\n const handleAddField = (sectionId, useMaster = false) => {\n setSelectedSection(selectedPage?.sections.find(s => s.sectionId === sectionId));\n if (useMaster) {\n setFieldForm({ ...fieldForm, useMaster: true });\n } else {\n setFieldForm({ fieldName: '', fieldKey: '', inputType: 'text', isRequired: false, description: '', useMaster: false });\n }\n setShowFieldModal(true);\n };\n\n const handleSaveField = () => {\n if (!selectedPage || !selectedSection) return;\n\n const newField = {\n fieldId: Date.now(),\n fieldKey: fieldForm.fieldKey || fieldForm.fieldName.toLowerCase().replace(/\\s+/g, '_'),\n fieldName: fieldForm.fieldName,\n inputType: fieldForm.inputType,\n isRequired: fieldForm.isRequired,\n };\n\n const updatedSections = selectedPage.sections.map(s =>\n s.sectionId === selectedSection.sectionId\n ? { ...s, fields: [...s.fields, newField] }\n : s\n );\n\n const updatedPage = { ...selectedPage, sections: updatedSections };\n setPageTemplates(pageTemplates.map(p => p.id === selectedPage.id ? updatedPage : p));\n setSelectedPage(updatedPage);\n setShowFieldModal(false);\n };\n\n // 필드 삭제\n const handleDeleteField = (sectionId, fieldId) => {\n if (!selectedPage) return;\n\n const updatedSections = selectedPage.sections.map(s =>\n s.sectionId === sectionId\n ? { ...s, fields: s.fields.filter(f => f.fieldId !== fieldId) }\n : s\n );\n\n const updatedPage = { ...selectedPage, sections: updatedSections };\n setPageTemplates(pageTemplates.map(p => p.id === selectedPage.id ? updatedPage : p));\n setSelectedPage(updatedPage);\n };\n\n // 섹션 삭제\n const handleDeleteSection = (sectionId) => {\n if (!selectedPage) return;\n\n const updatedPage = {\n ...selectedPage,\n sections: selectedPage.sections.filter(s => s.sectionId !== sectionId)\n };\n\n setPageTemplates(pageTemplates.map(p => p.id === selectedPage.id ? updatedPage : p));\n setSelectedPage(updatedPage);\n };\n\n // 카테고리 추가\n const handleAddCategory = (parentId = null) => {\n setCategoryForm({ code: '', name: '', parentId });\n setShowCategoryModal(true);\n };\n\n const handleSaveCategory = () => {\n const parent = categories.find(c => c.id === categoryForm.parentId);\n const newCategory = {\n id: categories.length + 10,\n code: categoryForm.code,\n name: categoryForm.name,\n parentId: categoryForm.parentId,\n level: parent ? parent.level + 1 : 1,\n path: parent ? `${parent.path}/${categories.length + 10}` : `/${categories.length + 10}`,\n sortOrder: categories.filter(c => c.parentId === categoryForm.parentId).length + 1\n };\n\n setCategories([...categories, newCategory]);\n setShowCategoryModal(false);\n };\n\n // 카테고리 트리 렌더링\n const renderCategoryTree = (nodes, depth = 0) => {\n const CategoryIcon = getIcon(config.tabs.find(t => t.id === 'category')?.icon || 'Package');\n return nodes.map(node => (\n
\n
setSelectedCategory(node)}\n >\n
\n {node.children?.length > 0 ? (\n
\n ) : (\n
\n )}\n
\n
{node.name}\n
({node.code})\n
\n
\n
\n
\n
\n
\n
\n {node.children?.length > 0 && renderCategoryTree(node.children, depth + 1)}\n
\n ));\n };\n\n return (\n
\n {/* 헤더 */}\n
\n
\n \n
\n
\n
{config.title}
\n
{config.description}
\n
\n
\n\n {/* 탭 */}\n
\n
\n {config.tabs.map(tab => {\n const TabIcon = getIcon(tab.icon);\n return (\n \n );\n })}\n
\n\n
\n {/* 계층구조 탭 - 페이지 빌더 */}\n {activeTab === 'hierarchy' && (\n
\n {/* 좌측: 페이지 목록 */}\n
\n
\n
\n {pageTemplates.map(page => (\n
setSelectedPage(page)}\n className={`p-3 rounded-lg cursor-pointer border transition-colors\n ${selectedPage?.id === page.id\n ? 'border-blue-500 bg-blue-50'\n : 'border-transparent hover:bg-gray-50'}`}\n >\n
\n
\n
{page.pageName}
\n
{page.itemTypes.map(t => {\n const type = entityTypes.find(it => it.code === t);\n return type ? `${type.name} (${t})` : t;\n }).join(', ')}
\n
\n
\n \n \n
\n
\n
{page.pagePath}
\n
\n ))}\n
\n
\n\n {/* 우측: 페이지 구성 */}\n
\n
\n
{selectedPage?.pageName || '페이지 선택'}\n
\n
\n\n {selectedPage ? (\n
\n {selectedPage.sections.map((section, sIdx) => (\n
\n {/* 섹션 헤더 */}\n
\n
\n \n \n {section.sectionName}\n
\n
\n
\n
\n
\n
\n
\n\n {/* 섹션 필드 */}\n
\n {section.fields.length === 0 ? (\n
\n \n
\n ) : (\n section.fields.map((field, fIdx) => (\n
\n
\n
\n
\n
\n {field.fieldName}\n {\n commonInputTypeOptions.find(o => o.value === field.inputType)?.label || field.inputType\n }\n {field.isRequired && (\n 필수\n )}\n
\n
필드키: {field.fieldKey}
\n
\n
\n
\n \n \n
\n
\n ))\n )}\n
\n
\n ))}\n\n {selectedPage.sections.length === 0 && (\n
\n
\n
섹션을 추가해주세요
\n
\n
\n )}\n
\n ) : (\n
\n )}\n
\n
\n )}\n\n {/* 섹션 탭 */}\n {activeTab === 'section' && (\n
\n
\n
\n \n \n
\n
\n
\n\n
\n
\n 섹션관리
\n 재사용 가능한 섹션 템플릿을 관리합니다\n
\n\n
\n {masterSections.map(section => (\n
\n
\n
\n
\n
\n
{section.sectionName}
\n
{section.description}
\n
\n
\n
\n {section.applicableTypes.map(type => (\n \n {entityTypes.find(t => t.code === type)?.name || type}\n \n ))}\n \n \n
\n
\n\n
\n
\n 포함된 항목\n \n
\n {section.fields.length > 0 ? (\n
\n {section.fields.map(fieldId => {\n const field = masterFields.find(f => f.id === fieldId);\n return field ? (\n \n {field.fieldName}\n \n ) : null;\n })}\n
\n ) : (\n
항목을 추가해주세요
\n )}\n
\n
\n ))}\n
\n
\n
\n )}\n\n {/* 항목 탭 */}\n {activeTab === 'field' && (\n
\n
\n
마스터 항목
\n
\n
\n\n
\n
\n \n \n | 항목명 | \n 필드키 | \n 입력방식 | \n 필수 | \n 설명 | \n 관리 | \n
\n \n \n {masterFields.map(field => (\n \n | {field.fieldName} | \n {field.fieldKey} | \n \n \n {commonInputTypeOptions.find(o => o.value === field.inputType)?.label || field.inputType}\n \n | \n \n {field.isRequired ? (\n 필수\n ) : (\n -\n )}\n | \n {field.description} | \n \n \n \n \n \n | \n
\n ))}\n \n
\n
\n
\n )}\n\n {/* 속성 탭 */}\n {activeTab === 'attribute' && (\n
\n
\n
속성 관리
\n \n\n
\n {/* 입력 타입별 속성 */}\n
\n
입력 타입
\n
\n {commonInputTypeOptions.map(type => (\n
\n {type.label}\n {type.value}\n
\n ))}\n
\n
\n\n {/* 조건 타입 */}\n
\n
조건부 표시 타입
\n
\n {commonConditionTypeOptions.map(type => (\n
\n {type.label}\n {type.value}\n
\n ))}\n
\n
\n
\n
\n )}\n\n {/* 검사 템플릿 탭 (품질기준관리 전용) */}\n {activeTab === 'templates' && configId === 'quality' &&
}\n\n {/* KS규격 탭 (품질기준관리 전용) */}\n {activeTab === 'ks-standards' && configId === 'quality' && (\n
\n
\n
\n
KS 규격 참조
\n
검사항목에 적용되는 KS 규격 정보입니다
\n
\n
\n\n
\n {config.ksStandards?.map(ks => (\n
\n
\n
\n
\n {ks.code}\n {ks.name}\n
\n
\n {ks.applicableTypes.map(type => (\n \n {type}\n \n ))}\n
\n
\n
{ks.description}
\n
\n
\n
\n \n \n | 검사항목 | \n 검사방법 | \n 규격/허용오차 | \n 단위 | \n
\n \n \n {ks.inspectionItems.map((item, idx) => (\n \n | {item.item} | \n {item.method} | \n \n {item.tolerance || item.standard || (item.minValue ? `${item.minValue} 이상` : '-')}\n | \n {item.unit || '-'} | \n
\n ))}\n \n
\n
\n
\n ))}\n
\n
\n )}\n\n {/* 샘플링 기준 탭 (품질기준관리 전용) */}\n {activeTab === 'sampling' && configId === 'quality' && (\n
\n
\n
\n
샘플링 기준 (AQL)
\n
검사 샘플링 계획 및 합격 판정 기준입니다
\n
\n
\n\n
\n {config.samplingPlans?.map(plan => (\n
\n
\n
{plan.name}\n
{plan.description}
\n
\n\n {plan.levels && (\n
\n
\n \n \n | 로트 크기 | \n 샘플 수량 | \n 합격판정개수 (Ac) | \n 불합격판정개수 (Re) | \n
\n \n \n {plan.levels.map((level, idx) => (\n \n | {level.lotSizeMin} ~ {level.lotSizeMax} | \n {level.sampleSize} | \n \n {level.acceptanceNumber}\n | \n \n {level.rejectionNumber}\n | \n
\n ))}\n \n
\n
\n )}\n\n {plan.measurementPoints && (\n
\n
\n
\n
기본 샘플 수
\n
{plan.defaultSampleSize}
\n
\n
\n
측정 포인트
\n
{plan.measurementPoints}개 (①~⑤)
\n
\n
\n
합격판정개수
\n
{plan.acceptanceNumber}
\n
\n
\n
\n )}\n
\n ))}\n
\n
\n )}\n\n {/* 분류 탭 */}\n {activeTab === 'category' && (\n
\n
\n
{config.tabs.find(t => t.id === 'category')?.label} (계층구조)
\n
\n
\n\n
\n {/* 트리 뷰 */}\n
\n {renderCategoryTree(categoryTree)}\n
\n\n {/* 선택된 분류 상세 */}\n {selectedCategory && (\n
\n
분류 상세
\n
\n
\n
\n
{selectedCategory.code}
\n
\n
\n
\n
{selectedCategory.name}
\n
\n
\n
\n
{selectedCategory.level}
\n
\n
\n
\n
{selectedCategory.path}
\n
\n
\n
\n )}\n
\n
\n )}\n\n {/* 템플릿 관리 탭 */}\n {activeTab === 'template' && (\n
\n
\n
템플릿 관리
\n \n\n
\n {/* 유형 목록 */}\n
\n
\n 유형\n
\n
\n {entityTypes.map(type => (\n
\n
\n
\n {type.code}\n \n
\n
{type.name}
\n
{type.description}
\n
\n
\n
\n \n
\n
\n ))}\n
\n
\n\n {/* 페이지 템플릿 목록 */}\n
\n
\n 페이지 템플릿\n
\n
\n {pageTemplates.map(page => (\n
\n
\n
\n
{page.pageName}
\n
{page.pagePath}
\n
\n
\n {page.itemTypes.map(type => (\n \n {type}\n \n ))}\n
\n
\n
\n {page.sections.length}개 섹션, {page.sections.reduce((sum, s) => sum + s.fields.length, 0)}개 필드\n
\n
\n ))}\n
\n
\n
\n
\n )}\n
\n
\n\n {/* 페이지 추가 모달 */}\n {showPageModal && (\n
\n
\n
\n
페이지 추가
\n \n \n
새로운 페이지를 생성합니다
\n\n
\n
\n \n setPageForm({ ...pageForm, pageName: e.target.value })}\n placeholder=\"예: 등록\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n
\n \n \n
\n
\n\n
\n \n \n
\n
\n
\n )}\n\n {/* 섹션 추가 모달 */}\n {showSectionModal && (\n
\n
\n
\n
섹션 추가
\n \n \n
마스터 섹션을 불러오거나 직접 입력하세요
\n\n {/* 탭: 직접 입력 / 마스터 섹션 불러오기 */}\n
\n \n \n
\n\n {!sectionForm.useMaster ? (\n
\n ) : (\n
\n {masterSections.map(section => (\n
setSectionForm({ ...sectionForm, sectionName: section.sectionName, sectionKey: section.sectionKey, masterSectionId: section.id })}\n className={`p-3 border rounded-lg cursor-pointer hover:bg-gray-50 ${sectionForm.masterSectionId === section.id ? 'border-blue-500 bg-blue-50' : ''}`}\n >\n
{section.sectionName}
\n
{section.description}
\n
\n ))}\n
\n )}\n\n
\n \n \n
\n
\n
\n )}\n\n {/* 항목 추가 모달 */}\n {showFieldModal && (\n
\n
\n
\n
항목 추가
\n \n \n
재사용 가능한 마스터 항목을 선택하거나 직접 입력하세요
\n\n {/* 탭 */}\n
\n \n \n
\n\n {!fieldForm.useMaster ? (\n
\n
\n
\n \n \n
\n
\n \n setFieldForm({ ...fieldForm, description: e.target.value })}\n placeholder=\"항목에 대한 설명\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n
\n setFieldForm({ ...fieldForm, isRequired: e.target.checked })}\n className=\"w-4 h-4\"\n />\n \n
\n
\n ) : (\n
\n {masterFields.map(field => (\n
setFieldForm({\n ...field,\n useMaster: true,\n masterFieldId: field.id\n })}\n className={`p-3 border rounded-lg cursor-pointer hover:bg-gray-50 ${fieldForm.masterFieldId === field.id ? 'border-blue-500 bg-blue-50' : ''}`}\n >\n
\n
\n
{field.fieldName}
\n
필드키: {field.fieldKey}
\n
\n
\n \n {commonInputTypeOptions.find(o => o.value === field.inputType)?.label}\n \n {field.isRequired && (\n 필수\n )}\n
\n
\n
\n ))}\n
\n )}\n\n
\n \n \n
\n
\n
\n )}\n\n {/* 분류 추가 모달 */}\n {showCategoryModal && (\n
\n
\n
\n
분류 추가
\n \n \n\n
\n
\n \n setCategoryForm({ ...categoryForm, code: e.target.value.toUpperCase() })}\n placeholder=\"예: NEW-CODE\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n
\n \n setCategoryForm({ ...categoryForm, name: e.target.value })}\n placeholder=\"예: 신규분류\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n {categoryForm.parentId && (\n
\n
\n
\n {categories.find(c => c.id === categoryForm.parentId)?.name}\n
\n
\n )}\n
\n\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// ============================================================\n// 품목관리 컴포넌트 (목록/등록/수정/상세)\n// ============================================================\n\n// 품목 초기 데이터 (단가는 단가관리에서 별도 관리)\nconst initialItems = itemPriceMaster.map((item, idx) => ({\n id: idx + 1,\n itemCode: item.itemCode,\n itemName: item.itemName,\n category: item.category,\n itemType: item.category.includes('절곡') || item.category.includes('가이드') || item.category.includes('케이스') || item.category.includes('하단') || item.category.includes('연기') ? '반제품' :\n item.category.includes('전동') || item.category.includes('연동') || item.category.includes('샤프트') || item.category.includes('브라켓') || item.category.includes('앵글') ? '부품' :\n item.category.includes('검사') ? '서비스' : '원자재',\n unit: item.unit,\n spec: item.itemName.match(/\\d+/) ? item.itemName.match(/[\\d×\\-]+/)?.[0] || '-' : '-',\n safetyStock: Math.floor(Math.random() * 20) + 5,\n currentStock: Math.floor(Math.random() * 50) + 10,\n leadTime: Math.floor(Math.random() * 7) + 1,\n isActive: true,\n createdAt: '2024-01-01',\n}));\n\n// 품목 목록 컴포넌트\n// props.items가 전달되면 공유 상태 사용, 없으면 기본 데이터 사용\nconst ItemList = ({ onNavigate, items: externalItems, onAddItem, onUpdateItem, onDeleteItem }) => {\n // 외부 items가 전달되면 사용, 없으면 로컬 상태 사용\n const [localItems, setLocalItems] = useState(initialItems);\n const items = externalItems || localItems;\n const setItems = externalItems ? () => { } : setLocalItems; // 외부 상태면 로컬 setter 비활성화\n\n const [search, setSearch] = useState('');\n const [activeTab, setActiveTab] = useState('all');\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n // 품목유형별 카운트\n const typeCount = {\n all: items.length,\n '제품': items.filter(i => i.itemType === '제품').length,\n '부품': items.filter(i => i.itemType === '부품').length,\n '부자재': items.filter(i => i.itemType === '부자재').length,\n '원자재': items.filter(i => i.itemType === '원자재').length,\n '소모품': items.filter(i => i.itemType === '소모품').length,\n };\n\n // 필터링된 품목 먼저 계산 (ID 내림차순 정렬 - 최신 등록 최상단)\n const filtered = items\n .filter(i => activeTab === 'all' || i.itemType === activeTab)\n .filter(i =>\n i.itemCode.toLowerCase().includes(search.toLowerCase()) ||\n i.itemName.includes(search) ||\n (i.spec && i.spec.includes(search))\n )\n .sort((a, b) => b.id - a.id);\n\n // 목록 선택 훅\n const {\n selectedIds,\n handleSelect,\n handleSelectAll,\n clearSelection,\n isAllSelected,\n hasSelection,\n isMultiSelect,\n isSelected,\n } = useListSelection(filtered);\n\n // 삭제 핸들러 (공유 상태 연동)\n const handleDelete = (item) => {\n if (window.confirm(`'${item.itemName}' 품목을 삭제하시겠습니까?`)) {\n if (onDeleteItem) {\n // 공유 상태 연동 시 부모로 삭제 요청\n onDeleteItem(item.id);\n } else {\n // 로컬 상태 사용 시\n setLocalItems(prev => prev.filter(i => i.id !== item.id));\n }\n }\n };\n\n const handleBulkDelete = () => {\n if (onDeleteItem) {\n // 공유 상태 연동 시 각 항목 삭제\n selectedIds.forEach(id => onDeleteItem(id));\n } else {\n // 로컬 상태 사용 시\n setLocalItems(prev => prev.filter(i => !selectedIds.includes(i.id)));\n }\n clearSelection();\n setShowDeleteModal(false);\n };\n\n const tabs = [\n { id: 'all', label: '전체', count: typeCount.all },\n { id: '제품', label: '제품', count: typeCount['제품'] },\n { id: '부품', label: '부품', count: typeCount['부품'] },\n { id: '부자재', label: '부자재', count: typeCount['부자재'] || 0 },\n { id: '원자재', label: '원자재', count: typeCount['원자재'] || 0 },\n { id: '소모품', label: '소모품', count: typeCount['소모품'] || 0 },\n ];\n\n return (\n
\n
onNavigate('item-create')}>\n 품목 등록\n \n }\n />\n\n {/* 통계 카드 */}\n \n
\n
\n
전체 품목
\n
{typeCount.all}
\n
\n
\n
\n
\n
\n
제품
\n
{typeCount['제품']}
\n
\n
\n
\n
\n
\n
부품
\n
{typeCount['부품']}
\n
\n
\n
\n
\n
\n
부자재
\n
{typeCount['부자재'] || 0}
\n
\n
\n
\n
\n\n {/* 검색바 */}\n \n
\n \n setSearch(e.target.value)}\n className=\"w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg text-sm\"\n />\n
\n
\n\n {/* 목록 */}\n \n
\n
전체 목록 ({filtered.length}개)
\n \n\n {/* 탭 */}\n
\n
\n {tabs.map(tab => (\n \n ))}\n
\n
\n\n {/* 선택 삭제 버튼 (2개 이상 선택 시) */}\n {isMultiSelect && (\n
\n
\n {selectedIds.length}개 선택됨\n \n
\n
\n )}\n\n {/* 테이블 */}\n
\n\n {/* 페이지네이션 */}\n
\n
총 {filtered.length}개 중 1-{Math.min(20, filtered.length)}개 표시\n
\n \n \n \n
\n
\n
\n\n {/* 삭제 확인 모달 */}\n {showDeleteModal && (\n \n
\n
품목 삭제
\n
선택한 {selectedIds.length}개 품목을 삭제하시겠습니까?
\n
\n \n \n
\n
\n
\n )}\n \n );\n};\n\n// 품목 등록/수정 컴포넌트\nconst ItemForm = ({ item, onNavigate, isEdit = false, onAddItem, onUpdateItem }) => {\n const [formData, setFormData] = useState(item || {\n itemType: '',\n partSubType: '',\n productName: '',\n itemName: '',\n itemCode: '',\n lotPrefix: '',\n isActive: true,\n description: '',\n certNumber: '',\n certStartDate: '',\n certEndDate: '',\n needsBom: false,\n spec: '',\n material: '',\n unit: 'EA',\n installType: '',\n finishing: '',\n sideWidth: '',\n sideHeight: '',\n length: '',\n drawingInputType: 'file',\n drawingFile: '',\n });\n\n // 유효성 검사 규칙\n const validationRules = {\n itemType: { required: true, label: '품목유형', message: '품목유형을 선택해주세요.' },\n itemName: { required: true, label: '품목명', message: '품목명을 입력해주세요.' },\n };\n\n const { errors, touched, hasError, getFieldError, validateForm, handleBlur, clearFieldError } = useFormValidation(formData, validationRules);\n\n const [bomItems, setBomItems] = useState([]);\n const [showDrawingEditor, setShowDrawingEditor] = useState(false);\n const [drawingDetails, setDrawingDetails] = useState([]);\n\n // 전개도 상세 행 추가\n const addDrawingDetailRow = () => {\n const newRow = {\n id: Date.now(),\n seq: drawingDetails.length + 1,\n input: 0,\n elongation: -1,\n elongationCalc: -1,\n total: -(drawingDetails.length + 1),\n shadow: '',\n angle: '각도',\n };\n setDrawingDetails([...drawingDetails, newRow]);\n };\n\n // 전개도 상세 행 삭제\n const removeDrawingDetailRow = (id) => {\n setDrawingDetails(drawingDetails.filter(row => row.id !== id).map((row, idx) => ({\n ...row,\n seq: idx + 1,\n total: -(idx + 1),\n })));\n };\n\n // 전개도 상세 행 업데이트\n const updateDrawingDetailRow = (id, field, value) => {\n setDrawingDetails(drawingDetails.map(row =>\n row.id === id ? { ...row, [field]: value } : row\n ));\n };\n\n const itemTypes = [\n { value: '제품', label: '제품 (Finished Goods)', desc: '완제품으로 판매되는 품목' },\n { value: '부품', label: '부품 (Part)', desc: '제품 조립에 사용되는 부품' },\n { value: '부자재', label: '부자재 (Sub Material)', desc: '생산 보조 자재' },\n { value: '원자재', label: '원자재 (Raw Material)', desc: '가공 전 기본 재료' },\n { value: '소모품', label: '소모품 (Consumables)', desc: '소모성 자재' },\n ];\n\n const partSubTypes = [\n { value: '조립 부품', label: '조립 부품 (Assembly Part)' },\n { value: '절곡 부품', label: '절곡 부품 (Bending Part) - 전개도만 사용' },\n { value: '단일 부품', label: '단일 부품 (Single Part)' },\n { value: '가공 부품', label: '가공 부품 (Processed Part)' },\n { value: '구매 부품', label: '구매 부품 (Purchased Part)' },\n ];\n\n const partItemNames = ['가이드레일', '케이스', '하단마감재', '연기차단재', '전동개폐기', '연동제어기', '감기샤프트', '브라켓', '앵글', '환봉', '조인트바', '각파이프', '하장바', '엘바', '보강평철', '무게평철', '마구리', '상부덮개', 'BASE'];\n\n // 절곡 부품용 품목명 (설치유형 코드 포함)\n const bendingPartItemNames = [\n { value: '가이드레일(벽면형)', label: '가이드레일(벽면형) (R)', code: 'R' },\n { value: '가이드레일(측면형)', label: '가이드레일(측면형) (S)', code: 'S' },\n { value: '케이스', label: '케이스 (C)', code: 'C' },\n { value: '하단마감재(스크린)', label: '하단마감재(스크린) (B)', code: 'B' },\n { value: '하단마감재(철재)', label: '하단마감재(철재) (T)', code: 'T' },\n { value: 'L-Bar', label: 'L-Bar (L)', code: 'L' },\n { value: '연기차단재', label: '연기차단재 (G)', code: 'G' },\n ];\n\n // 절곡 부품용 종류 옵션 (lotTypeCodes 기반)\n const bendingPartTypes = [\n { value: '본체', label: '본체 (M)', code: 'M' },\n { value: '본체(철재)', label: '본체(철재) (T)', code: 'T' },\n { value: 'C형', label: 'C형 (C)', code: 'C' },\n { value: 'D형', label: 'D형 (D)', code: 'D' },\n { value: 'SUS마감재', label: 'SUS마감재 (S)', code: 'S' },\n { value: 'SUS마감재①', label: 'SUS마감재① (S)', code: 'S' },\n { value: 'SUS마감재②', label: 'SUS마감재② (U)', code: 'U' },\n { value: '전면부', label: '전면부 (F)', code: 'F' },\n { value: '점검구', label: '점검구 (P)', code: 'P' },\n { value: '린텔부', label: '린텔부 (L)', code: 'L' },\n { value: '후면코너부', label: '후면코너부 (B)', code: 'B' },\n { value: 'SUS', label: 'SUS (S)', code: 'S' },\n { value: 'EGI', label: 'EGI (E)', code: 'E' },\n { value: '스크린용', label: '스크린용 (A)', code: 'A' },\n { value: '화이바원단(W50)', label: '화이바원단(W50) (I)', code: 'I' },\n { value: '화이바원단(W80)', label: '화이바원단(W80) (I)', code: 'I' },\n ];\n\n // 절곡 부품용 모양&길이 옵션 (lotSizeCodes 기반)\n const bendingSizeLengths = [\n { value: '1219', label: '1219 (12)', code: '12' },\n { value: '2438', label: '2438 (24)', code: '24' },\n { value: '3000', label: '3000 (30)', code: '30' },\n { value: '3500', label: '3500 (35)', code: '35' },\n { value: '4000', label: '4000 (40)', code: '40' },\n { value: '4150', label: '4150 (41)', code: '41' },\n { value: '4200', label: '4200 (42)', code: '42' },\n { value: '4300', label: '4300 (43)', code: '43' },\n ];\n\n const installTypes = [{ value: '벽면형', label: '벽면형 (R)' }, { value: '측면형', label: '측면형 (S)' }];\n const units = ['M', 'mm', 'EA', 'SET', 'KG', 'T', 'BOX', 'L', 'M2', 'M3', 'ROLL', 'SHEET', 'PACK'];\n const finishings = ['SUS마감', 'EGI마감'];\n const lengths = ['1219', '2438', '3000', '3500'];\n\n const isProductType = formData.itemType === '제품';\n const isPartType = formData.itemType === '부품';\n const isSubMaterialType = formData.itemType === '부자재';\n const isRawMaterialType = formData.itemType === '원자재';\n const isConsumableType = formData.itemType === '소모품';\n const isAssemblyPart = isPartType && formData.partSubType === '조립 부품';\n const isBendingPart = isPartType && formData.partSubType === '절곡 부품';\n const isPurchasedPart = isPartType && formData.partSubType === '구매 부품';\n\n // 부자재 전용 옵션들\n const subMaterialItems = [\n {\n value: '스크린원단',\n label: '스크린원단',\n specs: ['0.3T', '0.4T', '0.5T', '0.6T'],\n code: 'SCR'\n },\n {\n value: '슬랫',\n label: '슬랫',\n specs: ['50mm', '75mm', '100mm'],\n code: 'SLT'\n },\n {\n value: '가이드레일',\n label: '가이드레일',\n specs: ['50×50', '60×60', '70×70', '80×80'],\n code: 'GR'\n },\n {\n value: '샤프트',\n label: '샤프트',\n specs: ['4인치', '5인치', '6인치'],\n code: 'SFT'\n },\n {\n value: '스프링',\n label: '스프링',\n specs: ['소형', '중형', '대형'],\n code: 'SPR'\n },\n {\n value: '체인',\n label: '체인',\n specs: ['#25', '#35', '#40'],\n code: 'CHN'\n },\n ];\n\n // 부자재 규격 옵션 (품목명에 따라 필터링)\n const getSubMaterialSpecs = () => {\n const selectedItem = subMaterialItems.find(item => item.value === formData.itemName);\n return selectedItem?.specs || [];\n };\n\n // 부자재 품목코드 자동생성\n // 새 규칙: 품목명-규격\n // 예: 연동제어기-매립(유선)\n const getSubMaterialCode = () => {\n if (!formData.itemName) return '';\n const itemName = formData.itemName;\n const spec = formData.spec || '';\n // 패턴: 품목명-규격\n return spec ? `${itemName}-${spec}` : itemName;\n };\n\n // 원자재 전용 옵션들\n const rawMaterialItems = [\n {\n value: '철판',\n label: '철판',\n specs: ['1.0T', '1.2T', '1.6T', '2.0T', '2.3T', '3.0T'],\n code: 'STEEL'\n },\n {\n value: '아연도금강판',\n label: '아연도금강판',\n specs: ['0.8T', '1.0T', '1.2T', '1.6T'],\n code: 'EGI'\n },\n {\n value: '스테인리스',\n label: '스테인리스',\n specs: ['0.8T', '1.0T', '1.2T', '1.5T', '2.0T'],\n code: 'SUS'\n },\n {\n value: '알루미늄판',\n label: '알루미늄판',\n specs: ['1.0T', '1.5T', '2.0T', '3.0T'],\n code: 'AL'\n },\n {\n value: '각파이프',\n label: '각파이프',\n specs: ['30×30', '40×40', '50×50', '60×60'],\n code: 'PIPE'\n },\n ];\n\n // 원자재 규격 옵션 (품목명에 따라 필터링)\n const getRawMaterialSpecs = () => {\n const selectedItem = rawMaterialItems.find(item => item.value === formData.itemName);\n return selectedItem?.specs || [];\n };\n\n // 원자재 품목코드 자동생성\n // 새 규칙: 품목명-규격\n // 예: 철판-SUS-1.2T\n const getRawMaterialCode = () => {\n if (!formData.itemName) return '';\n const itemName = formData.itemName;\n const spec = formData.spec || '';\n // 패턴: 품목명-규격\n return spec ? `${itemName}-${spec}` : itemName;\n };\n\n // 소모품 품목코드 자동생성\n // 새 규칙: 품목명-규격\n // 예: 용접봉-3.2MM, 장갑-면장갑\n const getConsumableCode = () => {\n if (!formData.itemName) return '';\n const itemName = formData.itemName;\n const spec = formData.spec || '';\n // 패턴: 품목명-규격\n return spec ? `${itemName}-${spec}` : itemName;\n };\n\n // 구매 부품 전용 옵션들\n const purchasedPartItemNames = [\n { value: '전동개폐기', label: '전동개폐기 (E)', code: 'E' },\n { value: '수동개폐기', label: '수동개폐기 (M)', code: 'M' },\n { value: '제어반', label: '제어반 (CTL)', code: 'CTL' },\n { value: '연기감지기', label: '연기감지기 (SMK)', code: 'SMK' },\n { value: '열감지기', label: '열감지기 (HT)', code: 'HT' },\n { value: '비상스위치', label: '비상스위치 (SW)', code: 'SW' },\n ];\n const powerOptions = ['220V', '380V'];\n const capacityOptions = ['150 KG', '300 KG', '400 KG', '500 KG', '600 KG', '800 KG', '1000 KG'];\n\n // 구매 부품 품목코드 자동생성\n // 새 규칙: 품목명-규격 (규격 = 전원 + 용량, 공백 없이 연결)\n // 예: 전동개폐기-220V300KG\n const getPurchasedPartCode = () => {\n if (!formData.itemName) return '';\n const itemName = formData.itemName;\n const power = formData.power || '';\n const capacity = formData.capacity || '';\n // 규격 = 전원 + 용량 (공백 제거)\n const spec = `${power}${capacity}`.replace(/\\s/g, '');\n // 패턴: 품목명-규격\n return spec ? `${itemName}-${spec}` : itemName;\n };\n\n const handleSubmit = () => {\n // 유효성 검사 실행\n if (!validateForm()) {\n return;\n }\n if (isPartType && !formData.partSubType) {\n alert('부품 유형을 선택해주세요.');\n return;\n }\n\n // 공유 상태 연동: 품목 추가/수정 시 콜백 호출\n const itemData = {\n ...formData,\n itemCode: formData.itemCode || `ITEM-${Date.now()}`,\n createdAt: item?.createdAt || new Date().toISOString().slice(0, 10),\n purchasePrice: formData.purchasePrice || 0,\n sellingPrice: formData.sellingPrice || 0,\n marginRate: 20,\n };\n\n if (isEdit && onUpdateItem) {\n onUpdateItem({ ...item, ...itemData });\n } else if (onAddItem) {\n onAddItem(itemData);\n }\n\n alert(isEdit ? '품목이 수정되었습니다.' : '품목이 등록되었습니다.');\n onNavigate('item-list');\n };\n\n const addBomItem = () => {\n setBomItems([...bomItems, {\n id: Date.now(),\n itemCode: '',\n itemName: '',\n spec: '',\n material: '',\n quantity: 1,\n unit: 'EA',\n price: 0,\n description: '',\n }]);\n };\n\n const removeBomItem = (id) => {\n setBomItems(bomItems.filter(item => item.id !== id));\n };\n\n const updateBomItem = (id, field, value) => {\n setBomItems(bomItems.map(item =>\n item.id === id ? { ...item, [field]: value } : item\n ));\n };\n\n // 품목코드 자동생성 미리보기 (조립부품 새 규칙)\n // 패턴: 품목명 설치유형-측면규격(가로)*측면규격(세로)*길이(앞2자리)\n // 예: 가이드레일 벽면형-1211*1111*24\n const getAutoItemCode = () => {\n if (!formData.itemName) return '';\n const installType = formData.installType || '?';\n const sideWidth = formData.sideWidth || '?';\n const sideHeight = formData.sideHeight || '?';\n const lengthCode = formData.length ? String(formData.length).substring(0, 2) : '?';\n return `${formData.itemName} ${installType}-${sideWidth}*${sideHeight}*${lengthCode}`;\n };\n\n return (\n
\n
\n \n \n \n }\n />\n\n {/* 기본 정보 - 품목유형 드롭다운 방식 */}\n
\n
기본 정보
\n
\n {/* 품목유형 드롭다운 */}\n
\n
\n
\n {hasError('itemType') ? (\n
{getFieldError('itemType')}
\n ) : (\n
* 품목 유형에 따라 입력 항목이 다릅니다
\n )}\n
\n\n {/* 품목유형 미선택 시 경고 */}\n {!formData.itemType && (\n
\n
\n
품목유형을 먼저 선택해주세요. 선택한 유형에 따라 입력 항목이 달라집니다.\n
\n )}\n\n {/* 부품일 때 부품 유형 선택 */}\n {isPartType && (\n
\n
\n
\n {/* 절곡 부품 안내 메시지 */}\n {isBendingPart && (\n
* 절곡 부품은 전개도(바라시)만 있으며, 부품 구성(BOM)은 사용하지 않습니다.
\n )}\n
\n )}\n\n {/* 절곡 부품일 때 필드들 */}\n {isBendingPart && (\n <>\n
\n
\n
\n {hasError('itemName') &&
{getFieldError('itemName')}
}\n
\n
\n
\n
\n
* 절곡부품의 종류를 선택합니다 (본체, C형, D형 등)
\n
\n
\n
\n
\n
* 절곡부품의 길이를 선택합니다 (앞 2자리가 코드로 사용)
\n
\n
\n
\n
{\n const itemName = formData.itemName || '';\n const partType = formData.bendingPartType || '';\n const sizeLength = formData.bendingSizeLength || '';\n const productCode = bendingPartItemNames.find(i => i.value === itemName)?.code || '';\n const typeCode = bendingPartTypes.find(t => t.value === partType)?.code || '';\n const sizeCode = bendingSizeLengths.find(s => s.value === sizeLength)?.code || (sizeLength ? String(sizeLength).substring(0, 2) : '');\n return `${productCode}${typeCode}${sizeCode}` || '품목명, 종류, 모양&길이를 선택하세요';\n })()}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg bg-blue-50 text-blue-800 font-mono\"\n disabled\n />\n
\n 절곡부품 코드규칙: 품목코드 + 종류코드 + 모양&길이코드 (공통코드관리 설정)\n
\n
\n
\n
\n
\n
* 비활성 시 품목 사용이 제한됩니다
\n
\n >\n )}\n\n {/* 조립 부품일 때 추가 필드들 */}\n {isAssemblyPart && (\n <>\n
\n
\n
\n {hasError('itemName') &&
{getFieldError('itemName')}
}\n
\n\n
\n
\n
\n
\n 품목기준관리에서 관리되는 항목입니다 (13개 옵션)\n
\n
\n\n
\n
\n
\n
\n 품목기준관리에서 관리되는 항목입니다 (13개 옵션)\n
\n
\n\n
\n
\n
\n
\n 품목기준관리에서 관리되는 항목입니다 (2개 옵션)\n
\n
\n\n
\n \n setFormData({ ...formData, description: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n placeholder=\"비고 사항을 입력하세요\"\n />\n
\n >\n )}\n\n {/* 제품일 때 기존 필드들 */}\n {isProductType && (\n <>\n
\n \n setFormData({ ...formData, productName: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n placeholder=\"상품명을 입력하세요\"\n />\n
\n
\n
\n
{\n // 제품의 경우 품목명 = 품목코드 규칙 적용\n setFormData({ ...formData, itemName: e.target.value, itemCode: e.target.value });\n clearFieldError('itemName');\n }}\n onBlur={() => handleBlur('itemName')}\n className={getInputClassName(hasError('itemName'))}\n placeholder=\"품목명을 입력하세요 (예: 방화셔터 W3000×H4000)\"\n />\n {hasError('itemName') &&
{getFieldError('itemName')}
}\n
\n
\n
\n
\n
\n 제품은 품목명 = 품목코드 규칙이 적용됩니다 (공통코드관리 설정)\n
\n
\n >\n )}\n\n {/* 부자재일 때 필드들 */}\n {isSubMaterialType && (\n <>\n
\n
\n
\n {hasError('itemName') &&
{getFieldError('itemName')}
}\n
\n
\n
\n
\n
* 규격은 품목명 선택 시 자동으로 필터링됩니다
\n
\n
\n
\n
\n
\n 부자재 코드규칙: 품목명-규격 (공통코드관리 설정)\n
\n
\n
\n
\n
\n
* 비활성 시 품목 사용이 제한됩니다
\n
\n
\n
\n
\n
\n 품목기준관리에서 관리되는 항목입니다 (13개 옵션)\n
\n
\n
\n \n setFormData({ ...formData, description: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n placeholder=\"비고 사항을 입력하세요\"\n />\n
\n >\n )}\n\n {/* 원자재일 때 필드들 */}\n {isRawMaterialType && (\n <>\n
\n
\n
\n {hasError('itemName') &&
{getFieldError('itemName')}
}\n
\n
\n
\n
\n
* 규격은 품목명 선택 시 자동으로 필터링됩니다
\n
\n
\n
\n
\n
\n 원자재 코드규칙: 품목명-규격 (공통코드관리 설정)\n
\n
\n
\n
\n
\n
* 비활성 시 품목 사용이 제한됩니다
\n
\n
\n
\n
\n
\n 품목기준관리에서 관리되는 항목입니다 (13개 옵션)\n
\n
\n
\n \n setFormData({ ...formData, description: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n placeholder=\"비고 사항을 입력하세요\"\n />\n
\n >\n )}\n\n {/* 소모품일 때 필드들 */}\n {isConsumableType && (\n <>\n
\n
\n
{\n setFormData({ ...formData, itemName: e.target.value });\n clearFieldError('itemName');\n }}\n onBlur={() => handleBlur('itemName')}\n className={getInputClassName(hasError('itemName'))}\n placeholder=\"품목명을 입력하세요\"\n />\n {hasError('itemName') &&
{getFieldError('itemName')}
}\n
\n
\n \n setFormData({ ...formData, spec: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n placeholder=\"예: 면-L, 고급형, A4\"\n />\n
\n
\n
\n
\n
\n 소모품 코드규칙: 품목명-규격 (공통코드관리 설정)\n
\n
\n
\n
\n
\n
\n 품목기준관리에서 관리되는 항목입니다 (13개 옵션)\n
\n
\n
\n \n setFormData({ ...formData, description: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n placeholder=\"비고 사항을 입력하세요\"\n />\n
\n >\n )}\n\n {/* 구매 부품일 때 필드들 */}\n {isPurchasedPart && (\n <>\n
\n
\n
\n {hasError('itemName') &&
{getFieldError('itemName')}
}\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n
\n
\n 품목기준관리에서 관리되는 항목입니다 (13개 옵션)\n
\n
\n
\n \n setFormData({ ...formData, description: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n placeholder=\"비고 사항을 입력하세요\"\n />\n
\n
\n
\n
\n
\n 구매부품 코드규칙: 품목명-규격 (규격 = 전원 + 용량) (공통코드관리 설정)\n
\n
\n
\n
\n
\n
* 비활성 시 품목 사용이 제한됩니다
\n
\n
\n
\n
* 이 부품이 하위 구성품을 포함하는 경우 체크하세요
\n
\n >\n )}\n\n {/* 부품 - 비조립/비절곡/비구매 부품 선택 시 기본 필드 */}\n {isPartType && formData.partSubType && !isAssemblyPart && !isBendingPart && !isPurchasedPart && (\n <>\n
\n
\n
{\n setFormData({ ...formData, itemName: e.target.value });\n clearFieldError('itemName');\n }}\n onBlur={() => handleBlur('itemName')}\n className={getInputClassName(hasError('itemName'))}\n placeholder=\"품목명을 입력하세요\"\n />\n {hasError('itemName') &&
{getFieldError('itemName')}
}\n
\n
\n \n setFormData({ ...formData, spec: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n placeholder=\"규격을 입력하세요\"\n />\n
\n
\n \n \n
\n
\n \n
\n >\n )}\n
\n
\n\n {/* 절곡 부품일 때 전개도 섹션 */}\n {isBendingPart && (\n
\n
\n \n
절곡품 전개도 (바라시)
\n \n
\n
\n
\n
\n \n \n
\n
* 전개도 이미지를 파일로 업로드하거나 직접 그릴 수 있습니다
\n
\n\n {formData.drawingInputType === 'file' ? (\n
\n
\n
\n \n \n
\n
* 절곡품 전개도 이미지를 업로드하세요 (JPG, PNG, PDF 등)
\n
\n ) : (\n
\n
\n
* '전개도 그리기' 버튼을 클릭하여 캔버스에 직접 그릴 수 있습니다
\n
\n
\n )}\n\n {/* 미리보기 영역 */}\n
\n
\n 미리보기\n {formData.drawingFile && (\n \n )}\n
\n
\n {formData.drawingFile ? (\n
\n

\n
\n ) : (\n
전개도 이미지가 없습니다\n )}\n
\n
\n
\n
\n )}\n\n {/* 절곡 부품일 때 전개도 상세 입력 (치수 계산) */}\n {isBendingPart && (\n
\n
\n
전개도 상세 입력 (치수 계산)
\n
\n
\n\n {drawingDetails.length === 0 ? (\n
\n 전개도 상세 치수를 입력하세요. '행 추가' 버튼을 클릭하여 행을 추가합니다.\n
\n ) : (\n
\n
\n \n \n | 번호 | \n 입력 | \n 연신율 | \n 연신율계산후 | \n 합계 | \n 음영 | \n A각 | \n 삭제 | \n
\n \n \n {drawingDetails.map((row) => (\n \n | \n {row.seq}\n | \n \n updateDrawingDetailRow(row.id, 'input', parseFloat(e.target.value) || 0)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm text-center\"\n />\n | \n \n updateDrawingDetailRow(row.id, 'elongation', parseFloat(e.target.value) || 0)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm text-center\"\n />\n | \n \n updateDrawingDetailRow(row.id, 'elongationCalc', parseFloat(e.target.value) || 0)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm text-center bg-gray-50\"\n />\n | \n \n \n | \n \n \n | \n \n \n | \n \n \n | \n
\n ))}\n \n
\n
\n )}\n\n
\n
\n * 연신율: 절곡 시 소재가 늘어나는 비율 계산값\n
\n * 합계: 입력값 + 연신율계산후 값의 누적 합\n
\n * 음영: 전개도에서 절곡 방향을 표시할 때 사용\n
\n
\n
\n )}\n\n {/* 조립 부품일 때 측면 규격 및 길이 섹션 */}\n {isAssemblyPart && (\n
\n
측면 규격 및 길이
\n
\n
\n \n setFormData({ ...formData, sideWidth: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n placeholder=\"예: 50\"\n />\n
\n
\n \n setFormData({ ...formData, sideHeight: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n placeholder=\"예: 100\"\n />\n
\n
\n
\n
\n
\n 품목기준관리에서 관리되는 항목입니다 (4개 옵션)\n
\n
\n
\n
\n
\n
\n 조립부품 코드규칙: 품목명 설치유형-측면규격(가로)*측면규격(세로)*길이(앞2자리) (공통코드관리 설정)\n
\n
\n
\n
\n
\n
* 비활성 시 품목 사용이 제한됩니다
\n
\n
\n
\n
* 이 부품이 하위 구성품을 포함하는 경우 체크하세요
\n
\n
\n
\n )}\n\n {/* 조립 부품일 때 전개도 섹션 */}\n {isAssemblyPart && (\n
\n
\n \n
조립품 전개도 (바라시)
\n \n
\n
\n
\n
\n \n \n
\n
* 전개도 이미지를 파일로 업로드하거나 직접 그릴 수 있습니다
\n
\n\n {formData.drawingInputType === 'file' ? (\n
\n
\n
\n \n \n
\n
* 철곡품 전개도 이미지를 업로드하세요 (JPG, PNG, PDF 등)
\n
\n ) : (\n
\n
\n
* '전개도 그리기' 버튼을 클릭하여 캔버스에 직접 그릴 수 있습니다
\n
\n
\n )}\n\n {/* 미리보기 영역 */}\n
\n
\n 미리보기\n {formData.drawingFile && (\n \n )}\n
\n
\n {formData.drawingFile ? (\n
\n

\n
\n ) : (\n
전개도 이미지가 없습니다\n )}\n
\n
\n
\n
\n )}\n\n {/* 조립 부품일 때 BOM 섹션 */}\n {isAssemblyPart && formData.needsBom && (\n
\n
\n
부품 구성 (BOM)
\n
\n
\n\n {bomItems.length === 0 ? (\n
\n BOM 품목을 추가해주세요\n
\n ) : (\n
\n
\n \n \n | 품목코드 / 품목명 입력 | \n 품목명 | \n 규격 | \n 재질 | \n 수량 | \n 단위 | \n 단가 | \n 비고 | \n 삭제 | \n
\n \n \n {bomItems.map(bomItem => (\n \n | \n \n updateBomItem(bomItem.id, 'itemCode', e.target.value)}\n className=\"flex-1 px-2 py-1 border border-gray-300 rounded text-sm\"\n placeholder=\"CTL-001\"\n />\n \n \n \n | \n \n updateBomItem(bomItem.id, 'itemName', e.target.value)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm\"\n placeholder=\"제어기 기본형\"\n />\n | \n \n updateBomItem(bomItem.id, 'spec', e.target.value)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm\"\n placeholder=\"기본형\"\n />\n | \n \n updateBomItem(bomItem.id, 'material', e.target.value)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm\"\n placeholder=\"재질\"\n />\n | \n \n updateBomItem(bomItem.id, 'quantity', parseInt(e.target.value) || 0)}\n className=\"w-16 px-2 py-1 border border-gray-300 rounded text-sm text-center\"\n min=\"1\"\n />\n | \n \n EA\n | \n \n updateBomItem(bomItem.id, 'price', parseInt(e.target.value) || 0)}\n className=\"w-24 px-2 py-1 border border-gray-300 rounded text-sm text-right\"\n min=\"0\"\n />\n | \n \n updateBomItem(bomItem.id, 'description', e.target.value)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm\"\n placeholder=\"비고\"\n />\n | \n \n \n | \n
\n ))}\n \n
\n
\n )}\n
\n )}\n\n {/* 제품일 때 인정 정보 섹션 */}\n {isProductType && (\n
\n
인정 정보
\n
\n
\n \n setFormData({ ...formData, certNumber: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n placeholder=\"인정번호를 입력하세요\"\n />\n
\n
\n
\n \n setFormData({ ...formData, certStartDate: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n />\n
\n
\n \n setFormData({ ...formData, certEndDate: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-lg\"\n />\n
\n
\n
\n
\n \n \n
\n
\n
\n
\n
\n \n \n
\n
\n
\n
\n )}\n\n {/* 제품일 때 BOM 섹션 */}\n {isProductType && (\n
\n
\n
부품 구성 (BOM)
\n \n \n\n {formData.needsBom ? (\n
\n
\n\n {bomItems.length === 0 ? (\n
\n 부품을 추가해주세요\n
\n ) : (\n
\n
\n \n \n | 품목코드/품목명 입력 | \n 품목명 | \n 규격 | \n 재질 | \n 수량 | \n 단위 | \n 단가 | \n 비고 | \n 삭제 | \n
\n \n \n {bomItems.map(bomItem => (\n \n | \n updateBomItem(bomItem.id, 'itemCode', e.target.value)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm\"\n placeholder=\"품목코드\"\n />\n | \n \n updateBomItem(bomItem.id, 'itemName', e.target.value)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm\"\n placeholder=\"품목명\"\n />\n | \n \n updateBomItem(bomItem.id, 'spec', e.target.value)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm\"\n placeholder=\"규격\"\n />\n | \n \n updateBomItem(bomItem.id, 'material', e.target.value)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm\"\n placeholder=\"재질\"\n />\n | \n \n updateBomItem(bomItem.id, 'quantity', parseInt(e.target.value) || 0)}\n className=\"w-20 px-2 py-1 border border-gray-300 rounded text-sm\"\n min=\"1\"\n />\n | \n \n \n | \n \n updateBomItem(bomItem.id, 'price', parseInt(e.target.value) || 0)}\n className=\"w-24 px-2 py-1 border border-gray-300 rounded text-sm\"\n min=\"0\"\n />\n | \n \n updateBomItem(bomItem.id, 'description', e.target.value)}\n className=\"w-full px-2 py-1 border border-gray-300 rounded text-sm\"\n placeholder=\"비고\"\n />\n | \n \n \n | \n
\n ))}\n \n
\n
\n )}\n
\n ) : (\n
\n BOM 정보가 필요하면 위의 체크박스를 선택해주세요\n
\n )}\n
\n )}\n\n {/* 이미지 편집기 모달 */}\n {showDrawingEditor && (\n
\n
\n
\n
\n
이미지 편집기
\n
품목 이미지를 그리거나 편집합니다.
\n
\n
\n
\n
\n
\n \n \n \n \n \n \n \n \n
\n
\n {['#000000', '#FF0000', '#0000FF', '#00FF00', '#FF00FF', '#FFFF00', '#00FFFF', '#FFA500', '#800080', '#FFC0CB'].map(color => (\n \n ))}\n
\n
\n
\n
\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// 품목 상세 컴포넌트\nconst ItemDetail = ({ item, onNavigate }) => {\n if (!item) {\n return (\n
\n 품목 정보를 찾을 수 없습니다.\n \n
\n );\n }\n\n // 현재 날짜를 ISO 형식으로\n const currentDate = new Date().toISOString();\n\n return (\n
\n
\n \n \n \n }\n />\n\n {/* 기본 정보 */}\n
\n
기본 정보
\n
\n
\n
품목코드
\n
{item.itemCode}
\n
\n
\n
품목명
\n
{item.itemName}
\n
\n
\n
품목유형
\n
\n {item.itemType}\n \n
\n
\n\n
\n
비고
\n
{item.description || item.itemName}
\n
\n\n
\n\n
\n
등록일
\n
{item.createdAt || currentDate}
\n
\n
\n\n {/* 부품 구성 (BOM) */}\n
\n
\n
\n
\n
\n
등록된 BOM 품목이 없습니다
\n
품목 수정에서 BOM을 추가할 수 있습니다
\n
\n
\n
\n
\n );\n};\n\n// ============================================================\n// 기존 품목기준관리 컴포넌트 (호환성 유지)\n// ============================================================\nconst ItemMasterManagement = ({ onNavigate }) => {\n return
;\n};\n\n// ============================================================\n// 자재기준관리 컴포넌트\n// ============================================================\nconst MaterialMasterManagement = ({ onNavigate }) => {\n return
;\n};\n\n// 초기 데이터 - 품목유형 (레거시 호환)\nconst initialItemTypes = [\n { id: 1, code: 'FG', name: '제품', description: '완제품', sortOrder: 1, isActive: true },\n { id: 2, code: 'PT', name: '부품', description: '부품/파트', sortOrder: 2, isActive: true },\n { id: 3, code: 'WIP', name: '반제품', description: '반제품/공정품', sortOrder: 3, isActive: true },\n { id: 4, code: 'RM', name: '원자재', description: '원자재', sortOrder: 4, isActive: true },\n];\n\n// 초기 데이터 - 품목분류 (계층구조)\nconst initialItemCategories = [\n { id: 1, code: 'SCR', name: '스크린', parentId: null, level: 1, path: '/1', sortOrder: 1 },\n { id: 2, code: 'SCR-STD', name: '표준형', parentId: 1, level: 2, path: '/1/2', sortOrder: 1 },\n { id: 3, code: 'SCR-PRM', name: '프리미엄', parentId: 1, level: 2, path: '/1/3', sortOrder: 2 },\n { id: 4, code: 'SLT', name: '슬랫', parentId: null, level: 1, path: '/4', sortOrder: 2 },\n { id: 5, code: 'SLT-STD', name: '표준형', parentId: 4, level: 2, path: '/4/5', sortOrder: 1 },\n { id: 6, code: 'BND', name: '절곡', parentId: null, level: 1, path: '/6', sortOrder: 3 },\n { id: 7, code: 'MTR', name: '모터', parentId: null, level: 1, path: '/7', sortOrder: 4 },\n { id: 8, code: 'MTR-TUB', name: '튜블러모터', parentId: 7, level: 2, path: '/7/8', sortOrder: 1 },\n { id: 9, code: 'MTR-GRD', name: '기어드모터', parentId: 7, level: 2, path: '/7/9', sortOrder: 2 },\n];\n\n// 초기 데이터 - 마스터 필드\nconst initialMasterFields = [\n { id: 1, fieldKey: 'itemCode', fieldName: '품목코드', inputType: 'text', isRequired: true, validations: { maxLength: 20, pattern: '^[A-Z0-9-]+$' }, description: '품목 식별 코드' },\n { id: 2, fieldKey: 'itemName', fieldName: '품목명', inputType: 'text', isRequired: true, validations: { maxLength: 100 }, description: '품목 이름' },\n { id: 3, fieldKey: 'category', fieldName: '분류', inputType: 'select', isRequired: false, options: { source: 'api', apiUrl: '/api/item-categories' }, description: '품목 분류' },\n { id: 4, fieldKey: 'itemType', fieldName: '품목유형', inputType: 'select', isRequired: true, options: { items: ['제품', '부품', '반제품', '원자재'] }, description: '제품/부품/반제품/원자재' },\n { id: 5, fieldKey: 'unit', fieldName: '단위', inputType: 'select', isRequired: true, options: { items: ['EA', 'SET', 'M', 'KG', 'TON'] }, description: '수량 단위' },\n { id: 6, fieldKey: 'spec', fieldName: '규격', inputType: 'text', isRequired: false, description: '제품 규격' },\n { id: 7, fieldKey: 'bomRequired', fieldName: 'BOM 필요', inputType: 'checkbox', isRequired: false, description: 'BOM 구성 필요 여부' },\n { id: 8, fieldKey: 'inspectionRequired', fieldName: '검사 필요', inputType: 'checkbox', isRequired: false, description: '입고/출고 검사 필요 여부' },\n { id: 9, fieldKey: 'leadTime', fieldName: '리드타임', inputType: 'number', isRequired: false, validations: { min: 0 }, description: '조달/생산 소요일' },\n { id: 10, fieldKey: 'safetyStock', fieldName: '안전재고', inputType: 'number', isRequired: false, validations: { min: 0 }, description: '안전재고 수량' },\n { id: 11, fieldKey: 'description', fieldName: '설명', inputType: 'textarea', isRequired: false, description: '품목 상세 설명' },\n { id: 12, fieldKey: 'price', fieldName: '단가', inputType: 'number', isRequired: false, validations: { min: 0 }, description: '기준 단가' },\n];\n\n// 초기 데이터 - 마스터 섹션\nconst initialMasterSections = [\n { id: 1, sectionKey: 'basicInfo', sectionName: '기본정보', sectionType: 'general', applicableTypes: ['FG', 'PT', 'WIP', 'RM'], description: '품목 기본 정보', fields: [1, 2, 3, 4, 5, 6] },\n { id: 2, sectionKey: 'bom', sectionName: 'BOM', sectionType: 'module', applicableTypes: ['FG', 'WIP'], description: 'BOM 구성 정보', fields: [7] },\n { id: 3, sectionKey: 'inventory', sectionName: '재고정보', sectionType: 'general', applicableTypes: ['FG', 'PT', 'RM'], description: '재고 관련 정보', fields: [9, 10] },\n { id: 4, sectionKey: 'quality', sectionName: '품질정보', sectionType: 'module', applicableTypes: ['FG', 'PT', 'RM'], description: '품질 검사 정보', fields: [8] },\n { id: 5, sectionKey: 'pricing', sectionName: '단가정보', sectionType: 'general', applicableTypes: ['FG', 'PT', 'RM'], description: '단가 정보', fields: [12] },\n];\n\n// 초기 데이터 - 페이지 템플릿\nconst initialPageTemplates = [\n {\n id: 1,\n pageCode: 'item_registration',\n pageName: '품목 등록',\n pagePath: '/제품관리/품목_등록',\n itemTypes: ['FG'],\n sections: [\n {\n sectionId: 1, sectionKey: 'basicInfo', sectionName: '기본정보', sortOrder: 1, fields: [\n { fieldId: 1, fieldKey: 'itemCode', fieldName: '품목코드', inputType: 'text', isRequired: true },\n { fieldId: 2, fieldKey: 'itemName', fieldName: '품목명', inputType: 'text', isRequired: true },\n { fieldId: 3, fieldKey: 'category', fieldName: '분류', inputType: 'select', isRequired: false },\n ]\n },\n {\n sectionId: 2, sectionKey: 'bom', sectionName: 'BOM', sortOrder: 2, fields: [\n { fieldId: 7, fieldKey: 'bomRequired', fieldName: 'BOM 필요', inputType: 'checkbox', isRequired: false },\n ]\n },\n ],\n conditions: [\n { id: 1, sourceFieldKey: 'category', conditionType: 'equals', conditionValue: 'FG', displayType: 'show_section', targetSectionKey: 'bom' }\n ]\n }\n];\n\n// ============================================================\n// 코드기준 관리 컴포넌트\n// ============================================================\nconst CodeRuleManagement = ({ onNavigate }) => {\n const [showHelp, setShowHelp] = useState(false);\n\n // 코드기준 목록 (LOT 관련은 채번관리에서 관리)\n const [codeRules, setCodeRules] = useState([\n // === 기본 코드 ===\n { id: 1, name: '품목코드 기준', target: '품목코드', codeType: 'composite', prefix: '', useDate: false, dateFormat: '', useProcess: false, seqDigits: 3, separator: '-', components: ['itemType', 'category', 'seq'], example: 'BP-GR-001', status: '활성', description: '품목유형 + 카테고리 + 일련번호', category: '기본코드' },\n { id: 2, name: '거래처코드 기준', target: '거래처코드', codeType: 'simple', prefix: 'C', useDate: false, dateFormat: '', useProcess: false, seqDigits: 3, separator: '-', components: ['prefix', 'seq'], example: 'C-001', status: '활성', description: '거래처 식별코드', category: '기본코드' },\n { id: 3, name: '현장코드 기준', target: '현장코드', codeType: 'simple', prefix: 'S', useDate: false, dateFormat: '', useProcess: false, seqDigits: 3, separator: '-', components: ['prefix', 'seq'], example: 'S-001', status: '활성', description: '현장 식별코드', category: '기본코드' },\n // === 부자재 코드 ===\n { id: 4, name: '스크린원단', target: '부자재', codeType: 'mapping', prefix: 'SCR', example: 'SCR-0.3T', status: '활성', description: '패턴: {코드}-{규격}', category: '부자재' },\n { id: 5, name: '엔드바', target: '부자재', codeType: 'mapping', prefix: 'END', example: 'END-0.5T', status: '활성', description: '패턴: {코드}-{규격}', category: '부자재' },\n { id: 6, name: '하부바', target: '부자재', codeType: 'mapping', prefix: 'BTM', example: 'BTM-1.2T', status: '활성', description: '패턴: {코드}-{규격}', category: '부자재' },\n { id: 7, name: '사이드가이드', target: '부자재', codeType: 'mapping', prefix: 'SDG', example: 'SDG-1.6T', status: '활성', description: '패턴: {코드}-{규격}', category: '부자재' },\n { id: 8, name: '브라켓', target: '부자재', codeType: 'mapping', prefix: 'BKT', example: 'BKT-2.3T', status: '활성', description: '패턴: {코드}-{규격}', category: '부자재' },\n // === 원자재 코드 ===\n { id: 9, name: 'EGI코일', target: '원자재', codeType: 'mapping', prefix: 'EGI', example: 'EGI-0.5T', status: '활성', description: '패턴: {코드}-{규격}', category: '원자재' },\n { id: 10, name: 'SUS코일', target: '원자재', codeType: 'mapping', prefix: 'SUS', example: 'SUS-0.6T', status: '활성', description: '패턴: {코드}-{규격}', category: '원자재' },\n { id: 11, name: '슬랫원단', target: '원자재', codeType: 'mapping', prefix: 'SLT', example: 'SLT-1.2T', status: '활성', description: '패턴: {코드}-{규격}', category: '원자재' },\n { id: 12, name: '경첩', target: '원자재', codeType: 'mapping', prefix: 'HNG', example: 'HNG-STD', status: '활성', description: '패턴: {코드}-{규격}', category: '원자재' },\n // === 구매부품 코드 ===\n { id: 13, name: '전기개폐기', target: '구매부품', codeType: 'mapping', prefix: 'E', example: 'E-220V-300KG', status: '활성', description: '패턴: {코드}-{전원}-{용량}', category: '구매부품' },\n { id: 14, name: '수동개폐기', target: '구매부품', codeType: 'mapping', prefix: 'M', example: 'M-MANUAL', status: '활성', description: '패턴: {코드}-{타입}', category: '구매부품' },\n { id: 15, name: '제어반', target: '구매부품', codeType: 'mapping', prefix: 'CTL', example: 'CTL-220V', status: '활성', description: '패턴: {코드}-{전원}', category: '구매부품' },\n // === 절곡부품 품목코드 ===\n { id: 16, name: '가이드레일(벽면형)', target: '절곡부품', codeType: 'mapping', prefix: 'R', example: 'RC24', status: '활성', description: '{제품코드}{종류코드}{길이코드}', category: '절곡부품' },\n { id: 17, name: '가이드레일(측면형)', target: '절곡부품', codeType: 'mapping', prefix: 'S', example: 'SC30', status: '활성', description: '{제품코드}{종류코드}{길이코드}', category: '절곡부품' },\n { id: 18, name: '케이스', target: '절곡부품', codeType: 'mapping', prefix: 'C', example: 'CM24', status: '활성', description: '{제품코드}{종류코드}{길이코드}', category: '절곡부품' },\n { id: 19, name: '하단마감재(스크린)', target: '절곡부품', codeType: 'mapping', prefix: 'B', example: 'BM30', status: '활성', description: '{제품코드}{종류코드}{길이코드}', category: '절곡부품' },\n { id: 20, name: 'L-Bar', target: '절곡부품', codeType: 'mapping', prefix: 'L', example: 'LM24', status: '활성', description: '{제품코드}{종류코드}{길이코드}', category: '절곡부품' },\n { id: 21, name: '연기차단재', target: '절곡부품', codeType: 'mapping', prefix: 'G', example: 'G-I-4A05-53', status: '활성', description: '{제품}-{종류}-{날짜}-{규격}', category: '절곡부품' },\n { id: 22, name: '하단마감재(철재)', target: '절곡부품', codeType: 'mapping', prefix: 'T', example: 'TM24', status: '활성', description: '{제품코드}{종류코드}{길이코드}', category: '절곡부품' },\n // === 셔터종류 코드 ===\n { id: 29, name: '실리카', target: '셔터종류', codeType: 'mapping', prefix: 'SI', example: 'SI', status: '활성', description: '셔터종류 코드', category: '셔터종류' },\n { id: 30, name: '와이어', target: '셔터종류', codeType: 'mapping', prefix: 'WS', example: 'WS', status: '활성', description: '셔터종류 코드', category: '셔터종류' },\n { id: 31, name: '화이바', target: '셔터종류', codeType: 'mapping', prefix: 'FS', example: 'FS', status: '활성', description: '셔터종류 코드', category: '셔터종류' },\n { id: 32, name: '철재(조적용)', target: '셔터종류', codeType: 'mapping', prefix: 'IJ', example: 'IJ', status: '활성', description: '인정제품 조적용', category: '셔터종류' },\n // === 인정제품 코드 ===\n { id: 33, name: '실리카(EGI)', target: '인정제품', codeType: 'mapping', prefix: 'SE', example: 'KD-SE-251217-01', status: '활성', description: 'KSE01', category: '인정제품' },\n { id: 34, name: '실리카(SUS)', target: '인정제품', codeType: 'mapping', prefix: 'SS', example: 'KD-SS-251217-01', status: '활성', description: 'KSS01', category: '인정제품' },\n { id: 35, name: '와이어(EGI)', target: '인정제품', codeType: 'mapping', prefix: 'WE', example: 'KD-WE-251217-01', status: '활성', description: 'KWE01', category: '인정제품' },\n { id: 36, name: '철재(EGI)', target: '인정제품', codeType: 'mapping', prefix: 'TE', example: 'KD-TE-251217-01', status: '활성', description: 'KTE01', category: '인정제품' },\n { id: 37, name: '철재(SUS)', target: '인정제품', codeType: 'mapping', prefix: 'TS', example: 'KD-TS-251217-01', status: '활성', description: 'KQTS01', category: '인정제품' },\n // === 신규 인정제품 코드 ===\n { id: 38, name: '실리카(신규)', target: '신규인정', codeType: 'mapping', prefix: 'SA', example: 'KD-SA-251217-01', status: '활성', description: 'KSS02 (시험체 적용)', category: '신규인정' },\n { id: 39, name: '와이어(신규)', target: '신규인정', codeType: 'mapping', prefix: 'WW', example: 'KD-WW-251217-01', status: '활성', description: 'KWWS02 (시험체 적용)', category: '신규인정' },\n { id: 40, name: '조적와이어', target: '신규인정', codeType: 'mapping', prefix: 'JW', example: 'KD-JW-251217-01', status: '활성', description: 'KJWS01 (시험체 적용)', category: '신규인정' },\n { id: 41, name: 'DS셔터', target: '신규인정', codeType: 'mapping', prefix: 'DS', example: 'KD-DS-251217-01', status: '활성', description: 'KDSS01 (시험체 적용)', category: '신규인정' },\n ]);\n\n const [showModal, setShowModal] = useState(false);\n const [editingRule, setEditingRule] = useState(null);\n const [selectedCategory, setSelectedCategory] = useState('전체');\n\n // 카테고리 목록 추출\n const categories = ['전체', ...new Set(codeRules.map(r => r.category).filter(Boolean))];\n\n // 필터링된 코드 규칙\n const filteredCodeRules = selectedCategory === '전체'\n ? codeRules\n : codeRules.filter(r => r.category === selectedCategory);\n\n const [formData, setFormData] = useState({\n name: '',\n target: '',\n codeType: 'simple',\n prefix: '',\n useDate: false,\n dateFormat: 'YYMMDD',\n useProcess: false,\n seqDigits: 3,\n separator: '-',\n status: '활성',\n description: '',\n });\n\n // 적용대상 및 템플릿 (LOT 관련은 채번관리에서 관리)\n const targetTemplates = [\n { value: '품목코드', label: '품목코드', codeType: 'composite', prefix: '', useDate: false, useProcess: false, seqDigits: 3 },\n { value: '거래처코드', label: '거래처코드', codeType: 'simple', prefix: 'C', useDate: false, useProcess: false, seqDigits: 3 },\n { value: '현장코드', label: '현장코드', codeType: 'simple', prefix: 'S', useDate: false, useProcess: false, seqDigits: 3 },\n ];\n\n // 코드 유형 (LOT형은 채번관리에서 관리)\n const codeTypes = [\n { value: 'simple', label: '단순형', desc: '접두사 + 순번', example: 'C-001' },\n { value: 'composite', label: '조합형', desc: '품목유형 + 카테고리 + 순번', example: 'BP-GR-001' },\n ];\n\n // 날짜 형식\n const dateFormats = [\n { value: 'YYMMDD', label: 'YYMMDD (251205)', example: '251205' },\n { value: 'YYYYMMDD', label: 'YYYYMMDD (20251205)', example: '20251205' },\n { value: 'YYMM', label: 'YYMM (2512)', example: '2512' },\n ];\n\n // 순번 자릿수\n const seqDigitOptions = [\n { value: 2, label: '2자리 (01~99)' },\n { value: 3, label: '3자리 (001~999)' },\n { value: 4, label: '4자리 (0001~9999)' },\n ];\n\n // 구분자\n const separatorOptions = [\n { value: '-', label: '하이픈 (-)' },\n { value: '_', label: '언더스코어 (_)' },\n { value: '', label: '없음' },\n ];\n\n // 코드 미리보기 생성 (LOT형은 채번관리에서 관리)\n const generatePreview = (data) => {\n const sep = data.separator;\n const seq = '1'.padStart(data.seqDigits, '0');\n\n // 코드 유형별 생성\n switch (data.codeType) {\n case 'simple':\n return `${data.prefix}${sep}${seq}`;\n case 'composite':\n return `BP${sep}GR${sep}${seq}`;\n default:\n return `${data.prefix}${sep}${seq}`;\n }\n };\n\n // 구조 설명 생성 (LOT형은 채번관리에서 관리)\n const getStructureDescription = (data) => {\n const parts = [];\n switch (data.codeType) {\n case 'simple':\n parts.push('접두사');\n parts.push('순번');\n break;\n case 'composite':\n parts.push('품목유형코드');\n parts.push('카테고리코드');\n parts.push('일련번호');\n break;\n }\n return parts.join(' + ');\n };\n\n // 적용대상 선택 시 템플릿 적용\n const handleTargetChange = (target) => {\n const template = targetTemplates.find(t => t.value === target);\n if (template) {\n setFormData(prev => ({\n ...prev,\n target,\n name: `${target} 기준`,\n codeType: template.codeType,\n prefix: template.prefix,\n useDate: template.useDate,\n useProcess: template.useProcess,\n seqDigits: template.seqDigits,\n }));\n }\n };\n\n // 신규 등록 모달\n const openCreateModal = () => {\n setEditingRule(null);\n setFormData({\n name: '',\n target: '',\n codeType: 'simple',\n prefix: '',\n useDate: false,\n dateFormat: 'YYMMDD',\n useProcess: false,\n seqDigits: 3,\n separator: '-',\n status: '활성',\n description: '',\n });\n setShowModal(true);\n };\n\n // 수정 모달\n const openEditModal = (rule) => {\n setEditingRule(rule);\n setFormData({\n name: rule.name,\n target: rule.target,\n codeType: rule.codeType,\n prefix: rule.prefix,\n useDate: rule.useDate,\n dateFormat: rule.dateFormat || 'YYMMDD',\n useProcess: rule.useProcess,\n seqDigits: rule.seqDigits,\n separator: rule.separator,\n status: rule.status,\n description: rule.description || '',\n });\n setShowModal(true);\n };\n\n // 저장\n const handleSave = () => {\n const example = generatePreview(formData);\n const structure = getStructureDescription(formData);\n if (editingRule) {\n setCodeRules(prev => prev.map(r =>\n r.id === editingRule.id ? { ...r, ...formData, example, description: structure } : r\n ));\n } else {\n setCodeRules(prev => [...prev, {\n id: Math.max(...prev.map(r => r.id)) + 1,\n ...formData,\n example,\n description: structure,\n }]);\n }\n setShowModal(false);\n };\n\n // 삭제\n const handleDelete = (id) => {\n if (confirm('이 코드기준을 삭제하시겠습니까?')) {\n setCodeRules(prev => prev.filter(r => r.id !== id));\n }\n };\n\n // 상태 토글\n const toggleStatus = (id) => {\n setCodeRules(prev => prev.map(r =>\n r.id === id ? { ...r, status: r.status === '활성' ? '비활성' : '활성' } : r\n ));\n };\n\n return (\n
\n {/* 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n
\n \n 공통코드 목록\n
\n
\n
\n
\n
\n
\n\n {/* 도움말 모달 */}\n {showHelp && (\n
\n
\n
\n
코드기준 vs 번호기준 차이점
\n \n \n
\n
\n
\n
코드기준 (실물용)\n
\n
\n 실물/마스터에 부여하는 식별코드\n
\n
\n - • 품목코드 (RC30, CB24 등)
\n - • 입고LOT, 생산LOT
\n - • 거래처코드, 현장코드
\n
\n
\n
\n
번호기준 (문서용)\n
\n
\n 문서에 부여하는 일련번호\n
\n
\n - • 견적번호, 수주번호
\n - • 생산지시번호, 출하번호
\n - • 발주번호
\n
\n
\n
\n
\n 요약: 코드기준은 품목/거래처 등 마스터 데이터를 식별하고,\n 번호기준은 견적서/수주서 등 업무 문서를 식별합니다.\n
\n
\n
\n \n
\n
\n
\n )}\n\n {/* 카테고리 필터 */}\n
\n {categories.map(cat => (\n \n ))}\n
\n\n {/* 코드기준 목록 */}\n
\n
\n \n \n | 번호 | \n 코드기준 이름 | \n 적용 대상 | \n 접두사 | \n 설명 | \n 예시 | \n 상태 | \n 작업 | \n
\n \n \n {filteredCodeRules.map((rule, idx) => (\n \n | {filteredCodeRules.length - idx} | \n {rule.name} | \n \n {rule.target}\n | \n \n \n {rule.prefix}\n \n | \n {rule.description} | \n {rule.example} | \n \n \n | \n \n \n \n \n \n | \n
\n ))}\n \n
\n
\n\n {/* 등록/수정 모달 */}\n {showModal && (\n
\n
\n {/* 모달 헤더 */}\n
\n
\n {editingRule ? '코드기준 수정' : '코드기준 등록'}\n
\n \n \n\n {/* 모달 내용 */}\n
\n {/* 적용 대상 선택 */}\n
\n
\n 1\n 적용 대상 선택\n
\n\n
\n {targetTemplates.map(template => (\n
\n ))}\n
\n\n {/* 코드기준 이름 */}\n
\n \n setFormData(prev => ({ ...prev, name: e.target.value }))}\n className=\"w-full border rounded-lg px-3 py-2 text-sm\"\n placeholder=\"예: 입고LOT 기준\"\n />\n
\n
\n\n {/* 코드 유형 선택 */}\n
\n
\n 2\n 코드 유형\n
\n\n
\n {codeTypes.map(type => (\n
\n ))}\n
\n
\n\n {/* 코드 구성 설정 */}\n
\n
\n 3\n 코드 구성\n
\n\n {/* 단순형, LOT형일 때만 접두사 표시 */}\n {formData.codeType !== 'composite' && (\n
\n
\n \n setFormData(prev => ({ ...prev, prefix: e.target.value }))}\n className=\"w-full border rounded-lg px-3 py-2 text-sm font-mono\"\n placeholder=\"예: L, P, C\"\n />\n
\n
\n \n \n
\n
\n )}\n\n {/* LOT형일 때 날짜/공정 옵션 */}\n {formData.codeType === 'lot' && (\n
\n )}\n\n {/* 조합형 안내 */}\n {formData.codeType === 'composite' && (\n
\n
\n 조합형은 품목유형코드 + 카테고리코드 + 일련번호로 구성됩니다.\n
\n
\n 예: BP(절곡부품) + GR(가이드레일) + 001 = BP-GR-001\n
\n
\n )}\n\n {/* 순번 자릿수 */}\n
\n \n \n
\n
\n\n {/* 코드 미리보기 */}\n
\n
\n ✓\n 생성 코드 미리보기\n
\n\n {/* 시각적 분해 */}\n
\n {formData.codeType === 'composite' ? (\n <>\n
BP
\n
{formData.separator}
\n
GR
\n
{formData.separator}
\n
\n {'0'.repeat(formData.seqDigits - 1)}1\n
\n >\n ) : (\n <>\n {formData.prefix && (\n <>\n
\n {formData.prefix}\n
\n {formData.separator &&
{formData.separator}
}\n >\n )}\n {formData.useDate && (\n <>\n
\n {dateFormats.find(d => d.value === formData.dateFormat)?.example}\n
\n {formData.separator &&
{formData.separator}
}\n >\n )}\n {formData.useProcess && (\n <>\n
SC
\n {formData.separator &&
{formData.separator}
}\n >\n )}\n
\n {'0'.repeat(formData.seqDigits - 1)}1\n
\n >\n )}\n
\n\n {/* 구성 설명 */}\n
\n {formData.codeType === 'composite' ? (\n <>\n \n 품목유형\n \n \n 카테고리\n \n \n 일련번호\n \n >\n ) : (\n <>\n {formData.prefix && (\n \n 접두사\n \n )}\n {formData.useDate && (\n \n 날짜\n \n )}\n {formData.useProcess && (\n \n 공정코드\n \n )}\n \n 순번\n \n >\n )}\n
\n\n {/* 최종 결과 */}\n
\n 생성될 코드: \n \n {generatePreview(formData)}\n \n
\n
\n\n {/* LOT번호 생성 규칙 참조 */}\n
\n
LOT번호 생성 규칙 (참조)
\n
\n
KD
\n
-\n
XX
\n
-\n
YYMMDD
\n
-\n
NN
\n
\n
\n 회사코드\n 인정제품\n 생성일자\n 순번\n
\n
\n 예시: \n KD-SS-240919-01\n
\n
\n\n {/* 상태 */}\n
\n
\n\n {/* 모달 푸터 */}\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// ============================================================\n// 재고 현황 컴포넌트 (통합)\n// ============================================================\nconst StockList = ({ inventory = [], onNavigate }) => {\n const [selectedItemType, setSelectedItemType] = useState('전체');\n const [selectedCategory, setSelectedCategory] = useState('전체');\n const [search, setSearch] = useState('');\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n // 품목별 샘플 LOT 데이터 생성 (실제로는 서버에서 조회)\n const generateSampleLots = (item) => {\n const today = new Date();\n const lots = [];\n const lotCount = Math.min(Math.ceil(item.stock / 30), 5); // 최대 5개 LOT\n let remainingStock = item.stock;\n\n for (let i = 0; i < lotCount && remainingStock > 0; i++) {\n const daysAgo = (lotCount - i) * 7 + Math.floor(Math.random() * 5); // 오래된 순\n const inboundDate = new Date(today);\n inboundDate.setDate(inboundDate.getDate() - daysAgo);\n\n const qty = i === lotCount - 1 ? remainingStock : Math.floor(remainingStock / (lotCount - i) * (0.8 + Math.random() * 0.4));\n remainingStock -= qty;\n\n const suppliers = ['포스코', '현대제철', '동국제강', '세아제강', '한국철강'];\n const locations = ['A-01', 'A-02', 'B-01', 'B-02', 'C-01'];\n\n lots.push({\n id: `${item.materialCode}-LOT-${i + 1}`,\n lotNo: `${inboundDate.toISOString().slice(2, 10).replace(/-/g, '')}-${String(i + 1).padStart(2, '0')}`,\n inboundDate: inboundDate.toISOString().split('T')[0],\n supplier: suppliers[i % suppliers.length],\n qty: qty,\n remainingQty: qty,\n location: locations[i % locations.length],\n status: qty > 0 ? 'AVAILABLE' : 'DEPLETED',\n daysInStock: daysAgo,\n poNo: `PO-${inboundDate.toISOString().slice(2, 10).replace(/-/g, '')}-${String(Math.floor(Math.random() * 99) + 1).padStart(2, '0')}`,\n });\n }\n\n // FIFO 순서 (입고일 오름차순 - 오래된 것 먼저)\n return lots.sort((a, b) => new Date(a.inboundDate) - new Date(b.inboundDate));\n };\n\n // LOT 상태별 색상\n const getLotStatusStyle = (status, daysInStock) => {\n if (daysInStock > 30) return 'bg-orange-100 text-orange-700'; // 장기 재고\n if (status === 'AVAILABLE') return 'bg-green-100 text-green-700';\n if (status === 'RESERVED') return 'bg-blue-100 text-blue-700';\n if (status === 'HOLD') return 'bg-yellow-100 text-yellow-700';\n return 'bg-gray-100 text-gray-700';\n };\n\n // 행 클릭 시 상세 페이지로 이동\n const handleRowClick = (item) => {\n onNavigate?.('stock-detail', item);\n };\n\n // 품목유형 목록\n const itemTypes = [\n { code: '전체', name: '전체' },\n { code: '원자재', name: '원자재' },\n { code: '절곡부품', name: '절곡부품' },\n { code: '구매부품', name: '구매부품' },\n { code: '부자재', name: '부자재' },\n { code: '소모품', name: '소모품' },\n ];\n\n // 현재 품목유형에 해당하는 품목들\n const filteredByType = selectedItemType === '전체'\n ? inventory\n : inventory.filter(item => item.itemType === selectedItemType);\n\n // 카테고리 목록 추출 (현재 품목유형 기준)\n const categories = ['전체', ...new Set(filteredByType.map(p => p.category).filter(Boolean))];\n\n // 카테고리 필터 적용\n const filteredByCategory = selectedCategory === '전체'\n ? filteredByType\n : filteredByType.filter(item => item.category === selectedCategory);\n\n // 검색 필터 적용 (ID 내림차순 정렬 - 최신 등록 최상단)\n const filteredItems = filteredByCategory.filter(item => {\n if (!search) return true;\n return item.materialCode.toLowerCase().includes(search.toLowerCase()) ||\n item.materialName.toLowerCase().includes(search.toLowerCase());\n }).sort((a, b) => b.id - a.id);\n\n // 목록 선택 훅\n const {\n selectedIds,\n handleSelect,\n handleSelectAll,\n clearSelection,\n isAllSelected,\n hasSelection,\n isMultiSelect,\n isSelected,\n } = useListSelection(filteredItems);\n\n // 품목유형별 통계\n const getTypeStats = (typeCode) => {\n const items = typeCode === '전체' ? inventory : inventory.filter(i => i.itemType === typeCode);\n return {\n count: items.length,\n lowStock: items.filter(i => i.stock <= i.minStock).length,\n };\n };\n\n // 재고 상태 판단\n const getStockStatus = (item) => {\n if (item.stock === 0) return { label: '재고없음', color: 'bg-red-100 text-red-700' };\n if (item.stock <= item.minStock) return { label: '부족', color: 'bg-orange-100 text-orange-700' };\n return { label: '정상', color: 'bg-green-100 text-green-700' };\n };\n\n // 품목유형 변경 시 카테고리 초기화\n const handleTypeChange = (type) => {\n setSelectedItemType(type);\n setSelectedCategory('전체');\n };\n\n // 재고 통계 계산\n const totalItems = inventory.length;\n const lowStockItems = inventory.filter(i => i.stock <= i.minStock && i.stock > 0).length;\n const noStockItems = inventory.filter(i => i.stock === 0).length;\n const normalItems = inventory.filter(i => i.stock > i.minStock).length;\n\n return (\n
\n {/* 헤더 - PageHeader 사용 */}\n
{ }}>\n 엑셀 다운로드\n \n }\n />\n\n {/* 리포트 카드 - 4개 한 줄 */}\n \n handleTypeChange('전체')}\n />\n \n \n \n
\n\n {/* 품목유형 칩 탭 */}\n \n {itemTypes.map(type => {\n const stats = getTypeStats(type.code);\n const isActive = selectedItemType === type.code;\n return (\n \n );\n })}\n
\n\n {/* 검색바 */}\n \n \n
\n\n {/* 테이블 */}\n \n {/* 선택 삭제 버튼 (2개 이상 선택 시) */}\n {isMultiSelect && (\n
\n \n
\n )}\n
\n\n {/* 요약 */}\n {filteredItems.length > 0 && (\n
\n 총 {filteredItems.length}종 /\n 재고부족 {filteredItems.filter(i => i.stock <= i.minStock).length}종\n
\n )}\n
\n \n );\n};\n\n// ============================================================\n// 재고조정 컴포넌트 (실사/조정/폐기 관리)\n// ============================================================\nconst StockAdjustment = ({ inventory = [], onNavigate }) => {\n const [activeTab, setActiveTab] = useState('list'); // list, register\n const [search, setSearch] = useState('');\n const [filterType, setFilterType] = useState('전체');\n const [showDetailModal, setShowDetailModal] = useState(false);\n const [selectedAdjustment, setSelectedAdjustment] = useState(null);\n const [showRegisterModal, setShowRegisterModal] = useState(false);\n\n // 조정 유형\n const adjustmentTypes = [\n { code: 'PHYSICAL_COUNT', name: '실사조정', icon: ClipboardList, color: 'blue', desc: '실사 결과 반영' },\n { code: 'DEFECT_SCRAP', name: '불량폐기', icon: Trash2, color: 'red', desc: '불량품 재고 차감' },\n { code: 'EXPIRED_SCRAP', name: '만료폐기', icon: Clock, color: 'orange', desc: '유통기한 만료품' },\n { code: 'LOCATION_MOVE', name: '위치이동', icon: ArrowRight, color: 'green', desc: '창고/위치 간 이동' },\n { code: 'OTHER', name: '기타조정', icon: Edit3, color: 'gray', desc: '기타 사유 조정' },\n ];\n\n // 샘플 조정 이력 데이터\n const [adjustments, setAdjustments] = useState([\n {\n id: 1,\n adjustNo: 'ADJ-241218-01',\n adjustType: 'PHYSICAL_COUNT',\n adjustDate: '2024-12-18',\n itemCode: 'STEEL-1.2T',\n itemName: '철판 1.2T',\n lotNo: '241210-01',\n beforeQty: 100,\n adjustQty: -5,\n afterQty: 95,\n reason: '정기 실사 결과 차이 발생',\n location: 'A-01',\n status: '승인완료',\n approver: '김공장장',\n approvedAt: '2024-12-18 14:30',\n createdBy: '이재고',\n createdAt: '2024-12-18 10:00',\n },\n {\n id: 2,\n adjustNo: 'ADJ-241217-02',\n adjustType: 'DEFECT_SCRAP',\n adjustDate: '2024-12-17',\n itemCode: 'E-220V-300KG',\n itemName: '전동개폐기 220V 300KG',\n lotNo: '241205-01',\n beforeQty: 20,\n adjustQty: -2,\n afterQty: 18,\n reason: 'IQC 불합격 처리',\n location: 'B-02',\n status: '승인완료',\n approver: '김공장장',\n approvedAt: '2024-12-17 16:20',\n createdBy: '박품질',\n createdAt: '2024-12-17 11:00',\n },\n {\n id: 3,\n adjustNo: 'ADJ-241217-01',\n adjustType: 'LOCATION_MOVE',\n adjustDate: '2024-12-17',\n itemCode: 'SCR-0.3T',\n itemName: '스크린망 0.3T',\n lotNo: '241201-03',\n beforeQty: 50,\n adjustQty: 0,\n afterQty: 50,\n reason: '창고 재배치',\n location: 'A-01 → C-01',\n status: '승인완료',\n approver: '김공장장',\n approvedAt: '2024-12-17 09:30',\n createdBy: '이재고',\n createdAt: '2024-12-17 09:00',\n },\n {\n id: 4,\n adjustNo: 'ADJ-241216-01',\n adjustType: 'EXPIRED_SCRAP',\n adjustDate: '2024-12-16',\n itemCode: 'RUBBER-PAD',\n itemName: '고무패킹',\n lotNo: '240601-01',\n beforeQty: 30,\n adjustQty: -30,\n afterQty: 0,\n reason: '유통기한 만료 (6개월 경과)',\n location: 'D-01',\n status: '승인완료',\n approver: '김공장장',\n approvedAt: '2024-12-16 17:00',\n createdBy: '박품질',\n createdAt: '2024-12-16 14:00',\n },\n {\n id: 5,\n adjustNo: 'ADJ-241215-01',\n adjustType: 'OTHER',\n adjustDate: '2024-12-15',\n itemCode: 'BOLT-M8',\n itemName: '볼트 M8×20',\n lotNo: '241101-02',\n beforeQty: 500,\n adjustQty: -10,\n afterQty: 490,\n reason: '샘플 출고 (품질 테스트용)',\n location: 'E-01',\n status: '승인대기',\n approver: null,\n approvedAt: null,\n createdBy: '이재고',\n createdAt: '2024-12-15 11:30',\n },\n ]);\n\n // 등록 폼 상태\n const [registerForm, setRegisterForm] = useState({\n adjustType: '',\n itemCode: '',\n itemName: '',\n lotNo: '',\n currentQty: 0,\n adjustQty: '',\n reason: '',\n fromLocation: '',\n toLocation: '',\n });\n\n // 조정 유형별 색상\n const getTypeStyle = (typeCode) => {\n const type = adjustmentTypes.find(t => t.code === typeCode);\n if (!type) return 'bg-gray-100 text-gray-700';\n const colors = {\n blue: 'bg-blue-100 text-blue-700',\n red: 'bg-red-100 text-red-700',\n orange: 'bg-orange-100 text-orange-700',\n green: 'bg-green-100 text-green-700',\n gray: 'bg-gray-100 text-gray-700',\n };\n return colors[type.color] || colors.gray;\n };\n\n // 상태별 스타일\n const getStatusStyle = (status) => {\n if (status === '승인완료') return 'bg-green-100 text-green-700';\n if (status === '승인대기') return 'bg-yellow-100 text-yellow-700';\n if (status === '반려') return 'bg-red-100 text-red-700';\n return 'bg-gray-100 text-gray-700';\n };\n\n // 필터링된 데이터\n const filteredAdjustments = adjustments.filter(adj => {\n const matchType = filterType === '전체' || adj.adjustType === filterType;\n const matchSearch = !search ||\n adj.adjustNo.toLowerCase().includes(search.toLowerCase()) ||\n adj.itemCode.toLowerCase().includes(search.toLowerCase()) ||\n adj.itemName.toLowerCase().includes(search.toLowerCase()) ||\n adj.lotNo.toLowerCase().includes(search.toLowerCase());\n return matchType && matchSearch;\n }).sort((a, b) => b.id - a.id);\n\n // 통계 계산\n const stats = {\n total: adjustments.length,\n pending: adjustments.filter(a => a.status === '승인대기').length,\n thisMonth: adjustments.filter(a => a.adjustDate.startsWith('2024-12')).length,\n scrapQty: adjustments.filter(a => ['DEFECT_SCRAP', 'EXPIRED_SCRAP'].includes(a.adjustType))\n .reduce((sum, a) => sum + Math.abs(a.adjustQty), 0),\n };\n\n // 상세 보기\n const handleViewDetail = (adj) => {\n setSelectedAdjustment(adj);\n setShowDetailModal(true);\n };\n\n // 조정 유형명\n const getTypeName = (typeCode) => {\n const type = adjustmentTypes.find(t => t.code === typeCode);\n return type ? type.name : typeCode;\n };\n\n // 조정 등록 모달 열기\n const handleOpenRegister = (typeCode = '') => {\n setRegisterForm({\n adjustType: typeCode,\n itemCode: '',\n itemName: '',\n lotNo: '',\n currentQty: 0,\n adjustQty: '',\n reason: '',\n fromLocation: '',\n toLocation: '',\n });\n setShowRegisterModal(true);\n };\n\n // 조정 등록 처리\n const handleRegister = () => {\n const today = new Date();\n const dateStr = today.toISOString().slice(2, 10).replace(/-/g, '');\n const seq = String(adjustments.filter(a => a.adjustDate === today.toISOString().split('T')[0]).length + 1).padStart(2, '0');\n\n const newAdjustment = {\n id: adjustments.length + 1,\n adjustNo: `ADJ-${dateStr}-${seq}`,\n adjustType: registerForm.adjustType,\n adjustDate: today.toISOString().split('T')[0],\n itemCode: registerForm.itemCode,\n itemName: registerForm.itemName,\n lotNo: registerForm.lotNo,\n beforeQty: registerForm.currentQty,\n adjustQty: parseInt(registerForm.adjustQty) || 0,\n afterQty: registerForm.currentQty + (parseInt(registerForm.adjustQty) || 0),\n reason: registerForm.reason,\n location: registerForm.adjustType === 'LOCATION_MOVE'\n ? `${registerForm.fromLocation} → ${registerForm.toLocation}`\n : registerForm.fromLocation,\n status: '승인대기',\n approver: null,\n approvedAt: null,\n createdBy: '현재사용자',\n createdAt: today.toISOString().slice(0, 16).replace('T', ' '),\n };\n\n setAdjustments([newAdjustment, ...adjustments]);\n setShowRegisterModal(false);\n };\n\n // 품목 선택 시 (실제로는 모달이나 검색으로 선택)\n const handleSelectItem = (item) => {\n setRegisterForm(prev => ({\n ...prev,\n itemCode: item.materialCode,\n itemName: item.materialName,\n currentQty: item.stock,\n fromLocation: 'A-01', // 기본값\n }));\n };\n\n return (\n
\n {/* 헤더 */}\n
handleOpenRegister()}>\n 조정 등록\n \n }\n />\n\n {/* 리포트 카드 - 4개 한 줄 */}\n \n \n \n \n \n
\n\n {/* 조정 유형 버튼 */}\n \n \n 빠른 등록:\n {adjustmentTypes.map(type => {\n const Icon = type.icon;\n return (\n \n );\n })}\n
\n \n\n {/* 검색 및 필터 */}\n \n
\n \n setSearch(e.target.value)}\n className=\"w-full pl-10 pr-4 py-2.5 border rounded-lg\"\n />\n
\n
\n
\n\n {/* 조정 이력 테이블 */}\n \n \n
\n \n \n | 조정번호 | \n 유형 | \n 조정일 | \n 품목 | \n LOT | \n 조정 전 | \n 조정량 | \n 조정 후 | \n 위치 | \n 상태 | \n 작업 | \n
\n \n \n {filteredAdjustments.map(adj => (\n \n | {adj.adjustNo} | \n \n \n {getTypeName(adj.adjustType)}\n \n | \n {adj.adjustDate} | \n \n {adj.itemName} \n {adj.itemCode} \n | \n {adj.lotNo} | \n {adj.beforeQty} | \n \n 0 ? 'text-blue-600' : ''}>\n {adj.adjustQty > 0 ? '+' : ''}{adj.adjustQty}\n \n | \n {adj.afterQty} | \n {adj.location} | \n \n \n {adj.status}\n \n | \n \n \n | \n
\n ))}\n {filteredAdjustments.length === 0 && (\n \n | \n 조정 이력이 없습니다.\n | \n
\n )}\n \n
\n
\n\n {/* 요약 */}\n {filteredAdjustments.length > 0 && (\n \n 총 {filteredAdjustments.length}건 /\n 승인대기 {filteredAdjustments.filter(a => a.status === '승인대기').length}건\n
\n )}\n \n\n {/* 상세 모달 */}\n {showDetailModal && selectedAdjustment && (\n \n
\n
\n
조정 상세
\n \n \n\n
\n
\n \n {getTypeName(selectedAdjustment.adjustType)}\n \n \n {selectedAdjustment.status}\n \n
\n\n
\n
\n
조정번호
\n
{selectedAdjustment.adjustNo}
\n
\n
\n
조정일
\n
{selectedAdjustment.adjustDate}
\n
\n
\n
품목코드
\n
{selectedAdjustment.itemCode}
\n
\n
\n
품목명
\n
{selectedAdjustment.itemName}
\n
\n
\n
LOT번호
\n
{selectedAdjustment.lotNo}
\n
\n
\n
위치
\n
{selectedAdjustment.location}
\n
\n
\n\n
\n
\n
\n
조정 전
\n
{selectedAdjustment.beforeQty}
\n
\n
\n
조정량
\n
\n {selectedAdjustment.adjustQty > 0 ? '+' : ''}{selectedAdjustment.adjustQty}\n
\n
\n
\n
조정 후
\n
{selectedAdjustment.afterQty}
\n
\n
\n
\n\n
\n
조정 사유
\n
{selectedAdjustment.reason}
\n
\n\n
\n
\n
등록자
\n
{selectedAdjustment.createdBy}
\n
{selectedAdjustment.createdAt}
\n
\n {selectedAdjustment.approver && (\n
\n
승인자
\n
{selectedAdjustment.approver}
\n
{selectedAdjustment.approvedAt}
\n
\n )}\n
\n
\n\n
\n \n {selectedAdjustment.status === '승인대기' && (\n \n )}\n
\n
\n
\n )}\n\n {/* 등록 모달 */}\n {showRegisterModal && (\n \n
\n
\n
재고 조정 등록
\n \n \n\n
\n {/* 조정 유형 */}\n
\n
\n
\n {adjustmentTypes.map(type => {\n const Icon = type.icon;\n return (\n \n );\n })}\n
\n
\n\n {/* 품목 선택 */}\n
\n \n \n
\n\n {/* LOT 선택 */}\n {registerForm.itemCode && (\n
\n \n \n
\n )}\n\n {/* 현재 수량 / 조정량 */}\n
\n\n {/* 위치 (이동인 경우) */}\n {registerForm.adjustType === 'LOCATION_MOVE' ? (\n
\n ) : (\n
\n \n setRegisterForm(prev => ({ ...prev, fromLocation: e.target.value }))}\n placeholder=\"예: A-01\"\n className=\"w-full px-3 py-2 border rounded-lg\"\n />\n
\n )}\n\n {/* 조정 사유 */}\n
\n \n
\n\n {/* 조정 결과 미리보기 */}\n {registerForm.adjustQty && registerForm.adjustType !== 'LOCATION_MOVE' && (\n
\n
\n 조정 결과: {registerForm.currentQty} → {registerForm.currentQty + parseInt(registerForm.adjustQty)}\n
\n
\n )}\n
\n\n
\n \n \n
\n
\n
\n )}\n \n );\n};\n\n// ============================================================\n// 입고관리 컴포넌트 (견적 목록 스타일 적용)\n// ============================================================\nconst InboundManagement = ({\n purchaseOrders = [],\n inventory = [],\n onReceive,\n onNavigate,\n onCreateInspection\n}) => {\n const [activeTab, setActiveTab] = useState('all'); // all, pending, received\n const [selectedPO, setSelectedPO] = useState(null);\n const [showReceiveModal, setShowReceiveModal] = useState(false);\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n const [showInboundSlip, setShowInboundSlip] = useState(false);\n const [slipData, setSlipData] = useState(null);\n const [search, setSearch] = useState('');\n const [selectedIds, setSelectedIds] = useState([]);\n const [receiveForm, setReceiveForm] = useState({\n receivedQty: '',\n incomingLot: '',\n supplierLot: '',\n location: '',\n note: '',\n });\n\n // 입고대기: 발주완료 또는 배송중 상태\n const pendingList = purchaseOrders.filter(po =>\n po.status === '발주완료' || po.status === '배송중'\n );\n\n // 입고완료\n const receivedList = purchaseOrders.filter(po =>\n po.status === '입고완료' || po.status === '검사대기' || po.status === '검사완료'\n );\n\n // 금일 입고\n const todayReceived = receivedList.filter(po =>\n po.receivedDate === new Date().toISOString().split('T')[0]\n ).length;\n\n // 탭별 카운트\n const getStatusCount = (status) => {\n if (status === 'all') return purchaseOrders.length;\n if (status === 'pending') return pendingList.length;\n if (status === 'received') return receivedList.length;\n return 0;\n };\n\n // 탭 구성\n const tabs = [\n { id: 'all', label: '전체', count: getStatusCount('all') },\n { id: 'pending', label: '입고대기', count: getStatusCount('pending') },\n { id: 'received', label: '입고완료', count: getStatusCount('received') },\n ];\n\n // 필터링 로직\n const statusFilter = {\n all: () => true,\n pending: (po) => po.status === '발주완료' || po.status === '배송중',\n received: (po) => po.status === '입고완료' || po.status === '검사대기' || po.status === '검사완료',\n };\n\n const filteredList = purchaseOrders\n .filter(statusFilter[activeTab])\n .filter(po => {\n if (!search) return true;\n return (\n po.poNo?.toLowerCase().includes(search.toLowerCase()) ||\n po.materialCode?.toLowerCase().includes(search.toLowerCase()) ||\n po.materialName?.toLowerCase().includes(search.toLowerCase()) ||\n po.vendor?.toLowerCase().includes(search.toLowerCase())\n );\n })\n .sort((a, b) => b.id - a.id); // ID 기준 내림차순 (최신 등록 최상단)\n\n // 전체 선택/해제\n const handleSelectAll = (e) => {\n if (e.target.checked) {\n setSelectedIds(filteredList.map(po => po.id));\n } else {\n setSelectedIds([]);\n }\n };\n\n // 개별 선택\n const handleSelectOne = (id, e) => {\n e.stopPropagation();\n setSelectedIds(prev =>\n prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]\n );\n };\n\n // 선택 초기화\n const clearSelection = () => setSelectedIds([]);\n\n // 선택 상태 확인\n const isSelected = (id) => selectedIds.includes(id);\n const hasSelection = selectedIds.length > 0;\n const isMultiSelect = selectedIds.length >= 2;\n const isAllSelected = filteredList.length > 0 && selectedIds.length === filteredList.length;\n\n const handleOpenReceive = (po) => {\n setSelectedPO(po);\n const today = new Date();\n const dateStr = today.toISOString().slice(2, 10).replace(/-/g, '');\n const seq = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0');\n setReceiveForm({\n receivedQty: po.requestQty,\n incomingLot: `${dateStr}-${seq}`,\n supplierLot: '',\n location: '',\n note: '',\n });\n setShowReceiveModal(true);\n };\n\n const handleReceive = () => {\n if (!receiveForm.receivedQty || !receiveForm.location) {\n alert('입고수량과 입고위치를 입력해주세요.');\n return;\n }\n\n const receiveData = {\n ...selectedPO,\n status: '검사대기',\n receivedQty: Number(receiveForm.receivedQty),\n incomingLot: receiveForm.incomingLot,\n supplierLot: receiveForm.supplierLot,\n location: receiveForm.location,\n receivedDate: new Date().toISOString().split('T')[0],\n note: receiveForm.note,\n };\n\n onReceive?.(receiveData);\n setShowReceiveModal(false);\n setSelectedPO(null);\n alert(`✅ 입고 처리되었습니다.\\n\\n입고LOT: ${receiveForm.incomingLot}\\n수입검사가 필요합니다.`);\n };\n\n const getStatusBadge = (status) => {\n const styles = {\n '발주완료': 'bg-blue-100 text-blue-700',\n '배송중': 'bg-purple-100 text-purple-700',\n '입고완료': 'bg-green-100 text-green-700',\n '검사대기': 'bg-yellow-100 text-yellow-700',\n '검사완료': 'bg-green-100 text-green-700',\n '부분입고': 'bg-orange-100 text-orange-700',\n };\n return styles[status] || 'bg-gray-100 text-gray-700';\n };\n\n // 삭제 확인\n const handleDeleteConfirm = () => {\n // 삭제 로직 구현\n setSelectedIds([]);\n setShowDeleteModal(false);\n alert('선택한 입고 내역이 삭제되었습니다.');\n };\n\n return (\n
\n {/* 헤더 - 타이틀/버튼 분리 */}\n
\n {/* 타이틀 영역 */}\n
\n \n 입고 목록\n
\n \n\n {/* 리포트 카드 - 4개 한 줄 */}\n
\n
\n
입고대기
\n
{pendingList.length}건
\n
\n
\n
배송중
\n
{purchaseOrders.filter(po => po.status === '배송중').length}건
\n
\n
\n
검사대기
\n
{purchaseOrders.filter(po => po.status === '검사대기').length}건
\n
\n
\n
금일입고
\n
{todayReceived}건
\n
\n
\n\n {/* 검색바 (전체 너비) */}\n
\n
\n \n setSearch(e.target.value)}\n />\n
\n
\n\n {/* 탭 (칩 형태) + 선택 삭제 버튼 */}\n
\n
\n {tabs.map(tab => (\n \n ))}\n
\n {isMultiSelect && (\n
\n )}\n
\n\n {/* 테이블 */}\n
\n
\n\n {/* 요약 */}\n {filteredList.length > 0 && (\n
\n 총 {filteredList.length}건 /\n 입고대기 {pendingList.length}건 /\n 검사대기 {purchaseOrders.filter(po => po.status === '검사대기').length}건\n
\n )}\n
\n\n {/* 입고 처리 모달 */}\n {showReceiveModal && selectedPO && (\n
\n
\n
\n
입고 처리
\n \n \n
\n {/* 발주 정보 */}\n
\n
\n
\n 발주번호:\n {selectedPO.poNo}\n
\n
\n 공급업체:\n {selectedPO.vendor}\n
\n
\n 품목:\n {selectedPO.materialName}\n
\n
\n 발주수량:\n {selectedPO.requestQty} {selectedPO.unit}\n
\n
\n
\n\n {/* 입고 정보 입력 */}\n
\n
\n \n
\n
\n
\n \n \n
\n
\n
\n )}\n\n {/* 입고증 다이얼로그 */}\n {showInboundSlip && slipData && (\n
{\n setShowInboundSlip(false);\n setSlipData(null);\n }}\n />\n )}\n\n {/* 삭제 확인 모달 */}\n {showDeleteModal && (\n \n
\n
입고 내역 삭제
\n
\n 선택한 {selectedIds.length}개의 입고 내역을 삭제하시겠습니까?\n
\n 이 작업은 되돌릴 수 없습니다.\n
\n
\n \n \n
\n
\n
\n )}\n \n );\n};\n\n// ============================================================\n// 입고 상세페이지 컴포넌트\n// ============================================================\nconst InboundDetail = ({ inbound, onNavigate, onReceive, onUpdateStatus }) => {\n const [showReceiveModal, setShowReceiveModal] = useState(false);\n const [showInboundSlip, setShowInboundSlip] = useState(false);\n const [receiveForm, setReceiveForm] = useState({\n receivedQty: '',\n incomingLot: '',\n supplierLot: '',\n location: '',\n note: '',\n });\n\n // 입고 처리 모달 열기\n const handleOpenReceive = () => {\n const today = new Date();\n const dateStr = today.toISOString().slice(2, 10).replace(/-/g, '');\n const seq = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0');\n setReceiveForm({\n receivedQty: inbound.requestQty,\n incomingLot: `${dateStr}-${seq}`,\n supplierLot: '',\n location: '',\n note: '',\n });\n setShowReceiveModal(true);\n };\n\n // 입고 처리 실행\n const handleReceive = () => {\n if (!receiveForm.receivedQty || !receiveForm.location) {\n alert('입고수량과 입고위치를 입력해주세요.');\n return;\n }\n\n const receiveData = {\n ...inbound,\n status: '검사대기',\n receivedQty: Number(receiveForm.receivedQty),\n incomingLot: receiveForm.incomingLot,\n supplierLot: receiveForm.supplierLot,\n location: receiveForm.location,\n receivedDate: new Date().toISOString().split('T')[0],\n note: receiveForm.note,\n };\n\n onReceive?.(receiveData);\n setShowReceiveModal(false);\n alert(`✅ 입고 처리되었습니다.\\n\\n입고LOT: ${receiveForm.incomingLot}\\n수입검사가 필요합니다.`);\n };\n\n const getStatusBadge = (status) => {\n const styles = {\n '발주완료': 'bg-blue-100 text-blue-700',\n '배송중': 'bg-purple-100 text-purple-700',\n '입고완료': 'bg-green-100 text-green-700',\n '검사대기': 'bg-yellow-100 text-yellow-700',\n '검사완료': 'bg-green-100 text-green-700',\n };\n return styles[status] || 'bg-gray-100 text-gray-700';\n };\n\n return (\n
\n {/* 헤더 - 타이틀/버튼 분리 */}\n
\n {/* 타이틀 영역 */}\n
\n
\n \n 입고 상세\n
\n
\n {inbound.poNo}\n \n {inbound.status}\n \n
\n
\n\n {/* 버튼 영역 - 좌측:문서버튼 / 우측:액션버튼 */}\n
\n {/* 좌측: 문서 버튼 */}\n
\n {(inbound.status === '입고완료' || inbound.status === '검사대기' || inbound.status === '검사완료') && (\n
\n )}\n
\n\n {/* 우측: 액션 버튼 */}\n
\n \n {(inbound.status === '발주완료' || inbound.status === '배송중') && (\n \n )}\n {inbound.status === '검사대기' && (\n \n )}\n
\n
\n
\n\n {/* 발주 정보 */}\n
\n \n
\n
발주번호
\n
{inbound.poNo}
\n
\n
\n
발주일자
\n
{inbound.poDate || '-'}
\n
\n
\n
공급업체
\n
{inbound.vendor}
\n
\n
\n
품목코드
\n
{inbound.materialCode}
\n
\n
\n
품목명
\n
{inbound.materialName}
\n
\n
\n
규격
\n
{inbound.spec || '-'}
\n
\n
\n
발주수량
\n
{inbound.requestQty} {inbound.unit}
\n
\n
\n
납기일
\n
{inbound.dueDate || '-'}
\n
\n
\n \n\n {/* 입고 정보 (입고 처리 후 표시) */}\n {(inbound.status === '입고완료' || inbound.status === '검사대기' || inbound.status === '검사완료') && (\n
\n \n
\n
입고일자
\n
{inbound.receivedDate}
\n
\n
\n
입고수량
\n
{inbound.receivedQty} {inbound.unit}
\n
\n
\n
입고LOT
\n
{inbound.incomingLot}
\n
\n
\n
공급업체LOT
\n
{inbound.supplierLot || '-'}
\n
\n
\n
입고위치
\n
{inbound.location}
\n
\n
\n
입고담당
\n
{inbound.receiver || '자재팀'}
\n
\n
\n {inbound.note && (\n \n )}\n \n )}\n\n {/* 검사 정보 (검사 완료 후 표시) */}\n {inbound.status === '입고완료' && inbound.inspectionResult && (\n
\n \n
\n
검사일자
\n
{inbound.inspectionResult.inspectionDate}
\n
\n
\n
검사LOT
\n
{inbound.inspectionLot}
\n
\n
\n
검사결과
\n
\n {inbound.inspectionResult.result}\n
\n
\n
\n
검사자
\n
{inbound.inspectionResult.inspector}
\n
\n
\n \n )}\n\n {/* 입고 처리 모달 */}\n {showReceiveModal && (\n
\n
\n
\n
입고 처리
\n \n \n
\n {/* 발주 정보 */}\n
\n
\n
\n 발주번호:\n {inbound.poNo}\n
\n
\n 공급업체:\n {inbound.vendor}\n
\n
\n 품목:\n {inbound.materialName}\n
\n
\n 발주수량:\n {inbound.requestQty} {inbound.unit}\n
\n
\n
\n\n {/* 입고 정보 입력 */}\n
\n
\n \n
\n
\n
\n \n \n
\n
\n
\n )}\n\n {/* 입고증 다이얼로그 */}\n {showInboundSlip && (\n
setShowInboundSlip(false)}\n />\n )}\n \n );\n};\n\n// ============================================================\n// 통합 검사 등록 폼 (검사 유형 선택 - 드롭다운)\n// ============================================================\nconst InspectionRegisterForm = ({\n purchaseOrders = [],\n workOrders = [],\n shipments = [],\n onNavigate,\n onSave,\n}) => {\n const [inspectionType, setInspectionType] = useState('IQC');\n const [selectedTarget, setSelectedTarget] = useState(null);\n const [templateType, setTemplateType] = useState('EGI'); // 템플릿 유형 (EGI, SCREEN 등)\n const [inspectionForm, setInspectionForm] = useState({\n inspectorName: '',\n inspectionDate: new Date().toISOString().split('T')[0],\n processStep: '', // PQC용\n inspectionItems: [],\n note: '',\n });\n\n // 검사 템플릿 불러오기 (품질기준관리 연동)\n const loadInspectionTemplate = (type, subType) => {\n const config = masterConfigs['quality'];\n const templates = config?.inspectionItemTemplates?.[type]?.[subType];\n\n if (!templates) {\n // 템플릿이 없는 경우 기본 항목\n return [\n { item: '겉모양', standard: '사용상 해로운 결함이 없을 것', method: '육안검사', result: '적', note: '' },\n { item: '두께', standard: '규격 참조', method: '마이크로미터', result: '적', note: '' },\n { item: '폭', standard: '규격 참조', method: '줄자', result: '적', note: '' },\n { item: '길이', standard: '규격 참조', method: '줄자', result: '적', note: '' },\n ];\n }\n\n // 배열 형태 템플릿 처리 (IQC, FQC)\n if (Array.isArray(templates)) {\n return templates.map(template => ({\n item: template.item,\n standard: template.standard,\n method: template.method,\n result: '적',\n note: '',\n }));\n }\n\n // 객체 형태 템플릿 처리 (PQC - 상세 구조)\n if (templates.inspectionStandard) {\n const items = [];\n\n // sections 항목 추가 (가공상태, 재봉상태 등)\n if (templates.inspectionStandard.sections) {\n templates.inspectionStandard.sections.forEach(section => {\n items.push({\n category: section.category,\n item: section.name,\n standard: section.standard,\n method: section.method,\n judgementType: section.judgementType,\n relatedStandard: section.relatedStandard,\n result: '적',\n note: '',\n });\n });\n }\n\n // measurements 항목 추가 (치수 측정)\n if (templates.inspectionStandard.measurements) {\n templates.inspectionStandard.measurements.forEach(measurement => {\n items.push({\n category: measurement.category,\n item: measurement.item,\n point: measurement.point,\n standard: measurement.standard,\n method: measurement.method,\n unit: measurement.unit,\n relatedStandard: measurement.relatedStandard,\n result: '',\n measurementValue: '', // 측정값 입력 필드\n note: '',\n });\n });\n }\n\n return items;\n }\n\n // 기본 배열 변환\n return [];\n };\n\n // 검사 유형 또는 템플릿 유형 변경 시 검사 항목 갱신\n React.useEffect(() => {\n const items = loadInspectionTemplate(inspectionType, templateType);\n setInspectionForm(prev => ({ ...prev, inspectionItems: Array.isArray(items) ? items : [] }));\n }, [inspectionType, templateType]);\n\n // 검사 유형별 대상 목록\n const getTargetList = () => {\n if (inspectionType === 'IQC') {\n return purchaseOrders.filter(po => po.status === '검사대기' || po.status === '입고대기');\n } else if (inspectionType === 'PQC') {\n return workOrders.filter(wo => wo.status === '작업중');\n } else if (inspectionType === 'FQC') {\n return workOrders.filter(wo => wo.status === '작업완료' && wo.inspectionStatus !== '검사완료');\n }\n return [];\n };\n\n const targetList = getTargetList();\n\n const handleSubmit = () => {\n if (!selectedTarget) {\n alert('검사 대상을 선택해주세요.');\n return;\n }\n if (!inspectionForm.inspectorName) {\n alert('검사자를 입력해주세요.');\n return;\n }\n\n const hasFailure = (inspectionForm.inspectionItems || []).some(item => item.result === '부');\n const lotNo = `LOT-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${String(Math.floor(Math.random() * 999) + 1).padStart(3, '0')}`;\n\n // 공정명 매핑 (템플릿 타입 → 공정명)\n const processNameMap = {\n 'SCREEN': '스크린',\n 'SLAT': '슬랫',\n 'FOLD': '절곡',\n 'STOCK': '재고',\n 'PACK': '포장',\n };\n\n const inspectionData = {\n type: inspectionType === 'IQC' ? 'incoming' : inspectionType === 'PQC' ? 'process' : 'final',\n targetNo: selectedTarget.poNo || selectedTarget.woNo || selectedTarget.id,\n targetType: inspectionType,\n lotNo: lotNo,\n itemName: selectedTarget.itemName || selectedTarget.productName,\n qty: selectedTarget.qty || selectedTarget.orderQty,\n inspector: inspectionForm.inspectorName,\n inspectionDate: inspectionForm.inspectionDate,\n processStep: inspectionType === 'PQC' ? processNameMap[templateType] || templateType : '',\n processCode: inspectionType === 'PQC' ? templateType : '',\n result: hasFailure ? '불합격' : '합격',\n inspectionItems: inspectionForm.inspectionItems,\n note: inspectionForm.note,\n };\n\n onSave?.(inspectionData);\n };\n\n return (\n
\n {/* 헤더 */}\n
\n
\n \n 검사 등록\n
\n
\n \n \n
\n
\n\n {/* 검사 기본 정보 */}\n
\n
검사 기본 정보
\n
\n {/* 검사 유형 선택 */}\n
\n \n \n
\n\n {/* 검사 템플릿 선택 */}\n
\n \n \n
\n\n {/* 검사 대상 선택 */}\n
\n \n \n
\n\n {/* 검사일자 */}\n
\n \n setInspectionForm(prev => ({ ...prev, inspectionDate: e.target.value }))}\n className=\"w-full px-3 py-2 border rounded-lg\"\n />\n
\n\n\n {/* 검사자 */}\n
\n \n setInspectionForm(prev => ({ ...prev, inspectorName: e.target.value }))}\n placeholder=\"검사자 이름\"\n className=\"w-full px-3 py-2 border rounded-lg\"\n />\n
\n
\n
\n\n {/* 검사 항목 */}\n
\n
검사 항목
\n
\n
\n
\n\n {/* 비고 */}\n
\n
비고
\n \n
\n );\n};\n\n// ============================================================\n// 검사 성적서 문서 출력 컴포넌트\n// ============================================================\nconst InspectionCertificateDocument = ({ inspectionData, onClose }) => {\n const handlePrint = () => {\n window.print();\n };\n\n // 템플릿 정보 가져오기\n const getTemplateInfo = () => {\n if (inspectionData.type === 'process' && inspectionData.processCode) {\n const config = masterConfigs['quality'];\n return config?.inspectionItemTemplates?.PQC?.[inspectionData.processCode];\n }\n return null;\n };\n\n const templateInfo = getTemplateInfo();\n\n return (\n
\n
\n {/* 헤더 - 인쇄 시 숨김 */}\n
\n
검사 성적서
\n
\n
\n
\n
\n
\n\n {/* 문서 본문 */}\n
\n {/* 문서 제목 */}\n
\n
\n {templateInfo?.templateName || '검사 성적서'}\n
\n
\n {inspectionData.type === 'incoming' && '(수입검사 - IQC)'}\n {inspectionData.type === 'process' && '(중간검사 - PQC)'}\n {inspectionData.type === 'final' && '(제품검사 - FQC)'}\n
\n
\n\n {/* 기본 정보 */}\n
\n
\n \n \n | 검사번호 | \n {inspectionData.id || '-'} | \n 검사일자 | \n {inspectionData.inspectionDate} | \n
\n \n | 대상번호 | \n {inspectionData.targetNo} | \n 검사자 | \n {inspectionData.inspector} | \n
\n \n | 품목명 | \n {inspectionData.itemName} | \n 수량 | \n {inspectionData.qty} | \n
\n {inspectionData.processStep && (\n \n | 공정명 | \n {inspectionData.processStep} | \n 검사시점 | \n {templateInfo?.checkPoint || '-'} | \n
\n )}\n \n | LOT번호 | \n {inspectionData.lotNo} | \n 판정결과 | \n \n \n {inspectionData.result}\n \n | \n
\n \n
\n
\n\n {/* 도면 다이어그램 (PQC용) */}\n {templateInfo?.diagram && (\n
\n
측정 위치 도면
\n
\n
\n
측정 포인트: {templateInfo.diagram.measurements?.join(', ')}
\n
[도면은 실제 제품 도면을 첨부하세요]
\n
\n
\n
\n )}\n\n {/* 검사 항목 테이블 */}\n
\n
검사 항목 및 결과
\n\n {/* 상세 구조 템플릿 (PQC) */}\n {templateInfo?.inspectionStandard ? (\n
\n {/* 결모양 검사 섹션 */}\n {templateInfo.inspectionStandard.sections && templateInfo.inspectionStandard.sections.length > 0 && (\n
\n
결모양 검사
\n
\n \n \n | 항목 | \n 판정기준 | \n 검사방법 | \n 관련규격 | \n 판정 | \n
\n \n \n {(inspectionData.inspectionItems || [])\n .filter(item => item.category === '가공상태' || item.category === '재봉상태' || item.category === '조립상태')\n .map((item, idx) => (\n \n | {item.item} | \n {item.standard} | \n {item.method} | \n {item.relatedStandard || '-'} | \n \n \n {item.result}\n \n | \n
\n ))}\n \n
\n
\n )}\n\n {/* 치수 검사 섹션 */}\n {templateInfo.inspectionStandard.measurements && templateInfo.inspectionStandard.measurements.length > 0 && (\n
\n
치수 검사
\n
\n \n \n | 측정위치 | \n 항목 | \n 기준치수 | \n 측정값 | \n 검사방법 | \n 관련규격 | \n 판정 | \n
\n \n \n {(inspectionData.inspectionItems || [])\n .filter(item => item.category === '치수')\n .map((item, idx) => (\n \n | {item.point} | \n {item.item} | \n {item.standard} {item.unit} | \n {item.measurementValue || '-'} {item.unit} | \n {item.method} | \n {item.relatedStandard || '-'} | \n \n \n {item.result || '-'}\n \n | \n
\n ))}\n \n
\n
\n )}\n
\n ) : (\n /* 기본 테이블 (IQC, FQC) */\n
\n
\n \n \n | 검사항목 | \n 판정기준 | \n 검사방법 | \n 판정 | \n 비고 | \n
\n \n \n {(inspectionData.inspectionItems || []).map((item, idx) => (\n \n | {item.item} | \n {item.standard} | \n {item.method} | \n \n \n {item.result}\n \n | \n {item.note || '-'} | \n
\n ))}\n \n
\n
\n )}\n
\n\n {/* 비고 */}\n {inspectionData.note && (\n
\n
비고
\n
{inspectionData.note}
\n
\n )}\n\n {/* 서명란 */}\n
\n
\n
\n
검사자
\n
{inspectionData.inspector}
\n
\n
\n
\n
\n
\n\n {/* 발행일 */}\n
\n
발행일: {new Date().toLocaleDateString('ko-KR')}
\n
\n
\n
\n\n {/* 인쇄 스타일 */}\n \n
\n );\n};\n\n// ============================================================\n// 재고 상세페이지 컴포넌트\n// ============================================================\nconst StockDetail = ({ stock, inventory = [], onNavigate }) => {\n // 품목별 LOT 데이터 생성 (실제로는 서버에서 조회)\n const generateLots = () => {\n const today = new Date();\n const lots = [];\n const lotCount = Math.min(Math.ceil(stock.stock / 30), 5); // 최대 5개 LOT\n let remainingStock = stock.stock;\n\n for (let i = 0; i < lotCount && remainingStock > 0; i++) {\n const daysAgo = (lotCount - i) * 7 + Math.floor(Math.random() * 5);\n const inboundDate = new Date(today);\n inboundDate.setDate(inboundDate.getDate() - daysAgo);\n\n const qty = i === lotCount - 1 ? remainingStock : Math.floor(remainingStock / (lotCount - i) * (0.8 + Math.random() * 0.4));\n remainingStock -= qty;\n\n const suppliers = ['포스코', '현대제철', '동국제강', '세아제강', '한국철강'];\n const locations = ['A-01', 'A-02', 'B-01', 'B-02', 'C-01'];\n\n lots.push({\n id: `${stock.materialCode}-LOT-${i + 1}`,\n lotNo: `${inboundDate.toISOString().slice(2, 10).replace(/-/g, '')}-${String(i + 1).padStart(2, '0')}`,\n inboundDate: inboundDate.toISOString().split('T')[0],\n supplier: suppliers[i % suppliers.length],\n qty: qty,\n remainingQty: qty,\n location: locations[i % locations.length],\n status: qty > 0 ? '사용가능' : '소진',\n daysInStock: daysAgo,\n poNo: `PO-${inboundDate.toISOString().slice(2, 10).replace(/-/g, '')}-${String(Math.floor(Math.random() * 99) + 1).padStart(2, '0')}`,\n });\n }\n\n // FIFO 순서 (입고일 오름차순)\n return lots.sort((a, b) => new Date(a.inboundDate) - new Date(b.inboundDate));\n };\n\n const lots = generateLots();\n\n const getStockStatus = () => {\n if (stock.stock === 0) return { label: '재고없음', color: 'bg-red-100 text-red-700' };\n if (stock.stock <= stock.minStock) return { label: '부족', color: 'bg-orange-100 text-orange-700' };\n return { label: '정상', color: 'bg-green-100 text-green-700' };\n };\n\n const status = getStockStatus();\n\n return (\n
\n {/* 헤더 - 타이틀/버튼 분리 */}\n
\n {/* 타이틀 영역 */}\n
\n
\n \n 재고 상세\n
\n
\n {stock.materialCode}\n \n {status.label}\n \n
\n
\n\n {/* 버튼 영역 */}\n
\n \n
\n
\n\n {/* 기본 정보 */}\n
\n \n
\n
품목코드
\n
{stock.materialCode}
\n
\n
\n
품목명
\n
{stock.materialName}
\n
\n
\n
품목유형
\n
{stock.itemType || '-'}
\n
\n
\n
카테고리
\n
{stock.category || '-'}
\n
\n
\n
규격
\n
{stock.spec || '-'}
\n
\n
\n
\n \n\n {/* 재고 현황 */}\n
\n \n
\n
현재 재고량
\n
{stock.stock.toLocaleString()} {stock.unit}
\n
\n
\n
안전 재고
\n
{stock.minStock.toLocaleString()} {stock.unit}
\n
\n
\n
재고 위치
\n
{stock.location}
\n
\n
\n
LOT 개수
\n
{lots.length}개
\n
\n
\n
최근 입고일
\n
{lots.length > 0 ? lots[lots.length - 1].inboundDate : '-'}
\n
\n
\n
재고 상태
\n
\n {status.label}\n
\n
\n
\n \n\n {/* LOT별 상세 재고 */}\n
\n {lots.length > 0 ? (\n <>\n \n FIFO 순서\n 오래된 LOT부터 사용 권장\n
\n \n
\n \n \n | FIFO | \n LOT번호 | \n 입고일 | \n 경과일 | \n 공급업체 | \n 발주번호 | \n 수량 | \n 위치 | \n 상태 | \n
\n \n \n {lots.map((lot, idx) => (\n \n | \n {idx === 0 ? (\n 1\n ) : (\n {idx + 1}\n )}\n | \n {lot.lotNo} | \n {lot.inboundDate} | \n \n 30 ? 'bg-orange-100 text-orange-700' :\n lot.daysInStock > 14 ? 'bg-yellow-100 text-yellow-700' :\n 'bg-green-100 text-green-700'\n }`}>\n {lot.daysInStock}일\n \n | \n {lot.supplier} | \n {lot.poNo} | \n {lot.remainingQty.toLocaleString()} {stock.unit} | \n {lot.location} | \n \n \n {lot.status}\n \n | \n
\n ))}\n \n \n \n | 합계: | \n \n {lots.reduce((sum, l) => sum + l.remainingQty, 0).toLocaleString()} {stock.unit}\n | \n | \n
\n \n
\n
\n\n {/* FIFO 권장 안내 */}\n {lots.length > 0 && lots[0].daysInStock > 14 && (\n \n
\n
\n FIFO 권장: LOT {lots[0].lotNo}가 {lots[0].daysInStock}일 경과되었습니다. 우선 사용을 권장합니다.\n \n
\n )}\n >\n ) : (\n \n LOT 정보가 없습니다.\n
\n )}\n \n
\n );\n};\n\n// ============================================================\n// 수입검사 등록 폼 (IQC)\n// ============================================================\nconst IQCRegisterForm = ({\n purchaseOrders = [],\n initialData = null, // 입고관리에서 전달한 입고 데이터\n onNavigate,\n onSave,\n}) => {\n const [selectedPO, setSelectedPO] = useState(initialData || null);\n\n // LOT 번호 생성 함수 (채번 규칙: YYMMDD-##)\n const generateLotNo = () => {\n const now = new Date();\n const yy = String(now.getFullYear()).slice(-2);\n const mm = String(now.getMonth() + 1).padStart(2, '0');\n const dd = String(now.getDate()).padStart(2, '0');\n const seq = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0'); // 실제로는 당일 순번 조회 필요\n return `${yy}${mm}${dd}-${seq}`;\n };\n\n const [inspectionForm, setInspectionForm] = useState({\n lotNo: initialData?.incomingLot || generateLotNo(),\n inspectorName: '',\n inspectionDate: new Date().toISOString().split('T')[0],\n result: '합격',\n inspectionItems: [\n { item: '겉모양', standard: '사용상 해로운 결함이 없을 것', method: '육안검사', result: '적', note: '' },\n { item: '두께', standard: '규격 참조', method: '마이크로미터', result: '적', note: '' },\n { item: '폭', standard: '규격 참조', method: '줄자', result: '적', note: '' },\n { item: '길이', standard: '규격 참조', method: '줄자', result: '적', note: '' },\n ],\n note: '',\n });\n\n // 검사대기 목록\n const pendingPOs = purchaseOrders.filter(po => po.status === '검사대기' || po.status === '입고대기');\n\n const handleSubmit = () => {\n if (!selectedPO) {\n alert('검사 대상을 선택해주세요.');\n return;\n }\n if (!inspectionForm.inspectorName) {\n alert('검사자를 입력해주세요.');\n return;\n }\n\n const hasFailure = inspectionForm.inspectionItems.some(item => item.result === '부');\n const lotNo = inspectionForm.lotNo || generateLotNo(); // 입력된 LOT 또는 새로 생성\n\n const inspectionData = {\n targetNo: selectedPO.poNo || selectedPO.id,\n targetType: 'incoming',\n lotNo: lotNo,\n itemName: selectedPO.itemName || selectedPO.productName,\n qty: selectedPO.qty || selectedPO.orderQty,\n inspector: inspectionForm.inspectorName,\n inspectionDate: inspectionForm.inspectionDate,\n result: hasFailure ? '불합격' : '합격',\n inspectionItems: inspectionForm.inspectionItems,\n note: inspectionForm.note,\n };\n\n onSave?.(inspectionData);\n };\n\n return (\n
\n {/* 헤더 */}\n
\n
\n \n 수입검사 등록 (IQC)\n
\n
\n \n \n
\n
\n\n
\n {/* 검사 대상 선택 */}\n
\n
검사 대상 선택
\n {pendingPOs.length === 0 ? (\n
검사 대기중인 입고가 없습니다.
\n ) : (\n
\n {pendingPOs.map(po => (\n
setSelectedPO(po)}\n className={`p-3 rounded-lg cursor-pointer transition-all ${selectedPO?.id === po.id\n ? 'bg-blue-50 border-2 border-blue-500'\n : 'bg-gray-50 hover:bg-gray-100 border border-transparent'\n }`}\n >\n
{po.poNo || po.id}
\n
{po.itemName || po.productName}
\n
{po.supplierName || po.vendorName} · {po.qty || po.orderQty}EA
\n
\n ))}\n
\n )}\n
\n\n {/* 검사 정보 입력 */}\n
\n
검사 정보
\n {selectedPO ? (\n
\n
\n\n {/* 검사 항목 */}\n
\n\n
\n \n
\n
\n ) : (\n
\n 좌측에서 검사 대상을 선택해주세요.\n
\n )}\n
\n
\n
\n );\n};\n\n// ============================================================\n// 중간검사 등록 폼 (PQC)\n// ============================================================\nconst PQCRegisterForm = ({\n workOrders = [],\n onNavigate,\n onSave,\n}) => {\n const [selectedWO, setSelectedWO] = useState(null);\n const [inspectionForm, setInspectionForm] = useState({\n inspectorName: '',\n inspectionDate: new Date().toISOString().split('T')[0],\n processStep: '',\n inspectionItems: [\n { item: '외관상태', standard: '이상 없을 것', method: '육안검사', result: '적', note: '' },\n { item: '치수', standard: '도면 기준', method: '측정기', result: '적', note: '' },\n { item: '작업상태', standard: '기준서 준수', method: '확인', result: '적', note: '' },\n ],\n note: '',\n });\n\n // 검사대기 작업지시 (작업중)\n const pendingWOs = workOrders.filter(wo => wo.status === '작업중');\n\n const handleSubmit = () => {\n if (!selectedWO) {\n alert('검사 대상을 선택해주세요.');\n return;\n }\n if (!inspectionForm.inspectorName) {\n alert('검사자를 입력해주세요.');\n return;\n }\n\n const hasFailure = inspectionForm.inspectionItems.some(item => item.result === '부');\n\n const inspectionData = {\n targetNo: selectedWO.workOrderNo,\n targetType: 'process',\n processStep: inspectionForm.processStep || selectedWO.currentStep,\n itemName: selectedWO.productName,\n qty: selectedWO.totalQty,\n inspector: inspectionForm.inspectorName,\n inspectionDate: inspectionForm.inspectionDate,\n result: hasFailure ? '불합격' : '합격',\n inspectionItems: inspectionForm.inspectionItems,\n note: inspectionForm.note,\n };\n\n onSave?.(inspectionData);\n };\n\n return (\n
\n {/* 헤더 */}\n
\n
\n \n 중간검사 등록 (PQC)\n
\n
\n \n \n
\n
\n\n
\n {/* 검사 대상 선택 */}\n
\n
검사 대상 선택
\n {pendingWOs.length === 0 ? (\n
검사 대기중인 작업지시가 없습니다.
\n ) : (\n
\n {pendingWOs.map(wo => (\n
setSelectedWO(wo)}\n className={`p-3 rounded-lg cursor-pointer transition-all ${selectedWO?.id === wo.id\n ? 'bg-yellow-50 border-2 border-yellow-500'\n : 'bg-gray-50 hover:bg-gray-100 border border-transparent'\n }`}\n >\n
{wo.workOrderNo}
\n
{wo.customerName} · {wo.processType}
\n
{wo.currentStep || '진행중'} · {wo.totalQty}EA
\n
\n ))}\n
\n )}\n
\n\n {/* 검사 정보 입력 */}\n
\n
검사 정보
\n {selectedWO ? (\n
\n
\n\n {/* 검사 항목 */}\n
\n\n
\n \n
\n
\n ) : (\n
\n 좌측에서 검사 대상을 선택해주세요.\n
\n )}\n
\n
\n
\n );\n};\n\n// ============================================================\n// 제품검사 등록 폼 (FQC)\n// ============================================================\nconst FQCRegisterForm = ({\n workOrders = [],\n shipments = [],\n onNavigate,\n onSave,\n}) => {\n const [selectedWO, setSelectedWO] = useState(null);\n const [inspectionForm, setInspectionForm] = useState({\n inspectorName: '',\n inspectionDate: new Date().toISOString().split('T')[0],\n inspectionItems: [\n { item: '외관검사', standard: '이상 없을 것', method: '육안검사', result: '적', note: '' },\n { item: '치수검사', standard: '도면 기준 ±2mm', method: '측정기', result: '적', note: '' },\n { item: '작동검사', standard: '정상 작동', method: '작동 테스트', result: '적', note: '' },\n { item: '포장상태', standard: '파손 방지 포장', method: '확인', result: '적', note: '' },\n ],\n note: '',\n });\n\n // 제품검사 대기 (작업완료, 검사 미완료)\n const pendingWOs = workOrders.filter(wo =>\n wo.status === '작업완료' && wo.inspectionStatus !== '검사완료'\n );\n\n const handleSubmit = () => {\n if (!selectedWO) {\n alert('검사 대상을 선택해주세요.');\n return;\n }\n if (!inspectionForm.inspectorName) {\n alert('검사자를 입력해주세요.');\n return;\n }\n\n const hasFailure = inspectionForm.inspectionItems.some(item => item.result === '부');\n\n const inspectionData = {\n targetNo: selectedWO.workOrderNo,\n targetType: 'final',\n itemName: selectedWO.productName,\n qty: selectedWO.completedQty || selectedWO.totalQty,\n inspector: inspectionForm.inspectorName,\n inspectionDate: inspectionForm.inspectionDate,\n result: hasFailure ? '불합격' : '합격',\n inspectionItems: inspectionForm.inspectionItems,\n note: inspectionForm.note,\n };\n\n onSave?.(inspectionData);\n };\n\n return (\n
\n {/* 헤더 */}\n
\n
\n \n 제품검사 등록 (FQC)\n
\n
\n \n \n
\n
\n\n
\n {/* 검사 대상 선택 */}\n
\n
검사 대상 선택
\n {pendingWOs.length === 0 ? (\n
검사 대기중인 제품이 없습니다.
\n ) : (\n
\n {pendingWOs.map(wo => (\n
setSelectedWO(wo)}\n className={`p-3 rounded-lg cursor-pointer transition-all ${selectedWO?.id === wo.id\n ? 'bg-green-50 border-2 border-green-500'\n : 'bg-gray-50 hover:bg-gray-100 border border-transparent'\n }`}\n >\n
{wo.workOrderNo}
\n
{wo.customerName} · {wo.processType}
\n
완료수량: {wo.completedQty || 0}/{wo.totalQty}EA
\n
\n ))}\n
\n )}\n
\n\n {/* 검사 정보 입력 */}\n
\n
검사 정보
\n {selectedWO ? (\n
\n
\n\n {/* 검사 항목 */}\n
\n\n
\n \n
\n
\n ) : (\n
\n 좌측에서 검사 대상을 선택해주세요.\n
\n )}\n
\n
\n
\n );\n};\n\n// ============================================================\n// 수입검사 컴포넌트\n// ============================================================\nconst IncomingInspectionList = ({\n inspections = [],\n purchaseOrders = [],\n onSave,\n onNavigate,\n onApprovalRequest, // 결재요청 콜백\n onStockRegister, // 재고등록 콜백\n}) => {\n const [activeTab, setActiveTab] = useState('pending');\n const [showCreateModal, setShowCreateModal] = useState(false);\n const [showApprovalModal, setShowApprovalModal] = useState(false);\n const [selectedItem, setSelectedItem] = useState(null);\n const [selectedForApproval, setSelectedForApproval] = useState(null);\n const [inspectionForm, setInspectionForm] = useState({\n result: '합격',\n inspectorName: '',\n inspectionItems: [],\n note: '',\n // 결재라인 (APR-2LINE: 담당 → 부서장)\n approvalLine: {\n type: 'APR-2LINE',\n roles: [\n { id: 'staff', label: '담당', name: '', date: '', status: 'pending' },\n { id: 'manager', label: '부서장', name: '', date: '', status: 'pending' },\n ]\n }\n });\n\n // 검사대기 목록 (입고완료 but 검사 미완료)\n const pendingInspections = purchaseOrders.filter(po => po.status === '검사대기');\n\n // 검사완료 목록\n const completedInspections = inspections.filter(insp => insp.type === 'incoming');\n\n const handleOpenInspection = (po) => {\n setSelectedItem(po);\n // 자재 유형에 따른 검사항목 자동 설정 (KS 규격 기반)\n const materialType = po.materialType || 'EGI'; // EGI, SUS, AL 등\n\n // 샘플링 기준 (n=3, c=0)\n const sampleCount = 3;\n\n // KS D 3528 기반 수입검사 항목\n const defaultInspectionItems = [\n {\n item: '겉모양',\n standard: '사용상 해로운 결함이 없을 것',\n method: '육안검사',\n sampleValues: Array(sampleCount).fill({ value: '', result: 'OK' }),\n result: '적',\n note: ''\n },\n {\n item: '두께',\n standard: po.thickness ? `${po.thickness}mm` : '규격 참조',\n method: '체크검사',\n tolerance: po.thickness >= 1.6 ? '± 0.12' : po.thickness >= 1.25 ? '± 0.10' : '± 0.08',\n sampleValues: Array(sampleCount).fill({ value: '', result: '' }),\n result: '적',\n note: ''\n },\n {\n item: '너비',\n standard: po.width ? `${po.width}mm` : '규격 참조',\n method: '체크검사',\n tolerance: po.width && po.width < 1250 ? '+7/-0' : '+10/-0',\n sampleValues: Array(sampleCount).fill({ value: '', result: '' }),\n result: '적',\n note: ''\n },\n {\n item: '길이',\n standard: po.length ? `${po.length}mm` : '규격 참조',\n method: '체크검사',\n tolerance: '+15/-0',\n sampleValues: Array(sampleCount).fill({ value: '', result: '' }),\n result: '적',\n note: ''\n },\n {\n item: '인장강도',\n standard: '270 이상 (N/㎟)',\n method: '공급업체 밀시트',\n sampleValues: [{ value: '', result: '' }],\n result: '적',\n note: ''\n },\n {\n item: '연신율',\n standard: po.thickness >= 1.6 ? '38 이상 (%)' : po.thickness >= 1.0 ? '37 이상 (%)' : '36 이상 (%)',\n method: '공급업체 밀시트',\n sampleValues: [{ value: '', result: '' }],\n result: '적',\n note: ''\n },\n {\n item: '아연의 최소 부착량',\n standard: '한면 17 이상 (g/㎡)',\n method: '공급업체 밀시트',\n sampleValues: [{ value: '', result: '' }],\n result: '적',\n note: ''\n },\n ];\n\n setInspectionForm({\n result: '합격',\n inspectorName: '',\n lotSize: po.receivedQty || 200,\n sampleSize: sampleCount,\n acceptanceNumber: 0, // c=0\n inspectionItems: defaultInspectionItems,\n note: '',\n standardRef: 'KS F 4510', // 관련 KS 규격\n });\n setShowCreateModal(true);\n };\n\n const handleSaveInspection = () => {\n if (!inspectionForm.inspectorName) {\n alert('검사자를 입력해주세요.');\n return;\n }\n\n // 새로운 '적/부' 구조에 맞게 불합격 판정 (result가 '부'인 항목이 하나라도 있으면 불합격)\n const hasFailure = inspectionForm.inspectionItems.some(item => item.result === '부');\n const today = new Date();\n // LOT번호: YYMMDD-순번 형식\n const lotNo = `${today.toISOString().slice(2, 10).replace(/-/g, '')}-${String(Math.floor(Math.random() * 99) + 1).padStart(2, '0')}`;\n\n const inspectionData = {\n id: Date.now(),\n type: 'incoming',\n lotNo: lotNo,\n poNo: selectedItem.poNo,\n materialCode: selectedItem.materialCode,\n materialName: selectedItem.materialName,\n specification: selectedItem.specification,\n thickness: selectedItem.thickness,\n vendor: selectedItem.vendor,\n incomingLot: selectedItem.incomingLot,\n receivedQty: selectedItem.receivedQty,\n unit: selectedItem.unit,\n // KS규격 기반 검사 정보\n standardRef: inspectionForm.standardRef || 'KS D 3528',\n lotSize: inspectionForm.lotSize,\n sampleSize: inspectionForm.sampleSize,\n acceptanceNumber: inspectionForm.acceptanceNumber,\n inspectorName: inspectionForm.inspectorName,\n inspectionDate: today.toISOString().split('T')[0],\n result: hasFailure ? '불합격' : '합격',\n judgement: hasFailure ? '부적합' : '적합',\n inspectionItems: inspectionForm.inspectionItems,\n note: inspectionForm.note,\n // ★ 결재 및 프로세스 연동 정보 추가\n approvalStatus: '결재대기', // 결재대기 → 결재완료\n approvalLine: {\n type: 'APR-2LINE',\n roles: [\n { id: 'staff', label: '담당', name: inspectionForm.inspectorName, date: today.toISOString().split('T')[0], status: 'approved' },\n { id: 'manager', label: '부서장', name: '', date: '', status: 'pending' },\n ]\n },\n // 프로세스 연동\n processFlow: {\n trigger: '자재입고',\n currentStep: hasFailure ? '부적합품처리' : '재고등록대기',\n nextAction: hasFailure ? 'NCR발행' : '결재완료시재고등록',\n },\n // NCR 정보 (불합격 시)\n ncrInfo: hasFailure ? {\n ncrNo: `NCR-IQC-${today.toISOString().slice(2, 10).replace(/-/g, '')}-${String(Math.floor(Math.random() * 99) + 1).padStart(2, '0')}`,\n defectType: inspectionForm.inspectionItems.filter(i => i.result === '부').map(i => i.item).join(', '),\n status: '발행대기',\n } : null,\n };\n\n onSave?.(inspectionData, selectedItem.id);\n setShowCreateModal(false);\n setSelectedItem(null);\n\n if (hasFailure) {\n alert(`⚠️ 수입검사 불합격!\\n\\n검사LOT: ${lotNo}\\n품명: ${selectedItem.materialName}\\n판정: 부적합 (c>${inspectionForm.acceptanceNumber})\\nNCR번호: ${inspectionData.ncrInfo.ncrNo}\\n\\n▶ 부서장 결재 후 부적합품 처리가 진행됩니다.`);\n } else {\n alert(`✅ 수입검사 합격!\\n\\n검사LOT: ${lotNo}\\n품명: ${selectedItem.materialName}\\n판정: 적합 (c≤${inspectionForm.acceptanceNumber})\\n\\n▶ 부서장 결재 후 재고에 반영됩니다.`);\n }\n };\n\n // 결재 처리 함수\n const handleApproval = (inspection, approverName) => {\n const updatedApprovalLine = {\n ...inspection.approvalLine,\n roles: inspection.approvalLine.roles.map(role =>\n role.status === 'pending'\n ? { ...role, name: approverName, date: new Date().toISOString().split('T')[0], status: 'approved' }\n : role\n )\n };\n\n const allApproved = updatedApprovalLine.roles.every(r => r.status === 'approved');\n\n const updatedInspection = {\n ...inspection,\n approvalLine: updatedApprovalLine,\n approvalStatus: allApproved ? '결재완료' : '결재중',\n processFlow: {\n ...inspection.processFlow,\n currentStep: allApproved\n ? (inspection.result === '합격' ? '재고등록완료' : 'NCR처리중')\n : inspection.processFlow.currentStep,\n }\n };\n\n // 결재 완료 시 후속 처리\n if (allApproved && inspection.result === '합격') {\n // 재고 등록 트리거\n onStockRegister?.({\n materialCode: inspection.materialCode,\n materialName: inspection.materialName,\n quantity: inspection.receivedQty,\n unit: inspection.unit,\n lotNo: inspection.incomingLot,\n inspectionLot: inspection.lotNo,\n location: '원자재창고',\n registeredAt: new Date().toISOString(),\n });\n alert(`✅ 결재 완료!\\n\\n${inspection.materialName}\\n수량: ${inspection.receivedQty} ${inspection.unit}\\n\\n▶ 재고에 등록되었습니다.`);\n } else if (allApproved && inspection.result === '불합격') {\n // NCR 처리 트리거\n alert(`⚠️ 결재 완료!\\n\\nNCR번호: ${inspection.ncrInfo?.ncrNo}\\n\\n▶ 부적합품 관리에서 처리해주세요.\\n(반품/폐기/특채)`);\n }\n\n onSave?.(updatedInspection, inspection.id);\n setShowApprovalModal(false);\n setSelectedForApproval(null);\n };\n\n const getResultBadge = (result) => {\n return result === '합격'\n ? 'bg-green-100 text-green-700'\n : 'bg-red-100 text-red-700';\n };\n\n return (\n
\n {/* 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n
\n \n 수입검사(IQC) 목록\n
\n \n\n {/* 통계 카드 */}\n
\n
\n
검사대기
\n
{pendingInspections.length}
\n
\n
\n
금일완료
\n
\n {completedInspections.filter(i => i.inspectionDate === new Date().toISOString().split('T')[0]).length}\n
\n
\n
\n
합격
\n
\n {completedInspections.filter(i => i.result === '합격').length}\n
\n
\n
\n
불합격
\n
\n {completedInspections.filter(i => i.result === '불합격').length}\n
\n
\n
\n\n {/* 탭 */}\n
\n
\n
\n {[\n { id: 'pending', label: '검사대기', count: pendingInspections.length },\n { id: 'completed', label: '검사완료', count: completedInspections.length },\n ].map(tab => (\n \n ))}\n
\n
\n\n {/* 테이블 */}\n
\n
\n \n \n {activeTab === 'pending' ? (\n <>\n | 입고LOT | \n 발주번호 | \n 품목명 | \n 공급업체 | \n 입고수량 | \n 입고일 | \n 작업 | \n >\n ) : (\n <>\n 검사LOT | \n 입고LOT | \n 품목명 | \n 공급업체 | \n 검사수량 | \n 검사일 | \n 검사자 | \n 결과 | \n 작업 | \n >\n )}\n
\n \n \n {activeTab === 'pending' ? (\n pendingInspections.length === 0 ? (\n \n | \n 검사 대기 중인 입고건이 없습니다.\n | \n
\n ) : (\n pendingInspections.map(po => (\n \n | {po.incomingLot} | \n {po.poNo} | \n {po.materialName} | \n {po.vendor} | \n {po.receivedQty} {po.unit} | \n {po.receivedDate} | \n \n \n | \n
\n ))\n )\n ) : (\n completedInspections.length === 0 ? (\n \n | \n 완료된 검사가 없습니다.\n | \n
\n ) : (\n completedInspections.map(insp => (\n \n | {insp.lotNo} | \n {insp.incomingLot} | \n {insp.materialName} | \n {insp.vendor} | \n {insp.receivedQty} {insp.unit} | \n {insp.inspectionDate} | \n {insp.inspectorName} | \n \n \n {insp.result}\n \n | \n {/* 작업 버튼 */}\n \n \n {insp.result === '불합격' && insp.ncrInfo && (\n \n )}\n \n \n | \n
\n ))\n )\n )}\n \n
\n
\n
\n\n {/* 검사 실시 모달 */}\n {showCreateModal && selectedItem && (\n
\n
\n
\n
수입검사 성적서
\n \n \n
\n {/* 품목 기본 정보 */}\n
\n
\n
품목 정보
\n 입고일자: {selectedItem.receivedDate || new Date().toISOString().split('T')[0]}\n \n
\n
\n 품명:\n {selectedItem.materialName}\n ({selectedItem.standard || 'KS D 3528, SECC'})\n
\n
\n 납품업체:\n {selectedItem.vendor}\n
\n
\n 규격 (두께*너비*길이):\n {selectedItem.spec || `${selectedItem.thickness || '1.55'} * ${selectedItem.width || '1218'} * ${selectedItem.length || '480'}`}\n
\n
\n 로트번호:\n {selectedItem.incomingLot}\n
\n
\n 제조업체:\n {selectedItem.manufacturer || '-'}\n
\n
\n 자재번호:\n {selectedItem.materialCode}\n
\n
\n 로트크기:\n {inspectionForm.lotSize || selectedItem.receivedQty} 매\n
\n
\n
\n\n {/* 샘플링 정보 */}\n
\n 샘플링:\n n = {inspectionForm.sampleSize || 3}\n c = {inspectionForm.acceptanceNumber || 0}\n ({inspectionForm.standardRef || 'KS F 4510'} 기준)\n
\n\n {/* KS 규격 기반 검사 항목 테이블 */}\n
\n
검사 항목
\n
\n
\n \n \n | NO | \n 검사항목 | \n 검사기준 | \n 검사방식 | \n 검사주기 | \n 측정치 | \n 판정 (적/부) | \n
\n \n n1 양호/불량 | \n n2 양호/불량 | \n n3 양호/불량 | \n
\n \n \n {inspectionForm.inspectionItems.map((item, idx) => (\n \n | {idx + 1} | \n \n {item.item} \n {item.tolerance && {item.tolerance} }\n | \n {item.standard} | \n {item.method} | \n n={item.sampleValues?.length || 3} c=0 | \n {/* 측정치 n1, n2, n3 */}\n {(item.sampleValues || [{}, {}, {}]).slice(0, 3).map((sample, sIdx) => (\n \n {item.item === '겉모양' ? (\n \n \n \n \n ) : item.method === '공급업체 밀시트' && sIdx > 0 ? (\n - \n ) : (\n {\n const newItems = [...inspectionForm.inspectionItems];\n if (!newItems[idx].sampleValues) newItems[idx].sampleValues = [{}, {}, {}];\n newItems[idx].sampleValues[sIdx] = { ...newItems[idx].sampleValues[sIdx], value: e.target.value };\n setInspectionForm(prev => ({ ...prev, inspectionItems: newItems }));\n }}\n className=\"w-full px-1 py-1 border rounded text-xs text-center\"\n placeholder=\"-\"\n />\n )}\n | \n ))}\n \n \n | \n
\n ))}\n \n
\n
\n
\n\n {/* 비고 및 종합판정 */}\n
\n
\n \n
\n
\n 종합판정\n i.result === '적') ? 'text-green-600' : 'text-red-600'\n }`}>\n {inspectionForm.inspectionItems.every(i => i.result === '적') ? '합격' : '불합격'}\n \n
\n
\n\n {/* 검사자 정보 */}\n
\n
\n \n setInspectionForm(prev => ({ ...prev, inspectorName: e.target.value }))}\n placeholder=\"검사자 이름\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n
\n \n \n
\n
\n
\n
\n
\n \n \n
\n
\n
\n )}\n\n {/* 결재 모달 */}\n {showApprovalModal && selectedForApproval && (\n
\n
\n
\n
수입검사 결재
\n \n \n
\n {/* 검사 정보 요약 */}\n
\n
\n 검사LOT:\n {selectedForApproval.lotNo}\n
\n
\n 품목:\n {selectedForApproval.materialName}\n
\n
\n 검사결과:\n \n {selectedForApproval.result}\n \n
\n {selectedForApproval.ncrInfo && (\n
\n NCR번호:\n {selectedForApproval.ncrInfo.ncrNo}\n
\n )}\n
\n\n {/* 결재라인 표시 */}\n
\n
결재라인 (APR-2LINE)
\n
\n
\n \n \n {selectedForApproval.approvalLine?.roles?.map(role => (\n | {role.label} | \n ))}\n
\n \n \n \n {selectedForApproval.approvalLine?.roles?.map(role => (\n \n {role.status === 'approved' ? (\n \n {role.name} \n {role.date} \n \n ) : (\n 대기중 \n )}\n | \n ))}\n
\n \n
\n
\n
\n\n {/* 결재자 입력 */}\n
\n \n \n
\n\n {/* 결재 후 처리 안내 */}\n
\n
결재 후 처리:
\n {selectedForApproval.result === '합격' ? (\n
▶ 재고 자동 등록 (원자재창고)
\n ) : (\n
▶ 부적합품 관리로 이동 (NCR 처리)
\n )}\n
\n
\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// ============================================================\n// 중간검사 컴포넌트 (PQC)\n// ============================================================\nconst ProcessInspectionList = ({\n inspections = [],\n workOrders = [],\n onSave,\n onNavigate,\n onApprovalRequest, // 결재요청 콜백\n onProcessPermit, // 다음 공정 허용 콜백\n inspectionRequests = [], // 작업자화면에서 온 검사요청 목록\n}) => {\n const [activeTab, setActiveTab] = useState('pending');\n const [showCreateModal, setShowCreateModal] = useState(false);\n const [showApprovalModal, setShowApprovalModal] = useState(false);\n const [selectedWorkOrder, setSelectedWorkOrder] = useState(null);\n const [selectedForApproval, setSelectedForApproval] = useState(null);\n const [inspectionForm, setInspectionForm] = useState({\n processStep: '',\n inspectorName: '',\n inspectionItems: [],\n note: '',\n // 결재라인 (APR-3LINE: 작성 → 검토 → 승인)\n approvalLine: {\n type: 'APR-3LINE',\n roles: [\n { id: 'writer', label: '작성', name: '', date: '', status: 'pending' },\n { id: 'reviewer', label: '검토', name: '', date: '', status: 'pending' },\n { id: 'approver', label: '승인', name: '', date: '', status: 'pending' },\n ]\n }\n });\n\n // 검사가 필요한 작업지시 (작업중 상태)\n const pendingWorkOrders = workOrders.filter(wo =>\n wo.status === '작업중' && wo.approvalStatus === '승인완료'\n );\n\n // 완료된 중간검사\n const completedInspections = inspections.filter(insp => insp.type === 'process');\n\n // 공정단계 옵션\n const processSteps = [\n { code: '1', name: '원단절단' },\n { code: '2', name: '미싱' },\n { code: '3', name: '앤드락작업' },\n { code: '4', name: '중간검사' },\n { code: '5', name: '포장' },\n ];\n\n const handleOpenInspection = (wo) => {\n setSelectedWorkOrder(wo);\n\n // 제품 유형에 따른 측정 포인트 수 결정 (스크린/절곡 등)\n const measurementPoints = 5; // ①~⑤ 측정 포인트\n\n // 기본 검사항목 with 다중 측정 포인트 구조\n const defaultInspectionItems = [\n {\n item: '치수검사(폭)',\n standard: wo.items?.[0]?.width ? `${wo.items[0].width}mm` : '도면규격',\n tolerance: '± 3mm',\n result: '합격',\n measurements: Array(measurementPoints).fill(''), // ①~⑤ 측정값\n average: '',\n judgement: '적'\n },\n {\n item: '치수검사(길이)',\n standard: wo.items?.[0]?.height ? `${wo.items[0].height}mm` : '도면규격',\n tolerance: '± 5mm',\n result: '합격',\n measurements: Array(measurementPoints).fill(''),\n average: '',\n judgement: '적'\n },\n {\n item: '외관검사',\n standard: '스크래치/오염/변형 없음',\n tolerance: '-',\n result: '합격',\n measurements: Array(measurementPoints).fill('OK'),\n average: '-',\n judgement: '적'\n },\n {\n item: '직각도',\n standard: '90°',\n tolerance: '± 1°',\n result: '합격',\n measurements: Array(measurementPoints).fill(''),\n average: '',\n judgement: '적'\n },\n {\n item: '평탄도',\n standard: '평탄',\n tolerance: '굴곡 3mm 이하',\n result: '합격',\n measurements: Array(measurementPoints).fill('OK'),\n average: '-',\n judgement: '적'\n },\n ];\n\n setInspectionForm({\n processStep: '4', // 기본값: 중간검사 단계\n inspectorName: '',\n measurementPoints: measurementPoints,\n lotSize: wo.totalQty || wo.items?.[0]?.quantity || 10,\n sampleSize: measurementPoints,\n standardRef: 'KS F 4510',\n inspectionItems: defaultInspectionItems,\n note: '',\n });\n setShowCreateModal(true);\n };\n\n const handleSaveInspection = () => {\n if (!inspectionForm.inspectorName || !inspectionForm.processStep) {\n alert('검사자와 공정단계를 입력해주세요.');\n return;\n }\n\n // 새로운 '적/부' 구조에 맞게 불합격 판정 (judgement가 '부'인 항목이 있으면 불합격)\n const hasFailure = inspectionForm.inspectionItems.some(item => item.judgement === '부' || item.result === '불합격');\n const today = new Date();\n const dateStr = today.toISOString().slice(2, 10).replace(/-/g, '');\n const seq = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0');\n const stepName = processSteps.find(s => s.code === inspectionForm.processStep)?.name || '';\n const lotNo = `KD-SC-${dateStr}-${seq}-(${inspectionForm.processStep})`;\n\n const inspectionData = {\n id: Date.now(),\n type: 'process',\n lotNo: lotNo,\n workOrderNo: selectedWorkOrder.workOrderNo,\n orderNo: selectedWorkOrder.orderNo,\n productName: selectedWorkOrder.productName || selectedWorkOrder.items?.[0]?.productName,\n specification: selectedWorkOrder.items?.[0] ? `${selectedWorkOrder.items[0].width}×${selectedWorkOrder.items[0].height}` : '',\n processStep: inspectionForm.processStep,\n processStepName: stepName,\n // KS규격 기반 검사 정보\n standardRef: inspectionForm.standardRef || 'KS F 4510',\n lotSize: inspectionForm.lotSize,\n sampleSize: inspectionForm.sampleSize,\n measurementPoints: inspectionForm.measurementPoints,\n inspectorName: inspectionForm.inspectorName,\n inspectionDate: today.toISOString().split('T')[0],\n result: hasFailure ? '불합격' : '합격',\n judgement: hasFailure ? '부적합' : '적합',\n inspectionItems: inspectionForm.inspectionItems,\n note: inspectionForm.note,\n // ★ 결재 및 프로세스 연동 정보 추가\n approvalStatus: '결재대기',\n approvalLine: {\n type: 'APR-3LINE',\n roles: [\n { id: 'writer', label: '작성', name: inspectionForm.inspectorName, date: today.toISOString().split('T')[0], status: 'approved' },\n { id: 'reviewer', label: '검토', name: '', date: '', status: 'pending' },\n { id: 'approver', label: '승인', name: '', date: '', status: 'pending' },\n ]\n },\n processFlow: {\n trigger: '작업자검사요청',\n currentStep: hasFailure ? '재작업대기' : '다음공정허용대기',\n nextAction: hasFailure ? '재작업지시' : '결재완료시다음공정허용',\n requestedFrom: selectedWorkOrder.assignee || '작업자',\n requestedAt: today.toISOString(),\n },\n // 불합격 시 NCR 자동 발행\n ncrInfo: hasFailure ? {\n ncrNo: `NCR-PQC-${dateStr}-${seq}`,\n defectType: inspectionForm.inspectionItems.filter(i => i.judgement === '부').map(i => i.item).join(', '),\n status: '발행대기',\n defectiveQty: selectedWorkOrder.totalQty || 1,\n action: '재작업',\n } : null,\n };\n\n onSave?.(inspectionData);\n setShowCreateModal(false);\n setSelectedWorkOrder(null);\n\n if (hasFailure) {\n alert(`⚠️ 중간검사 불합격!\\n\\n검사LOT: ${lotNo}\\n공정: ${stepName}\\n판정: 부적합\\n\\nNCR번호: NCR-PQC-${dateStr}-${seq}\\n재작업이 필요합니다.`);\n } else {\n alert(`✅ 중간검사 합격!\\n\\n검사LOT: ${lotNo}\\n공정: ${stepName}\\n판정: 적합\\n\\n결재완료 후 다음 공정이 허용됩니다.`);\n }\n };\n\n // ★ 결재 처리 핸들러\n const handleApproval = (inspection, approverName, approverRole) => {\n const today = new Date();\n const updatedApprovalLine = {\n ...inspection.approvalLine,\n roles: inspection.approvalLine.roles.map(role => {\n // 현재 결재 순서에 해당하는 역할 찾기\n if (role.status === 'pending') {\n const pendingRoles = inspection.approvalLine.roles.filter(r => r.status === 'pending');\n if (pendingRoles[0]?.id === role.id) {\n return { ...role, name: approverName, date: today.toISOString().split('T')[0], status: 'approved' };\n }\n }\n return role;\n })\n };\n\n const allApproved = updatedApprovalLine.roles.every(r => r.status === 'approved');\n\n // 결재 완료 시 후속 처리\n if (allApproved && inspection.result === '합격') {\n // 다음 공정 허용 콜백\n onProcessPermit?.({\n workOrderNo: inspection.workOrderNo,\n processStep: inspection.processStep,\n inspectionLot: inspection.lotNo,\n permittedAt: today.toISOString(),\n });\n }\n\n // 결재 요청 콜백\n onApprovalRequest?.({\n ...inspection,\n approvalStatus: allApproved ? '결재완료' : '결재중',\n approvalLine: updatedApprovalLine,\n processFlow: {\n ...inspection.processFlow,\n currentStep: allApproved ? (inspection.result === '합격' ? '다음공정허용완료' : '재작업진행중') : '결재진행중',\n }\n });\n\n setShowApprovalModal(false);\n setSelectedForApproval(null);\n\n if (allApproved) {\n alert(`✅ 결재가 완료되었습니다.\\n\\n검사LOT: ${inspection.lotNo}\\n${inspection.result === '합격' ? '다음 공정이 허용되었습니다.' : '재작업을 진행해주세요.'}`);\n }\n };\n\n // 현재 결재 단계 확인\n const getCurrentApprovalStep = (approvalLine) => {\n if (!approvalLine?.roles) return null;\n return approvalLine.roles.find(r => r.status === 'pending');\n };\n\n const getResultBadge = (result) => {\n return result === '합격'\n ? 'bg-green-100 text-green-700'\n : 'bg-red-100 text-red-700';\n };\n\n return (\n
\n {/* 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n
\n \n 중간검사(PQC) 목록\n
\n \n\n {/* 통계 카드 */}\n
\n
\n
검사가능
\n
{pendingWorkOrders.length}
\n
작업중인 작업지시
\n
\n
\n
금일검사
\n
\n {completedInspections.filter(i => i.inspectionDate === new Date().toISOString().split('T')[0]).length}\n
\n
\n
\n
합격
\n
\n {completedInspections.filter(i => i.result === '합격').length}\n
\n
\n
\n
불합격
\n
\n {completedInspections.filter(i => i.result === '불합격').length}\n
\n
\n
\n\n {/* 탭 */}\n
\n
\n
\n {[\n { id: 'pending', label: '검사가능', count: pendingWorkOrders.length },\n { id: 'completed', label: '검사이력', count: completedInspections.length },\n ].map(tab => (\n \n ))}\n
\n
\n\n {/* 테이블 */}\n
\n
\n \n \n {activeTab === 'pending' ? (\n <>\n | 작업지시번호 | \n 수주번호 | \n 품목 | \n 수량 | \n 현재공정 | \n 상태 | \n 작업 | \n >\n ) : (\n <>\n 검사LOT | \n 작업지시번호 | \n 품목 | \n 공정단계 | \n 검사일 | \n 검사자 | \n 결과 | \n 작업 | \n >\n )}\n
\n \n \n {activeTab === 'pending' ? (\n pendingWorkOrders.length === 0 ? (\n \n | \n 검사 가능한 작업지시가 없습니다.\n | \n
\n ) : (\n pendingWorkOrders.map(wo => (\n \n | {wo.workOrderNo} | \n {wo.orderNo} | \n \n {wo.productName || wo.items?.[0]?.productName || '-'}\n | \n {wo.totalQty || wo.items?.[0]?.quantity} | \n \n \n {wo.currentStep || '미싱'}\n \n | \n \n \n {wo.status}\n \n | \n \n \n | \n
\n ))\n )\n ) : (\n completedInspections.length === 0 ? (\n \n | \n 완료된 검사가 없습니다.\n | \n
\n ) : (\n completedInspections.map(insp => (\n \n | {insp.lotNo} | \n {insp.workOrderNo} | \n {insp.productName} | \n \n \n {insp.processStepName}\n \n | \n {insp.inspectionDate} | \n {insp.inspectorName} | \n \n \n {insp.result}\n \n | \n \n \n {insp.ncrInfo && (\n \n )}\n \n \n | \n
\n ))\n )\n )}\n \n
\n
\n
\n\n {/* 검사 실시 모달 - 다중 측정 포인트 지원 */}\n {showCreateModal && selectedWorkOrder && (\n
\n
\n
\n
중간검사성적서
\n \n \n
\n {/* 품목 및 작업지시 정보 */}\n
\n
품목 정보
\n
\n
작업지시번호: {selectedWorkOrder.workOrderNo}
\n
수주번호: {selectedWorkOrder.orderNo}
\n
품목: {selectedWorkOrder.productName || selectedWorkOrder.items?.[0]?.productName}
\n
생산수량: {selectedWorkOrder.totalQty || selectedWorkOrder.items?.[0]?.quantity}EA
\n {selectedWorkOrder.items?.[0] && (\n <>\n
규격(폭): {selectedWorkOrder.items[0].width}mm
\n
규격(길이): {selectedWorkOrder.items[0].height}mm
\n >\n )}\n
\n
\n\n {/* 공정단계 및 샘플링 정보 */}\n
\n
\n \n \n
\n
\n
적용규격
\n
{inspectionForm.standardRef}
\n
\n
\n
샘플수
\n
n={inspectionForm.measurementPoints} (①~⑤)
\n
\n
\n\n {/* 검사 항목 - 다중 측정 포인트 테이블 */}\n
\n\n {/* 종합 판정 */}\n
\n
\n
종합판정
\n
i.judgement === '부')\n ? 'bg-red-100 text-red-700'\n : 'bg-green-100 text-green-700'\n }`}>\n {inspectionForm.inspectionItems.some(i => i.judgement === '부') ? '부적합' : '적합'}\n
\n
\n
\n\n {/* 검사자 정보 */}\n
\n
\n \n
\n
\n
\n \n \n
\n
\n
\n )}\n\n {/* ★ 결재 모달 (APR-3LINE) */}\n {showApprovalModal && selectedForApproval && (\n
\n
\n
\n
중간검사 결재
\n \n \n
\n {/* 검사 정보 */}\n
\n
\n
검사LOT: {selectedForApproval.lotNo}
\n
작업지시: {selectedForApproval.workOrderNo}
\n
품목: {selectedForApproval.productName}
\n
공정: {selectedForApproval.processStepName}
\n
\n 검사결과:\n \n {selectedForApproval.result}\n \n
\n {selectedForApproval.ncrInfo && (\n
NCR: {selectedForApproval.ncrInfo.ncrNo}
\n )}\n
\n
\n\n {/* 결재라인 표시 (APR-3LINE: 작성 → 검토 → 승인) */}\n
\n
결재라인 (3단계)
\n
\n {selectedForApproval.approvalLine?.roles.map((role, idx) => (\n
\n r.status === 'pending')\n ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-gray-50'\n }`}>\n
{role.label}
\n
{role.name || '-'}
\n
\n {role.status === 'approved' ? role.date : role.status === 'pending' ? '대기' : ''}\n
\n {role.status === 'approved' && (\n
\n )}\n
\n {idx < selectedForApproval.approvalLine.roles.length - 1 && (\n \n )}\n \n ))}\n
\n
\n\n {/* 현재 결재자 입력 */}\n {getCurrentApprovalStep(selectedForApproval.approvalLine) && (\n
\n \n \n
\n )}\n\n {/* 프로세스 플로우 정보 */}\n {selectedForApproval.processFlow && (\n
\n
\n 진행상태:\n \n {selectedForApproval.processFlow.currentStep}\n \n
\n
\n 결재완료 시: {selectedForApproval.result === '합격' ? '다음 공정 진행 허용' : '재작업 지시'}\n
\n
\n )}\n
\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// ============================================================\n// 검사관리 통합 허브 (IQC/PQC/FQC 통합)\n// ============================================================\nconst InspectionManagementHub = ({\n inspections = [],\n workOrders = [],\n purchaseOrders = [],\n shipments = [],\n onNavigate,\n}) => {\n const [activeTab, setActiveTab] = useState('all');\n const [searchTerm, setSearchTerm] = useState('');\n const [selectedIds, setSelectedIds] = useState([]);\n const [certificateData, setCertificateData] = useState(null);\n\n // 수입검사 대기\n const iqcPending = purchaseOrders.filter(po => po.status === '검사대기' || po.status === '입고대기');\n\n // 중간검사 대기 (작업중인 작업지시서)\n const pqcPending = workOrders.filter(wo => wo.status === '작업중');\n\n // 제품검사 대기 (작업완료, 검사 미완료)\n const fqcPending = workOrders.filter(wo =>\n wo.status === '작업완료' && wo.inspectionStatus !== '검사완료'\n );\n\n // 전체 대기 건수\n const totalPending = iqcPending.length + pqcPending.length + fqcPending.length;\n\n // 완료 검사 건수\n const completedCount = inspections.filter(i => i.result === '합격' || i.result === 'pass').length;\n\n // 불합격 건수\n const failedCount = inspections.filter(i => i.result === '불합격' || i.result === 'fail').length;\n\n // 검색 필터링된 검사 이력\n const filteredInspections = inspections.filter(insp => {\n const matchesSearch = !searchTerm ||\n (insp.targetNo || '').toLowerCase().includes(searchTerm.toLowerCase()) ||\n (insp.lotNo || '').toLowerCase().includes(searchTerm.toLowerCase()) ||\n (insp.inspector || '').toLowerCase().includes(searchTerm.toLowerCase());\n\n const matchesTab = activeTab === 'all' ||\n (activeTab === 'iqc' && insp.type === 'incoming') ||\n (activeTab === 'pqc' && insp.type === 'process') ||\n (activeTab === 'fqc' && insp.type === 'final');\n\n return matchesSearch && matchesTab;\n }).sort((a, b) => new Date(b.inspectionDate) - new Date(a.inspectionDate));\n\n // 선택 관련 함수\n const handleSelect = (id) => {\n setSelectedIds(prev =>\n prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]\n );\n };\n\n const handleSelectAll = (e) => {\n if (e.target.checked) {\n setSelectedIds(filteredInspections.map((_, idx) => idx));\n } else {\n setSelectedIds([]);\n }\n };\n\n const isSelected = (id) => selectedIds.includes(id);\n const hasSelection = selectedIds.length > 0;\n const isAllSelected = filteredInspections.length > 0 && selectedIds.length === filteredInspections.length;\n\n return (\n
\n {/* 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n
\n \n 검사 목록\n
\n {/* 버튼 영역 - 우측 정렬 */}\n
\n
\n
\n
\n\n {/* 리포트 카드 4개 */}\n
\n
\n
전체 대기
\n
{totalPending}건
\n
\n
\n
이번 달 검사 완료
\n
{completedCount}건
\n
\n
\n
불합격 건수
\n
{failedCount}건
\n
\n
\n
합격률
\n
\n {inspections.length > 0 ? Math.round((completedCount / inspections.length) * 100) : 0}%\n
\n
\n
\n\n {/* 검색바 */}\n
\n \n setSearchTerm(e.target.value)}\n />\n
\n\n {/* 검사유형별 탭 + 대기 현황 */}\n
\n \n \n \n \n
\n\n {/* 검사 대기 목록 (탭별) */}\n {(activeTab === 'iqc' || activeTab === 'all') && iqcPending.length > 0 && (\n
\n
수입검사 대기 목록
\n
\n {iqcPending.map(po => (\n
\n
\n
{po.poNo || po.id}
\n
{po.itemName || po.productName}
\n
{po.supplierName || po.vendorName} · {po.qty || po.orderQty}EA
\n
\n
\n \n {po.status}\n \n \n
\n
\n ))}\n
\n
\n )}\n\n {(activeTab === 'pqc' || activeTab === 'all') && pqcPending.length > 0 && (\n
\n
중간검사 대기 목록
\n
\n {pqcPending.map(wo => (\n
\n
\n
{wo.workOrderNo}
\n
{wo.customerName} · {wo.processType}
\n
{wo.currentStep || '진행중'} · {wo.totalQty}EA
\n
\n
\n \n {wo.status}\n \n \n
\n
\n ))}\n
\n
\n )}\n\n {(activeTab === 'fqc' || activeTab === 'all') && fqcPending.length > 0 && (\n
\n
제품검사 대기 목록
\n
\n {fqcPending.map(wo => (\n
\n
\n
{wo.workOrderNo}
\n
{wo.customerName} · {wo.processType}
\n
완료수량: {wo.completedQty || 0}/{wo.totalQty}EA
\n
\n
\n \n 작업완료\n \n \n
\n
\n ))}\n
\n
\n )}\n\n {/* 검사 이력 테이블 (체크박스 + 작업 컬럼) */}\n
\n
\n
검사 이력
\n {selectedIds.length > 1 && (\n \n )}\n \n
\n
\n\n {/* 검사 성적서 출력 모달 */}\n {certificateData && (\n
setCertificateData(null)}\n />\n )}\n \n );\n};\n\n// ============================================================\n// 부적합품관리 컴포넌트\n// ============================================================\nconst DefectManagement = ({\n defects = [],\n inspections = [],\n onSave,\n onDispose,\n onRework,\n onNavigate,\n onApprovalRequest, // NCR 결재요청 콜백\n onVendorClaim, // 업체 클레임 콜백\n}) => {\n const [activeTab, setActiveTab] = useState('all');\n const [searchTerm, setSearchTerm] = useState('');\n const [selectedIds, setSelectedIds] = useState([]);\n const [selectedDefect, setSelectedDefect] = useState(null);\n const [showActionModal, setShowActionModal] = useState(false);\n const [showApprovalModal, setShowApprovalModal] = useState(false);\n const [selectedForApproval, setSelectedForApproval] = useState(null);\n const [actionForm, setActionForm] = useState({\n action: 'rework', // rework, dispose, accept\n handler: '',\n note: '',\n // NCR 결재라인 (APR-3LINE)\n approvalLine: {\n type: 'APR-3LINE',\n roles: [\n { id: 'writer', label: '작성', name: '', date: '', status: 'pending' },\n { id: 'reviewer', label: '검토', name: '', date: '', status: 'pending' },\n { id: 'approver', label: '승인', name: '', date: '', status: 'pending' },\n ]\n }\n });\n\n // ★ NCR 결재 처리 핸들러\n const handleNCRApproval = (defect, approverName) => {\n const today = new Date();\n const updatedApprovalLine = {\n ...defect.approvalLine,\n roles: (defect.approvalLine?.roles || []).map(role => {\n if (role.status === 'pending') {\n const pendingRoles = defect.approvalLine.roles.filter(r => r.status === 'pending');\n if (pendingRoles[0]?.id === role.id) {\n return { ...role, name: approverName, date: today.toISOString().split('T')[0], status: 'approved' };\n }\n }\n return role;\n })\n };\n\n const allApproved = updatedApprovalLine.roles?.every(r => r.status === 'approved');\n\n // 결재 완료 시 후속 처리\n if (allApproved) {\n // 처리 방식에 따른 후속 액션\n if (defect.action === 'dispose' && defect.sourceType === '수입검사') {\n // 업체 클레임 발행\n onVendorClaim?.({\n ncrNo: defect.ncrNo || defect.defectNo,\n vendor: defect.vendor,\n defectType: defect.defectReason,\n quantity: defect.quantity,\n claimDate: today.toISOString().split('T')[0],\n });\n }\n }\n\n // 결재 요청 콜백\n onApprovalRequest?.({\n ...defect,\n approvalStatus: allApproved ? '결재완료' : '결재중',\n approvalLine: updatedApprovalLine,\n });\n\n setShowApprovalModal(false);\n setSelectedForApproval(null);\n\n if (allApproved) {\n alert(`✅ NCR 결재가 완료되었습니다.\\n\\nNCR번호: ${defect.ncrNo || defect.defectNo}\\n처리방식: ${defect.status}\\n${defect.action === 'dispose' && defect.sourceType === '수입검사' ? '업체 클레임이 발행됩니다.' : ''}`);\n }\n };\n\n // 현재 결재 단계 확인\n const getCurrentApprovalStep = (approvalLine) => {\n if (!approvalLine?.roles) return null;\n return approvalLine.roles.find(r => r.status === 'pending');\n };\n\n // 불합격 검사 목록에서 부적합품 추출 (NCR 정보 포함)\n const allDefects = [\n ...defects,\n ...inspections.filter(insp => insp.result === '불합격').map(insp => ({\n id: `insp-${insp.id}`,\n defectNo: `DF-${insp.lotNo}`,\n // ★ NCR 정보 연동\n ncrNo: insp.ncrInfo?.ncrNo || `NCR-${insp.type === 'incoming' ? 'IQC' : insp.type === 'process' ? 'PQC' : 'FQC'}-${insp.lotNo}`,\n sourceType: insp.type === 'incoming' ? '수입검사' : insp.type === 'process' ? '중간검사' : '제품검사',\n sourceLot: insp.lotNo,\n itemName: insp.materialName || insp.productName,\n quantity: insp.receivedQty || insp.ncrInfo?.defectiveQty || 1,\n unit: insp.unit || 'EA',\n defectReason: insp.ncrInfo?.defectType || insp.inspectionItems?.filter(i => i.result === '불합격' || i.judgement === '부').map(i => i.item).join(', ') || '검사 불합격',\n detectedDate: insp.inspectionDate,\n inspector: insp.inspectorName,\n status: insp.ncrInfo?.status === '처리완료' ? (insp.ncrInfo.action || '처리완료') : '처리대기',\n vendor: insp.vendor,\n // ★ 결재 정보\n approvalStatus: insp.ncrInfo?.approvalStatus || '결재대기',\n approvalLine: insp.ncrInfo?.approvalLine || {\n type: 'APR-3LINE',\n roles: [\n { id: 'writer', label: '작성', name: insp.inspectorName, date: insp.inspectionDate, status: 'approved' },\n { id: 'reviewer', label: '검토', name: '', date: '', status: 'pending' },\n { id: 'approver', label: '승인', name: '', date: '', status: 'pending' },\n ]\n },\n // ★ 프로세스 플로우\n processFlow: {\n trigger: insp.type === 'incoming' ? '수입검사불합격' : insp.type === 'process' ? '중간검사불합격' : '제품검사불합격',\n currentStep: '처리방법결정대기',\n nextAction: '처리결정후결재',\n },\n }))\n ];\n\n const pendingDefects = allDefects.filter(d => d.status === '처리대기');\n const processedDefects = allDefects.filter(d => d.status !== '처리대기');\n const reworkDefects = allDefects.filter(d => d.status === '재작업');\n const disposeDefects = allDefects.filter(d => d.status === '폐기');\n\n // 검색 및 탭 필터링\n const filteredDefects = allDefects\n .filter(d => {\n if (activeTab === 'pending') return d.status === '처리대기';\n if (activeTab === 'processed') return d.status !== '처리대기';\n return true;\n })\n .filter(d => {\n if (!searchTerm) return true;\n const term = searchTerm.toLowerCase();\n return (d.ncrNo || d.defectNo || '').toLowerCase().includes(term) ||\n (d.itemName || '').toLowerCase().includes(term) ||\n (d.sourceLot || '').toLowerCase().includes(term) ||\n (d.defectReason || '').toLowerCase().includes(term);\n })\n .sort((a, b) => {\n const aId = typeof a.id === 'string' ? parseInt(a.id.replace(/\\D/g, '')) : a.id;\n const bId = typeof b.id === 'string' ? parseInt(b.id.replace(/\\D/g, '')) : b.id;\n return bId - aId;\n });\n\n // 선택 핸들러\n const handleSelect = (id) => {\n setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]);\n };\n const handleSelectAll = (e) => {\n if (e.target.checked) {\n setSelectedIds(filteredDefects.map(d => d.id));\n } else {\n setSelectedIds([]);\n }\n };\n const isSelected = (id) => selectedIds.includes(id);\n const hasSelection = selectedIds.length > 0;\n const isAllSelected = filteredDefects.length > 0 && selectedIds.length === filteredDefects.length;\n const isMultiSelect = selectedIds.length > 1;\n\n const handleOpenAction = (defect) => {\n setSelectedDefect(defect);\n setActionForm({\n action: 'rework',\n handler: '',\n note: '',\n });\n setShowActionModal(true);\n };\n\n const handleProcessDefect = () => {\n if (!actionForm.handler) {\n alert('처리자를 입력해주세요.');\n return;\n }\n\n const actionLabels = {\n rework: '재작업',\n dispose: '폐기',\n accept: '특채(조건부합격)',\n };\n\n const processedData = {\n ...selectedDefect,\n status: actionLabels[actionForm.action],\n action: actionForm.action,\n handler: actionForm.handler,\n processedDate: new Date().toISOString().split('T')[0],\n processNote: actionForm.note,\n };\n\n if (actionForm.action === 'rework') {\n onRework?.(processedData);\n alert(`🔄 재작업 처리되었습니다.\\n\\n부적합번호: ${selectedDefect.defectNo}\\n재작업 후 재검사가 필요합니다.`);\n } else if (actionForm.action === 'dispose') {\n onDispose?.(processedData);\n alert(`🗑️ 폐기 처리되었습니다.\\n\\n부적합번호: ${selectedDefect.defectNo}\\n재고에서 차감됩니다.`);\n } else {\n onSave?.(processedData);\n alert(`✅ 특채 처리되었습니다.\\n\\n부적합번호: ${selectedDefect.defectNo}\\n조건부로 사용 가능합니다.`);\n }\n\n setShowActionModal(false);\n setSelectedDefect(null);\n };\n\n const getStatusBadge = (status) => {\n const styles = {\n '처리대기': 'bg-red-100 text-red-700',\n '재작업': 'bg-yellow-100 text-yellow-700',\n '폐기': 'bg-gray-100 text-gray-700',\n '특채(조건부합격)': 'bg-blue-100 text-blue-700',\n };\n return styles[status] || 'bg-gray-100 text-gray-700';\n };\n\n const getSourceBadge = (sourceType) => {\n const styles = {\n '수입검사': 'bg-blue-100 text-blue-700',\n '중간검사': 'bg-purple-100 text-purple-700',\n '제품검사': 'bg-green-100 text-green-700',\n };\n return styles[sourceType] || 'bg-gray-100 text-gray-700';\n };\n\n return (\n
\n {/* 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n
\n \n 부적합품 목록\n
\n
\n
\n
\n
\n\n {/* 리포트 카드 4개 */}\n
\n
\n
처리대기
\n
{pendingDefects.length}건
\n
\n
\n
재작업
\n
{reworkDefects.length}건
\n
\n
\n
폐기
\n
{disposeDefects.length}건
\n
\n
\n
처리완료
\n
{processedDefects.length}건
\n
\n
\n\n {/* 검색바 */}\n
\n \n setSearchTerm(e.target.value)}\n />\n
\n\n {/* 탭 */}\n
\n {[\n { id: 'all', label: '전체', count: allDefects.length },\n { id: 'pending', label: '처리대기', count: pendingDefects.length },\n { id: 'processed', label: '처리완료', count: processedDefects.length },\n ].map(tab => (\n \n ))}\n
\n\n {/* 선택 삭제 버튼 */}\n {isMultiSelect && (\n
\n \n
\n )}\n\n {/* 테이블 */}\n
\n \n \n\n {/* 처리 모달 */}\n {showActionModal && selectedDefect && (\n
\n
\n
\n
부적합품 처리
\n \n \n
\n {/* 부적합 정보 */}\n
\n
\n
부적합번호: {selectedDefect.defectNo}
\n
발생구분: {selectedDefect.sourceType}
\n
품목: {selectedDefect.itemName}
\n
수량: {selectedDefect.quantity} {selectedDefect.unit}
\n
불량원인: {selectedDefect.defectReason}
\n
\n
\n\n {/* 처리 방법 선택 */}\n
\n
\n
\n {[\n { value: 'rework', label: '재작업', icon: '🔄', color: 'yellow' },\n { value: 'dispose', label: '폐기', icon: '🗑️', color: 'gray' },\n { value: 'accept', label: '특채', icon: '✅', color: 'blue' },\n ].map(option => (\n
\n ))}\n
\n
\n\n {/* 처리자 및 비고 */}\n
\n \n setActionForm(prev => ({ ...prev, handler: e.target.value }))}\n placeholder=\"처리자 이름\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n
\n \n
\n
\n
\n \n \n
\n
\n
\n )}\n\n {/* ★ NCR 결재 모달 (APR-3LINE) */}\n {showApprovalModal && selectedForApproval && (\n
\n
\n
\n
NCR 결재
\n \n \n
\n {/* NCR 정보 */}\n
\n
\n
NCR번호: {selectedForApproval.ncrNo || selectedForApproval.defectNo}
\n
발생구분: {selectedForApproval.sourceType}
\n
품목: {selectedForApproval.itemName}
\n
수량: {selectedForApproval.quantity} {selectedForApproval.unit}
\n
\n 처리방식:\n \n {selectedForApproval.status}\n \n
\n
처리자: {selectedForApproval.handler}
\n
\n
\n 불량원인:\n {selectedForApproval.defectReason}\n
\n
\n\n {/* 결재라인 표시 (APR-3LINE: 작성 → 검토 → 승인) */}\n
\n
결재라인 (3단계)
\n
\n {(selectedForApproval.approvalLine?.roles || [\n { id: 'writer', label: '작성', name: '', status: 'pending' },\n { id: 'reviewer', label: '검토', name: '', status: 'pending' },\n { id: 'approver', label: '승인', name: '', status: 'pending' },\n ]).map((role, idx, arr) => (\n
\n r.status === 'pending')\n ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-gray-50'\n }`}>\n
{role.label}
\n
{role.name || '-'}
\n
\n {role.status === 'approved' ? role.date : role.status === 'pending' ? '대기' : ''}\n
\n {role.status === 'approved' && (\n
\n )}\n
\n {idx < arr.length - 1 && (\n \n )}\n \n ))}\n
\n
\n\n {/* 현재 결재자 입력 */}\n
\n \n \n
\n\n {/* 프로세스 플로우 정보 */}\n
\n
\n 후속처리:\n \n {selectedForApproval.status === '폐기' && selectedForApproval.sourceType === '수입검사' ? '업체 클레임 발행' :\n selectedForApproval.status === '재작업' ? '재작업 지시' :\n selectedForApproval.status === '특채(조건부합격)' ? '조건부 사용 허가' : '처리 완료'}\n \n
\n
\n 결재완료 시 해당 후속처리가 자동으로 진행됩니다.\n
\n
\n
\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// ============================================================\n// 배송현황 컴포넌트\n// ============================================================\nconst DeliveryStatusList = ({\n shipments = [],\n onUpdateStatus,\n onNavigate\n}) => {\n const [activeTab, setActiveTab] = useState('in-progress');\n const [selectedShipment, setSelectedShipment] = useState(null);\n const [showDetailModal, setShowDetailModal] = useState(false);\n\n // 배송 진행중\n const inProgressShipments = shipments.filter(s =>\n s.status === '출하준비' || s.status === '상차완료' || s.status === '배송중'\n );\n\n // 배송완료\n const completedShipments = shipments.filter(s => s.status === '배송완료');\n\n // 금일 배송\n const todayShipments = shipments.filter(s =>\n s.deliveryDate === new Date().toISOString().split('T')[0]\n );\n\n const getStatusBadge = (status) => {\n const styles = {\n '출고지시': 'bg-gray-100 text-gray-700',\n '출하준비': 'bg-yellow-100 text-yellow-700',\n '상차완료': 'bg-blue-100 text-blue-700',\n '배송중': 'bg-purple-100 text-purple-700',\n '배송완료': 'bg-green-100 text-green-700',\n };\n return styles[status] || 'bg-gray-100 text-gray-700';\n };\n\n const getStatusProgress = (status) => {\n const steps = ['출고지시', '출하준비', '상차완료', '배송중', '배송완료'];\n const currentIndex = steps.indexOf(status);\n return { current: currentIndex + 1, total: steps.length };\n };\n\n const handleOpenDetail = (shipment) => {\n setSelectedShipment(shipment);\n setShowDetailModal(true);\n };\n\n const handleUpdateDeliveryStatus = (shipmentId, newStatus) => {\n onUpdateStatus?.(shipmentId, newStatus);\n if (newStatus === '배송완료') {\n alert('✅ 배송이 완료되었습니다.');\n }\n setShowDetailModal(false);\n };\n\n return (\n
\n {/* 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n
\n \n 배송현황\n
\n \n\n {/* 통계 카드 */}\n
\n
\n
금일배송
\n
{todayShipments.length}
\n
\n
\n
출하준비
\n
\n {shipments.filter(s => s.status === '출하준비').length}\n
\n
\n
\n
배송중
\n
\n {shipments.filter(s => s.status === '배송중').length}\n
\n
\n
\n
배송완료
\n
{completedShipments.length}
\n
\n
\n
전체
\n
{shipments.length}
\n
\n
\n\n {/* 배송중 카드 뷰 */}\n {inProgressShipments.length > 0 && (\n
\n
🚚 현재 배송중
\n
\n {inProgressShipments.slice(0, 6).map(shipment => {\n const progress = getStatusProgress(shipment.status);\n return (\n
handleOpenDetail(shipment)}\n className=\"border rounded-lg p-4 cursor-pointer hover:shadow-md transition-shadow\"\n >\n
\n
\n
{shipment.customerName}
\n
{shipment.siteName}
\n
\n
\n {shipment.status}\n \n
\n
\n 분할번호: {shipment.splitNo}\n
\n {/* 진행 바 */}\n
\n
\n 진행상태\n \n {Math.round((progress.current / progress.total) * 100)}%\n \n
\n
\n
\n
\n
\n \n {shipment.driver || '미정'} / {shipment.vehicle || '미정'}\n
\n
\n \n 배송예정: {shipment.deliveryDate || '-'}\n
\n
\n
\n );\n })}\n
\n
\n )}\n\n {/* 탭 (칩 형태) */}\n
\n {[\n { id: 'in-progress', label: '진행중', count: inProgressShipments.length },\n { id: 'completed', label: '배송완료', count: completedShipments.length },\n { id: 'today', label: '금일배송', count: todayShipments.length },\n { id: 'all', label: '전체', count: shipments.length },\n ].map(tab => (\n \n ))}\n
\n\n {/* 테이블 */}\n
\n
\n
\n \n \n | 분할번호 | \n 고객사 | \n 현장 | \n 배송지 | \n 배송예정일 | \n 차량/기사 | \n 상태 | \n 작업 | \n
\n \n \n {(() => {\n const filteredList =\n activeTab === 'in-progress' ? inProgressShipments :\n activeTab === 'completed' ? completedShipments :\n activeTab === 'today' ? todayShipments :\n shipments;\n\n return filteredList.length === 0 ? (\n \n | \n 데이터가 없습니다.\n | \n
\n ) : (\n filteredList.map(shipment => (\n \n | {shipment.splitNo} | \n {shipment.customerName} | \n {shipment.siteName} | \n \n {shipment.address || '-'}\n | \n {shipment.deliveryDate || '-'} | \n \n {shipment.driver || '-'} / {shipment.vehicle || '-'}\n | \n \n \n {shipment.status}\n \n | \n \n \n | \n
\n ))\n );\n })()}\n \n
\n
\n
\n\n {/* 상세 모달 */}\n {showDetailModal && selectedShipment && (\n
\n
\n
\n
배송 상세
\n \n \n
\n {/* 배송 진행 상태 */}\n
\n
\n {['출고지시', '출하준비', '상차완료', '배송중', '배송완료'].map((step, idx) => {\n const progress = getStatusProgress(selectedShipment.status);\n const isActive = idx < progress.current;\n const isCurrent = idx === progress.current - 1;\n return (\n
\n
\n {isActive ? : idx + 1}\n
\n
\n {step}\n \n
\n );\n })}\n
\n
\n\n {/* 배송 정보 */}\n
\n
\n 분할번호:\n {selectedShipment.splitNo}\n
\n
\n 고객사:\n {selectedShipment.customerName}\n
\n
\n 현장명:\n {selectedShipment.siteName}\n
\n
\n 배송예정일:\n {selectedShipment.deliveryDate || '-'}\n
\n
\n 배송지:\n {selectedShipment.address || '-'}\n
\n
\n 차량:\n {selectedShipment.vehicle || '-'}\n
\n
\n 기사:\n {selectedShipment.driver || '-'}\n
\n
\n\n {/* 상태 변경 버튼 */}\n {selectedShipment.status !== '배송완료' && (\n
\n
\n
\n {['출하준비', '상차완료', '배송중', '배송완료'].map(status => (\n \n ))}\n
\n
\n )}\n
\n
\n \n
\n
\n
\n )}\n
\n );\n};\n\n// ============================================================\n// 납품확인서 문서 컴포넌트 (출하완료 후 발행)\n// ============================================================\nconst DeliveryConfirmationDoc = ({\n shipment,\n order,\n customer,\n onClose,\n onPrint\n}) => {\n if (!shipment || !order) return null;\n\n const today = new Date().toLocaleDateString('ko-KR');\n const lotNo = shipment.lotNo || `KD-WE-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01-(${shipment.splitIndex || 1})`;\n\n return (\n
\n
\n {/* 문서 헤더 */}\n
\n
납품확인서
\n
\n
\n
\n
\n
\n\n {/* 문서 내용 */}\n
\n {/* 회사 로고 및 제목 */}\n
\n
\n
KD 경동기업
\n
\n 전화: 031-938-5130 | 팩스: 02-6911-6315 | 이메일: kd5130@naver.com\n
\n
\n
\n
납 품 확 인 서
\n
로트번호: {lotNo}
\n
\n
\n\n {/* 기본 정보 테이블 */}\n
\n
\n
신청업체
\n
\n \n \n | 발주일 | \n {order.orderDate || '-'} | \n
\n \n | 발주처 | \n {customer?.name || order.customerName || '-'} | \n
\n \n | 발주 담당자 | \n {order.contactPerson || '-'} | \n
\n \n | 담당자 연락처 | \n {order.contactPhone || '-'} | \n
\n \n
\n
\n\n
\n
신청내용 / 납품정보
\n
\n \n \n | 현장명 | \n {order.siteName || '-'} | \n
\n \n | 납기요청일 | \n {order.dueDate || '-'} | \n
\n \n | 출고일 | \n {shipment.shipDate || today} | \n
\n \n | 셔터총수량 | \n {shipment.totalQty || order.totalQty || '-'} 개소 | \n
\n \n
\n
\n
\n\n {/* 배송지 주소 */}\n
\n 배송지 주소: \n {order.deliveryAddress || order.siteAddress || '-'}\n
\n\n {/* 부호별 자재내역 */}\n
\n
1. 부호별 자재내역
\n
\n
\n \n \n | 일련번호 | \n 종류 | \n 도면부호 | \n 오픈사이즈(mm) | \n 제작사이즈(mm) | \n 가이드레일유형 | \n 샤프트(인치) | \n 케이스(규격) | \n 모터 | \n
\n \n | \n | \n | \n 가로 | \n 세로 | \n 가로 | \n 세로 | \n | \n | \n | \n 브라켓트 | \n 용량(KG) | \n
\n \n \n {(order.items || []).slice(0, 5).map((item, idx) => (\n \n | {String(idx + 1).padStart(2, '0')} | \n {item.productType || '와이어'} | \n {item.drawingNo || `4층 FSS${idx + 1}`} | \n {item.openWidth || '-'} | \n {item.openHeight || '-'} | \n {item.prodWidth || '-'} | \n {item.prodHeight || '-'} | \n {item.guideRailType || '벽면형(120-70)'} | \n {item.shaftSize || '5'} | \n {item.caseSpec || '500-330'} | \n {item.bracketSpec || '380-180'} | \n {item.motorCapacity || '300'} | \n
\n ))}\n \n
\n
\n
\n\n {/* 2. 부호별 절곡내역 */}\n
\n
2. 부호별 절곡내역
\n
\n
\n \n \n | 도면부호 | \n 케이스(셔터박스) 길이별 수량 | \n 가이드레일 길이별 수량 | \n
\n \n {/* 케이스 컬럼 */}\n | 규격 | \n 1219 | \n 2438 | \n 3000 | \n 3500 | \n 4000 | \n 4150 | \n {/* 가이드레일 컬럼 */}\n 유형 | \n 2438 | \n 3000 | \n 3500 | \n 4000 | \n
\n \n \n {(order.items || []).map((item, idx) => {\n const drawingNo = item.productionSpec?.drawingNo || item.drawingNo || `FSS${idx + 1}`;\n const caseSpec = item.productionSpec?.caseSpec || item.caseSpec || '500-330';\n const grType = item.productionSpec?.guideRailType || item.guideRailType || '벽면형';\n const prodWidth = parseInt(item.prodWidth) || parseInt(item.openWidth) || 0;\n\n // 케이스 길이 결정 (prodWidth 기준)\n const caseLength = prodWidth <= 1219 ? 1219 :\n prodWidth <= 2438 ? 2438 :\n prodWidth <= 3000 ? 3000 :\n prodWidth <= 3500 ? 3500 :\n prodWidth <= 4000 ? 4000 : 4150;\n\n // 가이드레일 길이 결정 (prodHeight 기준)\n const prodHeight = parseInt(item.prodHeight) || parseInt(item.openHeight) || 0;\n const grLength = prodHeight <= 2438 ? 2438 :\n prodHeight <= 3000 ? 3000 :\n prodHeight <= 3500 ? 3500 : 4000;\n\n return (\n \n | {drawingNo} | \n {/* 케이스 데이터 */}\n {caseSpec} | \n {caseLength === 1219 ? 1 : '-'} | \n {caseLength === 2438 ? 1 : '-'} | \n {caseLength === 3000 ? 1 : '-'} | \n {caseLength === 3500 ? 1 : '-'} | \n {caseLength === 4000 ? 1 : '-'} | \n {caseLength === 4150 ? 1 : '-'} | \n {/* 가이드레일 데이터 */}\n {grType} | \n {grLength === 2438 ? 2 : '-'} | \n {grLength === 3000 ? 2 : '-'} | \n {grLength === 3500 ? 2 : '-'} | \n {grLength === 4000 ? 2 : '-'} | \n
\n );\n })}\n {/* 소계 행 */}\n \n | 소계 | \n - | \n {[1219, 2438, 3000, 3500, 4000, 4150].map(len => {\n const count = (order.items || []).filter(item => {\n const pw = parseInt(item.prodWidth) || parseInt(item.openWidth) || 0;\n const cl = pw <= 1219 ? 1219 : pw <= 2438 ? 2438 : pw <= 3000 ? 3000 : pw <= 3500 ? 3500 : pw <= 4000 ? 4000 : 4150;\n return cl === len;\n }).length;\n return {count || '-'} | ;\n })}\n - | \n {[2438, 3000, 3500, 4000].map(len => {\n const count = (order.items || []).filter(item => {\n const ph = parseInt(item.prodHeight) || parseInt(item.openHeight) || 0;\n const gl = ph <= 2438 ? 2438 : ph <= 3000 ? 3000 : ph <= 3500 ? 3500 : 4000;\n return gl === len;\n }).length * 2; // 가이드레일은 좌우 2개\n return {count || '-'} | ;\n })}\n
\n \n
\n
\n\n {/* 부자재 (상부덮개, 하단마감재, 연기차단재) 요약 */}\n
\n
\n
상부덮개 (1219-389)
\n
{(order.items || []).length}개
\n
\n
\n
하단마감재
\n
\n {(order.items || []).reduce((acc, item) => {\n const pw = parseInt(item.prodWidth) || parseInt(item.openWidth) || 0;\n return acc + (pw > 0 ? 1 : 0);\n }, 0)}개\n
\n
\n
\n
연기차단재
\n
\n {(order.items || []).reduce((acc, item) => {\n const pw = parseInt(item.prodWidth) || parseInt(item.openWidth) || 0;\n return acc + (pw > 0 ? 1 : 0);\n }, 0)}개\n
\n
\n
\n
\n\n {/* 3. 절곡품 도면 */}\n
\n
3. 절곡품 도면
\n
\n {/* 벽면형 도면 */}\n
\n
벽면형 (120-70)
\n
\n {/* 도면 SVG */}\n
\n
\n
\n 벽면 설치용 가이드레일 절곡 형태\n
\n
\n\n {/* 측면형 도면 */}\n
\n
측면형 (120-120)
\n
\n {/* 도면 SVG */}\n
\n
\n
\n 측면 설치용 가이드레일 절곡 형태\n
\n
\n
\n
\n\n {/* 4. 부자재 목록 */}\n
\n
4. 부자재
\n
\n
\n \n \n | NO | \n 품명 | \n 규격 | \n 단위 | \n 수량 | \n 비고 | \n
\n \n \n {[\n { name: '감기샤프트', spec: 'ø25', unit: 'EA', qty: (order.items || []).length, remark: '셔터 수량과 동일' },\n { name: '각파이프', spec: '30x30', unit: 'M', qty: Math.ceil((order.items || []).length * 2.5), remark: '케이스 프레임용' },\n { name: '앵글', spec: '30x30x3t', unit: 'M', qty: Math.ceil((order.items || []).length * 1.5), remark: '브라켓 제작용' },\n { name: '모터브라켓', spec: '-', unit: 'EA', qty: (order.items || []).length, remark: '셔터당 1개' },\n { name: '스톱퍼', spec: '-', unit: 'EA', qty: (order.items || []).length * 2, remark: '상하 각 1개' },\n { name: '리미트스위치', spec: '-', unit: 'EA', qty: (order.items || []).length * 2, remark: '상하 각 1개' },\n { name: '스프링', spec: '-', unit: 'EA', qty: (order.items || []).length, remark: '텐션 스프링' },\n { name: '볼트/너트세트', spec: 'M8', unit: 'SET', qty: (order.items || []).length * 4, remark: '고정용' },\n ].map((item, idx) => (\n \n | {String(idx + 1).padStart(2, '0')} | \n {item.name} | \n {item.spec} | \n {item.unit} | \n {item.qty} | \n {item.remark} | \n
\n ))}\n \n
\n
\n
\n\n {/* 수령확인 서명란 */}\n
\n
\n 상기 내역을 확인하고 납품물을 인수합니다.\n
\n
\n
\n\n {/* 인수담당자 정보 */}\n
\n
\n
인수담당자
\n
{shipment.receiverName || '-'}
\n
\n
\n
인수자연락처
\n
{shipment.receiverPhone || '-'}
\n
\n
\n
배송방법
\n
{shipment.deliveryMethod || '상차'} / {shipment.deliveryType || '선불'}
\n
\n
\n\n {/* 결재란 */}\n
\n
\n \n \n | 검재 | \n 작성 | \n 검토 | \n 승인 | \n
\n \n \n \n | 판매/전진 | \n | \n | \n | \n
\n \n
\n
\n
\n
\n
\n );\n};\n\n// ============================================================\n// 작업일지 문서 컴포넌트 (생산 현장 작업 기록)\n// ============================================================\nconst WorkLogDoc = ({\n workOrder,\n workResults = [],\n onClose,\n onPrint,\n onSave\n}) => {\n const [formData, setFormData] = useState({\n workDate: new Date().toISOString().split('T')[0],\n productionManager: '',\n items: workOrder?.items?.map(item => ({\n ...item,\n incomingLotNo: '',\n prodWidth: item.width || '',\n prodHeight: item.height || '',\n remainHeight: 810,\n qty1180: 0,\n qty900: 0,\n qty600: 0,\n qty400: 0,\n qty300: 0,\n })) || [],\n fabricLotNo: '',\n fabricUsage: { 1220: 0, 900: 0, 600: 0 },\n totalArea: 0,\n });\n\n if (!workOrder) return null;\n\n const lotNo = workOrder.lotNo || `KD-WE-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01-(${workOrder.splitIndex || 1})`;\n\n return (\n
\n
\n {/* 문서 헤더 */}\n
\n
작업일지
\n
\n
\n
\n
\n
\n
\n\n {/* 문서 내용 */}\n
\n {/* 상단 헤더 */}\n
\n
\n
\n
작 업 일 지
\n \n
\n
\n \n \n | 작성 | \n 검토 | \n 승인 | \n
\n \n \n \n | \n | \n | \n
\n \n | 판매/전진 | \n 생산 | \n 품질 | \n
\n \n
\n
\n
\n\n {/* 기본 정보 */}\n
\n
\n
신청업체
\n
\n \n \n | 발주일 | \n {workOrder.orderDate || '-'} | \n
\n \n | 업체명 | \n {workOrder.customerName || '-'} | \n
\n \n | 담당자 | \n {workOrder.manager || '-'} | \n
\n \n
\n
\n\n
\n
\n\n {/* 작업 내역 */}\n
\n
■ 작업 내역
\n
\n
\n \n \n | 일련번호 | \n 입고 LOT NO. | \n 제품명 | \n 부호 | \n 제작사이즈(mm) | \n 나머지높이 | \n 규격(매수) | \n
\n \n | \n | \n | \n | \n 가로 | \n 세로 | \n | \n 1180 | \n 900 | \n 600 | \n 400 | \n 300 | \n
\n \n \n {formData.items.map((item, idx) => (\n \n | {String(idx + 1).padStart(2, '0')} | \n \n {\n const newItems = [...formData.items];\n newItems[idx].incomingLotNo = e.target.value;\n setFormData(prev => ({ ...prev, items: newItems }));\n }}\n className=\"border rounded px-1 py-0.5 w-20 text-xs\"\n />\n | \n {item.productName || '와이어'} | \n {item.code || `4층 FSS${idx + 1}`} | \n {item.prodWidth} | \n {item.prodHeight || 2950} | \n {item.remainHeight} | \n {item.qty1180 || 2} | \n {item.qty900 || 1} | \n {item.qty600} | \n {item.qty400} | \n {item.qty300} | \n
\n ))}\n \n | 합 계 | \n | \n {formData.items.reduce((sum, i) => sum + (i.qty1180 || 2), 0)} | \n {formData.items.reduce((sum, i) => sum + (i.qty900 || 1), 0)} | \n {formData.items.reduce((sum, i) => sum + (i.qty600 || 0), 0)} | \n {formData.items.reduce((sum, i) => sum + (i.qty400 || 0), 0)} | \n {formData.items.reduce((sum, i) => sum + (i.qty300 || 0), 0)} | \n
\n \n
\n
\n
\n\n {/* 내화실 입고 LOT NO */}\n
\n
\n 내화실 입고 LOT.NO\n setFormData(prev => ({ ...prev, fabricLotNo: e.target.value }))}\n className=\"border rounded px-3 py-1.5 w-48\"\n placeholder=\"LOT 번호 입력\"\n />\n
\n\n {/* 사용량 */}\n
\n
\n\n
\n
사용량 (㎡)
\n
\n {(\n (formData.fabricUsage[1220] * 1.22) +\n (formData.fabricUsage[900] * 0.9) +\n (formData.fabricUsage[600] * 0.6)\n ).toFixed(2)} ㎡\n
\n
\n
\n
\n\n {/* 주의사항 */}\n
\n [비 고]사이즈 착오없이 부탁드립니다\n
\n
\n
\n
\n );\n};\n\n// ============================================================\n// 출고증 문서 컴포넌트 (출하 시 자재/부품 출고 내역)\n// ============================================================\nconst ShippingSlipDoc = ({\n shipment,\n order,\n materials = [],\n onClose,\n onPrint\n}) => {\n if (!shipment || !order) return null;\n\n const lotNo = shipment.lotNo || `KD-WE-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01-(${shipment.splitIndex || 1})`;\n\n // 부자재 카테고리별 데이터\n const subMaterials = {\n shaft2inch: { name: '감기샤프트 2인치', spec: 'L:300', qty: 11 },\n shaft4inch: [\n { spec: 'L:3,000', qty: 0 },\n { spec: 'L:4,500', qty: 3 },\n { spec: 'L:6,000', qty: 3 },\n ],\n shaft5inch: [\n { spec: 'L:6,000', qty: 0 },\n { spec: 'L:7,000', qty: 2 },\n { spec: 'L:8,200', qty: 3 },\n ],\n squarePipe: [\n { spec: 'L:3,000', qty: 0 },\n { spec: 'L:6,000', qty: 56 },\n ],\n roundBar: { spec: 'L:3,000', qty: 27 },\n bottomChannel: { spec: 'L:2,000', qty: 67 },\n angle: [\n { spec: 'L:380', qty: 0 },\n { spec: 'L:2,500', qty: 44 },\n ],\n };\n\n return (\n
\n
\n {/* 문서 헤더 */}\n
\n
출고증
\n
\n
\n
\n
\n
\n\n {/* 문서 내용 */}\n
\n {/* 상단 헤더 */}\n
\n
\n
KD 경동기업
\n
\n 전화: 031-938-5130 | 팩스: 02-6911-6315 | 이메일: kd5130@naver.com\n
\n
\n
\n
출 고 증
\n \n
\n
로트번호: {lotNo}
\n
\n \n \n | 작성 | \n 검토 | \n 승인 | \n
\n \n \n \n | \n | \n | \n
\n \n | 판매/전진 | \n 출하 | \n 생산관리 | \n
\n \n
\n
\n
\n\n {/* 기본 정보 */}\n
\n
\n
신청업체
\n
\n \n \n | 발주일 | \n {order.orderDate || '-'} | \n
\n \n | 발주처 | \n {order.customerName || '-'} | \n
\n \n | 발주 담당자 | \n {order.manager || '-'} | \n
\n \n | 담당자 연락처 | \n {order.contactPhone || '-'} | \n
\n \n
\n
\n\n
\n
신청내용 / 납품정보
\n
\n \n \n | 현장명 | \n {order.siteName || '-'} | \n
\n \n | 납기요청일 | \n {order.dueDate || '-'} | \n
\n \n | 출고일 | \n {shipment.shipDate || '-'} | \n
\n \n | 셔터총수량 | \n {order.totalQty || '-'} 개소 | \n
\n \n
\n
\n
\n\n {/* 배송지 주소 */}\n
\n 배송지 주소: \n {order.deliveryAddress || '-'}\n
\n\n {/* 1. 부자재 내역 */}\n
\n
1. 부자재 - 감기샤프트, 각파이프, 앵글
\n
\n {/* 감기샤프트 2인치 */}\n
\n
감기샤프트 2인치
\n
L : 300
\n
11
\n
\n\n {/* 감기샤프트 4인치 */}\n
\n
감기샤프트 4인치
\n
\n
L:3,000-
\n
L:4,5003
\n
L:6,0003
\n
\n
\n\n {/* 감기샤프트 5인치 */}\n
\n
감기샤프트 5인치
\n
\n
L:6,000-
\n
L:7,0002
\n
L:8,2003
\n
\n
\n\n {/* 각파이프 */}\n
\n
각파이프 (50*30*1.4T)
\n
\n
L:3,000-
\n
L:6,00056
\n
\n
\n\n {/* 앵글 */}\n
\n
\n\n
\n {/* 마철봉 */}\n
\n
\n
마철봉 (6mm)
\n
L : 3,000
\n
\n
27
\n
\n\n {/* 하단 무게평철 */}\n
\n
\n
하단 무게평철 [50*12T]
\n
L : 2,000
\n
\n
67
\n
\n
\n\n
※ 별도 추가사항 - 부자재
\n
\n\n {/* 2. 모터 */}\n
\n
2. 모터
\n
\n {/* 모터 220V 단상 */}\n
\n
2-1. 모터(220V 단상)
\n
\n \n \n | 모터 용량 | \n 수량 | \n 입고 LOT NO. | \n
\n \n \n \n | KD-150K | \n - | \n | \n
\n \n | KD-300K | \n - | \n | \n
\n \n
\n
\n\n {/* 모터 380V 삼상 */}\n
\n
2-2. 모터(380V 삼상)
\n
\n \n \n | 모터 용량 | \n 수량 | \n 입고 LOT NO. | \n
\n \n \n \n | KD-150K | \n 6 | \n | \n
\n \n | KD-300K | \n 5 | \n | \n
\n \n
\n
\n
\n\n {/* 브라켓트 & 연동제어기 */}\n
\n
\n
2-3. 브라켓트
\n
\n \n \n | 브라켓트 | \n 수량 | \n 입고 LOT NO. | \n
\n \n \n \n | 380*180 (2-4\") | \n 6 | \n | \n
\n \n | 380*180 (2-5\") | \n 5 | \n | \n
\n \n
\n
\n\n
\n
2-4. 연동제어기
\n
\n \n \n | 품명 | \n 수량 | \n 입고 LOT NO. | \n
\n \n \n \n | 매립형 | \n - | \n | \n
\n \n | 노출형 | \n - | \n | \n
\n \n | 뒷박스 | \n - | \n | \n
\n \n
\n
\n
\n\n
※ 별도 추가사항 - 모터
\n
\n
\n
\n
\n );\n};\n\n// ============================================================\n// 중간검사성적서 문서 컴포넌트 (공정 중 품질 검사 기록)\n// ============================================================\nconst ProcessInspectionDoc = ({\n inspection,\n workOrder,\n onClose,\n onPrint,\n onSave\n}) => {\n const [formData, setFormData] = useState({\n productName: inspection?.productName || '스크린',\n specification: inspection?.specification || '와이어 클라스 코팅직물',\n lotNo: inspection?.lotNo || workOrder?.lotNo || '',\n lotSize: inspection?.lotSize || 11,\n inspectionDate: inspection?.inspectionDate || new Date().toISOString().split('T')[0],\n inspector: inspection?.inspector || '',\n items: inspection?.items || Array(11).fill(null).map((_, idx) => ({\n no: idx + 1,\n processState: { good: true, bad: false },\n sewingState: { good: true, bad: false },\n assemblyState: { good: true, bad: false },\n length: { drawing: 0, measured: 0 },\n height: { drawing: 0, measured: 0 },\n gap: { standard: '400 이하', measured: 0 },\n result: 'OK',\n })),\n });\n\n const lotNo = formData.lotNo || `KD-WE-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01-(${workOrder?.splitIndex || 1})`;\n\n return (\n
\n
\n {/* 문서 헤더 */}\n
\n
스크린-중간 검사성적서
\n
\n
\n
\n
\n
\n
\n\n {/* 문서 내용 */}\n
\n {/* 상단 헤더 */}\n
\n
\n
KD 경동기업
\n \n
\n
스크린-중간 검사성적서
\n \n
\n
\n \n \n | 작성 | \n 검토 | \n 승인 | \n
\n \n \n \n | \n 전진 | \n | \n
\n \n | 판매/전진 | \n 생산 | \n 품질 | \n
\n \n
\n
\n
\n\n {/* 기본 정보 */}\n
\n
\n
품 명
\n
{formData.productName}
\n
\n
\n
규 격
\n
{formData.specification}
\n
\n
\n
\n
로트크기
\n
{formData.lotSize} 개소
\n
\n
\n\n
\n
\n
발주처
\n
{workOrder?.customerName || '-'}
\n
\n
\n
검사일자
\n
setFormData(prev => ({ ...prev, inspectionDate: e.target.value }))}\n className=\"border rounded px-2 py-1 w-full\"\n />\n
\n
\n
현장명
\n
{workOrder?.siteName || '-'}
\n
\n
\n\n {/* 검사기준서 */}\n
\n
중간검사 기준서
\n
\n
\n {/* 도해 */}\n
\n\n {/* 검사항목 */}\n
\n
\n \n \n | 검사항목 | \n 검사기준 | \n 검사방법 | \n 검사주기 | \n 관련규정 | \n
\n \n \n \n | 겉모양 - 가공상태 | \n 사용상 해로운 결함이 없을것 | \n 육안검사 | \n n=1, c=0 | \n KS F 4510 5.1항 | \n
\n \n | 겉모양 - 재봉상태 | \n 내화실에 의해 견고하게 접합되어야 함 | \n KS F 4510 9항 | \n
\n \n | 겉모양 - 조립상태 | \n 엔드락이 견고하게 조립되어야 함 | \n 체크검사 | \n KS F 4510 7항 표9 인용 | \n
\n \n | 치수 - 길이 ① | \n 도면치수 ± 4 | \n 자체규정 | \n
\n \n | 치수 - 높이 ② | \n 도면치수 + 제한없음 - 40 | \n
\n \n | 치수 - 간격 ③ | \n 400 이하 | \n GONO 게이지 | \n | \n 자체규정 | \n
\n \n
\n
\n
\n
\n
\n\n {/* 중간검사 DATA */}\n
\n
\n
\n
\n );\n};\n\n// ============================================================\n// 제품검사요청서 문서 컴포넌트 (인정기관 검사 요청)\n// ============================================================\nconst ProductInspectionRequestDoc = ({\n order,\n shipment,\n onClose,\n onPrint,\n onSave\n}) => {\n const [formData, setFormData] = useState({\n receiptDate: new Date().toISOString().split('T')[0],\n requesterName: order?.customerName || '',\n contactPerson: order?.contactPerson || '',\n contactPhone: order?.contactPhone || '',\n orderNo: order?.orderNo || '',\n siteName: order?.siteName || '',\n siteAddress: order?.siteAddress || '',\n deliveryDate: order?.dueDate || '',\n totalQty: order?.totalQty || 0,\n inspectionRequestDate: '',\n // 건축공사장 정보\n constructionSite: {\n name: '',\n address: '',\n lotNo: '',\n },\n // 자재유통업자 정보\n materialDistributor: {\n name: '',\n company: '',\n companyAddress: '',\n phone: '',\n },\n // 공사시공자 정보\n contractor: {\n name: '',\n company: '',\n companyAddress: '',\n phone: '',\n },\n // 공사감리자 정보\n supervisor: {\n name: '',\n office: '',\n officeAddress: '',\n phone: '',\n },\n // 검사대상 사전 고지 정보\n inspectionItems: order?.items?.map((item, idx) => ({\n floor: item.floor || `${idx + 1}층`,\n code: item.code || `FST-${String(idx + 1).padStart(2, '0')}`,\n orderWidth: item.width || 0,\n orderHeight: item.height || 0,\n actualWidth: item.width || 0,\n actualHeight: item.height || 0,\n changeReason: '',\n })) || [],\n });\n\n return (\n
\n
\n {/* 문서 헤더 */}\n
\n
자동방화셔터(인정제품) 제품검사요청서
\n
\n
\n
\n
\n
\n
\n\n {/* 문서 내용 */}\n
\n {/* 상단 헤더 */}\n
\n
\n
KD 경동기업
\n \n
\n
자동방화셔터(인정제품) 제품검사요청서
\n \n
\n
접수일
\n
setFormData(prev => ({ ...prev, receiptDate: e.target.value }))}\n className=\"border rounded px-2 py-1\"\n />\n
\n
\n\n {/* 기본정보 */}\n
\n\n {/* 검사방문요청일 */}\n
\n
검사방문요청일(전화예정)
\n
setFormData(prev => ({ ...prev, inspectionRequestDate: e.target.value }))}\n className=\"border rounded px-3 py-2\"\n />\n
\n\n {/* 입력사항 */}\n
\n
입력사항 (품질관리서 동일정보)
\n\n {/* 건축공사장 */}\n
\n\n {/* 자재유통업자 */}\n
\n\n {/* 공사시공자 */}\n
\n\n {/* 공사감리자 */}\n
\n
\n\n {/* 주의사항 */}\n
\n
※ 검사요청시 필독
\n
\n - √ 발주 size 와 시공완료된 size 가 다를 시, 일정 범위를 벗어난 경우 인정라벨을 부착할 수 없습니다.
\n - 제품검사를 위한 방문 전까지 변경사유를 고지하셔야 인정라벨을 부착할 수 있습니다.
\n - (사전고지를 하지 않음으로 발생하는 문제의 귀책은 신청업체에 있습니다)
\n
\n
\n\n {/* 검사대상 사전 고지 정보 */}\n
\n
\n
\n
\n );\n};\n\n// 품질기준 상세/등록/수정 패널\nconst InspectionStandardPanel = ({ mode, standard, onClose, onSave, onEdit }) => {\n const [formData, setFormData] = useState(standard || {\n code: '',\n name: '',\n inspectionType: '입고검사',\n targetType: '자재',\n targetCode: '',\n targetName: '',\n inspectionItems: [],\n samplingRule: '',\n samplingQty: '',\n frequency: '',\n responsibleTeam: '품질팀',\n status: '사용',\n });\n\n const [newItem, setNewItem] = useState({\n item: '',\n method: '',\n standard: '',\n unit: '-',\n min: null,\n max: null,\n critical: false,\n });\n\n const isReadOnly = mode === 'detail';\n\n const handleAddItem = () => {\n if (!newItem.item || !newItem.standard) {\n alert('검사항목과 기준을 입력해주세요.');\n return;\n }\n setFormData(prev => ({\n ...prev,\n inspectionItems: [...prev.inspectionItems, { ...newItem, id: Date.now() }],\n }));\n setNewItem({\n item: '',\n method: '',\n standard: '',\n unit: '-',\n min: null,\n max: null,\n critical: false,\n });\n };\n\n const handleRemoveItem = (id) => {\n setFormData(prev => ({\n ...prev,\n inspectionItems: prev.inspectionItems.filter(i => i.id !== id),\n }));\n };\n\n const handleSubmit = () => {\n if (!formData.code || !formData.name) {\n alert('기준코드와 기준명은 필수입니다.');\n return;\n }\n if (formData.inspectionItems.length === 0) {\n alert('검사항목을 1개 이상 등록해주세요.');\n return;\n }\n onSave?.(formData);\n };\n\n return (\n
\n
\n
\n
\n
\n
\n
\n
\n {mode === 'create' ? '품질기준 등록' : mode === 'edit' ? '품질기준 수정' : '품질기준 상세'}\n
\n {standard &&
{standard.code}
}\n
\n
\n
\n
\n
\n\n
\n {/* 기본 정보 */}\n
\n \n \n setFormData(prev => ({ ...prev, code: e.target.value }))}\n disabled={isReadOnly}\n placeholder=\"QS-INC-001\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg disabled:bg-gray-100\"\n />\n \n \n setFormData(prev => ({ ...prev, name: e.target.value }))}\n disabled={isReadOnly}\n placeholder=\"스크린 원단 입고검사\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg disabled:bg-gray-100\"\n />\n \n \n \n \n \n \n \n
\n \n\n {/* 검사 대상 */}\n
\n \n \n \n \n \n setFormData(prev => ({ ...prev, targetCode: e.target.value }))}\n disabled={isReadOnly}\n placeholder=\"SCR-MAT-001\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg disabled:bg-gray-100\"\n />\n \n \n setFormData(prev => ({ ...prev, targetName: e.target.value }))}\n disabled={isReadOnly}\n placeholder=\"스크린 원단\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg disabled:bg-gray-100\"\n />\n \n
\n \n\n {/* 검사 항목 */}\n
\n \n {/* 기존 항목 목록 */}\n {formData.inspectionItems.length > 0 ? (\n
\n
\n \n \n | 검사항목 | \n 검사방법 | \n 기준 | \n 단위 | \n 치명 | \n {!isReadOnly && 삭제 | }\n
\n \n \n {formData.inspectionItems.map((item, idx) => (\n \n | {item.item} | \n {item.method} | \n \n \n {item.standard}\n {(item.min !== null || item.max !== null) && (\n \n {item.min !== null && `Min: ${item.min}`}\n {item.min !== null && item.max !== null && ' / '}\n {item.max !== null && `Max: ${item.max}`}\n \n )}\n \n | \n {item.unit} | \n \n {item.critical ? (\n \n 치명\n \n ) : (\n -\n )}\n | \n {!isReadOnly && (\n \n \n | \n )}\n
\n ))}\n \n
\n
\n ) : (\n
\n )}\n\n {/* 새 항목 추가 */}\n {!isReadOnly && (\n
\n
검사항목 추가
\n
\n
\n
\n
\n
\n
\n )}\n
\n \n\n {/* 샘플링 규칙 */}\n
\n \n \n setFormData(prev => ({ ...prev, samplingRule: e.target.value }))}\n disabled={isReadOnly}\n placeholder=\"AQL 2.5, 일반검사 Level II\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg disabled:bg-gray-100\"\n />\n \n \n setFormData(prev => ({ ...prev, samplingQty: e.target.value }))}\n disabled={isReadOnly}\n placeholder=\"로트당 5개\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg disabled:bg-gray-100\"\n />\n \n \n setFormData(prev => ({ ...prev, frequency: e.target.value }))}\n disabled={isReadOnly}\n placeholder=\"입고시 전수 / 포장 전\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg disabled:bg-gray-100\"\n />\n \n \n setFormData(prev => ({ ...prev, responsibleTeam: e.target.value }))}\n disabled={isReadOnly}\n placeholder=\"품질팀\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg disabled:bg-gray-100\"\n />\n \n
\n \n\n {/* 이력 정보 (상세 모드) */}\n {mode === 'detail' && standard && (\n
\n \n \n \n
\n \n )}\n
\n\n {/* 하단 버튼 */}\n
\n \n {mode === 'detail' && (\n \n )}\n {(mode === 'create' || mode === 'edit') && (\n \n )}\n
\n
\n
\n );\n};\n\n// =============================================\n// 회계관리 컴포넌트\n// =============================================\n\n// 매출관리\nconst SalesAccountManagement = ({ shipments = [], onNavigate }) => {\n const [activeTab, setActiveTab] = useState('list');\n const [search, setSearch] = useState('');\n const [dateRange, setDateRange] = useState({ start: '2025-12-01', end: '2025-12-09' });\n const [showPanel, setShowPanel] = useState(null);\n const [selectedSale, setSelectedSale] = useState(null);\n const [statusFilter, setStatusFilter] = useState('all');\n\n // 샘플 매출 데이터 (신용등급 연동 확장)\n const [salesData, setSalesData] = useState([\n {\n id: 1, saleNo: 'SL-251201-001', orderNo: 'KD-TS-251201-01', customerCode: 'C001', customerName: '삼성전자',\n creditGrade: 'A', productName: '방화스크린 KSS01', qty: 5, unitPrice: 2500000, amount: 12500000, vat: 1250000, totalAmount: 13750000,\n saleDate: '2025-12-01', dueDate: '2025-12-31', status: '미수금', invoiceNo: '', invoiceStatus: '미발행',\n // 경리승인 프로세스\n accountingApprovalRequired: false, // A등급: 불필요\n accountingApprovalStatus: '-', // 대기중/승인/반려/-\n accountingApprovalDate: '',\n accountingApprover: '',\n // 입금확인 프로세스\n paymentConfirmRequired: false, // A등급: 불필요\n paymentConfirmStatus: '-',\n paymentConfirmDate: '',\n paymentConfirmAmount: 0,\n // 출고상태\n shipmentStatus: '출고완료',\n shipmentDate: '2025-12-01',\n },\n {\n id: 2, saleNo: 'SL-251203-001', orderNo: 'KD-SL-251203-01', customerCode: 'C002', customerName: 'LG전자',\n creditGrade: 'A', productName: '방화슬랫셔터 KSL01', qty: 3, unitPrice: 1800000, amount: 5400000, vat: 540000, totalAmount: 5940000,\n saleDate: '2025-12-03', dueDate: '2025-12-20', status: '수금완료', invoiceNo: 'INV-251203-001', invoiceStatus: '발행완료',\n accountingApprovalRequired: false,\n accountingApprovalStatus: '-',\n accountingApprovalDate: '',\n accountingApprover: '',\n paymentConfirmRequired: false,\n paymentConfirmStatus: '확인완료',\n paymentConfirmDate: '2025-12-18',\n paymentConfirmAmount: 5940000,\n shipmentStatus: '출고완료',\n shipmentDate: '2025-12-03',\n },\n {\n id: 3, saleNo: 'SL-251205-001', orderNo: 'KD-BD-251205-01', customerCode: 'C003', customerName: '현대건설',\n creditGrade: 'A', productName: '방화절곡셔터 KSB01', qty: 10, unitPrice: 3200000, amount: 32000000, vat: 3200000, totalAmount: 35200000,\n saleDate: '2025-12-05', dueDate: '2026-01-05', status: '미수금', invoiceNo: 'INV-251205-001', invoiceStatus: '발행완료',\n accountingApprovalRequired: false,\n accountingApprovalStatus: '-',\n accountingApprovalDate: '',\n accountingApprover: '',\n paymentConfirmRequired: false,\n paymentConfirmStatus: '-',\n paymentConfirmDate: '',\n paymentConfirmAmount: 0,\n shipmentStatus: '출고완료',\n shipmentDate: '2025-12-05',\n },\n {\n id: 4, saleNo: 'SL-251208-001', orderNo: 'KD-TS-251208-01', customerCode: 'C004', customerName: '대우건설',\n creditGrade: 'B', productName: '방화스크린 KSS02', qty: 8, unitPrice: 2800000, amount: 22400000, vat: 2240000, totalAmount: 24640000,\n saleDate: '2025-12-08', dueDate: '2026-01-08', status: '미수금', invoiceNo: '', invoiceStatus: '미발행',\n accountingApprovalRequired: false, // B등급: 경리승인 불필요\n accountingApprovalStatus: '-',\n accountingApprovalDate: '',\n accountingApprover: '',\n paymentConfirmRequired: true, // B등급: 입금확인 후 출고\n paymentConfirmStatus: '입금대기',\n paymentConfirmDate: '',\n paymentConfirmAmount: 0,\n shipmentStatus: '입금대기',\n shipmentDate: '',\n },\n {\n id: 5, saleNo: 'SL-251210-001', orderNo: 'KD-BD-251210-01', customerCode: 'C005', customerName: '신흥건설',\n creditGrade: 'C', productName: '방화절곡셔터 KSB02', qty: 5, unitPrice: 3500000, amount: 17500000, vat: 1750000, totalAmount: 19250000,\n saleDate: '2025-12-10', dueDate: '2026-01-10', status: '미수금', invoiceNo: '', invoiceStatus: '미발행',\n accountingApprovalRequired: true, // C등급: 경리승인 필요\n accountingApprovalStatus: '승인대기',\n accountingApprovalDate: '',\n accountingApprover: '',\n paymentConfirmRequired: true, // C등급: 입금확인 후 출고\n paymentConfirmStatus: '입금대기',\n paymentConfirmDate: '',\n paymentConfirmAmount: 0,\n shipmentStatus: '경리승인대기',\n shipmentDate: '',\n },\n ]);\n\n const tabs = [\n { id: 'list', label: '매출목록' },\n { id: 'approval', label: '경리승인' },\n { id: 'payment', label: '입금확인' },\n { id: 'summary', label: '매출현황' },\n { id: 'customer', label: '거래처별' },\n ];\n\n // 통계\n const totalSales = salesData.reduce((sum, s) => sum + s.totalAmount, 0);\n const collectedAmount = salesData.filter(s => s.status === '수금완료').reduce((sum, s) => sum + s.totalAmount, 0);\n const outstandingAmount = salesData.filter(s => s.status === '미수금').reduce((sum, s) => sum + s.totalAmount, 0);\n const invoicedCount = salesData.filter(s => s.invoiceStatus === '발행완료').length;\n\n const filtered = salesData.filter(s =>\n s.saleNo.toLowerCase().includes(search.toLowerCase()) ||\n s.customerName.includes(search) ||\n s.orderNo.toLowerCase().includes(search.toLowerCase())\n );\n\n // 공통 UX: 매출목록 선택\n const {\n selectedIds: salesSelectedIds,\n handleSelect: handleSalesSelect,\n handleSelectAll: handleSalesSelectAll,\n isAllSelected: isSalesAllSelected,\n hasSelection: hasSalesSelection,\n isMultiSelect: isSalesMultiSelect,\n isSelected: isSalesSelected,\n } = useListSelection(filtered);\n\n // 공통 UX: 경리승인 목록 선택\n const approvalFiltered = salesData.filter(s => s.accountingApprovalRequired);\n const {\n selectedIds: approvalSelectedIds,\n handleSelect: handleApprovalSelect,\n handleSelectAll: handleApprovalSelectAll,\n isAllSelected: isApprovalAllSelected,\n hasSelection: hasApprovalSelection,\n isMultiSelect: isApprovalMultiSelect,\n isSelected: isApprovalSelected,\n } = useListSelection(approvalFiltered);\n\n // 공통 UX: 입금확인 목록 선택\n const paymentFiltered = salesData.filter(s => s.paymentConfirmRequired);\n const {\n selectedIds: paymentSelectedIds,\n handleSelect: handlePaymentSelect,\n handleSelectAll: handlePaymentSelectAll,\n isAllSelected: isPaymentAllSelected,\n hasSelection: hasPaymentSelection,\n isMultiSelect: isPaymentMultiSelect,\n isSelected: isPaymentSelected,\n } = useListSelection(paymentFiltered);\n\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n const handleCreateSale = () => {\n setSelectedSale(null);\n setShowPanel(true);\n };\n\n const handleViewSale = (sale) => {\n setSelectedSale(sale);\n setShowPanel(true);\n };\n\n const handleIssueInvoice = (sale) => {\n if (sale.invoiceStatus === '발행완료') {\n alert('이미 세금계산서가 발행되었습니다.');\n return;\n }\n // C등급 거래처는 경리 승인 필요\n if (sale.creditGrade === 'C' && sale.accountingApprovalStatus !== '승인') {\n alert('C등급 거래처는 경리 승인 후 세금계산서 발행이 가능합니다.');\n return;\n }\n const invoiceNo = `INV-${sale.saleNo.split('-').slice(1).join('-')}`;\n setSalesData(prev => prev.map(s =>\n s.id === sale.id ? { ...s, invoiceNo, invoiceStatus: '발행완료' } : s\n ));\n alert(`세금계산서가 발행되었습니다.\\n계산서번호: ${invoiceNo}`);\n };\n\n // 경리 승인 처리\n const handleAccountingApproval = (sale, action) => {\n const today = new Date().toISOString().split('T')[0];\n if (action === 'approve') {\n setSalesData(prev => prev.map(s =>\n s.id === sale.id ? {\n ...s,\n accountingApprovalStatus: '승인',\n accountingApprovalDate: today,\n accountingApprover: '경리팀 김경리',\n shipmentStatus: s.paymentConfirmRequired && s.paymentConfirmStatus !== '확인완료' ? '입금대기' : '출고가능'\n } : s\n ));\n alert(`경리 승인이 완료되었습니다.\\n매출번호: ${sale.saleNo}\\n거래처: ${sale.customerName}`);\n } else {\n const reason = prompt('반려 사유를 입력하세요:');\n if (reason) {\n setSalesData(prev => prev.map(s =>\n s.id === sale.id ? {\n ...s,\n accountingApprovalStatus: '반려',\n accountingApprovalDate: today,\n accountingApprover: '경리팀 김경리'\n } : s\n ));\n alert(`경리 승인이 반려되었습니다.\\n사유: ${reason}`);\n }\n }\n };\n\n // 입금 확인 처리\n const handlePaymentConfirm = (sale) => {\n setSelectedSale(sale);\n setShowPanel('paymentConfirm');\n };\n\n const processPaymentConfirm = (sale, amount) => {\n const today = new Date().toISOString().split('T')[0];\n const isFullPayment = amount >= sale.totalAmount;\n setSalesData(prev => prev.map(s =>\n s.id === sale.id ? {\n ...s,\n paymentConfirmStatus: isFullPayment ? '확인완료' : '부분입금',\n paymentConfirmDate: today,\n paymentConfirmAmount: amount,\n status: isFullPayment ? '수금완료' : '부분수금',\n shipmentStatus: isFullPayment ? '출고가능' : s.shipmentStatus\n } : s\n ));\n setShowPanel(null);\n alert(`입금이 확인되었습니다.\\n입금액: ${amount.toLocaleString()}원\\n상태: ${isFullPayment ? '전액입금' : '부분입금'}`);\n };\n\n // 통계\n const pendingApprovalCount = salesData.filter(s => s.accountingApprovalStatus === '승인대기').length;\n const pendingPaymentCount = salesData.filter(s => s.paymentConfirmStatus === '입금대기').length;\n\n return (\n
\n
\n\n {/* 통계 카드 */}\n
\n \n \n \n \n \n \n
\n\n {/* 탭 및 검색 */}\n
\n \n
\n {tabs.map(tab => (\n \n ))}\n
\n
\n
\n setDateRange(prev => ({ ...prev, start: e.target.value }))}\n className=\"px-3 py-2 border rounded-lg text-sm\" />\n ~\n setDateRange(prev => ({ ...prev, end: e.target.value }))}\n className=\"px-3 py-2 border rounded-lg text-sm\" />\n
\n
\n {isSalesMultiSelect && (\n
\n )}\n
\n
\n
\n\n {activeTab === 'list' && (\n \n )}\n\n {activeTab === 'approval' && (\n \n
\n
C등급(경리승인 거래처) 거래처의 매출 건은 경리 승인 후 생산지시 및 출고가 가능합니다.
\n
\n {salesData.filter(s => s.accountingApprovalRequired).length === 0 ? (\n
\n 경리 승인이 필요한 매출 건이 없습니다.\n
\n ) : (\n
\n )}\n
\n )}\n\n {activeTab === 'payment' && (\n \n
\n
B등급(관리대상), C등급(경리승인) 거래처는 입금 확인 후 출고가 가능합니다.
\n
\n {salesData.filter(s => s.paymentConfirmRequired).length === 0 ? (\n
\n 입금 확인이 필요한 매출 건이 없습니다.\n
\n ) : (\n
\n )}\n
\n )}\n\n {activeTab === 'summary' && (\n \n
\n
\n \n {['10월', '11월', '12월'].map((month, i) => (\n
\n
\n
{month}\n
{[3200, 4800, 5489][i]}만\n
\n ))}\n
\n \n
\n \n {[\n { name: '현대건설', amount: 35200000, percent: 64 },\n { name: '삼성전자', amount: 13750000, percent: 25 },\n { name: 'LG전자', amount: 5940000, percent: 11 },\n ].map(item => (\n
\n
{item.name}\n
\n
{(item.amount / 10000).toLocaleString()}만\n
\n ))}\n
\n \n
\n
\n )}\n\n {activeTab === 'customer' && (\n \n \n \n | 거래처 | \n 매출건수 | \n 총매출액 | \n 수금완료 | \n 미수금 | \n 수금률 | \n
\n \n \n {[\n { name: '현대건설', count: 1, total: 35200000, collected: 0, outstanding: 35200000 },\n { name: '삼성전자', count: 1, total: 13750000, collected: 0, outstanding: 13750000 },\n { name: 'LG전자', count: 1, total: 5940000, collected: 5940000, outstanding: 0 },\n ].map(row => (\n \n | {row.name} | \n {row.count}건 | \n {row.total.toLocaleString()} | \n {row.collected.toLocaleString()} | \n {row.outstanding.toLocaleString()} | \n \n = 1 ? 'bg-green-100 text-green-700' :\n row.collected / row.total >= 0.5 ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700'\n }`}>\n {Math.round(row.collected / row.total * 100)}%\n \n | \n
\n ))}\n \n
\n )}\n \n\n {/* 입금확인 모달 */}\n {showPanel === 'paymentConfirm' && selectedSale && (\n
\n
\n
\n
입금 확인
\n \n \n
\n
\n
매출번호{selectedSale.saleNo}
\n
거래처{selectedSale.customerName}
\n
청구금액{selectedSale.totalAmount.toLocaleString()}원
\n
기입금액{selectedSale.paymentConfirmAmount.toLocaleString()}원
\n
잔액{(selectedSale.totalAmount - selectedSale.paymentConfirmAmount).toLocaleString()}원
\n
\n
\n \n \n
\n \n \n
\n \n \n
\n \n \n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 세금계산서 관리\nconst InvoiceManagement = () => {\n const [activeTab, setActiveTab] = useState('issued');\n const [search, setSearch] = useState('');\n const [showPanel, setShowPanel] = useState(false);\n const [selectedInvoice, setSelectedInvoice] = useState(null);\n\n const [invoices, setInvoices] = useState([\n {\n id: 1, invoiceNo: 'INV-251203-001', saleNo: 'SL-251203-001', customerCode: 'C002', customerName: 'LG전자',\n businessNo: '123-45-67890', amount: 5400000, vat: 540000, totalAmount: 5940000,\n issueDate: '2025-12-03', reportDate: '2025-12-05', status: '전송완료', type: '발행'\n },\n {\n id: 2, invoiceNo: 'INV-251205-001', saleNo: 'SL-251205-001', customerCode: 'C003', customerName: '현대건설',\n businessNo: '234-56-78901', amount: 32000000, vat: 3200000, totalAmount: 35200000,\n issueDate: '2025-12-05', reportDate: '', status: '발행완료', type: '발행'\n },\n {\n id: 3, invoiceNo: 'INV-R-251201-001', customerCode: 'S001', customerName: '금강철강',\n businessNo: '345-67-89012', amount: 8500000, vat: 850000, totalAmount: 9350000,\n issueDate: '2025-12-01', reportDate: '2025-12-02', status: '전송완료', type: '수취'\n },\n ]);\n\n const tabs = [\n { id: 'issued', label: '매출계산서', count: invoices.filter(i => i.type === '발행').length },\n { id: 'received', label: '매입계산서', count: invoices.filter(i => i.type === '수취').length },\n { id: 'pending', label: '미전송', count: invoices.filter(i => i.status === '발행완료').length },\n ];\n\n const filtered = invoices.filter(i => {\n const matchSearch = i.invoiceNo.toLowerCase().includes(search.toLowerCase()) ||\n i.customerName.includes(search);\n if (activeTab === 'issued') return matchSearch && i.type === '발행';\n if (activeTab === 'received') return matchSearch && i.type === '수취';\n if (activeTab === 'pending') return matchSearch && i.status === '발행완료';\n return matchSearch;\n });\n\n // 통계\n const issuedTotal = invoices.filter(i => i.type === '발행').reduce((sum, i) => sum + i.totalAmount, 0);\n const receivedTotal = invoices.filter(i => i.type === '수취').reduce((sum, i) => sum + i.totalAmount, 0);\n const pendingCount = invoices.filter(i => i.status === '발행완료').length;\n const transmittedCount = invoices.filter(i => i.status === '전송완료').length;\n\n const handleSendInvoice = (invoice) => {\n if (invoice.status === '전송완료') {\n alert('이미 국세청에 전송된 계산서입니다.');\n return;\n }\n setInvoices(prev => prev.map(i =>\n i.id === invoice.id ? { ...i, status: '전송완료', reportDate: new Date().toISOString().split('T')[0] } : i\n ));\n alert(`세금계산서가 국세청에 전송되었습니다.\\n계산서번호: ${invoice.invoiceNo}`);\n };\n\n return (\n
\n
\n\n {/* 통계 카드 */}\n
\n \n \n \n \n
\n\n {/* 탭 및 검색 */}\n
\n \n
\n {tabs.map(tab => (\n \n ))}\n
\n
\n \n \n
\n
\n\n \n \n
\n );\n};\n\n// 수금관리\nconst CollectionManagement = () => {\n const [search, setSearch] = useState('');\n const [filterStatus, setFilterStatus] = useState('all');\n const [showPanel, setShowPanel] = useState(false);\n const [selectedItem, setSelectedItem] = useState(null);\n\n const [collections, setCollections] = useState([\n {\n id: 1, collectionNo: 'COL-251209-001', saleNo: 'SL-251203-001', customerCode: 'C002', customerName: 'LG전자',\n saleAmount: 5940000, collectedAmount: 5940000, remainAmount: 0, collectionDate: '2025-12-09',\n paymentMethod: '계좌이체', bankAccount: '국민 123-456-789012', memo: '정상 수금', status: '완료'\n },\n {\n id: 2, collectionNo: 'COL-251208-001', saleNo: 'SL-251201-001', customerCode: 'C001', customerName: '삼성전자',\n saleAmount: 13750000, collectedAmount: 7000000, remainAmount: 6750000, collectionDate: '2025-12-08',\n paymentMethod: '계좌이체', bankAccount: '신한 111-222-333444', memo: '1차 부분수금', status: '진행중'\n },\n ]);\n\n const statusFilters = [\n { id: 'all', label: '전체' },\n { id: '완료', label: '완료' },\n { id: '진행중', label: '진행중' },\n ];\n\n // 통계\n const totalSaleAmount = collections.reduce((sum, c) => sum + c.saleAmount, 0);\n const totalCollected = collections.reduce((sum, c) => sum + c.collectedAmount, 0);\n const totalRemain = collections.reduce((sum, c) => sum + c.remainAmount, 0);\n const completedCount = collections.filter(c => c.status === '완료').length;\n\n const filtered = collections.filter(c => {\n const matchSearch = c.collectionNo.toLowerCase().includes(search.toLowerCase()) ||\n c.customerName.includes(search) || c.saleNo.toLowerCase().includes(search.toLowerCase());\n const matchStatus = filterStatus === 'all' || c.status === filterStatus;\n return matchSearch && matchStatus;\n });\n\n const handleRegisterCollection = () => {\n setSelectedItem(null);\n setShowPanel(true);\n };\n\n return (\n
\n
\n\n {/* 통계 카드 */}\n
\n \n \n \n \n
\n\n {/* 필터 및 검색 */}\n
\n \n
\n {statusFilters.map(filter => (\n \n ))}\n
\n
\n
\n\n \n \n \n | 수금번호 | \n 매출번호 | \n 거래처 | \n 매출금액 | \n 수금금액 | \n 잔액 | \n 수금일 | \n 결제방법 | \n 상태 | \n 관리 | \n
\n \n \n {filtered.map(col => (\n \n | {col.collectionNo} | \n {col.saleNo} | \n {col.customerName} | \n {col.saleAmount.toLocaleString()} | \n {col.collectedAmount.toLocaleString()} | \n {col.remainAmount.toLocaleString()} | \n {col.collectionDate} | \n {col.paymentMethod} | \n \n {col.status}\n | \n \n \n | \n
\n ))}\n \n
\n \n
\n );\n};\n\n// 미수금관리\nconst OutstandingManagement = () => {\n const [search, setSearch] = useState('');\n const [filterOverdue, setFilterOverdue] = useState('all');\n\n const outstandingData = [\n {\n id: 1, customerCode: 'C001', customerName: '삼성전자', creditGrade: 'A',\n totalAmount: 13750000, collectedAmount: 7000000, outstandingAmount: 6750000,\n dueDate: '2025-12-31', overdueDays: 0, lastCollectionDate: '2025-12-08', contactPerson: '김부장', phone: '010-1234-5678'\n },\n {\n id: 2, customerCode: 'C003', customerName: '현대건설', creditGrade: 'A',\n totalAmount: 35200000, collectedAmount: 0, outstandingAmount: 35200000,\n dueDate: '2026-01-05', overdueDays: 0, lastCollectionDate: '-', contactPerson: '이과장', phone: '010-2345-6789'\n },\n {\n id: 3, customerCode: 'C004', customerName: '대우건설', creditGrade: 'B',\n totalAmount: 8800000, collectedAmount: 0, outstandingAmount: 8800000,\n dueDate: '2025-12-01', overdueDays: 8, lastCollectionDate: '-', contactPerson: '박대리', phone: '010-3456-7890'\n },\n ];\n\n // 통계\n const totalOutstanding = outstandingData.reduce((sum, d) => sum + d.outstandingAmount, 0);\n const overdueAmount = outstandingData.filter(d => d.overdueDays > 0).reduce((sum, d) => sum + d.outstandingAmount, 0);\n const overdueCount = outstandingData.filter(d => d.overdueDays > 0).length;\n const customerCount = outstandingData.length;\n\n const filtered = outstandingData.filter(d => {\n const matchSearch = d.customerCode.toLowerCase().includes(search.toLowerCase()) ||\n d.customerName.includes(search);\n const matchOverdue = filterOverdue === 'all' ||\n (filterOverdue === 'overdue' && d.overdueDays > 0) ||\n (filterOverdue === 'normal' && d.overdueDays === 0);\n return matchSearch && matchOverdue;\n });\n\n // 공통 UX: 미수금 목록 선택\n const {\n selectedIds: outstandingSelectedIds,\n handleSelect: handleOutstandingSelect,\n handleSelectAll: handleOutstandingSelectAll,\n isAllSelected: isOutstandingAllSelected,\n hasSelection: hasOutstandingSelection,\n isMultiSelect: isOutstandingMultiSelect,\n isSelected: isOutstandingSelected,\n } = useListSelection(filtered);\n\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n return (\n
\n
\n\n {/* 통계 카드 */}\n
\n \n \n \n \n
\n\n {/* 필터 및 검색 */}\n
\n \n
\n {[\n { id: 'all', label: '전체' },\n { id: 'overdue', label: '연체' },\n { id: 'normal', label: '정상' },\n ].map(filter => (\n \n ))}\n
\n
\n \n {isOutstandingMultiSelect && (\n \n )}\n \n
\n
\n\n \n\n {/* 연체 경고 */}\n {overdueCount > 0 && (\n \n
\n
\n
연체 경고: {overdueCount}건의 미수금이 결제예정일을 초과했습니다.\n
\n
총 연체금액: {overdueAmount.toLocaleString()}원
\n
\n )}\n \n
\n );\n};\n\n// =============================================\n// 회계관리 추가 컴포넌트 (기능정의서 A0~A5 기준)\n// =============================================\n\n// 회계관리 거래처 샘플 데이터\nconst accCustomerData = [\n { id: 1, customerCode: 'CUS-001', customerName: '삼성물산(주)', customerType: '매출', businessNo: '1248100998', ceoName: '오세철', phone: '02-2145-2114', creditGrade: 'A', creditLimit: 500000000, usedCredit: 320000000, paymentTerm: '60일', receivableTotal: 85000000, receivableOverdue: 0, overdueDays: 0, isActive: true, requireApproval: false, isCreditWarning: false },\n { id: 2, customerCode: 'CUS-002', customerName: '현대건설(주)', customerType: '매출', businessNo: '1018116756', ceoName: '윤영준', phone: '02-746-1114', creditGrade: 'A', creditLimit: 800000000, usedCredit: 450000000, paymentTerm: '60일', receivableTotal: 120000000, receivableOverdue: 0, overdueDays: 0, isActive: true, requireApproval: false, isCreditWarning: false },\n { id: 3, customerCode: 'CUS-003', customerName: '대우건설(주)', customerType: '매출', businessNo: '1168108831', ceoName: '백정완', phone: '02-2109-3114', creditGrade: 'B', creditLimit: 300000000, usedCredit: 280000000, paymentTerm: '30일', receivableTotal: 95000000, receivableOverdue: 15000000, overdueDays: 35, isActive: true, requireApproval: true, isCreditWarning: true },\n { id: 4, customerCode: 'CUS-004', customerName: '(주)서울인테리어', customerType: '매출', businessNo: '2148556789', ceoName: '정서울', phone: '02-555-1234', creditGrade: 'C', creditLimit: 50000000, usedCredit: 48000000, paymentTerm: '현금', receivableTotal: 32000000, receivableOverdue: 25000000, overdueDays: 62, isActive: true, requireApproval: true, isCreditWarning: true },\n { id: 5, customerCode: 'SUP-001', customerName: '(주)한국스크린', customerType: '매입', businessNo: '3148109876', ceoName: '이원단', phone: '031-481-5500', creditGrade: 'A', creditLimit: 0, usedCredit: 0, paymentTerm: '30일', receivableTotal: 0, receivableOverdue: 0, overdueDays: 0, isActive: true, requireApproval: false, isCreditWarning: false },\n];\n\n// 회계관리 거래처 목록 (판매관리 스타일)\nconst AccCustomerList = ({ onNavigate }) => {\n const [searchTerm, setSearchTerm] = useState('');\n const [activeTab, setActiveTab] = useState('all');\n const { selectedIds, handleSelect, handleSelectAll, isAllSelected, hasSelection, isMultiSelect, isSelected } = useListSelection(accCustomerData);\n const tabs = [{ id: 'all', label: '전체', count: accCustomerData.length }, { id: 'sales', label: '매출거래처', count: accCustomerData.filter(c => c.customerType === '매출').length }, { id: 'purchase', label: '매입거래처', count: accCustomerData.filter(c => c.customerType === '매입').length }, { id: 'warning', label: '신용위험', count: accCustomerData.filter(c => c.isCreditWarning).length }];\n const filteredData = accCustomerData.filter(item => { if (activeTab === 'sales') return item.customerType === '매출'; if (activeTab === 'purchase') return item.customerType === '매입'; if (activeTab === 'warning') return item.isCreditWarning; return true; }).filter(item => item.customerCode.toLowerCase().includes(searchTerm.toLowerCase()) || item.customerName.toLowerCase().includes(searchTerm.toLowerCase())).sort((a, b) => b.id - a.id);\n const getGradeBadge = (grade) => { const colors = { A: 'bg-green-100 text-green-800', B: 'bg-yellow-100 text-yellow-800', C: 'bg-red-100 text-red-800' }; const labels = { A: '우량', B: '관리', C: '위험' }; return
{grade} ({labels[grade]}); };\n const formatCurrency = (val) => val ? `${val.toLocaleString()}원` : '-';\n const totalReceivable = accCustomerData.filter(c => c.customerType === '매출').reduce((sum, c) => sum + c.receivableTotal, 0);\n const overdueReceivable = accCustomerData.reduce((sum, c) => sum + c.receivableOverdue, 0);\n const warningCount = accCustomerData.filter(c => c.isCreditWarning).length;\n const creditCustomers = accCustomerData.filter(c => c.creditLimit > 0);\n const avgCreditUsage = creditCustomers.length > 0 ? Math.round(creditCustomers.reduce((sum, c) => sum + (c.usedCredit / c.creditLimit * 100), 0) / creditCustomers.length) : 0;\n return (
거래처 목록
setSearchTerm(e.target.value)} />
{isMultiSelect &&
}
);\n};\n\n// 회계관리 거래처 상세\nconst AccCustomerDetail = ({ customer, onNavigate }) => {\n if (!customer) return
거래처를 선택해주세요.
;\n const formatCurrency = (val) => val ? `${val.toLocaleString()}원` : '-';\n const getGradeLabel = (grade) => { const labels = { A: 'A (우량)', B: 'B (관리)', C: 'C (위험)' }; return labels[grade] || grade; };\n const availableCredit = customer.creditLimit - customer.usedCredit;\n const creditUsageRate = customer.creditLimit > 0 ? Math.round((customer.usedCredit / customer.creditLimit) * 100) : 0;\n\n return (\n
\n {/* 헤더 - 타이틀/버튼 분리 */}\n
\n
\n \n 거래처 상세\n
\n
\n
\n
\n
\n
\n
\n \n \n
\n
\n
\n\n {/* 기본 정보 */}\n
\n \n
거래처코드
{customer.customerCode}
\n
거래처명
{customer.customerName}
\n
거래처구분
{customer.customerType}
\n
사업자번호
{customer.businessNo}
\n
\n
\n
\n \n\n {/* 신용/여신 정보 */}\n
\n \n
신용등급
{getGradeLabel(customer.creditGrade)}
\n
결제조건
{customer.paymentTerm}
\n
여신한도
{formatCurrency(customer.creditLimit)}
\n
사용금액
{formatCurrency(customer.usedCredit)}
\n
가용여신
{formatCurrency(availableCredit)}
\n
\n
\n \n\n {/* 미수금 현황 */}\n
\n \n
총 미수금
{formatCurrency(customer.receivableTotal)}
\n
연체 미수금
{formatCurrency(customer.receivableOverdue)}
\n
연체일수
{customer.overdueDays}일
\n
\n \n\n {/* 프로세스 제어 */}\n
\n \n
수주 승인
{customer.requireApproval ? '회계승인 필요' : '자동승인'}
\n
신용상태
{customer.isCreditWarning ? '위험 (관리대상)' : '정상'}
\n
\n \n
\n );\n};\n\n// 회계관리 거래처 등록/수정\nconst AccCustomerRegister = ({ customer, onNavigate, isEdit = false }) => {\n const [formData, setFormData] = useState({ customerCode: customer?.customerCode || '', customerName: customer?.customerName || '', customerType: customer?.customerType || '매출', businessNo: customer?.businessNo || '', ceoName: customer?.ceoName || '', phone: customer?.phone || '', creditGrade: customer?.creditGrade || 'B', creditLimit: customer?.creditLimit || 0, paymentTerm: customer?.paymentTerm || '30일', requireApproval: customer?.requireApproval || false });\n const handleChange = (field, value) => setFormData(prev => ({ ...prev, [field]: value }));\n const handleSave = () => { alert(isEdit ? '수정되었습니다.' : '등록되었습니다.'); onNavigate('acc-customer-list'); };\n return (
거래처 {isEdit ? '수정' : '등록'}
handleChange('customerCode', e.target.value)} placeholder=\"CUS-001\" /> handleChange('customerName', e.target.value)} placeholder=\"거래처명 입력\" /> handleChange('businessNo', e.target.value)} placeholder=\"0000000000\" /> handleChange('ceoName', e.target.value)} placeholder=\"대표자명\" /> handleChange('phone', e.target.value)} placeholder=\"02-0000-0000\" />
handleChange('creditLimit', Number(e.target.value))} placeholder=\"0\" />
);\n};\n\n// 거래처 신용등급 관리 (A0-1) - Legacy\nconst CustomerCreditManagement = ({ shipments = [], onUpdateShipment, mode = 'list' }) => {\n const [activeTab, setActiveTab] = useState('list');\n const [search, setSearch] = useState('');\n const [showPanel, setShowPanel] = useState(null);\n const [selectedCustomer, setSelectedCustomer] = useState(null);\n const [gradeFilter, setGradeFilter] = useState('all');\n\n // 신용등급별 프로세스 제어 규칙\n const creditGradeRules = {\n A: {\n name: '우량',\n groupName: '정상거래처',\n description: '입금지연 거의 없음, 거래이력 안정적',\n orderConfirm: '자동 승인',\n productionOrder: '자동',\n shipment: '자동',\n taxInvoice: '자동 발행',\n color: 'green',\n },\n B: {\n name: '관리',\n groupName: '관리대상거래처',\n description: '가끔 입금 지연 또는 할인 요청 발생',\n orderConfirm: '자동 승인',\n productionOrder: '자동',\n shipment: '입금 확인 후 출고',\n taxInvoice: '할인 적용 후 발행 가능',\n color: 'blue',\n },\n C: {\n name: '위험',\n groupName: '경리승인 거래처',\n description: '입금약속 미이행, 신용불량 이력',\n orderConfirm: '자동(생산 보류)',\n productionOrder: '경리 승인 후 진행',\n shipment: '입금 확인 후 출고',\n taxInvoice: '입금 후 발행 또는 경리 승인 필요',\n color: 'red',\n },\n };\n\n // 거래처 통합 마스터 데이터 (회계관리 기준)\n // customerType: 매출(판매처), 매입(구매처), 매입매출(양방향)\n // supplyType: 자재(자재공급업체), 외주(외주협력사) - 매입 거래처만 해당\n const [customers, setCustomers] = useState([\n // ========== 매출 거래처 (건설사 - 블라인드/셔터 납품처) ==========\n {\n id: 1, customerCode: 'CUS-001', customerName: '삼성물산(주)', businessNo: '124-81-00998',\n ceoName: '오세철', businessType: '건설업', businessItem: '종합건설',\n customerType: '매출',\n creditGrade: 'A', previousGrade: 'A', gradeDate: '2025-01-01',\n paymentTerms: '60일', paymentDay: 25,\n creditLimit: 200000000, usedCredit: 48890000, availableCredit: 151110000,\n contactPerson: '김건설', phone: '010-1234-5678', email: 'procurement@samsungcnt.com',\n address: '서울특별시 서초구 서초대로 74길 11',\n requireAccountingApproval: false, requirePaymentBeforeShipment: false, isCreditWarning: false,\n discountRate: 5, note: '대형 건설사 - 우량 거래처',\n transactions: { totalCount: 45, totalAmount: 580000000, lastDate: '2025-12-01' },\n },\n {\n id: 2, customerCode: 'CUS-002', customerName: '현대건설(주)', businessNo: '101-81-16756',\n ceoName: '윤영준', businessType: '건설업', businessItem: '종합건설',\n customerType: '매출',\n creditGrade: 'A', previousGrade: 'A', gradeDate: '2025-01-01',\n paymentTerms: '60일', paymentDay: 30,\n creditLimit: 150000000, usedCredit: 35200000, availableCredit: 114800000,\n contactPerson: '이현장', phone: '010-2345-6789', email: 'order@hdec.kr',\n address: '서울특별시 종로구 율곡로 75',\n requireAccountingApproval: false, requirePaymentBeforeShipment: false, isCreditWarning: false,\n discountRate: 5, note: '대형 건설사',\n transactions: { totalCount: 38, totalAmount: 890000000, lastDate: '2025-12-05' },\n },\n {\n id: 3, customerCode: 'CUS-003', customerName: '대우건설(주)', businessNo: '116-81-08831',\n ceoName: '백정완', businessType: '건설업', businessItem: '종합건설',\n customerType: '매출',\n creditGrade: 'B', previousGrade: 'A', gradeDate: '2025-09-01',\n paymentTerms: '30일', paymentDay: 30,\n creditLimit: 80000000, usedCredit: 28800000, availableCredit: 51200000,\n contactPerson: '박건설', phone: '010-3456-7890', email: 'purchase@dwconst.co.kr',\n address: '서울특별시 중구 을지로 170',\n requireAccountingApproval: false, requirePaymentBeforeShipment: true, isCreditWarning: false,\n discountRate: 2, note: '결제 지연 이력 있음 - 등급 하향',\n transactions: { totalCount: 22, totalAmount: 420000000, lastDate: '2025-11-28' },\n },\n {\n id: 4, customerCode: 'CUS-004', customerName: '(주)서울인테리어', businessNo: '214-85-56789',\n ceoName: '정서울', businessType: '건설업', businessItem: '실내건축',\n customerType: '매출',\n creditGrade: 'B', previousGrade: 'B', gradeDate: '2025-01-01',\n paymentTerms: '30일', paymentDay: 25,\n creditLimit: 30000000, usedCredit: 12000000, availableCredit: 18000000,\n contactPerson: '김담당', phone: '010-4567-8901', email: 'order@seoulinterior.co.kr',\n address: '서울특별시 강남구 테헤란로 123',\n requireAccountingApproval: false, requirePaymentBeforeShipment: true, isCreditWarning: false,\n discountRate: 0, note: '중소 인테리어 업체',\n transactions: { totalCount: 15, totalAmount: 85000000, lastDate: '2025-12-03' },\n },\n {\n id: 5, customerCode: 'CUS-005', customerName: '신흥건설(주)', businessNo: '567-89-01234',\n ceoName: '정민수', businessType: '건설업', businessItem: '전문건설',\n customerType: '매출',\n creditGrade: 'C', previousGrade: 'B', gradeDate: '2025-11-01',\n paymentTerms: '선입금필수', paymentDay: null,\n creditLimit: 20000000, usedCredit: 15000000, availableCredit: 5000000,\n contactPerson: '정사원', phone: '010-5678-9012', email: 'jung@shinheung.com',\n address: '경기도 성남시 분당구 판교로 228',\n requireAccountingApproval: true, requirePaymentBeforeShipment: true, isCreditWarning: true,\n discountRate: 0, note: '신용불량 경고 - 경리승인 필요',\n transactions: { totalCount: 8, totalAmount: 95000000, lastDate: '2025-10-15' },\n },\n // ========== 매입 거래처 - 자재 (자재 공급업체) ==========\n {\n id: 6, customerCode: 'SUP-001', customerName: '(주)한국스크린', businessNo: '314-81-09876',\n ceoName: '이원단', businessType: '제조업', businessItem: '섬유제품',\n customerType: '매입', supplyType: '자재',\n creditGrade: 'A', previousGrade: 'A', gradeDate: '2025-01-01',\n paymentTerms: '30일', paymentDay: 25,\n creditLimit: null, usedCredit: null, availableCredit: null,\n contactPerson: '최판매', phone: '010-6789-0123', email: 'sales@koreascreen.co.kr',\n address: '경기도 안산시 단원구 산업로 200',\n requireAccountingApproval: false, requirePaymentBeforeShipment: false, isCreditWarning: false,\n discountRate: 0, note: '스크린 원단 주력 공급처',\n transactions: { totalCount: 52, totalAmount: 180000000, lastDate: '2025-12-10' },\n supplyItems: ['스크린 원단', '방화스크린 원단'],\n purchasePrices: [\n { itemCode: 'RM-SCR-1016', itemName: '스크린 원단 1016', price: 15000, unit: 'M', effectiveDate: '2025-01-01' },\n { itemCode: 'RM-SCR-1520', itemName: '스크린 원단 1520', price: 18000, unit: 'M', effectiveDate: '2025-01-01' },\n { itemCode: 'RM-SCR-FIRE', itemName: '방화스크린 원단', price: 35000, unit: 'M', effectiveDate: '2025-01-01' },\n ],\n },\n {\n id: 7, customerCode: 'SUP-002', customerName: '(주)대한슬랫', businessNo: '416-82-05432',\n ceoName: '박슬랫', businessType: '제조업', businessItem: '금속제품',\n customerType: '매입', supplyType: '자재',\n creditGrade: 'A', previousGrade: 'A', gradeDate: '2025-01-01',\n paymentTerms: '30일', paymentDay: 25,\n creditLimit: null, usedCredit: null, availableCredit: null,\n contactPerson: '정자재', phone: '010-7890-1234', email: 'order@dhslat.co.kr',\n address: '경기도 화성시 정남면 공단로 50',\n requireAccountingApproval: false, requirePaymentBeforeShipment: false, isCreditWarning: false,\n discountRate: 0, note: '슬랫 주력 공급처',\n transactions: { totalCount: 48, totalAmount: 220000000, lastDate: '2025-12-08' },\n supplyItems: ['알루미늄 슬랫', '우드 슬랫'],\n purchasePrices: [\n { itemCode: 'RM-SLT-AL25', itemName: '알루미늄 슬랫 25mm', price: 2500, unit: 'EA', effectiveDate: '2025-01-01' },\n { itemCode: 'RM-SLT-AL50', itemName: '알루미늄 슬랫 50mm', price: 4500, unit: 'EA', effectiveDate: '2025-01-01' },\n { itemCode: 'RM-SLT-WD50', itemName: '우드 슬랫 50mm', price: 6000, unit: 'EA', effectiveDate: '2025-01-01' },\n ],\n },\n {\n id: 8, customerCode: 'SUP-003', customerName: '(주)세종철강', businessNo: '514-83-06543',\n ceoName: '김철강', businessType: '제조업', businessItem: '철강제품',\n customerType: '매입', supplyType: '자재',\n creditGrade: 'B', previousGrade: 'A', gradeDate: '2025-06-01',\n paymentTerms: '현금', paymentDay: null,\n creditLimit: null, usedCredit: null, availableCredit: null,\n contactPerson: '이철판', phone: '010-8901-2345', email: 'supply@sejongsteel.co.kr',\n address: '충청남도 당진시 송악읍 철강로 100',\n requireAccountingApproval: false, requirePaymentBeforeShipment: false, isCreditWarning: false,\n discountRate: 0, note: '철강 자재 공급처',\n transactions: { totalCount: 25, totalAmount: 95000000, lastDate: '2025-12-01' },\n supplyItems: ['철판', '아연도금강판', '스테인리스'],\n purchasePrices: [\n { itemCode: 'RM-STL-1.0', itemName: '철판 1.0T', price: 85000, unit: 'SHEET', effectiveDate: '2025-01-01' },\n { itemCode: 'RM-STL-1.2', itemName: '철판 1.2T', price: 95000, unit: 'SHEET', effectiveDate: '2025-01-01' },\n { itemCode: 'RM-GAL-1.0', itemName: '아연도금강판 1.0T', price: 120000, unit: 'SHEET', effectiveDate: '2025-01-01' },\n ],\n },\n {\n id: 9, customerCode: 'SUP-004', customerName: '(주)동양모터', businessNo: '612-84-07654',\n ceoName: '최모터', businessType: '제조업', businessItem: '전기기기',\n customerType: '매입', supplyType: '자재',\n creditGrade: 'A', previousGrade: 'A', gradeDate: '2025-01-01',\n paymentTerms: '30일', paymentDay: 25,\n creditLimit: null, usedCredit: null, availableCredit: null,\n contactPerson: '강전기', phone: '010-9012-3456', email: 'sales@dongyang-motor.co.kr',\n address: '경기도 부천시 오정구 산업로 88',\n requireAccountingApproval: false, requirePaymentBeforeShipment: false, isCreditWarning: false,\n discountRate: 0, note: '모터/컨트롤러 주력 공급처',\n transactions: { totalCount: 38, totalAmount: 150000000, lastDate: '2025-12-09' },\n supplyItems: ['셔터 모터', '블라인드 모터', '컨트롤러'],\n purchasePrices: [\n { itemCode: 'RM-MTR-SH300', itemName: '셔터모터 300W', price: 85000, unit: 'EA', effectiveDate: '2025-01-01' },\n { itemCode: 'RM-MTR-SH500', itemName: '셔터모터 500W', price: 120000, unit: 'EA', effectiveDate: '2025-01-01' },\n { itemCode: 'RM-MTR-BL', itemName: '블라인드모터', price: 45000, unit: 'EA', effectiveDate: '2025-01-01' },\n { itemCode: 'RM-CTR-01', itemName: '리모컨 컨트롤러', price: 25000, unit: 'EA', effectiveDate: '2025-01-01' },\n ],\n },\n // ========== 매입 거래처 - 외주 (외주협력사) ==========\n {\n id: 10, customerCode: 'PAR-001', customerName: '(주)금성도금', businessNo: '615-84-07654',\n ceoName: '최도금', businessType: '제조업', businessItem: '표면처리',\n customerType: '매입', supplyType: '외주',\n creditGrade: 'B', previousGrade: 'B', gradeDate: '2025-01-01',\n paymentTerms: '30일', paymentDay: 25,\n creditLimit: null, usedCredit: null, availableCredit: null,\n contactPerson: '박협력', phone: '010-0123-4567', email: 'work@ksplating.co.kr',\n address: '경기도 시흥시 정왕동 공단로 88',\n requireAccountingApproval: false, requirePaymentBeforeShipment: false, isCreditWarning: false,\n discountRate: 0, note: '도금가공 협력사',\n transactions: { totalCount: 65, totalAmount: 85000000, lastDate: '2025-12-11' },\n partnerType: '도금가공',\n processingPrices: [\n { processName: '아연도금', price: 500, unit: 'EA', effectiveDate: '2025-01-01' },\n { processName: '크롬도금', price: 800, unit: 'EA', effectiveDate: '2025-01-01' },\n { processName: '니켈도금', price: 700, unit: 'EA', effectiveDate: '2025-01-01' },\n ],\n },\n {\n id: 11, customerCode: 'PAR-002', customerName: '(주)대성분체도장', businessNo: '718-85-08765',\n ceoName: '한분체', businessType: '제조업', businessItem: '표면처리',\n customerType: '매입', supplyType: '외주',\n creditGrade: 'A', previousGrade: 'A', gradeDate: '2025-01-01',\n paymentTerms: '30일', paymentDay: 25,\n creditLimit: null, usedCredit: null, availableCredit: null,\n contactPerson: '김도장', phone: '010-1234-5670', email: 'work@dscoating.co.kr',\n address: '경기도 시흥시 정왕동 공단로 120',\n requireAccountingApproval: false, requirePaymentBeforeShipment: false, isCreditWarning: false,\n discountRate: 0, note: '분체도장 협력사',\n transactions: { totalCount: 72, totalAmount: 120000000, lastDate: '2025-12-10' },\n partnerType: '분체도장',\n processingPrices: [\n { processName: '분체도장(백색)', price: 600, unit: 'EA', effectiveDate: '2025-01-01' },\n { processName: '분체도장(유색)', price: 700, unit: 'EA', effectiveDate: '2025-01-01' },\n { processName: '분체도장(특수)', price: 1000, unit: 'EA', effectiveDate: '2025-01-01' },\n ],\n },\n ]);\n\n const tabs = [\n { id: 'list', label: '거래처 목록' },\n { id: 'credit', label: '신용등급 관리' },\n { id: 'process', label: '등급별 프로세스' },\n { id: 'history', label: '등급변경 이력' },\n ];\n\n // 통계 (매출 거래처만 - 매입 거래처는 신용한도 없음)\n const customerOnly = customers.filter(c => c.customerType === '매출');\n const gradeA = customerOnly.filter(c => c.creditGrade === 'A').length;\n const gradeB = customerOnly.filter(c => c.creditGrade === 'B').length;\n const gradeC = customerOnly.filter(c => c.creditGrade === 'C').length;\n const totalCreditLimit = customerOnly.reduce((sum, c) => sum + (c.creditLimit || 0), 0);\n const totalUsedCredit = customerOnly.reduce((sum, c) => sum + (c.usedCredit || 0), 0);\n\n const filtered = customers.filter(c => {\n const matchSearch = c.customerCode.toLowerCase().includes(search.toLowerCase()) ||\n c.customerName.includes(search) ||\n c.creditGrade.includes(search.toUpperCase());\n const matchGrade = gradeFilter === 'all' || c.creditGrade === gradeFilter;\n return matchSearch && matchGrade;\n });\n\n const handleChangeGrade = (customer, newGrade) => {\n const reason = prompt(`${customer.customerName}의 신용등급을 ${customer.creditGrade} → ${newGrade}로 변경합니다.\\n변경 사유를 입력하세요:`);\n if (reason) {\n setCustomers(prev => prev.map(c =>\n c.id === customer.id ? {\n ...c,\n previousGrade: c.creditGrade,\n creditGrade: newGrade,\n gradeDate: new Date().toISOString().split('T')[0],\n note: `등급변경: ${c.creditGrade}→${newGrade} (${reason})`\n } : c\n ));\n alert(`신용등급이 변경되었습니다.\\n${customer.customerName}: ${customer.creditGrade} → ${newGrade}`);\n }\n };\n\n return (\n
\n
\n\n {/* 통계 카드 - 반응형 */}\n
\n \n \n \n \n \n
\n\n {/* 탭 */}\n
\n \n
\n {tabs.map(tab => (\n \n ))}\n
\n
\n
\n
\n
\n
\n
\n\n {activeTab === 'list' && (\n \n
\n \n \n | 거래처코드 | \n 거래처명 | \n 신용등급 | \n 이전등급 | \n 결제조건 | \n 여신한도 | \n 사용금액 | \n 잔여한도 | \n 등급변경일 | \n 관리 | \n
\n \n \n {filtered.map(customer => (\n \n | {customer.customerCode} | \n {customer.customerName} | \n \n {customer.creditGrade}등급\n | \n \n {customer.previousGrade !== customer.creditGrade ? (\n {customer.previousGrade}→{customer.creditGrade}\n ) : -}\n | \n {customer.paymentTerms} | \n {customer.creditLimit ? customer.creditLimit.toLocaleString() : '-'} | \n {customer.usedCredit ? customer.usedCredit.toLocaleString() : '-'} | \n {customer.availableCredit ? customer.availableCredit.toLocaleString() : '-'} | \n {customer.gradeDate} | \n \n \n \n \n \n | \n
\n ))}\n \n
\n
\n )}\n\n {activeTab === 'credit' && (\n \n
\n {['A', 'B', 'C'].map(grade => {\n const rule = creditGradeRules[grade];\n const gradeCustomers = customers.filter(c => c.creditGrade === grade);\n return (\n
\n
\n \n {grade}등급 - {rule.name}\n \n {gradeCustomers.length}개사\n
\n
{rule.description}
\n
\n
여신총액: {(gradeCustomers.reduce((sum, c) => sum + c.creditLimit, 0) / 100000000).toFixed(1)}억
\n
사용금액: {(gradeCustomers.reduce((sum, c) => sum + c.usedCredit, 0) / 100000000).toFixed(1)}억
\n
\n
\n
\n );\n })}\n
\n
\n )}\n\n {activeTab === 'process' && (\n \n
\n
신용등급에 따라 수주확인, 생산지시, 출고, 세금계산서 발행 프로세스가 자동으로 제어됩니다.
\n
\n
\n
\n \n \n | 프로세스 | \n \n A등급 (우량)\n | \n \n B등급 (관리)\n | \n \n C등급 (위험)\n | \n
\n \n \n \n | 수주확인 | \n {creditGradeRules.A.orderConfirm} | \n {creditGradeRules.B.orderConfirm} | \n {creditGradeRules.C.orderConfirm} | \n
\n \n | 생산지시 | \n {creditGradeRules.A.productionOrder} | \n {creditGradeRules.B.productionOrder} | \n {creditGradeRules.C.productionOrder} | \n
\n \n | 출고 | \n {creditGradeRules.A.shipment} | \n {creditGradeRules.B.shipment} | \n {creditGradeRules.C.shipment} | \n
\n \n | 세금계산서 | \n {creditGradeRules.A.taxInvoice} | \n {creditGradeRules.B.taxInvoice} | \n {creditGradeRules.C.taxInvoice} | \n
\n \n
\n
\n
\n
\n
A등급 - 정상거래처
\n
\n - - 모든 프로세스 자동 진행
\n - - 여신한도 내 자유 거래
\n - - 세금계산서 자동 발행
\n
\n
\n
\n
B등급 - 관리대상거래처
\n
\n - - 출고 시 입금 확인 필요
\n - - 할인 적용 시 주의
\n - - 정기적 신용 재평가
\n
\n
\n
\n
C등급 - 경리승인 거래처
\n
\n - - 생산지시 전 경리 승인 필수
\n - - 출고 전 입금 완료 필수
\n - - 세금계산서 경리 승인 필요
\n
\n
\n
\n
\n )}\n\n {activeTab === 'history' && (\n \n {[\n { date: '2025-11-01', customer: '신흥건설', from: 'B', to: 'C', reason: '연속 3회 결제 지연', by: '경리팀 김경리' },\n { date: '2025-09-01', customer: '대우건설', from: 'A', to: 'B', reason: '결제 지연 1회 발생', by: '경리팀 김경리' },\n { date: '2025-06-01', customer: 'LG전자', from: 'B', to: 'A', reason: '1년간 정상 결제', by: '경리팀 박회계' },\n ].map((log, i) => (\n
\n
{log.date}
\n
{log.customer}
\n
\n
{log.from}\n
\n
{log.to}\n
\n
{log.reason}
\n
{log.by}
\n
\n ))}\n
\n )}\n\n {activeTab === 'analysis' && (\n \n
\n \n {[\n { grade: 'A', count: gradeA, color: 'green', desc: '우량 - 자동 출고' },\n { grade: 'B', count: gradeB, color: 'blue', desc: '관리 - 입금확인 후 출고' },\n { grade: 'C', count: gradeC, color: 'red', desc: '위험 - 경리승인 필요' },\n ].map(item => (\n
\n
\n {item.grade}등급\n \n
\n
{item.count}개사\n
{item.desc}\n
\n ))}\n
\n \n
\n \n {customers.slice(0, 5).map(c => (\n
\n
{c.customerName}\n
\n
\n
0.8 ? 'bg-red-500' :\n (c.usedCredit / c.creditLimit) > 0.5 ? 'bg-yellow-500' : 'bg-green-500'\n }`} style={{ width: `${(c.usedCredit / c.creditLimit) * 100}%` }} />\n
\n
\n
{Math.round(c.usedCredit / c.creditLimit * 100)}%\n
\n ))}\n
\n \n
\n )}\n \n\n {/* 거래처 상세보기 모달 */}\n {showPanel === 'detail' && selectedCustomer && (\n
\n
\n
\n
거래처 상세정보
\n \n \n
\n
\n
\n
\n {selectedCustomer.customerName}\n \n {selectedCustomer.creditGrade}등급 ({creditGradeRules[selectedCustomer.creditGrade].name})\n \n {selectedCustomer.isCreditWarning && (\n 신용경고\n )}\n
\n
{selectedCustomer.customerCode} | {selectedCustomer.businessNo}
\n
\n
\n\n
\n
\n
기본정보
\n
\n
대표자
\n
{selectedCustomer.ceoName}
\n
업태
\n
{selectedCustomer.businessType}
\n
종목
\n
{selectedCustomer.businessItem}
\n
주소
\n
{selectedCustomer.address}
\n
\n
\n
\n
담당자정보
\n
\n
담당자
\n
{selectedCustomer.contactPerson}
\n
연락처
\n
{selectedCustomer.phone}
\n
이메일
\n
{selectedCustomer.email}
\n
\n
\n
\n\n
\n
신용/결제정보
\n
\n
\n
여신한도
\n
{selectedCustomer.creditLimit ? `${(selectedCustomer.creditLimit / 10000).toLocaleString()}만` : '-'}
\n
\n
\n
사용금액
\n
{selectedCustomer.usedCredit ? `${(selectedCustomer.usedCredit / 10000).toLocaleString()}만` : '-'}
\n
\n
\n
잔여한도
\n
{selectedCustomer.availableCredit ? `${(selectedCustomer.availableCredit / 10000).toLocaleString()}만` : '-'}
\n
\n
\n
사용률
\n
0.8 ? 'text-red-600' : 'text-gray-600'}`}>\n {selectedCustomer.creditLimit ? `${Math.round((selectedCustomer.usedCredit || 0) / selectedCustomer.creditLimit * 100)}%` : '-'}\n
\n
\n
\n
\n
결제조건: {selectedCustomer.paymentTerms}
\n
결제일: {selectedCustomer.paymentDay ? `매월 ${selectedCustomer.paymentDay}일` : '-'}
\n
할인율: {selectedCustomer.discountRate}%
\n
\n
\n\n
\n
프로세스 제어
\n
\n
\n
경리 승인
\n
\n {selectedCustomer.requireAccountingApproval ? '필요' : '불필요'}\n
\n
\n
\n
입금 후 출고
\n
\n {selectedCustomer.requirePaymentBeforeShipment ? '필요' : '불필요'}\n
\n
\n
\n
신용경고
\n
\n {selectedCustomer.isCreditWarning ? '경고 상태' : '정상'}\n
\n
\n
\n
\n\n
\n
거래현황
\n
\n
총 거래횟수: {selectedCustomer.transactions.totalCount}회
\n
총 거래금액: {(selectedCustomer.transactions.totalAmount / 100000000).toFixed(1)}억
\n
최근 거래일: {selectedCustomer.transactions.lastDate}
\n
\n
\n\n {selectedCustomer.note && (\n
\n
{selectedCustomer.note}
\n
\n )}\n\n
\n \n \n
\n
\n
\n
\n )}\n\n {/* 등급 변경 모달 */}\n {showPanel === 'grade' && selectedCustomer && (\n
\n
\n
\n
신용등급 변경
\n \n \n
\n
\n
\n
거래처
{selectedCustomer.customerName}
\n
현재 등급
{selectedCustomer.creditGrade}등급\n
\n
\n
\n \n {['A', 'B', 'C'].map(grade => (\n
\n ))}\n
\n \n
\n
등급 변경 시 해당 거래처의 출고 조건이 자동으로 변경됩니다.
\n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 거래명세서 발행 (A1-1)\nconst TransactionStatementIssue = ({ shipments = [], onUpdateShipment, onNavigate }) => {\n const [search, setSearch] = useState('');\n const [filterStatus, setFilterStatus] = useState('pending');\n const [selectedItems, setSelectedItems] = useState([]);\n const [showPreview, setShowPreview] = useState(null);\n const [selectedTemplate, setSelectedTemplate] = useState('SL'); // 문서양식관리 연동\n\n // 문서양식관리에서 거래명세서 템플릿 가져오기\n const templateConfig = documentTemplateConfig.documentTemplates?.['SL'] || {\n name: '거래명세서',\n blocks: ['HDR-COMPANY', 'PTY-CUSTOMER', 'PTY-SUPPLIER', 'TBL-DELIVERY-ITEMS', 'AMT-TABLE']\n };\n\n // 출하완료 건 중 거래명세서 미발행 건\n const statementData = [\n {\n id: 1, shipmentNo: 'SH-251201-001', splitNo: 'KD-TS-251201-01-S1', orderNo: 'KD-TS-251201-01',\n customerName: '삼성전자', siteName: '삼성 서초사옥', shipmentDate: '2025-12-01',\n items: [{ name: '방화스크린 KSS01', qty: 5, unitPrice: 2500000, amount: 12500000 }],\n totalAmount: 12500000, vat: 1250000, grandTotal: 13750000,\n statementNo: '', statementDate: '', statementStatus: '미발행', sentMethod: '', sentDate: ''\n },\n {\n id: 2, shipmentNo: 'SH-251203-001', splitNo: 'KD-SL-251203-01-S1', orderNo: 'KD-SL-251203-01',\n customerName: 'LG전자', siteName: 'LG 트윈타워', shipmentDate: '2025-12-03',\n items: [{ name: '방화슬랫셔터 KSL01', qty: 3, unitPrice: 1800000, amount: 5400000 }],\n totalAmount: 5400000, vat: 540000, grandTotal: 5940000,\n statementNo: 'TS-251203-001', statementDate: '2025-12-03', statementStatus: '발행완료', sentMethod: '이메일', sentDate: '2025-12-03'\n },\n {\n id: 3, shipmentNo: 'SH-251205-001', splitNo: 'KD-BD-251205-01-S1', orderNo: 'KD-BD-251205-01',\n customerName: '현대건설', siteName: '현대 본사', shipmentDate: '2025-12-05',\n items: [{ name: '방화절곡셔터 KSB01', qty: 10, unitPrice: 3200000, amount: 32000000 }],\n totalAmount: 32000000, vat: 3200000, grandTotal: 35200000,\n statementNo: '', statementDate: '', statementStatus: '미발행', sentMethod: '', sentDate: ''\n },\n ];\n\n const [statements, setStatements] = useState(statementData);\n\n const statusFilters = [\n { id: 'all', label: '전체', count: statements.length },\n { id: 'pending', label: '미발행', count: statements.filter(s => s.statementStatus === '미발행').length },\n { id: 'issued', label: '발행완료', count: statements.filter(s => s.statementStatus === '발행완료').length },\n ];\n\n const filtered = statements.filter(s => {\n const matchSearch = s.shipmentNo.toLowerCase().includes(search.toLowerCase()) ||\n s.customerName.includes(search) || s.splitNo.toLowerCase().includes(search.toLowerCase());\n const matchStatus = filterStatus === 'all' ||\n (filterStatus === 'pending' && s.statementStatus === '미발행') ||\n (filterStatus === 'issued' && s.statementStatus === '발행완료');\n return matchSearch && matchStatus;\n });\n\n const handleIssueStatement = (item) => {\n const statementNo = `TS-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-${String(statements.filter(s => s.statementStatus === '발행완료').length + 1).padStart(3, '0')}`;\n setStatements(prev => prev.map(s =>\n s.id === item.id ? {\n ...s,\n statementNo,\n statementDate: new Date().toISOString().split('T')[0],\n statementStatus: '발행완료'\n } : s\n ));\n alert(`거래명세서가 발행되었습니다.\\n명세서번호: ${statementNo}`);\n };\n\n const handleBulkIssue = () => {\n if (selectedItems.length === 0) {\n alert('발행할 건을 선택해주세요.');\n return;\n }\n const pendingItems = selectedItems.filter(id => {\n const item = statements.find(s => s.id === id);\n return item && item.statementStatus === '미발행';\n });\n if (pendingItems.length === 0) {\n alert('미발행 건만 발행할 수 있습니다.');\n return;\n }\n pendingItems.forEach((id, idx) => {\n const statementNo = `TS-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-${String(statements.filter(s => s.statementStatus === '발행완료').length + idx + 1).padStart(3, '0')}`;\n setStatements(prev => prev.map(s =>\n s.id === id ? { ...s, statementNo, statementDate: new Date().toISOString().split('T')[0], statementStatus: '발행완료' } : s\n ));\n });\n alert(`${pendingItems.length}건의 거래명세서가 발행되었습니다.`);\n setSelectedItems([]);\n };\n\n const handleSendStatement = (item, method) => {\n if (item.statementStatus !== '발행완료') {\n alert('먼저 거래명세서를 발행해주세요.');\n return;\n }\n setStatements(prev => prev.map(s =>\n s.id === item.id ? { ...s, sentMethod: method, sentDate: new Date().toISOString().split('T')[0] } : s\n ));\n alert(`거래명세서가 ${method}로 발송되었습니다.\\n거래처: ${item.customerName}`);\n };\n\n return (\n \n
\n\n {/* 통계 */}\n
\n \n s.statementStatus === '미발행').length}건`} color=\"orange\" />\n s.statementStatus === '발행완료').length}건`} color=\"green\" />\n s.statementStatus === '발행완료').reduce((sum, s) => sum + s.grandTotal, 0).toLocaleString()}원`} color=\"purple\" />\n
\n\n
\n \n
\n {statusFilters.map(filter => (\n \n ))}\n
\n
\n \n \n
\n
\n\n \n \n \n | \n 0 && selectedItems.length === filtered.length}\n onChange={(e) => setSelectedItems(e.target.checked ? filtered.map(f => f.id) : [])}\n className=\"rounded border-gray-300\"\n />\n | \n 출하번호 | \n 분할번호 | \n 거래처 | \n 현장명 | \n 출하일 | \n 공급가 | \n 합계 | \n 명세서번호 | \n 발행상태 | \n 발송 | \n 관리 | \n
\n \n \n {filtered.map(item => (\n \n | \n setSelectedItems(prev =>\n e.target.checked ? [...prev, item.id] : prev.filter(id => id !== item.id)\n )}\n className=\"rounded border-gray-300\"\n />\n | \n {item.shipmentNo} | \n {item.splitNo} | \n {item.customerName} | \n {item.siteName} | \n {item.shipmentDate} | \n {item.totalAmount.toLocaleString()} | \n {item.grandTotal.toLocaleString()} | \n {item.statementNo || '-'} | \n \n {item.statementStatus}\n | \n \n {item.sentDate ? (\n {item.sentMethod}\n ) : (\n -\n )}\n | \n \n \n \n {item.statementStatus === '미발행' && (\n \n )}\n {item.statementStatus === '발행완료' && !item.sentDate && (\n <>\n \n \n >\n )}\n \n \n | \n
\n ))}\n \n
\n \n\n {/* 미리보기 모달 - 문서양식관리 연동 */}\n {showPreview && (\n
setShowPreview(null)}>\n
e.stopPropagation()}>\n
\n
\n
거래명세서 미리보기
\n
\n 양식:\n \n \n
\n
\n
\n
\n
\n {/* A4 비율 미리보기 영역 */}\n
\n {/* HDR-COMPANY 블록 */}\n
\n
\n [로고]\n
\n
거 래 명 세 서
\n
\n {showPreview.statementNo || '(미발행)'}\n
\n
\n\n {/* PTY-CUSTOMER & PTY-SUPPLIER 블록 */}\n
\n
\n
수 요 자
\n
\n
업체명:{showPreview.customerName}
\n
현장명:{showPreview.siteName}
\n
주소:-
\n
\n
\n
\n
공 급 자
\n
\n
상호:(주)금도방재
\n
대표자:홍길동
\n
사업자:123-45-67890
\n
\n
\n
\n\n {/* TBL-DELIVERY-ITEMS 블록 */}\n
\n \n \n | No | \n 품목명 | \n 규격 | \n 수량 | \n 단가 | \n 금액 | \n
\n \n \n {showPreview.items.map((item, i) => (\n \n | {i + 1} | \n {item.name} | \n - | \n {item.qty} | \n {item.unitPrice.toLocaleString()} | \n {item.amount.toLocaleString()} | \n
\n ))}\n {/* 빈 행 채우기 */}\n {[...Array(Math.max(0, 5 - showPreview.items.length))].map((_, i) => (\n \n | {showPreview.items.length + i + 1} | \n | \n | \n | \n | \n | \n
\n ))}\n \n
\n\n {/* AMT-TABLE 블록 */}\n
\n
\n
\n
공급가액
\n
{showPreview.totalAmount.toLocaleString()}원
\n
부가세
\n
{showPreview.vat.toLocaleString()}원
\n
합계금액
\n
{showPreview.grandTotal.toLocaleString()}원
\n
\n
\n
\n\n {/* RMK-STANDARD 블록 */}\n
\n
\n\n {/* 템플릿 정보 */}\n
\n
\n \n 사용 양식: {templateConfig.name}\n
\n
\n 블록 구성: {templateConfig.blocks?.map(b => typeof b === 'string' ? b : b.blockId).join(' → ') || 'HDR-COMPANY → PTY-CUSTOMER → TBL-ITEMS → AMT-TABLE'}\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n {showPreview.statementStatus === '미발행' && (\n \n )}\n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 세금계산서 발행 (A1-2) - 기존 InvoiceManagement 확장\nconst TaxInvoiceIssue = ({ shipments = [], onUpdateShipment }) => {\n const [search, setSearch] = useState('');\n const [filterStatus, setFilterStatus] = useState('pending');\n const [selectedItems, setSelectedItems] = useState([]);\n\n // 출하완료 건 중 세금계산서 미발행 건\n const [invoices, setInvoices] = useState([\n {\n id: 1, shipmentNo: 'SH-251201-001', splitNo: 'KD-TS-251201-01-S1',\n customerName: '삼성전자', businessNo: '123-45-67890', ceoName: '이재용',\n totalAmount: 12500000, vat: 1250000, grandTotal: 13750000,\n invoiceNo: '', invoiceDate: '', invoiceStatus: '미발행', ntsStatus: ''\n },\n {\n id: 2, shipmentNo: 'SH-251203-001', splitNo: 'KD-SL-251203-01-S1',\n customerName: 'LG전자', businessNo: '234-56-78901', ceoName: '구광모',\n totalAmount: 5400000, vat: 540000, grandTotal: 5940000,\n invoiceNo: 'TX-251203-001', invoiceDate: '2025-12-03', invoiceStatus: '발행완료', ntsStatus: '전송완료'\n },\n {\n id: 3, shipmentNo: 'SH-251205-001', splitNo: 'KD-BD-251205-01-S1',\n customerName: '현대건설', businessNo: '345-67-89012', ceoName: '윤영준',\n totalAmount: 32000000, vat: 3200000, grandTotal: 35200000,\n invoiceNo: 'TX-251205-001', invoiceDate: '2025-12-05', invoiceStatus: '발행완료', ntsStatus: '미전송'\n },\n ]);\n\n const statusFilters = [\n { id: 'all', label: '전체', count: invoices.length },\n { id: 'pending', label: '미발행', count: invoices.filter(i => i.invoiceStatus === '미발행').length },\n { id: 'issued', label: '발행완료', count: invoices.filter(i => i.invoiceStatus === '발행완료').length },\n { id: 'nts-pending', label: '미전송', count: invoices.filter(i => i.invoiceStatus === '발행완료' && i.ntsStatus !== '전송완료').length },\n ];\n\n const filtered = invoices.filter(i => {\n const matchSearch = i.shipmentNo.toLowerCase().includes(search.toLowerCase()) ||\n i.customerName.includes(search) || i.businessNo.includes(search);\n const matchStatus = filterStatus === 'all' ||\n (filterStatus === 'pending' && i.invoiceStatus === '미발행') ||\n (filterStatus === 'issued' && i.invoiceStatus === '발행완료') ||\n (filterStatus === 'nts-pending' && i.invoiceStatus === '발행완료' && i.ntsStatus !== '전송완료');\n return matchSearch && matchStatus;\n });\n\n const handleIssueInvoice = (item) => {\n const invoiceNo = `TX-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-${String(invoices.filter(i => i.invoiceStatus === '발행완료').length + 1).padStart(3, '0')}`;\n setInvoices(prev => prev.map(i =>\n i.id === item.id ? {\n ...i,\n invoiceNo,\n invoiceDate: new Date().toISOString().split('T')[0],\n invoiceStatus: '발행완료',\n ntsStatus: '미전송'\n } : i\n ));\n // 출하관리 taxInvoiceIssued 업데이트 (실제 연동)\n if (onUpdateShipment) {\n onUpdateShipment(item.shipmentNo, { taxInvoiceIssued: true });\n }\n alert(`세금계산서가 발행되었습니다.\\n계산서번호: ${invoiceNo}`);\n };\n\n const handleSendNTS = (item) => {\n if (item.ntsStatus === '전송완료') {\n alert('이미 국세청에 전송되었습니다.');\n return;\n }\n setInvoices(prev => prev.map(i =>\n i.id === item.id ? { ...i, ntsStatus: '전송완료' } : i\n ));\n alert(`세금계산서가 국세청에 전송되었습니다.\\n계산서번호: ${item.invoiceNo}`);\n };\n\n const handleBulkSendNTS = () => {\n const pendingNTS = invoices.filter(i => i.invoiceStatus === '발행완료' && i.ntsStatus !== '전송완료');\n if (pendingNTS.length === 0) {\n alert('전송할 계산서가 없습니다.');\n return;\n }\n setInvoices(prev => prev.map(i =>\n i.invoiceStatus === '발행완료' && i.ntsStatus !== '전송완료' ? { ...i, ntsStatus: '전송완료' } : i\n ));\n alert(`${pendingNTS.length}건의 세금계산서가 국세청에 전송되었습니다.`);\n };\n\n return (\n \n
\n\n {/* 통계 */}\n
\n \n i.invoiceStatus === '미발행').length}건`} color=\"orange\" />\n i.ntsStatus !== '전송완료' && i.invoiceStatus === '발행완료').length}건`} color=\"red\" />\n i.ntsStatus === '전송완료').length}건`} color=\"green\" />\n
\n\n
\n \n
\n {statusFilters.map(filter => (\n \n ))}\n
\n
\n \n \n
\n
\n\n \n \n \n | 출하번호 | \n 거래처 | \n 사업자번호 | \n 공급가 | \n 부가세 | \n 합계 | \n 계산서번호 | \n 발행상태 | \n 국세청전송 | \n 관리 | \n
\n \n \n {filtered.map(item => (\n \n | {item.shipmentNo} | \n {item.customerName} | \n {item.businessNo} | \n {item.totalAmount.toLocaleString()} | \n {item.vat.toLocaleString()} | \n {item.grandTotal.toLocaleString()} | \n {item.invoiceNo || '-'} | \n \n {item.invoiceStatus}\n | \n \n {item.ntsStatus || '-'}\n | \n \n \n {item.invoiceStatus === '미발행' && (\n \n )}\n {item.invoiceStatus === '발행완료' && item.ntsStatus !== '전송완료' && (\n \n )}\n \n \n | \n
\n ))}\n \n
\n \n
\n );\n};\n\n// 품의서 관리 (A2)\nconst PurchaseRequestManagement = ({ mode = 'list' }) => {\n const [activeTab, setActiveTab] = useState('list');\n const [search, setSearch] = useState('');\n const [showPanel, setShowPanel] = useState(mode === 'register');\n const [selectedRequest, setSelectedRequest] = useState(null);\n\n // 결재 흐름: 임직원(작성) → 경리(1차승인) → 대표(최종승인)\n // 금액별 결재선: 100만원 미만: 경리 승인만, 100만원 이상: 대표 승인 필요\n const [requests, setRequests] = useState([\n {\n id: 1, requestNo: 'PR-251201-001', title: '철강자재 구매건', requestDate: '2025-12-01',\n dept: '생산부', requester: '이생산', amount: 8500000, status: '승인완료',\n // 결재 흐름 확장\n approvalFlow: [\n { step: 1, role: '경리', approver: '김경리', status: '승인', date: '2025-12-01', comment: '' },\n { step: 2, role: '대표', approver: '박대표', status: '승인', date: '2025-12-02', comment: '승인합니다' },\n ],\n currentStep: 2, // 현재 결재 단계\n approver: '박대표', approvedDate: '2025-12-02', note: '긴급 구매', purchaseType: '원자재'\n },\n {\n id: 2, requestNo: 'PR-251205-001', title: '모터 부품 구매건', requestDate: '2025-12-05',\n dept: '생산부', requester: '박생산', amount: 3200000, status: '경리승인대기',\n approvalFlow: [\n { step: 1, role: '경리', approver: '', status: '대기', date: '', comment: '' },\n { step: 2, role: '대표', approver: '', status: '대기', date: '', comment: '' },\n ],\n currentStep: 1,\n approver: '', approvedDate: '', note: '정기 구매', purchaseType: '부품'\n },\n {\n id: 3, requestNo: 'PR-251208-001', title: '사무용품 구매건', requestDate: '2025-12-08',\n dept: '총무부', requester: '최총무', amount: 450000, status: '승인완료',\n approvalFlow: [\n { step: 1, role: '경리', approver: '김경리', status: '승인', date: '2025-12-08', comment: '100만원 미만 경리 단독 승인' },\n ],\n currentStep: 1,\n approver: '김경리', approvedDate: '2025-12-08', note: '100만원 미만', purchaseType: '사무용품'\n },\n {\n id: 4, requestNo: 'PR-251209-001', title: '포장재 대량 구매건', requestDate: '2025-12-09',\n dept: '생산부', requester: '김생산', amount: 5500000, status: '대표승인대기',\n approvalFlow: [\n { step: 1, role: '경리', approver: '김경리', status: '승인', date: '2025-12-09', comment: '경리 승인 완료' },\n { step: 2, role: '대표', approver: '', status: '대기', date: '', comment: '' },\n ],\n currentStep: 2,\n approver: '', approvedDate: '', note: '연말 재고 확보', purchaseType: '포장재'\n },\n {\n id: 5, requestNo: 'PR-251210-001', title: '장비 수리 외주', requestDate: '2025-12-10',\n dept: '생산부', requester: '이생산', amount: 2800000, status: '반려',\n approvalFlow: [\n { step: 1, role: '경리', approver: '김경리', status: '반려', date: '2025-12-10', comment: '예산 초과, 차기 분기로 연기 요청' },\n ],\n currentStep: 1,\n approver: '김경리', approvedDate: '2025-12-10', note: '장비 수리', purchaseType: '외주비'\n },\n ]);\n\n const tabs = [\n { id: 'list', label: '품의서 목록', count: requests.length },\n { id: 'accountingPending', label: '경리승인대기', count: requests.filter(r => r.status === '경리승인대기').length },\n { id: 'ceoPending', label: '대표승인대기', count: requests.filter(r => r.status === '대표승인대기').length },\n { id: 'approved', label: '승인완료', count: requests.filter(r => r.status === '승인완료').length },\n ];\n\n // 결재 처리 함수\n const handleApproval = (request, action, comment = '') => {\n const today = new Date().toISOString().split('T')[0];\n const currentStep = request.currentStep;\n const isAccountingStep = currentStep === 1;\n const needsCeoApproval = request.amount >= 1000000; // 100만원 이상은 대표 승인 필요\n\n setSalesData(prev => prev.map(r => {\n if (r.id !== request.id) return r;\n\n const newApprovalFlow = [...r.approvalFlow];\n newApprovalFlow[currentStep - 1] = {\n ...newApprovalFlow[currentStep - 1],\n approver: isAccountingStep ? '김경리' : '박대표',\n status: action === 'approve' ? '승인' : '반려',\n date: today,\n comment: comment,\n };\n\n let newStatus = r.status;\n let newCurrentStep = currentStep;\n let newApprovedDate = r.approvedDate;\n let newApprover = r.approver;\n\n if (action === 'approve') {\n if (isAccountingStep && needsCeoApproval) {\n // 경리 승인 완료 -> 대표 승인 대기\n newStatus = '대표승인대기';\n newCurrentStep = 2;\n } else {\n // 최종 승인 완료\n newStatus = '승인완료';\n newApprovedDate = today;\n newApprover = isAccountingStep ? '김경리' : '박대표';\n }\n } else {\n // 반려\n newStatus = '반려';\n newApprovedDate = today;\n newApprover = isAccountingStep ? '김경리' : '박대표';\n }\n\n return {\n ...r,\n approvalFlow: newApprovalFlow,\n status: newStatus,\n currentStep: newCurrentStep,\n approvedDate: newApprovedDate,\n approver: newApprover,\n };\n }));\n\n alert(`${action === 'approve' ? '승인' : '반려'}이 완료되었습니다.\\n품의번호: ${request.requestNo}`);\n };\n\n // setRequests를 사용해야 하므로 수정\n const processApproval = (request, action, comment = '') => {\n const today = new Date().toISOString().split('T')[0];\n const currentStep = request.currentStep;\n const isAccountingStep = currentStep === 1;\n const needsCeoApproval = request.amount >= 1000000;\n\n setRequests(prev => prev.map(r => {\n if (r.id !== request.id) return r;\n\n const newApprovalFlow = [...r.approvalFlow];\n newApprovalFlow[currentStep - 1] = {\n ...newApprovalFlow[currentStep - 1],\n approver: isAccountingStep ? '김경리' : '박대표',\n status: action === 'approve' ? '승인' : '반려',\n date: today,\n comment: comment,\n };\n\n let newStatus = r.status;\n let newCurrentStep = currentStep;\n let newApprovedDate = r.approvedDate;\n let newApprover = r.approver;\n\n if (action === 'approve') {\n if (isAccountingStep && needsCeoApproval) {\n newStatus = '대표승인대기';\n newCurrentStep = 2;\n } else {\n newStatus = '승인완료';\n newApprovedDate = today;\n newApprover = isAccountingStep ? '김경리' : '박대표';\n }\n } else {\n newStatus = '반려';\n newApprovedDate = today;\n newApprover = isAccountingStep ? '김경리' : '박대표';\n }\n\n return {\n ...r,\n approvalFlow: newApprovalFlow,\n status: newStatus,\n currentStep: newCurrentStep,\n approvedDate: newApprovedDate,\n approver: newApprover,\n };\n }));\n\n setShowPanel(null);\n alert(`${action === 'approve' ? '승인' : '반려'}이 완료되었습니다.\\n품의번호: ${request.requestNo}`);\n };\n\n const filtered = requests.filter(r => {\n const matchSearch = r.requestNo.toLowerCase().includes(search.toLowerCase()) ||\n r.title.includes(search) || r.requester.includes(search);\n if (activeTab === 'list') return matchSearch;\n if (activeTab === 'accountingPending') return matchSearch && r.status === '경리승인대기';\n if (activeTab === 'ceoPending') return matchSearch && r.status === '대표승인대기';\n if (activeTab === 'approved') return matchSearch && r.status === '승인완료';\n return matchSearch;\n });\n\n // 공통 UX: 품의서 목록 선택\n const {\n selectedIds: requestSelectedIds,\n handleSelect: handleRequestSelect,\n handleSelectAll: handleRequestSelectAll,\n isAllSelected: isRequestAllSelected,\n hasSelection: hasRequestSelection,\n isMultiSelect: isRequestMultiSelect,\n isSelected: isRequestSelected,\n } = useListSelection(filtered);\n\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n // 통계\n const accountingPendingCount = requests.filter(r => r.status === '경리승인대기').length;\n const ceoPendingCount = requests.filter(r => r.status === '대표승인대기').length;\n const approvedAmount = requests.filter(r => r.status === '승인완료').reduce((sum, r) => sum + r.amount, 0);\n\n return (\n \n
\n\n
\n \n \n \n r.status === '승인완료').length}건`} color=\"green\" />\n \n
\n\n {/* 결재 흐름 안내 */}\n
\n
\n 결재 흐름: 임직원(작성) → 경리(1차승인) → 대표(최종승인)\n ※ 100만원 미만: 경리 단독 승인 / 100만원 이상: 대표 승인 필요\n
\n
\n\n
\n \n
\n {tabs.map(tab => (\n \n ))}\n
\n
\n
\n {isRequestMultiSelect && (\n
\n )}\n
\n
\n
\n\n \n \n\n {/* 결재 모달 */}\n {showPanel === 'approve' && selectedRequest && (\n
\n
\n
\n
품의서 결재
\n \n \n
\n
\n
품의번호{selectedRequest.requestNo}
\n
제목{selectedRequest.title}
\n
신청자{selectedRequest.dept} {selectedRequest.requester}
\n
금액{selectedRequest.amount.toLocaleString()}원
\n
현재 결재선\n {selectedRequest.currentStep === 1 ? '경리 승인' : '대표 승인'}\n
\n
\n\n {/* 결재 흐름 표시 */}\n
\n
결재 진행현황
\n
\n {selectedRequest.approvalFlow.map((flow, idx) => (\n
\n
{flow.step}차 결재
\n
{flow.role}
\n
\n {flow.status === '대기' ? '대기중' : flow.status}\n {flow.date && ` (${flow.date})`}\n
\n
\n ))}\n
\n
\n\n
\n \n \n\n
\n \n \n \n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 지출결의서 관리 (A2-3)\nconst ExpenseApprovalManagement = ({ mode = 'list' }) => {\n const [activeTab, setActiveTab] = useState('list');\n const [search, setSearch] = useState('');\n const [showPanel, setShowPanel] = useState(mode === 'register');\n const [selectedExpense, setSelectedExpense] = useState(null);\n\n // 지출결의서: 품의서 승인 후 작성 → 경리승인 → 대표승인 → 지급처리\n const [expenses, setExpenses] = useState([\n {\n id: 1, expenseNo: 'EX-251201-001', purchaseRequestNo: 'PR-251201-001', title: '철강자재 대금지급', expenseDate: '2025-12-01',\n vendor: '금강철강', vendorAccount: '신한 111-222-333444', amount: 8500000, vat: 850000, totalAmount: 9350000,\n paymentMethod: '계좌이체', scheduledPaymentDate: '2025-12-05', actualPaymentDate: '2025-12-05',\n status: '지급완료', requester: '이생산', dept: '생산부',\n approvalFlow: [\n { step: 1, role: '경리', approver: '김경리', status: '승인', date: '2025-12-02', comment: '' },\n { step: 2, role: '대표', approver: '박대표', status: '승인', date: '2025-12-03', comment: '지급 승인' },\n ],\n currentStep: 2\n },\n {\n id: 2, expenseNo: 'EX-251205-001', purchaseRequestNo: 'PR-251205-001', title: '모터부품 대금지급', expenseDate: '2025-12-05',\n vendor: '모터공급사', vendorAccount: '국민 999-888-777666', amount: 3200000, vat: 320000, totalAmount: 3520000,\n paymentMethod: '계좌이체', scheduledPaymentDate: '2025-12-15', actualPaymentDate: '',\n status: '경리승인대기', requester: '박생산', dept: '생산부',\n approvalFlow: [\n { step: 1, role: '경리', approver: '', status: '대기', date: '', comment: '' },\n { step: 2, role: '대표', approver: '', status: '대기', date: '', comment: '' },\n ],\n currentStep: 1\n },\n {\n id: 3, expenseNo: 'EX-251209-001', purchaseRequestNo: 'PR-251209-001', title: '포장재 대금지급', expenseDate: '2025-12-09',\n vendor: '포장재공급사', vendorAccount: '우리 555-666-777888', amount: 5500000, vat: 550000, totalAmount: 6050000,\n paymentMethod: '계좌이체', scheduledPaymentDate: '2025-12-20', actualPaymentDate: '',\n status: '대표승인대기', requester: '김생산', dept: '생산부',\n approvalFlow: [\n { step: 1, role: '경리', approver: '김경리', status: '승인', date: '2025-12-10', comment: '경리 확인' },\n { step: 2, role: '대표', approver: '', status: '대기', date: '', comment: '' },\n ],\n currentStep: 2\n },\n {\n id: 4, expenseNo: 'EX-251210-001', purchaseRequestNo: '', title: '공과금(전기료)', expenseDate: '2025-12-10',\n vendor: '한국전력', vendorAccount: '자동이체', amount: 1250000, vat: 125000, totalAmount: 1375000,\n paymentMethod: '자동이체', scheduledPaymentDate: '2025-12-25', actualPaymentDate: '',\n status: '승인완료', requester: '최총무', dept: '총무부',\n approvalFlow: [\n { step: 1, role: '경리', approver: '김경리', status: '승인', date: '2025-12-10', comment: '정기 지출' },\n ],\n currentStep: 1\n },\n ]);\n\n const tabs = [\n { id: 'list', label: '결의서 목록', count: expenses.length },\n { id: 'accountingPending', label: '경리승인대기', count: expenses.filter(e => e.status === '경리승인대기').length },\n { id: 'ceoPending', label: '대표승인대기', count: expenses.filter(e => e.status === '대표승인대기').length },\n { id: 'approved', label: '지급대기', count: expenses.filter(e => e.status === '승인완료').length },\n { id: 'paid', label: '지급완료', count: expenses.filter(e => e.status === '지급완료').length },\n ];\n\n // 결재 처리\n const processExpenseApproval = (expense, action, comment = '') => {\n const today = new Date().toISOString().split('T')[0];\n const currentStep = expense.currentStep;\n const isAccountingStep = currentStep === 1;\n const needsCeoApproval = expense.totalAmount >= 1000000;\n\n setExpenses(prev => prev.map(e => {\n if (e.id !== expense.id) return e;\n\n const newApprovalFlow = [...e.approvalFlow];\n newApprovalFlow[currentStep - 1] = {\n ...newApprovalFlow[currentStep - 1],\n approver: isAccountingStep ? '김경리' : '박대표',\n status: action === 'approve' ? '승인' : '반려',\n date: today,\n comment: comment,\n };\n\n let newStatus = e.status;\n let newCurrentStep = currentStep;\n\n if (action === 'approve') {\n if (isAccountingStep && needsCeoApproval) {\n newStatus = '대표승인대기';\n newCurrentStep = 2;\n } else {\n newStatus = '승인완료';\n }\n } else {\n newStatus = '반려';\n }\n\n return {\n ...e,\n approvalFlow: newApprovalFlow,\n status: newStatus,\n currentStep: newCurrentStep,\n };\n }));\n\n setShowPanel(null);\n alert(`${action === 'approve' ? '승인' : '반려'}이 완료되었습니다.\\n결의번호: ${expense.expenseNo}`);\n };\n\n // 지급 처리\n const processPayment = (expense) => {\n const today = new Date().toISOString().split('T')[0];\n setExpenses(prev => prev.map(e =>\n e.id === expense.id ? { ...e, status: '지급완료', actualPaymentDate: today } : e\n ));\n alert(`지급이 완료되었습니다.\\n결의번호: ${expense.expenseNo}\\n금액: ${expense.totalAmount.toLocaleString()}원`);\n };\n\n const filtered = expenses.filter(e => {\n const matchSearch = e.expenseNo.toLowerCase().includes(search.toLowerCase()) ||\n e.title.includes(search) || e.vendor.includes(search);\n if (activeTab === 'list') return matchSearch;\n if (activeTab === 'accountingPending') return matchSearch && e.status === '경리승인대기';\n if (activeTab === 'ceoPending') return matchSearch && e.status === '대표승인대기';\n if (activeTab === 'approved') return matchSearch && e.status === '승인완료';\n if (activeTab === 'paid') return matchSearch && e.status === '지급완료';\n return matchSearch;\n });\n\n // 공통 UX: 지출결의서 목록 선택\n const {\n selectedIds: expenseSelectedIds,\n handleSelect: handleExpenseSelect,\n handleSelectAll: handleExpenseSelectAll,\n isAllSelected: isExpenseAllSelected,\n hasSelection: hasExpenseSelection,\n isMultiSelect: isExpenseMultiSelect,\n isSelected: isExpenseSelected,\n } = useListSelection(filtered);\n\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n // 통계\n const accountingPendingAmount = expenses.filter(e => e.status === '경리승인대기').reduce((sum, e) => sum + e.totalAmount, 0);\n const ceoPendingAmount = expenses.filter(e => e.status === '대표승인대기').reduce((sum, e) => sum + e.totalAmount, 0);\n const approvedAmount = expenses.filter(e => e.status === '승인완료').reduce((sum, e) => sum + e.totalAmount, 0);\n const paidAmount = expenses.filter(e => e.status === '지급완료').reduce((sum, e) => sum + e.totalAmount, 0);\n\n return (\n \n
\n\n
\n \n \n \n \n \n
\n\n {/* 결재 흐름 안내 */}\n
\n
\n 지출결의 흐름: 품의서 승인 → 지출결의서 작성 → 경리(1차승인) → 대표(최종승인) → 지급처리\n ※ 100만원 미만: 경리 단독 승인\n
\n
\n\n
\n \n
\n {tabs.map(tab => (\n \n ))}\n
\n
\n
\n {isExpenseMultiSelect && (\n
\n )}\n
\n
\n
\n\n \n \n\n {/* 결재 모달 */}\n {showPanel === 'approve' && selectedExpense && (\n
\n
\n
\n
지출결의서 결재
\n \n \n
\n
\n
결의번호{selectedExpense.expenseNo}
\n
제목{selectedExpense.title}
\n
거래처{selectedExpense.vendor}
\n
계좌번호{selectedExpense.vendorAccount}
\n
금액{selectedExpense.totalAmount.toLocaleString()}원
\n
지급예정일{selectedExpense.scheduledPaymentDate}
\n
\n\n
\n
결재 진행현황
\n
\n {selectedExpense.approvalFlow.map((flow, idx) => (\n
\n
{flow.step}차 결재
\n
{flow.role}
\n
\n {flow.status === '대기' ? '대기중' : flow.status}\n
\n
\n ))}\n
\n
\n\n
\n \n \n\n
\n \n \n \n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 금전출납부 관리 (A3)\nconst CashbookManagement = ({ mode = 'list' }) => {\n const [mainTab, setMainTab] = useState('ledger'); // ledger, account, card, monthly, daily\n const [typeFilter, setTypeFilter] = useState('all');\n const [search, setSearch] = useState('');\n const [dateRange, setDateRange] = useState({ start: '2025-12-01', end: '2025-12-15' });\n const [showRegisterModal, setShowRegisterModal] = useState(mode === 'register');\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n const [selectedEntry, setSelectedEntry] = useState(null);\n const [selectedAccount, setSelectedAccount] = useState('all');\n\n // 계좌 정보\n const accounts = [\n { id: 'KB001', name: '국민은행 법인계좌', accountNo: '123-456-789012', balance: 45670000, type: '보통예금' },\n { id: 'SH001', name: '신한은행 운영계좌', accountNo: '111-222-333444', balance: 12350000, type: '보통예금' },\n { id: 'KE001', name: '기업은행 급여계좌', accountNo: '999-888-777666', balance: 8500000, type: '보통예금' },\n ];\n\n // 카드 정보\n const cards = [\n { id: 'CARD001', name: '법인카드 (대표)', cardNo: '**** **** **** 1234', limit: 50000000, used: 12500000, bank: '삼성카드' },\n { id: 'CARD002', name: '법인카드 (경리)', cardNo: '**** **** **** 5678', limit: 10000000, used: 3200000, bank: '신한카드' },\n ];\n\n // 출납부 데이터 (확장)\n const [entries, setEntries] = useState([\n {\n id: 1, entryNo: 'CB-251201-001', entryDate: '2025-12-01', type: '입금', category: '매출수금',\n description: 'LG전자 매출대금', counterpart: 'LG전자', amount: 5940000, balance: 15940000,\n accountId: 'KB001', account: '국민 123-456-789012', registrant: '김경리', relatedNo: 'SH-251128-001', source: '수금관리'\n },\n {\n id: 2, entryNo: 'CB-251203-001', entryDate: '2025-12-03', type: '출금', category: '원자재구매',\n description: '철강자재 구매대금', counterpart: '금강철강', amount: 9350000, balance: 6590000,\n accountId: 'SH001', account: '신한 111-222-333444', registrant: '박회계', relatedNo: 'EX-251203-001', source: '지출결의서'\n },\n {\n id: 3, entryNo: 'CB-251205-001', entryDate: '2025-12-05', type: '입금', category: '매출수금',\n description: '삼성전자 1차 부분수금', counterpart: '삼성전자', amount: 7000000, balance: 13590000,\n accountId: 'KB001', account: '국민 123-456-789012', registrant: '김경리', relatedNo: 'SH-251201-001', source: '수금관리'\n },\n {\n id: 4, entryNo: 'CB-251208-001', entryDate: '2025-12-08', type: '출금', category: '급여',\n description: '12월 급여 지급', counterpart: '직원급여', amount: 25000000, balance: -11410000,\n accountId: 'KE001', account: '기업 999-888-777666', registrant: '박회계', relatedNo: '', source: '직접등록'\n },\n {\n id: 5, entryNo: 'CB-251210-001', entryDate: '2025-12-10', type: '입금', category: '매출수금',\n description: '현대건설 계약금', counterpart: '현대건설', amount: 35200000, balance: 23790000,\n accountId: 'KB001', account: '국민 123-456-789012', registrant: '김경리', relatedNo: 'SH-251205-001', source: '수금관리'\n },\n ]);\n\n // 카드 사용내역\n const [cardEntries, setCardEntries] = useState([\n { id: 1, cardId: 'CARD001', useDate: '2025-12-05', merchant: '주유소', amount: 150000, category: '차량유지', approvalNo: '12345678', status: '승인' },\n { id: 2, cardId: 'CARD001', useDate: '2025-12-08', merchant: '사무용품점', amount: 85000, category: '소모품', approvalNo: '23456789', status: '승인' },\n { id: 3, cardId: 'CARD002', useDate: '2025-12-10', merchant: '식당', amount: 120000, category: '접대비', approvalNo: '34567890', status: '승인' },\n { id: 4, cardId: 'CARD001', useDate: '2025-12-12', merchant: '호텔', amount: 350000, category: '출장비', approvalNo: '45678901', status: '승인' },\n ]);\n\n // 통계 계산\n const totalIncome = entries.filter(e => e.type === '입금').reduce((sum, e) => sum + e.amount, 0);\n const totalExpense = entries.filter(e => e.type === '출금').reduce((sum, e) => sum + e.amount, 0);\n const totalBalance = accounts.reduce((sum, a) => sum + a.balance, 0);\n const totalCardUsed = cards.reduce((sum, c) => sum + c.used, 0);\n\n // 카테고리별 집계\n const categoryStats = entries.reduce((acc, e) => {\n if (!acc[e.category]) acc[e.category] = { income: 0, expense: 0 };\n if (e.type === '입금') acc[e.category].income += e.amount;\n else acc[e.category].expense += e.amount;\n return acc;\n }, {});\n\n // 일별 집계\n const dailyStats = entries.reduce((acc, e) => {\n if (!acc[e.entryDate]) acc[e.entryDate] = { income: 0, expense: 0, count: 0 };\n if (e.type === '입금') acc[e.entryDate].income += e.amount;\n else acc[e.entryDate].expense += e.amount;\n acc[e.entryDate].count += 1;\n return acc;\n }, {});\n\n // 월별 집계\n const monthlyStats = entries.reduce((acc, e) => {\n const month = e.entryDate.substring(0, 7);\n if (!acc[month]) acc[month] = { income: 0, expense: 0, count: 0 };\n if (e.type === '입금') acc[month].income += e.amount;\n else acc[month].expense += e.amount;\n acc[month].count += 1;\n return acc;\n }, {});\n\n // 필터링\n const filtered = entries.filter(e => {\n const matchSearch = e.entryNo.toLowerCase().includes(search.toLowerCase()) ||\n e.description.includes(search) || e.counterpart.includes(search);\n const matchType = typeFilter === 'all' || e.type === typeFilter;\n const matchAccount = selectedAccount === 'all' || e.accountId === selectedAccount;\n return matchSearch && matchType && matchAccount;\n });\n\n // 공통 UX: 출납원장 선택\n const {\n selectedIds: ledgerSelectedIds,\n handleSelect: handleLedgerSelect,\n handleSelectAll: handleLedgerSelectAll,\n isAllSelected: isLedgerAllSelected,\n hasSelection: hasLedgerSelection,\n isMultiSelect: isLedgerMultiSelect,\n isSelected: isLedgerSelected,\n } = useListSelection(filtered);\n\n // 공통 UX: 카드 사용내역 선택\n const {\n selectedIds: cardSelectedIds,\n handleSelect: handleCardSelect,\n handleSelectAll: handleCardSelectAll,\n isAllSelected: isCardAllSelected,\n hasSelection: hasCardSelection,\n isMultiSelect: isCardMultiSelect,\n isSelected: isCardSelected,\n } = useListSelection(cardEntries);\n\n // 출납 등록\n const handleRegisterEntry = (formData) => {\n const today = new Date().toISOString().split('T')[0];\n const newEntry = {\n id: entries.length + 1,\n entryNo: `CB-${today.replace(/-/g, '').substring(2)}-${String(entries.length + 1).padStart(3, '0')}`,\n entryDate: formData.entryDate || today,\n type: formData.type,\n category: formData.category,\n description: formData.description,\n counterpart: formData.counterpart,\n amount: parseInt(formData.amount),\n balance: 0,\n accountId: formData.accountId,\n account: accounts.find(a => a.id === formData.accountId)?.name || '',\n registrant: '김경리',\n relatedNo: '',\n source: '직접등록'\n };\n setEntries(prev => [...prev, newEntry]);\n alert('출납 내역이 등록되었습니다.');\n setShowRegisterModal(false);\n };\n\n const mainTabs = [\n { id: 'ledger', label: '출납원장' },\n { id: 'account', label: '계좌별 현황' },\n { id: 'card', label: '법인카드 관리' },\n { id: 'daily', label: '일별 현황' },\n { id: 'monthly', label: '월별 현황' },\n { id: 'category', label: '항목별 집계' },\n ];\n\n return (\n \n
\n\n
\n \n \n \n \n \n
\n\n {/* 연동 안내 */}\n
\n
\n
\n 수금관리에서 수금 등록 시 자동으로 입금 내역이 생성됩니다.\n 지출결의서 지급 완료 시 자동으로 출금 내역이 생성됩니다.\n
\n
\n\n {/* 메인 탭 */}\n
\n {mainTabs.map(tab => (\n \n ))}\n
\n\n {/* 출납원장 탭 */}\n {mainTab === 'ledger' && (\n
\n \n
\n {[\n { id: 'all', label: '전체' },\n { id: '입금', label: '입금' },\n { id: '출금', label: '출금' },\n ].map(tab => (\n \n ))}\n \n
\n
\n
setDateRange(prev => ({ ...prev, start: e.target.value }))}\n className=\"px-3 py-2 border rounded-lg text-sm\" />\n
~\n
setDateRange(prev => ({ ...prev, end: e.target.value }))}\n className=\"px-3 py-2 border rounded-lg text-sm\" />\n
\n {isLedgerMultiSelect && (\n
\n )}\n
\n
\n
\n\n \n \n )}\n\n {/* 계좌별 현황 탭 */}\n {mainTab === 'account' && (\n
\n
\n {accounts.map(account => (\n
\n \n
\n
\n \n
\n
\n
{account.name}
\n
{account.accountNo}
\n
\n
\n
{account.type}\n
\n \n
현재잔액
\n
{account.balance.toLocaleString()}원
\n
\n \n 입금 {entries.filter(e => e.accountId === account.id && e.type === '입금').length}건\n 출금 {entries.filter(e => e.accountId === account.id && e.type === '출금').length}건\n
\n \n ))}\n
\n\n
\n 계좌별 입출금 내역
\n \n \n \n | 계좌명 | \n 계좌번호 | \n 입금 합계 | \n 출금 합계 | \n 차액 | \n 현재잔액 | \n
\n \n \n {accounts.map(account => {\n const income = entries.filter(e => e.accountId === account.id && e.type === '입금').reduce((sum, e) => sum + e.amount, 0);\n const expense = entries.filter(e => e.accountId === account.id && e.type === '출금').reduce((sum, e) => sum + e.amount, 0);\n return (\n \n | {account.name} | \n {account.accountNo} | \n +{income.toLocaleString()} | \n -{expense.toLocaleString()} | \n = 0 ? 'text-blue-600' : 'text-red-600'}`}>\n {(income - expense).toLocaleString()}\n | \n {account.balance.toLocaleString()} | \n
\n );\n })}\n \n \n \n | 합계 | \n +{totalIncome.toLocaleString()} | \n -{totalExpense.toLocaleString()} | \n {(totalIncome - totalExpense).toLocaleString()} | \n {totalBalance.toLocaleString()} | \n
\n \n
\n \n
\n )}\n\n {/* 법인카드 관리 탭 */}\n {mainTab === 'card' && (\n
\n
\n {cards.map(card => (\n
\n \n
\n
\n \n
\n
\n
{card.name}
\n
{card.cardNo}
\n
\n
\n
{card.bank}\n
\n \n
\n 한도\n {card.limit.toLocaleString()}원\n
\n
\n 사용액\n {card.used.toLocaleString()}원\n
\n
\n
\n 잔여한도\n {(card.limit - card.used).toLocaleString()}원\n
\n
\n \n ))}\n
\n\n
\n \n
카드 사용내역
\n
\n {isCardMultiSelect && (\n \n )}\n \n
\n
\n \n \n
\n )}\n\n {/* 일별 현황 탭 */}\n {mainTab === 'daily' && (\n
\n 일별 입출금 현황
\n \n \n \n | 일자 | \n 건수 | \n 입금 | \n 출금 | \n 차액 | \n
\n \n \n {Object.entries(dailyStats).sort((a, b) => b[0].localeCompare(a[0])).map(([date, stats]) => (\n \n | {date} | \n {stats.count}건 | \n +{stats.income.toLocaleString()} | \n -{stats.expense.toLocaleString()} | \n = 0 ? 'text-blue-600' : 'text-red-600'}`}>\n {(stats.income - stats.expense).toLocaleString()}\n | \n
\n ))}\n \n
\n \n )}\n\n {/* 월별 현황 탭 */}\n {mainTab === 'monthly' && (\n
\n 월별 입출금 현황
\n \n \n \n | 월 | \n 건수 | \n 입금 | \n 출금 | \n 차액 | \n
\n \n \n {Object.entries(monthlyStats).sort((a, b) => b[0].localeCompare(a[0])).map(([month, stats]) => (\n \n | {month} | \n {stats.count}건 | \n +{stats.income.toLocaleString()} | \n -{stats.expense.toLocaleString()} | \n = 0 ? 'text-blue-600' : 'text-red-600'}`}>\n {(stats.income - stats.expense).toLocaleString()}\n | \n
\n ))}\n \n
\n \n )}\n\n {/* 항목별 집계 탭 */}\n {mainTab === 'category' && (\n
\n 항목별 입출금 집계
\n \n \n \n | 항목 | \n 입금 | \n 출금 | \n 차액 | \n
\n \n \n {Object.entries(categoryStats).map(([category, stats]) => (\n \n | {category} | \n {stats.income > 0 ? `+${stats.income.toLocaleString()}` : '-'} | \n {stats.expense > 0 ? `-${stats.expense.toLocaleString()}` : '-'} | \n = 0 ? 'text-blue-600' : 'text-red-600'}`}>\n {(stats.income - stats.expense).toLocaleString()}\n | \n
\n ))}\n \n \n \n | 합계 | \n +{totalIncome.toLocaleString()} | \n -{totalExpense.toLocaleString()} | \n {(totalIncome - totalExpense).toLocaleString()} | \n
\n \n
\n \n )}\n\n {/* 출납 등록 모달 */}\n {showRegisterModal && (\n
\n
\n
\n
출납 등록
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// 수금 등록 (출하관리 연동) (A4-1)\nconst CollectionRegister = ({ shipments = [], onUpdateShipment, onNavigate, mode = 'list' }) => {\n const [search, setSearch] = useState('');\n const [activeTab, setActiveTab] = useState('pending'); // pending, history, customer, reminder\n const [showRegisterPanel, setShowRegisterPanel] = useState(mode === 'register');\n const [selectedShipment, setSelectedShipment] = useState(null);\n const [showReminderModal, setShowReminderModal] = useState(false);\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n const [selectedForReminder, setSelectedForReminder] = useState([]);\n const [reminderHistory, setReminderHistory] = useState([\n { id: 1, date: '2025-12-10', customerName: '대우건설', type: '이메일', amount: 8800000, result: '발송완료', sender: '김경리' },\n { id: 2, date: '2025-12-05', customerName: '신흥건설', type: '팩스', amount: 12500000, result: '발송완료', sender: '김경리' },\n ]);\n\n // 출하완료 건 중 수금 미완료 건 (확장된 데이터)\n const pendingCollections = [\n {\n id: 1, shipmentNo: 'SH-251201-001', splitNo: 'KD-TS-251201-01-S1', orderNo: 'KD-TS-251201-01',\n customerCode: 'C001', customerName: '삼성전자', creditGrade: 'A', grandTotal: 13750000, collectedAmount: 7000000, remainAmount: 6750000,\n dueDate: '2025-12-31', overdueDays: 0, paymentConfirmed: false, lastCollectionDate: '2025-12-08', contactPerson: '김부장', phone: '010-1234-5678', email: 'kim@samsung.com'\n },\n {\n id: 2, shipmentNo: 'SH-251205-001', splitNo: 'KD-BD-251205-01-S1', orderNo: 'KD-BD-251205-01',\n customerCode: 'C003', customerName: '현대건설', creditGrade: 'A', grandTotal: 35200000, collectedAmount: 0, remainAmount: 35200000,\n dueDate: '2026-01-05', overdueDays: 0, paymentConfirmed: false, lastCollectionDate: '-', contactPerson: '이과장', phone: '010-2345-6789', email: 'lee@hyundai.com'\n },\n {\n id: 3, shipmentNo: 'SH-251120-001', splitNo: 'KD-DW-251120-01-S1', orderNo: 'KD-DW-251120-01',\n customerCode: 'C004', customerName: '대우건설', creditGrade: 'B', grandTotal: 8800000, collectedAmount: 0, remainAmount: 8800000,\n dueDate: '2025-12-01', overdueDays: 14, paymentConfirmed: false, lastCollectionDate: '-', contactPerson: '박대리', phone: '010-3456-7890', email: 'park@daewoo.com'\n },\n {\n id: 4, shipmentNo: 'SH-251115-001', splitNo: 'KD-SH-251115-01-S1', orderNo: 'KD-SH-251115-01',\n customerCode: 'C005', customerName: '신흥건설', creditGrade: 'C', grandTotal: 12500000, collectedAmount: 0, remainAmount: 12500000,\n dueDate: '2025-11-30', overdueDays: 15, paymentConfirmed: false, lastCollectionDate: '-', contactPerson: '최과장', phone: '010-4567-8901', email: 'choi@shinheung.com'\n },\n ];\n\n const [collections, setCollections] = useState(pendingCollections);\n\n // 공통 UX: 미수금 목록 선택\n const pendingFiltered = collections.filter(c =>\n c.remainAmount > 0 &&\n (c.shipmentNo.toLowerCase().includes(search.toLowerCase()) ||\n c.customerName.includes(search))\n );\n const {\n selectedIds: pendingSelectedIds,\n handleSelect: handlePendingSelect,\n handleSelectAll: handlePendingSelectAll,\n isAllSelected: isPendingAllSelected,\n hasSelection: hasPendingSelection,\n isMultiSelect: isPendingMultiSelect,\n isSelected: isPendingSelected,\n } = useListSelection(pendingFiltered);\n\n // 공통 UX: 수금 이력 선택\n const historyFiltered = collectionHistory.filter(h =>\n h.shipmentNo.toLowerCase().includes(search.toLowerCase()) ||\n h.customerName.includes(search)\n );\n const {\n selectedIds: historySelectedIds,\n handleSelect: handleHistorySelect,\n handleSelectAll: handleHistorySelectAll,\n isAllSelected: isHistoryAllSelected,\n hasSelection: hasHistorySelection,\n isMultiSelect: isHistoryMultiSelect,\n isSelected: isHistorySelected,\n } = useListSelection(historyFiltered);\n\n // 수금 이력\n const [collectionHistory, setCollectionHistory] = useState([\n { id: 1, date: '2025-12-08', shipmentNo: 'SH-251201-001', customerName: '삼성전자', amount: 7000000, method: '계좌이체', account: '국민 123-456-789012', registrar: '김경리', note: '1차 입금' },\n { id: 2, date: '2025-12-01', shipmentNo: 'SH-251128-001', customerName: 'LG전자', amount: 15000000, method: '계좌이체', account: '신한 111-222-333444', registrar: '김경리', note: '전액 입금' },\n { id: 3, date: '2025-11-28', shipmentNo: 'SH-251125-001', customerName: '한화건설', amount: 22000000, method: '어음', account: '-', registrar: '김경리', note: '3개월 어음' },\n ]);\n\n const handleRegisterCollection = (item, amount, method) => {\n const today = new Date().toISOString().split('T')[0];\n const newCollectedAmount = item.collectedAmount + amount;\n const newRemainAmount = item.grandTotal - newCollectedAmount;\n const isFullyPaid = newRemainAmount <= 0;\n\n setCollections(prev => prev.map(c =>\n c.id === item.id ? {\n ...c,\n collectedAmount: newCollectedAmount,\n remainAmount: newRemainAmount,\n paymentConfirmed: isFullyPaid,\n lastCollectionDate: today\n } : c\n ));\n\n // 수금 이력 추가\n setCollectionHistory(prev => [{\n id: prev.length + 1,\n date: today,\n shipmentNo: item.shipmentNo,\n customerName: item.customerName,\n amount: amount,\n method: method,\n account: '국민 123-456-789012',\n registrar: '김경리',\n note: isFullyPaid ? '전액 입금' : '부분 입금'\n }, ...prev]);\n\n // 출하관리 paymentConfirmed 업데이트 (실제 연동)\n if (onUpdateShipment && isFullyPaid) {\n onUpdateShipment(item.shipmentNo, { paymentConfirmed: true });\n }\n\n alert(`수금이 등록되었습니다.\\n\\n수금금액: ${amount.toLocaleString()}원\\n결제방법: ${method}\\n잔액: ${newRemainAmount.toLocaleString()}원${isFullyPaid ? '\\n\\n✅ 전액 수금 완료 - 출하 가능 상태로 변경됨' : ''}`);\n setShowRegisterPanel(false);\n };\n\n // 독촉장 발송\n const handleSendReminder = (type) => {\n const today = new Date().toISOString().split('T')[0];\n const newReminders = selectedForReminder.map((item, idx) => ({\n id: reminderHistory.length + idx + 1,\n date: today,\n customerName: item.customerName,\n type: type,\n amount: item.remainAmount,\n result: '발송완료',\n sender: '김경리'\n }));\n setReminderHistory(prev => [...newReminders, ...prev]);\n alert(`${selectedForReminder.length}건의 독촉장이 ${type}로 발송되었습니다.`);\n setShowReminderModal(false);\n setSelectedForReminder([]);\n };\n\n const totalRemain = collections.reduce((sum, c) => sum + c.remainAmount, 0);\n const totalCollected = collections.reduce((sum, c) => sum + c.collectedAmount, 0);\n const overdueCount = collections.filter(c => c.overdueDays > 0).length;\n const overdueAmount = collections.filter(c => c.overdueDays > 0).reduce((sum, c) => sum + c.remainAmount, 0);\n\n // 거래처별 집계\n const customerSummary = collections.reduce((acc, c) => {\n if (!acc[c.customerCode]) {\n acc[c.customerCode] = {\n customerCode: c.customerCode,\n customerName: c.customerName,\n creditGrade: c.creditGrade,\n totalAmount: 0,\n collectedAmount: 0,\n remainAmount: 0,\n count: 0,\n maxOverdueDays: 0,\n contactPerson: c.contactPerson,\n phone: c.phone\n };\n }\n acc[c.customerCode].totalAmount += c.grandTotal;\n acc[c.customerCode].collectedAmount += c.collectedAmount;\n acc[c.customerCode].remainAmount += c.remainAmount;\n acc[c.customerCode].count += 1;\n acc[c.customerCode].maxOverdueDays = Math.max(acc[c.customerCode].maxOverdueDays, c.overdueDays);\n return acc;\n }, {});\n\n const tabs = [\n { id: 'pending', label: '미수금 현황', count: collections.filter(c => c.remainAmount > 0).length },\n { id: 'overdue', label: '연체 관리', count: overdueCount },\n { id: 'history', label: '수금 이력' },\n { id: 'customer', label: '거래처별 현황' },\n { id: 'reminder', label: '독촉장 발송' },\n ];\n\n return (\n \n
\n\n
\n c.remainAmount > 0).length}건`} color=\"orange\" />\n \n \n \n \n
\n\n {/* 연동 안내 */}\n
\n
\n
\n 수금 완료 시 해당 출하건의 '입금확인' 상태가 자동으로 변경됩니다.\n B/C등급 거래처는 입금확인 후 출고 가능합니다.\n
\n
\n\n {/* 탭 네비게이션 */}\n
\n {tabs.map(tab => (\n \n ))}\n
\n\n {/* 미수금 현황 탭 */}\n {activeTab === 'pending' && (\n
\n \n
미수금 현황
\n
\n {isPendingMultiSelect && (\n \n )}\n \n
\n
\n\n \n \n )}\n\n {/* 연체 관리 탭 */}\n {activeTab === 'overdue' && (\n
\n \n
연체 현황
\n
\n \n
\n
\n\n {overdueCount === 0 ? (\n 연체된 미수금이 없습니다.
\n ) : (\n <>\n {/* 연체 경고 */}\n \n
\n
\n
연체 경고: {overdueCount}건 / {overdueAmount.toLocaleString()}원\n
\n
결제예정일이 지난 미수금입니다. 신속한 수금 조치가 필요합니다.
\n
\n\n \n \n \n | 출하번호 | \n 거래처 | \n 신용등급 | \n 미수잔액 | \n 결제예정일 | \n 연체일수 | \n 담당자 | \n 조치 | \n
\n \n \n {collections.filter(c => c.overdueDays > 0).map(item => (\n \n | {item.shipmentNo} | \n {item.customerName} | \n \n {item.creditGrade}\n | \n {item.remainAmount.toLocaleString()} | \n {item.dueDate} | \n \n 30 ? 'bg-red-200 text-red-800' :\n item.overdueDays > 14 ? 'bg-orange-200 text-orange-800' : 'bg-yellow-200 text-yellow-800'\n }`}>{item.overdueDays}일\n | \n \n {item.contactPerson} \n {item.phone} \n | \n \n \n \n \n \n \n | \n
\n ))}\n \n
\n >\n )}\n \n )}\n\n {/* 수금 이력 탭 */}\n {activeTab === 'history' && (\n
\n \n
수금 이력
\n
\n {isHistoryMultiSelect && (\n \n )}\n \n
\n
\n\n \n \n )}\n\n {/* 거래처별 현황 탭 */}\n {activeTab === 'customer' && (\n
\n \n
거래처별 미수금 현황
\n \n\n \n \n \n | 거래처코드 | \n 거래처명 | \n 신용등급 | \n 건수 | \n 매출총액 | \n 수금액 | \n 미수잔액 | \n 최대연체 | \n 담당자 | \n
\n \n \n {Object.values(customerSummary).filter(c => c.remainAmount > 0).map(item => (\n 0 ? 'bg-red-50' : ''}`}>\n | {item.customerCode} | \n {item.customerName} | \n \n {item.creditGrade}\n | \n {item.count}건 | \n {item.totalAmount.toLocaleString()} | \n {item.collectedAmount.toLocaleString()} | \n {item.remainAmount.toLocaleString()} | \n \n {item.maxOverdueDays > 0 ? (\n {item.maxOverdueDays}일\n ) : (\n -\n )}\n | \n \n {item.contactPerson} \n {item.phone} \n | \n
\n ))}\n \n
\n \n )}\n\n {/* 독촉장 발송 탭 */}\n {activeTab === 'reminder' && (\n
\n \n
독촉장 발송 이력
\n \n \n\n \n \n \n | 발송일 | \n 거래처 | \n 발송방법 | \n 독촉금액 | \n 결과 | \n 담당자 | \n
\n \n \n {reminderHistory.map(item => (\n \n | {item.date} | \n {item.customerName} | \n \n {item.type}\n | \n {item.amount.toLocaleString()} | \n \n {item.result}\n | \n {item.sender} | \n
\n ))}\n \n
\n \n )}\n\n {/* 수금 등록 모달 */}\n {showRegisterPanel && selectedShipment && (\n
\n
\n
\n
수금 등록
\n \n \n
\n
\n
\n
출하번호
{selectedShipment.shipmentNo}
\n
거래처
{selectedShipment.customerName}
\n
매출금액
{selectedShipment.grandTotal.toLocaleString()}원
\n
미수잔액
{selectedShipment.remainAmount.toLocaleString()}원
\n {selectedShipment.overdueDays > 0 && (\n
\n
연체현황
\n
{selectedShipment.overdueDays}일 연체
\n
\n )}\n
\n
\n
\n \n \n \n \n
\n \n
\n \n \n
\n \n \n
\n \n \n
\n \n \n
\n
\n
\n
\n )}\n\n {/* 독촉장 발송 모달 */}\n {showReminderModal && (\n
\n
\n
\n
독촉장 발송
\n \n \n
\n
\n
발송 대상: {selectedForReminder.length}건
\n
총 독촉금액: {selectedForReminder.reduce((sum, i) => sum + i.remainAmount, 0).toLocaleString()}원
\n
\n
\n {selectedForReminder.map(item => (\n
\n {item.customerName}\n {item.remainAmount.toLocaleString()}원\n
\n ))}\n
\n
\n \n {['이메일', '팩스', 'SMS'].map(type => (\n \n ))}\n
\n \n
\n \n \n
\n \n \n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 어음관리 (A4-3)\nconst BillManagement = () => {\n const [search, setSearch] = useState('');\n const [filterStatus, setFilterStatus] = useState('all');\n\n const [bills, setBills] = useState([\n {\n id: 1, billNo: 'BL-251201-001', customerName: '삼성전자', amount: 6750000,\n issueDate: '2025-12-01', dueDate: '2026-03-01', status: '보유중', note: '3개월 어음'\n },\n {\n id: 2, billNo: 'BL-251105-001', customerName: '대우건설', amount: 8800000,\n issueDate: '2025-11-05', dueDate: '2026-02-05', status: '보유중', note: '3개월 어음'\n },\n ]);\n\n const statusFilters = [\n { id: 'all', label: '전체' },\n { id: '보유중', label: '보유중' },\n { id: '만기도래', label: '만기도래' },\n { id: '추심완료', label: '추심완료' },\n ];\n\n return (\n \n
\n\n
\n \n sum + b.amount, 0).toLocaleString()}원`} color=\"purple\" />\n b.status === '만기도래').length}건`} color=\"orange\" />\n b.status === '추심완료').length}건`} color=\"green\" />\n
\n\n
\n \n
\n {statusFilters.map(filter => (\n \n ))}\n
\n
\n
\n\n \n \n \n | 어음번호 | \n 거래처 | \n 금액 | \n 발행일 | \n 만기일 | \n 상태 | \n 비고 | \n 관리 | \n
\n \n \n {bills.map(bill => (\n \n | {bill.billNo} | \n {bill.customerName} | \n {bill.amount.toLocaleString()} | \n {bill.issueDate} | \n {bill.dueDate} | \n | \n {bill.note} | \n \n \n | \n
\n ))}\n \n
\n \n
\n );\n};\n\n// 원가관리 (A5)\nconst CostAnalysisManagement = () => {\n const [activeTab, setActiveTab] = useState('summary');\n const [search, setSearch] = useState('');\n const [selectedOrder, setSelectedOrder] = useState(null);\n\n // 수주별 원가 데이터\n const costData = [\n {\n id: 1, orderNo: 'KD-TS-251201-01', customerName: '삼성전자', siteName: '삼성 서초사옥',\n saleAmount: 12500000, materialCost: 4500000, laborCost: 2000000, outsourcingCost: 1500000, overheadCost: 800000,\n totalCost: 8800000, profit: 3700000, profitRate: 29.6\n },\n {\n id: 2, orderNo: 'KD-SL-251203-01', customerName: 'LG전자', siteName: 'LG 트윈타워',\n saleAmount: 5400000, materialCost: 1800000, laborCost: 900000, outsourcingCost: 600000, overheadCost: 350000,\n totalCost: 3650000, profit: 1750000, profitRate: 32.4\n },\n {\n id: 3, orderNo: 'KD-BD-251205-01', customerName: '현대건설', siteName: '현대 본사',\n saleAmount: 32000000, materialCost: 12000000, laborCost: 5000000, outsourcingCost: 4000000, overheadCost: 2000000,\n totalCost: 23000000, profit: 9000000, profitRate: 28.1\n },\n ];\n\n const tabs = [\n { id: 'summary', label: '원가요약', icon: BarChart3 },\n { id: 'material', label: '자재비', icon: Box },\n { id: 'outsourcing', label: '외주비', icon: Truck },\n { id: 'labor', label: '인건비', icon: Users },\n { id: 'overhead', label: '경비', icon: CreditCard },\n { id: 'manufacturing', label: '제조원가', icon: Factory },\n ];\n\n const totalSale = costData.reduce((sum, c) => sum + c.saleAmount, 0);\n const totalCost = costData.reduce((sum, c) => sum + c.totalCost, 0);\n const totalProfit = costData.reduce((sum, c) => sum + c.profit, 0);\n const avgProfitRate = (totalProfit / totalSale * 100).toFixed(1);\n\n return (\n \n
\n\n
\n \n \n \n \n
\n\n {/* 탭 */}\n
\n {tabs.map(tab => (\n \n ))}\n
\n\n {activeTab === 'summary' && (\n
\n \n
수주별 원가 현황
\n \n \n\n \n \n \n | 수주번호 | \n 거래처 | \n 매출액 | \n 자재비 | \n 인건비 | \n 외주비 | \n 경비 | \n 총원가 | \n 이익 | \n 이익률 | \n
\n \n \n {costData.map(item => (\n \n | {item.orderNo} | \n {item.customerName} | \n {item.saleAmount.toLocaleString()} | \n {item.materialCost.toLocaleString()} | \n {item.laborCost.toLocaleString()} | \n {item.outsourcingCost.toLocaleString()} | \n {item.overheadCost.toLocaleString()} | \n {item.totalCost.toLocaleString()} | \n {item.profit.toLocaleString()} | \n \n = 30 ? 'bg-green-100 text-green-700' :\n item.profitRate >= 20 ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700'\n }`}>{item.profitRate}%\n | \n
\n ))}\n \n \n \n | 합계 | \n {totalSale.toLocaleString()} | \n {costData.reduce((sum, c) => sum + c.materialCost, 0).toLocaleString()} | \n {costData.reduce((sum, c) => sum + c.laborCost, 0).toLocaleString()} | \n {costData.reduce((sum, c) => sum + c.outsourcingCost, 0).toLocaleString()} | \n {costData.reduce((sum, c) => sum + c.overheadCost, 0).toLocaleString()} | \n {totalCost.toLocaleString()} | \n {totalProfit.toLocaleString()} | \n {avgProfitRate}% | \n
\n \n
\n \n )}\n\n {activeTab === 'material' && (\n
\n \n {costData.map(item => (\n
\n
\n {item.orderNo}\n {item.materialCost.toLocaleString()}원\n
\n
{item.customerName} - {item.siteName}
\n
\n
매출대비 {((item.materialCost / item.saleAmount) * 100).toFixed(1)}%
\n
\n ))}\n
\n \n )}\n\n {activeTab === 'manufacturing' && (\n
\n \n
\n
원가 구성비
\n
\n {[\n { label: '자재비', value: costData.reduce((sum, c) => sum + c.materialCost, 0), color: 'orange' },\n { label: '인건비', value: costData.reduce((sum, c) => sum + c.laborCost, 0), color: 'blue' },\n { label: '외주비', value: costData.reduce((sum, c) => sum + c.outsourcingCost, 0), color: 'purple' },\n { label: '경비', value: costData.reduce((sum, c) => sum + c.overheadCost, 0), color: 'gray' },\n ].map(item => (\n
\n
{item.label}\n
\n
{((item.value / totalCost) * 100).toFixed(1)}%\n
{(item.value / 10000).toLocaleString()}만\n
\n ))}\n
\n
\n
\n
이익률 분포
\n
\n {costData.map(item => (\n
\n
{item.customerName}\n
\n
= 30 ? 'bg-green-500' : item.profitRate >= 20 ? 'bg-blue-500' : 'bg-red-500'\n }`} style={{ width: `${item.profitRate}%` }} />\n
\n
{item.profitRate}%\n
\n ))}\n
\n
\n
\n \n )}\n\n {['outsourcing', 'labor', 'overhead'].includes(activeTab) && (\n
t.id === activeTab)?.label} 상세`}>\n 상세 데이터가 표시됩니다.
\n \n )}\n
\n );\n};\n\n// =============================================\n// 시스템관리 컴포넌트\n// =============================================\n\n// 사용자관리\nconst UserManagement = () => {\n const [search, setSearch] = useState('');\n const [showPanel, setShowPanel] = useState(false);\n const [selectedUser, setSelectedUser] = useState(null);\n const [panelMode, setPanelMode] = useState('create');\n\n const [users, setUsers] = useState([\n {\n id: 1, userId: 'admin', userName: '관리자', email: 'admin@kd.com', dept: '시스템관리',\n role: '시스템관리자', phone: '010-1111-1111', status: '활성', lastLogin: '2025-12-09 09:30', createdAt: '2024-01-01'\n },\n {\n id: 2, userId: 'sales01', userName: '김판매', email: 'sales01@kd.com', dept: '판매부',\n role: '판매담당', phone: '010-2222-2222', status: '활성', lastLogin: '2025-12-09 08:45', createdAt: '2024-03-15'\n },\n {\n id: 3, userId: 'prod01', userName: '이생산', email: 'prod01@kd.com', dept: '생산부',\n role: '생산담당', phone: '010-3333-3333', status: '활성', lastLogin: '2025-12-08 17:20', createdAt: '2024-06-01'\n },\n {\n id: 4, userId: 'qa01', userName: '박품질', email: 'qa01@kd.com', dept: '품질관리부',\n role: '품질담당', phone: '010-4444-4444', status: '비활성', lastLogin: '2025-11-30 15:00', createdAt: '2024-09-01'\n },\n ]);\n\n // 통계\n const activeCount = users.filter(u => u.status === '활성').length;\n const inactiveCount = users.filter(u => u.status === '비활성').length;\n const deptGroups = [...new Set(users.map(u => u.dept))].length;\n\n const filtered = users.filter(u =>\n u.userId.toLowerCase().includes(search.toLowerCase()) ||\n u.userName.includes(search) ||\n u.dept.includes(search) ||\n u.email.toLowerCase().includes(search.toLowerCase())\n );\n\n const handleCreateUser = () => {\n setPanelMode('create');\n setSelectedUser(null);\n setShowPanel(true);\n };\n\n const handleEditUser = (user) => {\n setPanelMode('edit');\n setSelectedUser(user);\n setShowPanel(true);\n };\n\n const handleToggleStatus = (user) => {\n const newStatus = user.status === '활성' ? '비활성' : '활성';\n setUsers(prev => prev.map(u => u.id === user.id ? { ...u, status: newStatus } : u));\n alert(`${user.userName} 사용자가 ${newStatus} 상태로 변경되었습니다.`);\n };\n\n return (\n \n
\n\n {/* 통계 카드 */}\n
\n \n \n \n \n
\n\n {/* 검색 및 등록 */}\n
\n \n\n \n \n \n | 사용자ID | \n 이름 | \n 이메일 | \n 부서 | \n 권한 | \n 상태 | \n 최종로그인 | \n 관리 | \n
\n \n \n {filtered.map(user => (\n \n | {user.userId} | \n {user.userName} | \n {user.email} | \n {user.dept} | \n \n {user.role}\n | \n \n {user.status}\n | \n {user.lastLogin} | \n \n \n \n \n \n \n | \n
\n ))}\n \n
\n \n
\n );\n};\n\n// 권한관리\nconst RoleManagement = () => {\n const [selectedRole, setSelectedRole] = useState(null);\n const [showPanel, setShowPanel] = useState(false);\n\n const roles = [\n {\n id: 1, roleCode: 'ADMIN', roleName: '시스템관리자', description: '시스템 전체 관리 권한',\n userCount: 1, permissions: ['전체관리', '사용자관리', '권한관리', '시스템설정'], status: '활성'\n },\n {\n id: 2, roleCode: 'SALES', roleName: '판매담당', description: '판매 관련 권한',\n userCount: 5, permissions: ['견적관리', '수주관리', '거래처관리', '매출조회'], status: '활성'\n },\n {\n id: 3, roleCode: 'PRODUCTION', roleName: '생산담당', description: '생산 관련 권한',\n userCount: 8, permissions: ['작업지시관리', '작업실적', '생산현황'], status: '활성'\n },\n {\n id: 4, roleCode: 'QA', roleName: '품질담당', description: '품질 검사 관련 권한',\n userCount: 3, permissions: ['검사관리', '부적합관리', '품질현황'], status: '활성'\n },\n {\n id: 5, roleCode: 'ACCOUNTING', roleName: '회계담당', description: '회계 및 재무 관련 권한',\n userCount: 2, permissions: ['매출관리', '세금계산서', '수금관리', '미수금관리'], status: '활성'\n },\n ];\n\n const menuPermissions = [\n { group: '기준정보', menus: ['품목기준관리', '공정기준관리', '품질기준관리', '거래처기준관리'] },\n { group: '판매관리', menus: ['견적관리', '수주관리', '거래처관리', '출하관리'] },\n { group: '생산관리', menus: ['작업지시관리', '작업실적', '생산현황판'] },\n { group: '품질관리', menus: ['수입검사', '중간검사', '제품검사', '부적합관리'] },\n { group: '회계관리', menus: ['매출관리', '세금계산서', '수금관리', '미수금관리'] },\n { group: '시스템관리', menus: ['사용자관리', '권한관리', '시스템설정'] },\n ];\n\n return (\n \n
\n\n
\n {/* 권한 목록 */}\n
\n \n {roles.map(role => (\n
setSelectedRole(role)}\n className={`p-3 rounded-lg border cursor-pointer transition-colors ${selectedRole?.id === role.id ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:bg-gray-50'\n }`}\n >\n
\n
\n
{role.roleName}
\n
{role.roleCode}
\n
\n
{role.userCount}명\n
\n
{role.description}
\n
\n ))}\n
\n \n \n\n {/* 권한 상세 */}\n
\n {selectedRole ? (\n \n {/* 기본 정보 */}\n
\n\n {/* 메뉴별 권한 설정 */}\n
\n
\n
\n {menuPermissions.map(group => (\n
\n ))}\n
\n
\n\n
\n \n \n
\n
\n ) : (\n \n )}\n \n
\n
\n );\n};\n\n// 시스템설정\nconst SystemSettings = () => {\n const [activeTab, setActiveTab] = useState('general');\n const [settings, setSettings] = useState({\n companyName: '(주)금동방화',\n businessNo: '123-45-67890',\n ceo: '홍길동',\n address: '서울시 강남구 테헤란로 123',\n phone: '02-1234-5678',\n fax: '02-1234-5679',\n email: 'info@kd.com',\n logo: '',\n // 번호 설정\n orderPrefix: 'KD',\n quotePrefix: 'KD-PR',\n lotPrefix: 'KD',\n // 시스템 설정\n sessionTimeout: 30,\n maxLoginAttempts: 5,\n passwordExpireDays: 90,\n autoLogout: true,\n // 알림 설정\n emailNotification: true,\n smsNotification: false,\n overdueAlert: true,\n stockAlert: true,\n });\n\n const tabs = [\n { id: 'general', label: '회사정보', icon: Building },\n { id: 'number', label: '번호규칙', icon: Hash },\n { id: 'security', label: '보안설정', icon: ShieldCheck },\n { id: 'notification', label: '알림설정', icon: Bell },\n ];\n\n const handleSave = () => {\n alert('설정이 저장되었습니다.');\n };\n\n return (\n \n
\n\n
\n {/* 탭 메뉴 */}\n
\n {tabs.map(tab => (\n \n ))}\n
\n\n {/* 설정 내용 */}\n
\n {activeTab === 'general' && (\n \n )}\n\n {activeTab === 'number' && (\n \n
번호 생성 규칙
\n
\n
\n
\n
검사 LOT 형식
\n
\n
입고검사:YYMMDD-##
\n
중간검사:KD-WE-YYMMDD-##-(공정)
\n
제품검사:KD-SA-YYMMDD-##
\n
검사LOT:KD-QC-YYMMDD-##
\n
\n
\n
\n
\n )}\n\n {activeTab === 'security' && (\n \n
보안 설정
\n
\n
\n
\n
세션 타임아웃
\n
미사용 시 자동 로그아웃 시간
\n
\n
\n setSettings(prev => ({ ...prev, sessionTimeout: parseInt(e.target.value) }))}\n className=\"w-20 px-3 py-2 border rounded-lg text-right\" />\n 분\n
\n
\n
\n
\n
로그인 시도 제한
\n
연속 로그인 실패 시 계정 잠금
\n
\n
\n setSettings(prev => ({ ...prev, maxLoginAttempts: parseInt(e.target.value) }))}\n className=\"w-20 px-3 py-2 border rounded-lg text-right\" />\n 회\n
\n
\n
\n
\n
비밀번호 유효기간
\n
비밀번호 변경 주기
\n
\n
\n setSettings(prev => ({ ...prev, passwordExpireDays: parseInt(e.target.value) }))}\n className=\"w-20 px-3 py-2 border rounded-lg text-right\" />\n 일\n
\n
\n
\n
\n )}\n\n {activeTab === 'notification' && (\n \n
알림 설정
\n
\n
\n
\n
이메일 알림
\n
중요 이벤트 이메일 알림
\n
\n
\n
\n
\n
\n
연체 알림
\n
미수금 연체 발생 시 알림
\n
\n
\n
\n
\n
\n
재고 알림
\n
안전재고 미달 시 알림
\n
\n
\n
\n
\n
\n )}\n\n \n \n \n
\n \n
\n
\n );\n};\n\n// 거래처 목록\nconst CustomerList = ({ customers = initialCustomers, onNavigate, hideAccountingInfo = false }) => {\n const [customerList, setCustomerList] = useState(customers);\n const [search, setSearch] = useState('');\n const [activeTab, setActiveTab] = useState('all');\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n const tabs = [\n { id: 'all', label: '전체', count: customerList.length },\n { id: 'sales', label: '매출', count: customerList.filter(c => c.customerType === '매출').length },\n { id: 'purchase', label: '매입', count: customerList.filter(c => c.customerType === '매입').length },\n { id: 'both', label: '매입매출', count: customerList.filter(c => c.customerType === '매입매출').length },\n { id: 'active', label: '활성', count: customerList.filter(c => c.isActive !== false).length },\n { id: 'inactive', label: '비활성', count: customerList.filter(c => c.isActive === false).length },\n ];\n\n const filtered = customerList\n .filter(c => activeTab === 'all' ||\n (activeTab === 'sales' && c.customerType === '매출') ||\n (activeTab === 'purchase' && c.customerType === '매입') ||\n (activeTab === 'both' && c.customerType === '매입매출') ||\n (activeTab === 'active' && c.isActive !== false) ||\n (activeTab === 'inactive' && c.isActive === false))\n .filter(c =>\n c.customerCode.toLowerCase().includes(search.toLowerCase()) ||\n c.customerName.includes(search) ||\n c.ceo?.includes(search) ||\n c.businessNo?.includes(search)\n )\n .sort((a, b) => b.id - a.id); // ID 기준 내림차순 (최신 등록 최상단)\n\n // 목록 선택 훅\n const {\n selectedIds,\n handleSelect,\n handleSelectAll,\n clearSelection,\n isAllSelected,\n hasSelection,\n isMultiSelect,\n isSelected,\n } = useListSelection(filtered);\n\n // 삭제 핸들러\n const handleDelete = (customer) => {\n if (window.confirm(`'${customer.customerName}' 거래처를 삭제하시겠습니까?`)) {\n setCustomerList(prev => prev.filter(c => c.id !== customer.id));\n }\n };\n\n const handleBulkDelete = () => {\n setCustomerList(prev => prev.filter(c => !selectedIds.includes(c.id)));\n clearSelection();\n setShowDeleteModal(false);\n };\n\n // 통계\n const totalReceivables = customerList.reduce((sum, c) => sum + (c.receivables?.total || 0), 0);\n const overdueReceivables = customerList.reduce((sum, c) => sum + (c.receivables?.overdue || 0), 0);\n const gradeACount = customerList.filter(c => c.creditGrade === 'A').length;\n const gradeCCount = customerList.filter(c => c.creditGrade === 'C').length;\n\n return (\n \n
onNavigate('customer-register')}>\n 거래처 등록\n \n }\n />\n\n {/* 대시보드 */}\n \n \n {!hideAccountingInfo && (\n <>\n \n \n \n >\n )}\n {hideAccountingInfo && (\n <>\n c.customerType === '매출').length}개`} color=\"green\" />\n c.customerType === '매입').length}개`} color=\"orange\" />\n c.isActive !== false).length}개`} color=\"purple\" />\n >\n )}\n
\n\n \n \n\n {/* 선택 삭제 버튼 (2개 이상 선택 시) */}\n {isMultiSelect && (\n \n \n
\n )}\n\n {/* 테이블 */}\n \n\n {/* 삭제 확인 모달 */}\n {showDeleteModal && (\n \n
\n
거래처 삭제
\n
선택한 {selectedIds.length}개 거래처를 삭제하시겠습니까?
\n
\n \n \n
\n
\n
\n )}\n \n );\n};\n\n// 거래처 상세 페이지 - 섹션 기반 (탭 제거)\nconst CustomerDetail = ({ customer, onNavigate, hideAccountingInfo = false }) => {\n if (!customer) {\n return (\n \n
\n
거래처 정보를 찾을 수 없습니다.
\n
\n
\n );\n }\n\n return (\n \n
\n \n \n \n } />\n\n {/* 거래처 요약 정보 */}\n \n
\n
\n
\n
\n
{customer.customerName}
\n
{customer.customerCode}
\n
\n
\n
\n {!hideAccountingInfo && }\n {customer.customerType}\n
\n
\n
\n
대표자{customer.ceo || customer.ceoName || '-'}
\n
사업자번호{customer.businessNo || '-'}
\n {!hideAccountingInfo &&
결제조건{customer.paymentTerms || customer.paymentTerm || '-'}
}\n
\n
\n\n {/* 기본정보 섹션 */}\n \n \n
\n
\n
사업자 정보
\n
\n
업태{customer.businessType || '-'}
\n
종목{customer.businessItem || '-'}
\n {!hideAccountingInfo &&
결제방법{customer.paymentMethod || '-'}
}\n
\n
\n
\n
연락처 정보
\n
\n
전화{customer.phone || '-'}
\n
팩스{customer.fax || '-'}
\n
이메일{customer.email || '-'}
\n
\n
\n
\n
\n
주소
\n
{customer.address || '-'} {customer.addressDetail || ''}
\n
\n
\n
담당자 목록
\n
\n {(customer.contacts || []).length > 0 ? (customer.contacts || []).map(contact => (\n
\n
\n {contact.name}\n {contact.position}\n {contact.isPrimary && 주담당}\n
\n
{contact.phone}{contact.email}
\n
\n )) :
등록된 담당자가 없습니다.
}\n
\n
\n {!hideAccountingInfo && customer.creditNote && (\n
\n
신용 메모
\n
{customer.creditNote}
\n
\n )}\n
\n \n\n {/* 거래내역 섹션 (회계정보 표시 시) */}\n {!hideAccountingInfo && (\n \n \n
\n \n \n | 일자 | \n 구분 | \n 금액 | \n 잔액 | \n 비고 | \n
\n \n \n {(customer.transactions || []).length > 0 ? (customer.transactions || []).map((tx, idx) => (\n \n | {tx.date || '-'} | \n {tx.type} | \n {(tx.amount || 0).toLocaleString()}원 | \n {(tx.balance || 0).toLocaleString()}원 | \n {tx.note || '-'} | \n
\n )) : (\n | 거래내역이 없습니다. |
\n )}\n \n
\n
\n \n )}\n\n {/* 미수금현황 섹션 (회계정보 표시 시) */}\n {!hideAccountingInfo && (\n \n \n
\n
총 미수금
\n
{(customer.receivables?.total || 0).toLocaleString()}원
\n
\n
\n
연체 미수금
\n
{(customer.receivables?.overdue || 0).toLocaleString()}원
\n
\n
\n
연체 일수
\n
{customer.receivables?.overdueDays || 0}일
\n
\n
\n \n )}\n\n {/* 변경이력 섹션 */}\n \n 변경이력이 없습니다.
\n \n \n );\n};\n\n// 거래처 등록/수정 페이지\nconst CustomerRegister = ({ customer, onNavigate, isEdit = false, hideAccountingInfo = false }) => {\n const [formData, setFormData] = useState(customer || {\n customerType: '매출', creditGrade: 'B', paymentMethod: '계좌이체', customerCode: '', customerName: '', ceo: '',\n businessNo: '', businessType: '', businessItem: '', phone: '', fax: '', email: '', address: '', addressDetail: '',\n paymentTerms: '', creditLimit: 0, creditNote: '', contacts: [],\n });\n\n // 유효성 검사 규칙\n const validationRules = {\n customerType: { required: true, label: '거래처구분', message: '거래처구분을 선택해주세요.' },\n customerName: { required: true, label: '거래처명', message: '거래처명을 입력해주세요.' },\n };\n\n const { errors, hasError, getFieldError, validateForm, clearFieldError } = useFormValidation(formData, validationRules);\n\n const handleChange = (field, value) => {\n setFormData({ ...formData, [field]: value });\n clearFieldError(field);\n };\n\n const handleSave = () => {\n if (!validateForm()) {\n // 첫 번째 에러 필드로 스크롤\n const firstErrorField = Object.keys(errors).find(key => errors[key]);\n if (firstErrorField) {\n const errorElement = document.querySelector(`[data-field=\"${firstErrorField}\"]`);\n errorElement?.scrollIntoView({ behavior: 'smooth', block: 'center' });\n }\n return;\n }\n alert(isEdit ? '거래처가 수정되었습니다.' : '거래처가 등록되었습니다.');\n onNavigate('customer-list');\n };\n\n return (\n \n
\n \n \n \n } />\n \n \n \n \n \n \n \n handleChange('customerName', e.target.value)} placeholder=\"거래처명 입력\" className={getInputClassName(hasError('customerName'))} />\n \n setFormData({ ...formData, ceo: e.target.value })} placeholder=\"대표자명\" className=\"w-full px-3 py-2 border rounded-lg\" />\n setFormData({ ...formData, businessNo: e.target.value })} placeholder=\"000-00-00000\" className=\"w-full px-3 py-2 border rounded-lg\" />\n setFormData({ ...formData, businessType: e.target.value })} placeholder=\"업태\" className=\"w-full px-3 py-2 border rounded-lg\" />\n setFormData({ ...formData, businessItem: e.target.value })} placeholder=\"종목\" className=\"w-full px-3 py-2 border rounded-lg\" />\n
\n \n \n \n setFormData({ ...formData, phone: e.target.value })} placeholder=\"000-0000-0000\" className=\"w-full px-3 py-2 border rounded-lg\" />\n setFormData({ ...formData, fax: e.target.value })} placeholder=\"000-0000-0000\" className=\"w-full px-3 py-2 border rounded-lg\" />\n setFormData({ ...formData, email: e.target.value })} placeholder=\"example@company.com\" className=\"w-full px-3 py-2 border rounded-lg\" />\n
\n \n \n setFormData({ ...formData, address: e.target.value })} placeholder=\"주소\" className=\"w-full px-3 py-2 border rounded-lg mb-2\" />\n setFormData({ ...formData, addressDetail: e.target.value })} placeholder=\"상세주소\" className=\"w-full px-3 py-2 border rounded-lg\" />\n \n
\n \n {!hideAccountingInfo && (\n \n \n \n \n \n \n \n \n setFormData({ ...formData, paymentTerms: e.target.value })} placeholder=\"예: 월말 익월 15일\" className=\"w-full px-3 py-2 border rounded-lg\" />\n setFormData({ ...formData, creditLimit: parseInt(e.target.value) || 0 })} placeholder=\"0\" className=\"w-full px-3 py-2 border rounded-lg\" />\n
\n \n \n
\n \n )}\n \n );\n};\n\n// 거래처 상세 패널 (레거시 - 호환용) - 섹션 기반 (탭 제거)\nconst CustomerDetailPanel = ({ customer, onEdit, onClose, hideAccountingInfo = false }) => {\n return (\n
\n
\n {/* 기본 정보 요약 */}\n
\n
\n
\n
\n
\n
{customer.customerName}
\n
{customer.customerCode}
\n
\n
\n
\n {!hideAccountingInfo && }\n \n {customer.customerType}\n \n
\n
\n
\n
\n
대표자\n
{customer.ceo}
\n
\n
\n
사업자번호\n
{customer.businessNo}
\n
\n {!hideAccountingInfo && (\n
\n
결제조건\n
{customer.paymentTerms}
\n
\n )}\n
\n
\n\n {/* 기본정보 섹션 */}\n
\n
기본정보
\n
\n
\n
\n
사업자 정보
\n
\n
업태{customer.businessType || '-'}
\n
종목{customer.businessItem || '-'}
\n {!hideAccountingInfo &&
결제방법{customer.paymentMethod || '-'}
}\n
\n
\n
\n
연락처 정보
\n
\n
전화{customer.phone || '-'}
\n
팩스{customer.fax || '-'}
\n
이메일{customer.email || '-'}
\n
\n
\n
\n
\n
주소
\n
{customer.address || '-'} {customer.addressDetail || ''}
\n
\n
\n
담당자 목록
\n
\n {(customer.contacts || []).length > 0 ? customer.contacts.map(contact => (\n
\n
\n {contact.name}\n {contact.position}\n {contact.isPrimary && 주담당}\n
\n
\n {contact.phone}\n {contact.email}\n
\n
\n )) :
등록된 담당자가 없습니다.
}\n
\n
\n {!hideAccountingInfo && customer.creditNote && (\n
\n
신용 메모
\n
{customer.creditNote}
\n
\n )}\n
\n
\n\n {/* 거래내역 섹션 (회계정보 표시 시) */}\n {!hideAccountingInfo && (\n
\n
거래내역
\n
\n
\n \n \n | 일자 | \n 구분 | \n 금액 | \n 잔액 | \n 비고 | \n
\n \n \n {(customer.transactions || []).length > 0 ? customer.transactions.map((tx, idx) => (\n \n | {tx.date} | \n \n {tx.type}\n | \n {(tx.amount || 0).toLocaleString()}원 | \n {(tx.balance || 0).toLocaleString()}원 | \n {tx.note || '-'} | \n
\n )) : (\n | 거래내역이 없습니다. |
\n )}\n \n
\n
\n
\n )}\n\n {/* 미수금현황 섹션 (회계정보 표시 시) */}\n {!hideAccountingInfo && (\n
\n
미수금현황
\n
\n
\n
총 미수금
\n
{(customer.receivables?.total || 0).toLocaleString()}원
\n
\n
\n
연체 미수금
\n
{(customer.receivables?.overdue || 0).toLocaleString()}원
\n
\n
\n
연체 일수
\n
{customer.receivables?.overdueDays || 0}일
\n
\n
\n
\n )}\n\n {/* 변경이력 섹션 */}\n
\n
변경이력
\n
\n 변경이력이 없습니다.\n
\n
\n
\n\n {/* 하단 버튼 */}\n
\n \n \n
\n
\n );\n};\n\n// 거래처 폼 패널\nconst CustomerFormPanel = ({ formData, setFormData, onSave, onCancel, isEdit, hideAccountingInfo = false }) => {\n return (\n
\n
\n {/* 기본정보 */}\n
\n\n {/* 연락처 정보 */}\n
\n\n {/* 신용정보 - 회계 정보 숨김 옵션 적용 */}\n {!hideAccountingInfo && (\n
\n
신용 및 결제 정보
\n
\n
\n \n \n
\n \n \n
\n \n setFormData({ ...formData, paymentTerms: e.target.value })} />\n \n
\n
\n \n \n
\n
\n
\n )}\n
\n\n {/* 하단 버튼 */}\n
\n \n \n
\n
\n );\n};\n\n// ============ 현장 관리 ============\n\nconst SiteList = ({ sites = initialSites, onNavigate }) => {\n const [siteList, setSiteList] = useState(sites);\n const [search, setSearch] = useState('');\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n const filtered = siteList\n .filter(s =>\n s.siteCode.toLowerCase().includes(search.toLowerCase()) ||\n s.siteName.includes(search) ||\n s.customerName.includes(search)\n )\n .sort((a, b) => b.id - a.id); // ID 기준 내림차순 (최신 등록 최상단)\n\n // 목록 선택 훅\n const {\n selectedIds,\n handleSelect,\n handleSelectAll,\n clearSelection,\n isAllSelected,\n hasSelection,\n isMultiSelect,\n isSelected,\n } = useListSelection(filtered);\n\n // 삭제 핸들러\n const handleDelete = (site) => {\n if (window.confirm(`'${site.siteName}' 현장을 삭제하시겠습니까?`)) {\n setSiteList(prev => prev.filter(s => s.id !== site.id));\n }\n };\n\n const handleBulkDelete = () => {\n setSiteList(prev => prev.filter(s => !selectedIds.includes(s.id)));\n clearSelection();\n setShowDeleteModal(false);\n };\n\n return (\n
\n
onNavigate('site-register')}>\n 현장 등록\n \n }\n />\n\n \n\n {/* 선택 삭제 버튼 (2개 이상 선택 시) */}\n {isMultiSelect && (\n \n \n
\n )}\n\n {/* 테이블 */}\n \n\n {/* 삭제 확인 모달 */}\n {showDeleteModal && (\n \n
\n
현장 삭제
\n
선택한 {selectedIds.length}개 현장을 삭제하시겠습니까?
\n
\n \n \n
\n
\n
\n )}\n\n \n );\n};\n\n// 현장 상세 패널\nconst SiteDetailPanel = ({ site, onEdit, onClose }) => {\n const getStatusColor = (status) => {\n const colors = {\n '설치완료': 'bg-green-100 text-green-700',\n '설치중': 'bg-blue-100 text-blue-700',\n '출하대기': 'bg-yellow-100 text-yellow-700',\n '생산중': 'bg-purple-100 text-purple-700',\n };\n return colors[status] || 'bg-gray-100 text-gray-700';\n };\n\n return (\n
\n
\n {/* 현장 정보 요약 */}\n
\n
\n
\n
\n
\n
{site.siteName}
\n
{site.siteCode}
\n
\n
\n
{site.status}\n
\n
\n
\n
거래처\n
{site.customerName}
\n
\n
\n
설치예정일\n
{site.installScheduledDate}
\n
\n
\n
\n
\n\n {/* 기본정보 */}\n
\n
\n
현장 주소
\n
{site.siteAddress}
\n
연락처: {site.siteContact}
\n
\n
\n
설치 담당자
\n
\n {site.installManager}\n {site.installManagerPhone}\n
\n
\n {site.note && (\n
\n )}\n
\n
\n\n
\n \n \n
\n
\n );\n};\n\n// 현장 폼 패널\nconst SiteFormPanel = ({ formData, setFormData, onSave, onCancel, isEdit }) => {\n return (\n
\n
\n\n
\n \n \n
\n
\n );\n};\n\n// 현장 상세 페이지\nconst SiteDetail = ({ site, onNavigate }) => {\n const getStatusColor = (status) => { const colors = { '설치완료': 'bg-green-100 text-green-700', '설치중': 'bg-blue-100 text-blue-700', '출하대기': 'bg-yellow-100 text-yellow-700', '생산중': 'bg-purple-100 text-purple-700' }; return colors[status] || 'bg-gray-100 text-gray-700'; };\n\n // site가 없을 때 에러 처리\n if (!site) {\n return (\n
\n
onNavigate('site')}> 목록으로} />\n \n
\n
현장 정보를 찾을 수 없습니다
\n
요청하신 현장 정보가 존재하지 않거나 삭제되었습니다.
\n
\n
\n \n );\n }\n\n return (\n
\n {/* 상단 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n
\n \n 현장 상세\n
\n
\n \n \n
\n
\n {/* 기본정보 섹션 */}\n
\n \n
\n
현장코드
\n
{site.siteCode}
\n
\n
\n
현장명
\n
{site.siteName}
\n
\n
\n
진행상태
\n
{site.status}\n
\n
\n
거래처
\n
{site.customerName}
\n
\n
\n
설치예정일
\n
{site.installScheduledDate || '-'}
\n
\n
\n
\n
현장주소
\n
{site.siteAddress}
\n
\n
\n
현장연락처
\n
{site.siteContact || '-'}
\n
\n
\n
설치담당자
\n
{site.installManager || '-'}
\n
\n
\n
담당자 연락처
\n
{site.installManagerPhone || '-'}
\n
\n {site.note && (\n
\n )}\n
\n \n {/* 수주내역 섹션 */}\n
| 수주번호 | 제품 | 수량 | 금액 |
{site.orders?.length > 0 ? site.orders.map((order, idx) => (| {order.orderNo} | {order.product} | {order.qty}EA | {order.amount.toLocaleString()}원 |
)) : (| 수주 내역이 없습니다. |
)}
\n {/* 출고내역 섹션 */}\n
| 출하번호 | 출하일 | 수량 | 상태 |
{site.shipments?.length > 0 ? site.shipments.map((ship, idx) => (| {ship.shipmentNo} | {ship.date} | {ship.qty}EA | {ship.status} |
)) : (| 출고 내역이 없습니다. |
)}
\n {/* 변경이력 섹션 */}\n
{site.history?.length > 0 ? site.history.map((h, idx) => (
{h.action}{h.date}
{h.note}
{h.by}
)) : (
변경 이력이 없습니다.
)}
\n
\n );\n};\n\n// 현장 등록/수정 페이지\nconst SiteRegister = ({ site, onNavigate, isEdit = false }) => {\n // 채번규칙에 따른 현장코드 생성 (PJ-YYMMDD-##)\n const generateSiteCode = () => {\n const now = new Date();\n const yy = String(now.getFullYear()).slice(-2);\n const mm = String(now.getMonth() + 1).padStart(2, '0');\n const dd = String(now.getDate()).padStart(2, '0');\n const seq = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0');\n return `PJ-${yy}${mm}${dd}-${seq}`;\n };\n const [formData, setFormData] = useState(site || { siteCode: generateSiteCode(), siteName: '', customerId: '', customerName: '', siteAddress: '', siteContact: '', status: '수주확정', installManager: '', installManagerPhone: '', installScheduledDate: '', installCompletedDate: '', note: '', progress: 0 });\n const handleSave = () => { alert(isEdit ? '현장 정보가 수정되었습니다.' : `현장이 등록되었습니다.\\n현장코드: ${formData.siteCode}`); onNavigate('site'); };\n return (\n
\n
onNavigate('site')}> 목록으로} />\n \n \n \n \n );\n};\n\n// ============ 견적수식관리 ============\n\n// 견적수식관리 메인 컴포넌트\nconst QuoteFormulaManagement = ({ onNavigate }) => {\n const [activeSection, setActiveSection] = useState('formula'); // formula, classification, price-formula, auto-quote\n const [selectedProduct, setSelectedProduct] = useState('common');\n const [selectedCategory, setSelectedCategory] = useState('basic-info');\n const [showProductDropdown, setShowProductDropdown] = useState(false);\n const [editMode, setEditMode] = useState(false);\n const [showFormulaModal, setShowFormulaModal] = useState(false);\n const [showCategoryModal, setShowCategoryModal] = useState(false);\n const [showClassificationModal, setShowClassificationModal] = useState(false);\n const [showPriceFormulaModal, setShowPriceFormulaModal] = useState(false);\n const [formulas, setFormulas] = useState(initialQuoteFormulas);\n const [categories, setCategories] = useState(formulaCategories);\n const [classifications, setClassifications] = useState(initialPriceClassifications);\n const [priceFormulas, setPriceFormulas] = useState(initialPriceFormulas);\n const [editingItem, setEditingItem] = useState(null);\n\n // 새 수식 폼\n const [formulaForm, setFormulaForm] = useState({\n productId: 'common',\n categoryId: 'basic-info',\n name: '',\n variable: '',\n type: 'formula',\n formula: '',\n resultType: 'item',\n description: '',\n process: null,\n itemCode: '',\n itemName: '',\n unit: 'EA',\n qtyFormula: '',\n });\n\n // 새 카테고리 폼\n const [categoryForm, setCategoryForm] = useState({ name: '' });\n\n // 새 분류 폼\n const [classificationForm, setClassificationForm] = useState({\n name: '',\n description: '',\n selectedCategories: [],\n });\n\n // 단가 수식 폼\n const [priceFormulaForm, setPriceFormulaForm] = useState({\n classificationId: null,\n formula: '',\n description: '',\n isActive: true,\n qtyCalc: 'UP',\n });\n\n // 현재 제품의 수식 필터링\n const filteredFormulas = formulas.filter(f =>\n f.productId === selectedProduct && f.categoryId === selectedCategory\n );\n\n // 카테고리별 수식 개수\n const getCategoryFormulaCount = (categoryId) => {\n return formulas.filter(f => f.productId === selectedProduct && f.categoryId === categoryId).length;\n };\n\n // 제품 선택\n const handleProductSelect = (productId) => {\n setSelectedProduct(productId);\n setShowProductDropdown(false);\n };\n\n // 수식 추가\n const handleAddFormula = () => {\n if (!formulaForm.name || !formulaForm.variable) {\n alert('이름과 변수는 필수입니다.');\n return;\n }\n\n const newFormula = {\n id: Date.now(),\n ...formulaForm,\n order: filteredFormulas.length + 1,\n };\n\n setFormulas([...formulas, newFormula]);\n setShowFormulaModal(false);\n setFormulaForm({\n productId: selectedProduct,\n categoryId: selectedCategory,\n name: '',\n variable: '',\n type: 'formula',\n formula: '',\n resultType: 'item',\n description: '',\n process: null,\n itemCode: '',\n itemName: '',\n unit: 'EA',\n qtyFormula: '',\n });\n };\n\n // 카테고리 추가\n const handleAddCategory = () => {\n if (!categoryForm.name) {\n alert('카테고리 이름을 입력하세요.');\n return;\n }\n\n const newCategory = {\n id: categoryForm.name.toLowerCase().replace(/\\s+/g, '-'),\n name: categoryForm.name,\n order: categories.length + 1,\n };\n\n setCategories([...categories, newCategory]);\n setShowCategoryModal(false);\n setCategoryForm({ name: '' });\n };\n\n // 수식 삭제\n const handleDeleteFormula = (id) => {\n if (confirm('이 수식을 삭제하시겠습니까?')) {\n setFormulas(formulas.filter(f => f.id !== id));\n }\n };\n\n // 분류 추가\n const handleAddClassification = () => {\n if (!classificationForm.name) {\n alert('분류명을 입력하세요.');\n return;\n }\n\n const newClassification = {\n id: Date.now(),\n name: classificationForm.name,\n description: classificationForm.description,\n categories: classificationForm.selectedCategories,\n itemCount: classificationForm.selectedCategories.length,\n };\n\n setClassifications([...classifications, newClassification]);\n setShowClassificationModal(false);\n setClassificationForm({ name: '', description: '', selectedCategories: [] });\n };\n\n // 단가 수식 추가\n const handleAddPriceFormula = () => {\n if (!priceFormulaForm.classificationId || !priceFormulaForm.formula) {\n alert('분류 그룹과 수식을 입력하세요.');\n return;\n }\n\n const classification = classifications.find(c => c.id === priceFormulaForm.classificationId);\n const newPriceFormula = {\n id: Date.now(),\n ...priceFormulaForm,\n classificationName: classification?.name || '',\n };\n\n setPriceFormulas([...priceFormulas, newPriceFormula]);\n setShowPriceFormulaModal(false);\n setPriceFormulaForm({ classificationId: null, formula: '', description: '', isActive: true, qtyCalc: 'UP' });\n };\n\n const sections = [\n { id: 'formula', label: '품목 수식 관리', icon: Calculator },\n { id: 'classification', label: '분류 관리', icon: Layers },\n { id: 'price-formula', label: '단가 수식 관리', icon: DollarSign },\n { id: 'auto-quote', label: '자동 견적 산출', icon: Zap },\n ];\n\n return (\n
\n {/* 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n
\n \n 견적수식 목록\n
\n \n\n {/* 섹션 탭 */}\n
\n {sections.map(section => (\n \n ))}\n
\n\n {/* 품목 수식 관리 섹션 */}\n {activeSection === 'formula' && (\n
\n \n {/* 제품 선택 */}\n
\n
\n
제품 선택\n
\n
\n\n {showProductDropdown && (\n
\n
\n
\n {quoteProducts.map(product => (\n \n ))}\n
\n
\n )}\n
\n
\n\n
\n
\n
\n
\n
\n\n {/* 카테고리 탭 */}\n
\n
카테고리 선택 (실행 순서) - 예: 1 2 3\n
\n {categories.map((cat, idx) => (\n \n ))}\n
\n
\n\n {/* 수식 테이블 */}\n
\n
\n
\n {categories.find(c => c.id === selectedCategory)?.name}\n \n
\n
\n
\n\n
\n \n \n | 순서 | \n 이름 | \n 변수 | \n 타입 | \n 수식/범위 | \n 결과 타입 | \n 설명 | \n 작업 | \n
\n \n \n {filteredFormulas.map((formula, idx) => (\n \n | \n \n \n {idx + 1}\n \n | \n {formula.name} | \n {formula.variable} | \n \n \n {formula.type === 'input' ? '계산식' : formula.type === 'formula' ? '계산식' : '조회'}\n \n | \n {formula.formula} | \n {formula.resultType === 'item' ? '품목' : '출력'} | \n {formula.description} | \n \n \n \n \n \n | \n
\n ))}\n {filteredFormulas.length === 0 && (\n \n | \n 이 카테고리에 등록된 수식이 없습니다.\n | \n
\n )}\n \n
\n
\n
\n \n )}\n\n {/* 분류 관리 섹션 */}\n {activeSection === 'classification' && (\n
setShowClassificationModal(true)}\n className=\"flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600\"\n >\n \n 분류 추가\n \n }\n >\n \n\n \n {classifications.map(classification => (\n
\n
\n
\n
{classification.name}
\n
{classification.description}
\n
\n
\n {classification.itemCount}개\n \n \n
\n
\n
\n {classification.categories.map(cat => (\n \n {cat}\n \n ))}\n
\n
\n ))}\n
\n \n )}\n\n {/* 단가 수식 관리 섹션 */}\n {activeSection === 'price-formula' && (\n
setShowPriceFormulaModal(true)}\n className=\"flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600\"\n >\n \n 단가 수식 추가\n \n }\n >\n \n\n \n {priceFormulas.map(pf => {\n const classification = classifications.find(c => c.id === pf.classificationId);\n return (\n
\n
\n
\n
{pf.classificationName}
\n
그룹\n
{classification?.itemCount || 0}개 품목\n
\n \n \n
\n
\n
\n {classification?.categories.map(cat => (\n \n {cat}\n \n ))}\n
\n
\n 적용 수식: {pf.formula}\n 수량 계산: {pf.qtyCalc}\n
\n
\n );\n })}\n\n {priceFormulas.length === 0 && (\n
\n 등록된 단가 수식이 없습니다.\n
\n )}\n
\n \n )}\n\n {/* 자동 견적 산출 섹션 */}\n {activeSection === 'auto-quote' && (\n
\n )}\n\n {/* 수식 추가 모달 */}\n {showFormulaModal && (\n
\n
\n
\n
\n
수식 추가
\n
수식 정보를 입력하세요.
\n
\n
\n
\n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n
\n \n setFormulaForm({ ...formulaForm, name: e.target.value })}\n placeholder=\"예: 스크린 가로\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n\n
\n \n setFormulaForm({ ...formulaForm, variable: e.target.value })}\n placeholder=\"예: W1_스크린\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm font-mono\"\n />\n
\n\n
\n \n \n
\n\n
\n\n
\n
\n
setFormulaForm({ ...formulaForm, formula: e.target.value })}\n placeholder=\"예: W0 + 140, SUM(W0, H0), ROUND(M * 2.5, 2)\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm font-mono\"\n />\n
\n \n \n
\n
\n\n
\n \n setFormulaForm({ ...formulaForm, description: e.target.value })}\n placeholder=\"수식에 대한 설명을 입력하세요.\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n
\n
\n \n \n
\n
\n
\n )}\n\n {/* 카테고리 추가 모달 */}\n {showCategoryModal && (\n
\n
\n
\n
\n
카테고리 추가
\n
새로운 카테고리 이름을 입력하세요.
\n
\n
\n
\n
\n \n setCategoryForm({ name: e.target.value })}\n placeholder=\"예: 공정비, 원단비 등\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n
\n
\n
\n
\n
\n
\n )}\n\n {/* 분류 추가 모달 */}\n {showClassificationModal && (\n
\n
\n
\n
\n
분류 추가
\n
카테고리들을 묶는 분류를 설정합니다.
\n
\n
\n
\n
\n
\n \n setClassificationForm({ ...classificationForm, name: e.target.value })}\n placeholder=\"예: 오픈형 제품, 면적 기반 제품\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n
\n \n setClassificationForm({ ...classificationForm, description: e.target.value })}\n placeholder=\"분류에 대한 설명\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n
\n
\n
수식 관리에서 생성된 카테고리를 선택하세요
\n
\n {categories.map(cat => (\n \n ))}\n
\n {classificationForm.selectedCategories.length > 0 && (\n
\n
선택된 카테고리 ({classificationForm.selectedCategories.length}개)
\n
\n {classificationForm.selectedCategories.map(cat => (\n \n {cat}\n \n ))}\n
\n
\n )}\n
\n
\n
\n \n \n
\n
\n
\n )}\n\n {/* 단가 수식 연결 모달 */}\n {showPriceFormulaModal && (\n
\n
\n
\n
\n
단가 수식 연결
\n
분류 그룹 또는 품목에 적용할 단가 계산 수식을 연결합니다
\n
\n
\n
\n
\n
\n \n \n
\n
\n
\n
setPriceFormulaForm({ ...priceFormulaForm, formula: e.target.value })}\n placeholder=\"예: W0 * H0 / 1000000\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm font-mono\"\n />\n
\n \n \n
\n
\n
\n \n setPriceFormulaForm({ ...priceFormulaForm, description: e.target.value })}\n placeholder=\"단가 수식 연결에 대한 설명\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm\"\n />\n
\n
\n
\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// 자동 견적 산출 컴포넌트\nconst AutoQuoteCalculator = ({ formulas, categories }) => {\n const [inputs, setInputs] = useState({\n PC: '스크린', // 제품 카테고리\n W0: 2000, // 오픈사이즈 가로\n H0: 2500, // 오픈사이즈 세로\n GT: '벽면형', // 가이드레일 설치유형\n V: '220', // 모터 전원\n WIRE: '유선', // 유/무선\n CT: '매립', // 연동제어기 유형\n QTY: 1, // 수량\n floor: '', // 층수\n symbol: '', // 부호\n });\n const [calculatedResult, setCalculatedResult] = useState(null);\n const [showSummary, setShowSummary] = useState(true);\n const [groupByProcess, setGroupByProcess] = useState(false);\n const [showQuoteModal, setShowQuoteModal] = useState(false);\n\n // 견적 계산\n const handleCalculate = () => {\n const result = formulaEngine.calculateQuoteItems(inputs, formulas, itemPriceMaster);\n setCalculatedResult(result);\n };\n\n // 초기화\n const handleReset = () => {\n setInputs({\n PC: '스크린',\n W0: 2000,\n H0: 2500,\n GT: '벽면형',\n V: '220',\n WIRE: '유선',\n CT: '매립',\n QTY: 1,\n floor: '',\n symbol: '',\n });\n setCalculatedResult(null);\n };\n\n // 공정별 그룹화\n const getGroupedItems = () => {\n if (!calculatedResult?.items) return {};\n return calculatedResult.items.reduce((acc, item) => {\n const key = item.process || '기타';\n if (!acc[key]) acc[key] = [];\n acc[key].push(item);\n return acc;\n }, {});\n };\n\n // 카테고리별 합계\n const getCategorySummary = () => {\n if (!calculatedResult?.items) return [];\n const summary = {};\n calculatedResult.items.forEach(item => {\n const cat = item.category || 'etc';\n if (!summary[cat]) summary[cat] = { name: cat, count: 0, amount: 0 };\n summary[cat].count += item.qty;\n summary[cat].amount += item.amount;\n });\n return Object.values(summary);\n };\n\n const groupedItems = getGroupedItems();\n const categorySummary = getCategorySummary();\n\n return (\n
\n \n {/* 입력 폼 */}\n
\n
\n
\n \n 견적 입력\n
\n \n \n\n
\n {/* 기본정보 */}\n
\n\n {/* 제품유형 */}\n
\n
\n
\n {['스크린', '철재'].map(type => (\n \n ))}\n
\n
\n\n {/* 오픈사이즈 */}\n
\n\n {/* 설치유형 */}\n
\n \n \n
\n\n {/* 모터 설정 */}\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n {/* 제어기 설정 */}\n
\n
\n \n \n
\n
\n \n setInputs({ ...inputs, QTY: Math.max(1, Number(e.target.value)) })}\n min={1}\n className=\"w-full px-3 py-2 border rounded-lg text-sm font-mono focus:ring-2 focus:ring-blue-500\"\n />\n
\n
\n
\n\n {/* 계산 버튼 */}\n
\n\n {/* 산출 요약 (계산 결과가 있을 때) */}\n {calculatedResult?.summary && (\n
\n
\n
산출 정보
\n \n \n {showSummary && (\n
\n
\n 제품유형\n {calculatedResult.summary.productType}\n
\n
\n 오픈사이즈\n {calculatedResult.summary.openSize}\n
\n
\n 제작사이즈\n {calculatedResult.summary.productionSize}\n
\n
\n 면적 (M)\n {calculatedResult.summary.area} ㎡\n
\n
\n 중량 (K)\n {calculatedResult.summary.weight} kg\n
\n
\n 모터용량\n {calculatedResult.summary.motorCapacity}\n
\n
\n 샤프트\n {calculatedResult.summary.shaftInch}\n
\n
\n )}\n
\n )}\n
\n\n {/* 산출 결과 */}\n
\n
\n
\n \n 산출 결과\n {calculatedResult?.items && (\n \n ({calculatedResult.items.length}개 품목)\n \n )}\n
\n {calculatedResult?.items && (\n
\n \n \n
\n )}\n
\n\n {calculatedResult?.items ? (\n
\n {/* 품목 테이블 */}\n
\n {groupByProcess ? (\n // 공정별 그룹 뷰\n Object.entries(groupedItems).map(([process, items]) => (\n
\n
\n {process} ({items.length}개)\n
\n
\n \n {items.map(item => (\n \n | \n {item.itemName} \n {item.itemCode} \n | \n {item.spec} | \n {item.qty} {item.unit} | \n {item.unitPrice.toLocaleString()} | \n {item.amount.toLocaleString()} | \n
\n ))}\n \n
\n
\n ))\n ) : (\n // 전체 목록 뷰\n
\n \n \n | 품목 | \n 규격 | \n 수량 | \n 단가 | \n 금액 | \n 공정 | \n
\n \n \n {calculatedResult.items.map(item => (\n \n | \n {item.itemName} \n {item.itemCode} \n | \n {item.spec} | \n {item.qty} {item.unit} | \n {item.unitPrice.toLocaleString()} | \n {item.amount.toLocaleString()} | \n \n \n {item.process || '-'}\n \n | \n
\n ))}\n \n
\n )}\n
\n\n {/* 카테고리별 요약 */}\n
\n {categorySummary.slice(0, 8).map(cat => (\n
\n
{cat.name}
\n
{cat.amount.toLocaleString()}원
\n
\n ))}\n
\n\n {/* 합계 */}\n
\n
\n
\n
{calculatedResult.totalAmount.toLocaleString()}원
\n
\n VAT 포함: {Math.round(calculatedResult.totalAmount * 1.1).toLocaleString()}원\n
\n
\n
\n\n {/* 액션 버튼 */}\n
\n \n \n \n
\n
\n ) : (\n
\n
\n
견적 산출 대기
\n
왼쪽에서 오픈사이즈와 옵션을 입력하고
\"견적 산출\" 버튼을 클릭하세요.
\n
\n )}\n
\n
\n\n {/* 견적서 생성 모달 */}\n {showQuoteModal && calculatedResult && (\n \n
\n
\n
견적서 미리보기
\n \n \n
\n {/* 견적서 헤더 */}\n
\n\n {/* 기본 정보 */}\n
\n
\n
견적번호:QT-{Date.now().toString().slice(-8)}
\n
견적일자:{new Date().toLocaleDateString()}
\n
유효기간:견적일로부터 30일
\n
\n
\n
제품유형:{calculatedResult.summary.productType}
\n
사이즈:{calculatedResult.summary.openSize}
\n
수량:{calculatedResult.summary.qty}SET
\n
\n
\n\n {/* 품목 리스트 */}\n
\n \n \n | 품목명 | \n 규격 | \n 수량 | \n 단가 | \n 금액 | \n
\n \n \n {calculatedResult.items.map((item, idx) => (\n \n | {item.itemName} | \n {item.spec} | \n {item.qty} | \n {item.unitPrice.toLocaleString()} | \n {item.amount.toLocaleString()} | \n
\n ))}\n \n \n \n | 공급가액 | \n {calculatedResult.totalAmount.toLocaleString()}원 | \n
\n \n | 부가세 (10%) | \n {Math.round(calculatedResult.totalAmount * 0.1).toLocaleString()}원 | \n
\n \n | 총 견적금액 | \n {Math.round(calculatedResult.totalAmount * 1.1).toLocaleString()}원 | \n
\n \n
\n\n {/* 버튼 */}\n
\n \n \n \n
\n
\n
\n
\n )}\n \n );\n};\n\n// ============================================================\n// 문서양식관리 (DocumentTemplateManager)\n// ============================================================\n\nconst OldDocumentTemplateManager = ({ onNavigate }) => {\n // 문서 유형별 상세 템플릿 정의\n const documentTemplates = {\n // 견적서 템플릿\n 'QT': {\n title: '견 적 서',\n layout: 'quote',\n showCompanyLogo: true,\n showApprovalBox: false,\n sections: [\n { id: 'header', type: 'titleHeader', fields: ['docTitle'] },\n { id: 'recipient', type: 'recipientBox', label: '수 신', fields: ['customerName', 'contactPerson'] },\n {\n id: 'quoteInfo', type: 'infoTable', rows: [\n [{ label: '현 장 명', field: 'siteName', colSpan: 3 }],\n [{ label: '제 품 명', field: 'itemName', colSpan: 3 }],\n [{ label: '총견적금액', field: 'totalAmount', colSpan: 3, highlight: true }],\n ]\n },\n { id: 'validity', type: 'infoBox', content: '※ 위 견적의 유효기간은 견적일로부터 30일 입니다' },\n { id: 'date', type: 'dateBox', label: '견적일' },\n { id: 'supplier', type: 'companyBox', fields: ['companyName', 'ceoName', 'address', 'phone', 'fax'] },\n ],\n approvalLine: []\n },\n // 견적산출내역서 템플릿\n 'QT-DTL': {\n title: '견적산출내역서',\n layout: 'detail',\n showCompanyLogo: true,\n showApprovalBox: false,\n sections: [\n { id: 'header', type: 'titleHeader' },\n {\n id: 'basicInfo', type: 'infoTable', rows: [\n [{ label: '수주번호', field: 'orderNo' }, { label: '수주일자', field: 'orderDate' }],\n [{ label: '거래처', field: 'customerName' }, { label: '현장명', field: 'siteName' }],\n [{ label: '제품명', field: 'itemName' }, { label: '규격', field: 'spec' }],\n ]\n },\n { id: 'itemTable', type: 'productTable', columns: ['순번', '부품명', '규격', '단위', '수량', '단가', '금액', '비고'] },\n {\n id: 'summary', type: 'summaryTable', rows: [\n [{ label: '합계금액', field: 'subtotal' }, { label: '부가세', field: 'tax' }, { label: '총액', field: 'total', highlight: true }]\n ]\n },\n ],\n approvalLine: []\n },\n // 수주확인서 템플릿\n 'SO': {\n title: '수 주 확 인 서',\n layout: 'order',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'titleHeader' },\n {\n id: 'orderInfo', type: 'twoColumnTable', left: {\n title: '수요자',\n rows: [\n { label: '상 호', field: 'customerName' },\n { label: '대표자', field: 'customerCeo' },\n { label: '주 소', field: 'customerAddress' },\n { label: '전 화', field: 'customerPhone' },\n ]\n }, right: {\n title: '공급자',\n rows: [\n { label: '상 호', field: 'companyName' },\n { label: '대표자', field: 'ceoName' },\n { label: '주 소', field: 'address' },\n { label: '전 화', field: 'phone' },\n ]\n }\n },\n {\n id: 'productInfo', type: 'infoTable', rows: [\n [{ label: '현장명', field: 'siteName', colSpan: 3 }],\n [{ label: '제품명', field: 'itemName' }, { label: '규격', field: 'spec' }],\n [{ label: '납기일', field: 'dueDate' }, { label: '금액', field: 'totalAmount', highlight: true }],\n ]\n },\n { id: 'remarks', type: 'remarksBox', label: '비고' },\n ],\n approvalLine: [{ role: 'write' }, { role: 'review' }, { role: 'approve' }]\n },\n // 거래명세서 템플릿\n 'SL': {\n title: '거 래 명 세 서',\n layout: 'statement',\n showCompanyLogo: true,\n showApprovalBox: false,\n sections: [\n { id: 'header', type: 'titleHeader' },\n {\n id: 'tradeInfo', type: 'twoColumnTable', left: {\n title: '공급받는자',\n rows: [\n { label: '상호', field: 'customerName' },\n { label: '사업자번호', field: 'customerBusinessNo' },\n { label: '주소', field: 'customerAddress' },\n ]\n }, right: {\n title: '공급자',\n rows: [\n { label: '상호', field: 'companyName' },\n { label: '사업자번호', field: 'companyBusinessNo' },\n { label: '주소', field: 'address' },\n ]\n }\n },\n { id: 'itemTable', type: 'productTable', columns: ['일자', '품목', '규격', '수량', '단가', '공급가액', '세액', '비고'] },\n { id: 'summary', type: 'amountSummary' },\n ],\n approvalLine: []\n },\n // 작업지시서 템플릿\n 'WO': {\n title: '작 업 지 시 서',\n layout: 'workOrder',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'titleWithNo', label: '지시번호' },\n {\n id: 'orderInfo', type: 'infoTable', rows: [\n [{ label: '수주번호', field: 'orderNo' }, { label: '수주일자', field: 'orderDate' }],\n [{ label: '거래처', field: 'customerName' }, { label: '현장명', field: 'siteName' }],\n [{ label: '제품명', field: 'itemName' }, { label: '규격', field: 'spec' }],\n [{ label: '지시수량', field: 'qty' }, { label: '납기일', field: 'dueDate' }],\n ]\n },\n {\n id: 'sizeInfo', type: 'sizeTable', rows: [\n [{ label: '오픈사이즈', field: 'openSize' }, { label: '제작사이즈', field: 'makeSize' }],\n [{ label: '케이스', field: 'caseSize' }, { label: '특이사항', field: 'specialNote' }],\n ]\n },\n { id: 'processTable', type: 'processTable', columns: ['공정', '작업내용', '담당', '시작일', '완료일', '비고'] },\n { id: 'remarks', type: 'remarksBox', label: '특기사항' },\n ],\n approvalLine: [{ role: 'write', department: '생산관리' }, { role: 'review', department: '품질관리' }, { role: 'approve', position: '공장장' }]\n },\n // 발주서 템플릿\n 'PO': {\n title: '발 주 서',\n layout: 'purchase',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'titleHeader' },\n {\n id: 'supplierInfo', type: 'recipientTable', title: '수 신', rows: [\n [{ label: '업체명', field: 'supplierName' }, { label: '담당자', field: 'supplierContact' }],\n [{ label: '연락처', field: 'supplierPhone' }, { label: 'FAX', field: 'supplierFax' }],\n ]\n },\n {\n id: 'deliveryInfo', type: 'infoTable', rows: [\n [{ label: '납품현장', field: 'siteName' }, { label: '납기일', field: 'dueDate' }],\n [{ label: '현장주소', field: 'siteAddress', colSpan: 3 }],\n ]\n },\n { id: 'itemTable', type: 'productTable', columns: ['NO', '품명', '규격', '단위', '수량', '비고'] },\n { id: 'remarks', type: 'remarksBox', label: '특이사항' },\n { id: 'footer', type: 'companySignature' },\n ],\n approvalLine: [{ role: 'write' }, { role: 'review' }, { role: 'approve' }]\n },\n // 수입검사성적서 템플릿\n 'IQC': {\n title: '수입검사 성적서',\n layout: 'incomingInspection',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'companyHeader' },\n { id: 'approvalBox', type: 'approvalTable', roles: ['담당', '부서장'], showDate: true },\n {\n id: 'basicInfo', type: 'infoTable', rows: [\n [{ label: '품명', field: 'itemName' }, { label: '납품업체/제조업체', field: 'supplierName' }],\n [{ label: '규격 (두께*너비*길이)', field: 'spec' }, { label: '로트번호', field: 'lotNo' }],\n [{ label: '자재번호', field: 'materialNo' }, { label: '검사일자', field: 'inspectionDate' }],\n [{ label: '로트크기', field: 'lotSize' }, { label: '검사자', field: 'inspector' }],\n ]\n },\n { id: 'inspectionTable', type: 'incomingInspectionTable', columns: ['NO', '검사항목', '검사기준', '검사방식', '검사주기', '측정치 (n1/n2/n3)', '판정'] },\n { id: 'remarks', type: 'remarksSection', title: '특이사항' },\n { id: 'defectContent', type: 'defectSection', title: '부적합 내용' },\n { id: 'judgement', type: 'judgementBox', options: ['합격', '불합격'] },\n ],\n inspectionItems: [\n { no: 1, category: '겉모양', standard: '사용상 해로운 결함이 없을 것', method: '육안검사', period: '', sampleSize: '', measureType: 'okng' },\n {\n no: 2, category: '치수', items: [\n {\n name: '두께', standardOptions: [\n { range: '0.8 이상 ~ 1.0 미만', tolerance: '± 0.07' },\n { range: '1.0 이상 ~ 1.25 미만', tolerance: '± 0.08' },\n { range: '1.25 이상 ~ 1.6 미만', tolerance: '± 0.10' },\n { range: '1.6 이상 ~ 2.0 미만', tolerance: '± 0.12' },\n ], method: '체크검사', period: 'n=3, c=0', measureType: 'value'\n },\n {\n name: '너비', standardOptions: [\n { range: '1250 미만', tolerance: '+7/-0' },\n ], method: '체크검사', period: 'n=3, c=0', measureType: 'value'\n },\n {\n name: '길이', standardOptions: [\n { range: '1250 미만', tolerance: '+10/-0' },\n { range: '2000 이상 ~ 4000 미만', tolerance: '+15/-0' },\n { range: '4000 이상 ~ 6000 미만', tolerance: '+20/-0' },\n ], method: '체크검사', period: 'n=3, c=0', measureType: 'value'\n },\n ]\n },\n { no: 3, category: '인장강도 (N/㎟)', standard: '270 이상', method: '', period: '', measureType: 'value' },\n {\n no: 4, category: '연신율 %', standardOptions: [\n { range: '두께 0.6 이상 ~ 1.0 미만', value: '36 이상' },\n { range: '두께 1.0 이상 ~ 1.6 미만', value: '37 이상' },\n { range: '두께 1.6 이상 ~ 2.3 미만', value: '38 이상' },\n ], method: '공급업체 밀시트', period: '입고시', measureType: 'value'\n },\n { no: 5, category: '아연의 최소 부착량 (g/㎡)', standard: '한면 17 이상', method: '', period: '', measureType: 'value' },\n ],\n remarks: [\n '# 1.55mm의 경우 KS F 4510에 따른 MIN 1.5의 기준에 따름',\n '# 두께의 경우 너비 1000 이상 ~ 1250 미만 기준에 따름',\n ],\n approvalLine: [{ role: 'staff', label: '담당' }, { role: 'manager', label: '부서장' }]\n },\n // 중간검사성적서 템플릿 (스크린/절곡품 호환)\n 'PQC': {\n title: '중간 검사성적서',\n layout: 'midInspection',\n showCompanyLogo: true,\n showApprovalBox: true,\n productTypes: ['스크린', '절곡품'], // 제품유형별 호환\n sections: [\n { id: 'header', type: 'companyHeader' },\n { id: 'approvalBox', type: 'approvalTable', roles: ['작성', '검토', '승인'], departments: ['판매/전진', '생산', '품질'] },\n {\n id: 'basicInfo', type: 'infoTable', rows: [\n [{ label: '품명', field: 'itemName' }, { label: '제품 LOT NO', field: 'lotNo' }],\n [{ label: '규격', field: 'spec' }, { label: '로트크기', field: 'lotSize' }, { label: '개소', field: 'qty' }],\n [{ label: '발주처', field: 'supplierName' }, { label: '검사일자', field: 'inspectionDate' }],\n [{ label: '현장명', field: 'siteName' }, { label: '검사자', field: 'inspector' }],\n ]\n },\n { id: 'inspectionStandard', type: 'inspectionStandardBox', title: '중간검사 기준서' },\n { id: 'inspectionData', type: 'midInspectionTable' },\n ],\n approvalLine: [{ role: 'write', department: '판매/전진' }, { role: 'review', department: '생산' }, { role: 'approve', department: '품질' }]\n },\n // 스크린 중간검사성적서 템플릿\n 'PQC-SCR': {\n title: '스크린-중간 검사성적서',\n layout: 'screenMidInspection',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'companyHeader' },\n { id: 'basicInfo', type: 'screenBasicInfo' },\n { id: 'inspectionStandard', type: 'screenInspectionStandard', diagram: 'screenCrossSection' },\n { id: 'inspectionData', type: 'screenInspectionData', columns: ['일련번호', '가공상태', '재봉상태', '조립상태', '길이', '높이', '간격', '판정'] },\n ],\n inspectionItems: {\n appearance: [\n { id: 'processing', name: '가공상태', standard: '사용상 해로운 결함이 없을것', method: '육안검사', regulation: 'KS F 4510 5.1항' },\n { id: 'sewing', name: '재봉상태', standard: '내화실에 의해 견고하게 접합되어야 함', method: '육안검사', regulation: 'KS F 4510 9항' },\n { id: 'assembly', name: '조립상태', standard: '엔드락이 견고하게 조립되어야 함', method: '육안검사', regulation: 'KS F 4510 7항 표9 인용' },\n ],\n dimension: [\n { id: 'length', name: '길이', standard: '도면치수 ± 4', method: '체크검사', unit: 'mm', sampleSize: 'n=1, c=0' },\n { id: 'height', name: '높이', standard: '도면치수 + 제한없음 - 40', method: '체크검사', unit: 'mm' },\n { id: 'interval', name: '간격', standard: '400 이하', method: 'GONO게이지', regulation: '자체규정' },\n ]\n },\n approvalLine: [{ role: 'write', department: '판매/전진' }, { role: 'review', department: '생산' }, { role: 'approve', department: '품질' }]\n },\n // 절곡품 중간검사성적서 템플릿\n 'PQC-BND': {\n title: '절곡품-중간 검사성적서',\n layout: 'bendingMidInspection',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'companyHeader' },\n { id: 'basicInfo', type: 'bendingBasicInfo' },\n { id: 'inspectionStandard', type: 'bendingInspectionStandard' },\n { id: 'inspectionData', type: 'bendingInspectionData' },\n ],\n productList: ['가이드레일', '케이스', '하단마감재', '하단 L-BAR', '연기차단재'],\n inspectionItems: {\n appearance: [\n { id: 'bending', name: '절곡상태', standard: '사용상 해로운 결함이 없을것', method: '육안검사', regulation: 'KS F 4510 5.1항' },\n ],\n dimension: [\n { id: 'length', name: '길이', standard: '도면치수 ± 4', method: '체크검사', unit: 'mm', sampleSize: 'n=1, c=0' },\n { id: 'width', name: '너비', standard: 'W50: 50±5 / W80: 80±5', method: '체크검사', unit: 'mm' },\n { id: 'interval', name: '간격', standard: '도면치수 ± 2', method: '체크검사', regulation: '자체규정' },\n ]\n },\n approvalLine: [{ role: 'write', department: '판매/전진' }, { role: 'review', department: '생산' }, { role: 'approve', department: '품질' }]\n },\n // 슬랫 중간검사성적서 템플릿\n 'PQC-SLT': {\n title: '슬랫-중간 검사성적서',\n layout: 'slatMidInspection',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'companyHeader' },\n { id: 'basicInfo', type: 'slatBasicInfo' },\n { id: 'inspectionStandard', type: 'slatInspectionStandard' },\n { id: 'inspectionData', type: 'slatInspectionData' },\n ],\n inspectionItems: {\n appearance: [\n { id: 'processing', name: '가공상태', standard: '사용상 해로운 결함이 없을것', method: '육안검사', regulation: 'KS F 4510 5.1항' },\n { id: 'curling', name: '컬링상태', standard: '컬링이 일정하게 성형', method: '육안검사', regulation: 'KS F 4510 9항' },\n { id: 'hooking', name: '후킹상태', standard: '슬랫간 결합이 견고할 것', method: '육안검사', regulation: 'KS F 4510 9항' },\n ],\n dimension: [\n { id: 'length', name: '길이', standard: '도면치수 ± 4', method: '체크검사', unit: 'mm', regulation: 'KS F 4510 7항 표9 인용' },\n { id: 'width', name: '너비', standard: '도면치수 ± 2', method: '체크검사', unit: 'mm', regulation: '자체규정' },\n { id: 'gap', name: '간격', standard: '400 이하', method: 'GONO 게이지', regulation: '자체규정' },\n ]\n },\n approvalLine: [{ role: 'write', department: '판매/전진' }, { role: 'review', department: '생산' }, { role: 'approve', department: '품질' }]\n },\n // 조인트바 중간검사성적서 템플릿\n 'PQC-JB': {\n title: '조인트바-중간 검사성적서',\n layout: 'jointBarMidInspection',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'companyHeader' },\n { id: 'basicInfo', type: 'jointBarBasicInfo' },\n { id: 'inspectionStandard', type: 'jointBarInspectionStandard' },\n { id: 'inspectionData', type: 'jointBarInspectionData' },\n ],\n inspectionItems: {\n appearance: [\n { id: 'processing', name: '가공상태', standard: '사용상 해로운 결함이 없을것', method: '육안검사', regulation: 'KS F 4510 5.1항' },\n { id: 'welding', name: '용접상태', standard: '용접부위가 견고할 것', method: '육안검사', regulation: 'KS F 4510 9항' },\n { id: 'assembly', name: '조립상태', standard: '조인트바가 정확히 결합될 것', method: '육안검사', regulation: 'KS F 4510 9항' },\n ],\n dimension: [\n { id: 'length', name: '길이', standard: '도면치수 ± 3', method: '체크검사', unit: 'mm', regulation: 'KS F 4510 7항 표9 인용' },\n { id: 'width', name: '너비', standard: '도면치수 ± 2', method: '체크검사', unit: 'mm', regulation: '자체규정' },\n { id: 'hole', name: '홀위치', standard: '도면치수 ± 1', method: '체크검사', regulation: '자체규정' },\n ]\n },\n approvalLine: [{ role: 'write', department: '판매/전진' }, { role: 'review', department: '생산' }, { role: 'approve', department: '품질' }]\n },\n // 제품검사성적서 템플릿\n 'FQC': {\n title: '제품검사성적서',\n layout: 'productInspection',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'companyHeader' },\n { id: 'approvalBox', type: 'approvalTable', roles: ['작성', '승인'], showLine: true },\n {\n id: 'basicInfo', type: 'infoTable', rows: [\n [{ label: '상품명', field: 'productName' }, { label: '제품 LOT NO', field: 'lotNo' }],\n [{ label: '제품명', field: 'itemName' }, { label: '로트크기', field: 'lotSize' }, { label: 'EA', type: 'unit' }],\n [{ label: '발주처', field: 'customerName' }, { label: '검사일자', field: 'inspectionDate' }],\n [{ label: '현장명', field: 'siteName' }, { label: '검사자', field: 'inspector' }],\n ]\n },\n { id: 'productPhoto', type: 'diagramSection', title: '제품사진' },\n { id: 'inspectionTable', type: 'productInspectionTable' },\n { id: 'remarks', type: 'remarksSection', title: '특이사항' },\n { id: 'judgement', type: 'judgementBox', options: ['적합', '부적합'] },\n ],\n inspectionItems: [\n {\n no: 1, category: '겉모양', items: [\n { id: 'processing', name: '가공상태', standard: '사용상 해로운 결함이 없을 것', method: '육안검사', period: '전수검사' },\n { id: 'sewing', name: '재봉상태', standard: '내화실에 의해 견고하게 접합되어야 함', method: '육안검사', period: '전수검사' },\n { id: 'assembly', name: '조립상태', standard: '엔드락이 견고하게 조립되어야 함', method: '육안검사', period: '전수검사' },\n { id: 'smokeBarrier', name: '연기차단재', standard: '연기차단재 설치여부(케이스 W80, 가이드레일 W50(양쪽설치))', method: '육안검사', period: '전수검사' },\n { id: 'bottomFinish', name: '하단마감재', standard: '내부 무게평철 설치 유무', method: '육안검사', period: '전수검사' },\n ]\n },\n {\n no: 2, category: '모터', items: [\n { id: 'motor', name: '', standard: '인정제품과 동일사양', method: '', period: '' },\n ]\n },\n {\n no: 3, category: '재질', items: [\n { id: 'material', name: '', standard: 'WY-SC780 인쇄상태 확인', method: '', period: '' },\n ]\n },\n {\n no: 4, category: '치수\\n(오픈사이즈)', items: [\n { id: 'length', name: '길이', standard: '발주치수 ± 30mm', method: '체크검사', period: '전수검사', hasValue: true },\n { id: 'height', name: '높이', standard: '발주치수 ± 30mm', method: '체크검사', period: '전수검사', hasValue: true },\n { id: 'guideRailGap', name: '가이드레일 홈간격', standard: '10 ± 5mm(측정부위: ⓐ 높이 100 이내)', method: '체크검사', period: '전수검사', hasValue: true },\n { id: 'bottomGap', name: '하단마감재간격', standard: '간격 (③+④) 가이드레일과 하단마감재 틈새 25mm 이내', method: '체크검사', period: '전수검사', hasValue: true },\n ]\n },\n {\n no: 5, category: '작동테스트', items: [\n { id: 'operation', name: '개폐성능', standard: '작동 유무 확인(일부 및 완전폐쇄)', method: '', period: '' },\n ]\n },\n {\n no: 6, category: '내화시험', items: [\n { id: 'fire1', name: '비차열성', standard: '6mm 균열게이지 관통 후 150mm 이동 유무', method: '', period: '' },\n { id: 'fire2', name: '', standard: '25mm 균열게이지 관통 유무', method: '', period: '' },\n { id: 'fire3', name: '', standard: '10초 이상 지속되는 화염발생 유무', method: '', period: '' },\n ]\n },\n {\n no: 7, category: '차연시험', items: [\n { id: 'smoke', name: '공기누설량', standard: '25Pa 일때 공기누설량 0.9㎥/min·㎡ 이하', method: '', period: '' },\n ]\n },\n {\n no: 8, category: '개폐시험', items: [\n { id: 'open1', name: '', standard: '개폐의 원활한 작동', method: '', period: '', isCertTest: true },\n { id: 'open2', name: '평균속도', standard: '전도개폐 2.5 ~ 6.5m/min', method: '', period: '', isCertTest: true },\n { id: 'open3', name: '', standard: '자중강하 3 ~ 7m/min', method: '', period: '', isCertTest: true },\n { id: 'open4', name: '', standard: '개폐 시 상부 및 하부 끝부분에서 자동정지', method: '', period: '', isCertTest: true },\n { id: 'open5', name: '', standard: '강하 중 임의의 위치에서 정지', method: '', period: '', isCertTest: true },\n ]\n },\n {\n no: 9, category: '내충격시험', items: [\n { id: 'impact', name: '', standard: '방화상 유해한 파괴, 박리 탈락 유무', method: '', period: '' },\n ]\n },\n ],\n certTestInfo: {\n method: '공인 시험기관\\n시험성적서',\n period: '1회 / 5년',\n },\n approvalLine: [{ role: 'write' }, { role: 'approve' }]\n },\n // 세금계산서 템플릿\n 'INV': {\n title: '세 금 계 산 서',\n layout: 'taxInvoice',\n showCompanyLogo: false,\n showApprovalBox: false,\n sections: [\n { id: 'header', type: 'taxInvoiceHeader' },\n { id: 'parties', type: 'taxInvoiceParties' },\n { id: 'itemTable', type: 'taxInvoiceTable' },\n { id: 'summary', type: 'taxInvoiceSummary' },\n ],\n approvalLine: []\n },\n // ========== 작업일지 템플릿 ==========\n // 스크린 작업일지\n 'WL-SCR': {\n title: '작 업 일 지',\n subtitle: '스크린 생산부서',\n layout: 'workLog',\n showCompanyLogo: true,\n showApprovalBox: true,\n processType: '스크린',\n sections: [\n { id: 'header', type: 'workLogHeader', logo: { text: 'KD', subText: '경동기업' } },\n { id: 'approvalBox', type: 'approvalTable', roles: ['작성', '검토', '승인'], footer: ['판매/전진', '생산', '품질'] },\n { id: 'department', type: 'departmentBox', name: '스크린 생산부서' },\n {\n id: 'orderInfo', type: 'workLogOrderInfo', columns: [\n { section: '신청업체', rows: [['발주일', 'orderDate'], ['업체명', 'company'], ['담당자', 'manager'], ['연락처', 'contact']] },\n { section: '신청내용', rows: [['현장명', 'siteName'], ['작업일자', 'workDate'], ['제품 LOT NO.', 'productLotNo'], ['생산담당자', 'productionManager']] },\n ]\n },\n { id: 'workTable', type: 'screenWorkTable', columns: ['일련번호', '입고 LOT NO.', '제품명', '부호', '가로', '세로', '나머지높이', '1180', '900', '600', '400', '300'] },\n { id: 'materialUsage', type: 'materialUsageTable' },\n { id: 'remarks', type: 'remarksSection' },\n ],\n approvalLine: [{ role: 'write', department: '판매/전진' }, { role: 'review', department: '생산' }, { role: 'approve', department: '품질' }]\n },\n // 슬랫 작업일지\n 'WL-SLT': {\n title: '작 업 일 지',\n subtitle: '슬랫 생산부서',\n layout: 'workLog',\n showCompanyLogo: true,\n showApprovalBox: true,\n processType: '슬랫',\n sections: [\n { id: 'header', type: 'workLogHeader', logo: { text: 'KD', subText: '경동기업' } },\n { id: 'approvalBox', type: 'approvalTable', roles: ['작성', '검토', '승인'], footer: ['판매/전진', '생산', '품질'] },\n { id: 'department', type: 'departmentBox', name: '슬랫 생산부서' },\n { id: 'orderInfo', type: 'workLogOrderInfo' },\n { id: 'workTable', type: 'slatWorkTable', columns: ['일련번호', '입고 LOT NO.', '방화유리수량', '가로', '세로', '매수(세로)', '조인트바수량', '코일사용량'] },\n { id: 'summary', type: 'slatSummaryTable' },\n { id: 'remarks', type: 'remarksSection' },\n ],\n approvalLine: [{ role: 'write', department: '판매/전진' }, { role: 'review', department: '생산' }, { role: 'approve', department: '품질' }]\n },\n // 절곡 작업일지\n 'WL-FLD': {\n title: '작 업 일 지',\n subtitle: '절곡 생산부서',\n layout: 'workLog',\n showCompanyLogo: true,\n showApprovalBox: true,\n processType: '절곡',\n sections: [\n { id: 'header', type: 'workLogHeader', logo: { text: 'KD', subText: '경동기업' } },\n { id: 'approvalBox', type: 'approvalTable', roles: ['작성', '검토', '승인'], footer: ['판매/전진', '생산', '품질'] },\n { id: 'department', type: 'departmentBox', name: '절곡 생산부서' },\n { id: 'orderInfo', type: 'workLogOrderInfo' },\n { id: 'productInfo', type: 'productInfoTable' },\n { id: 'workTable', type: 'bendingWorkTable', columns: ['세부품명', '재질', '입고 & 생산 LOT NO', '길이/규격', '수량'] },\n { id: 'productionTotal', type: 'productionTotalTable', items: ['SUS', 'EGI'] },\n { id: 'remarks', type: 'remarksSection' },\n ],\n approvalLine: [{ role: 'write', department: '판매/전진' }, { role: 'review', department: '생산' }, { role: 'approve', department: '품질' }]\n },\n // 재고생산 작업일지 (중간검사성적서)\n 'WL-STK': {\n title: '절곡품 재고생산 작업일지',\n subtitle: '(중간검사성적서)',\n layout: 'workLog',\n showCompanyLogo: true,\n showApprovalBox: true,\n processType: '재고생산',\n sections: [\n { id: 'header', type: 'workLogHeader', logo: { text: 'KD', subText: '경동기업' }, title: '절곡품 재고생산 작업일지', subtitle: '(중간검사성적서)' },\n { id: 'approvalBox', type: 'approvalTable', roles: ['작성', '검토', '승인'], footer: ['판매/개발자', '생산', '품질'] },\n { id: 'department', type: 'departmentBox', name: '절곡 생산부서' },\n {\n id: 'productInfo', type: 'stockProductInfoTable', rows: [\n [{ label: '품명', field: 'productName' }, { label: '규격', field: 'spec' }, { label: '길이', field: 'length' }],\n [{ label: '입고 LOT NO', field: 'inboundLotNo' }, { label: '생산 LOT NO', field: 'productLotNo' }, { label: '로트크기', field: 'lotSize' }],\n ]\n },\n { id: 'inspectionStandard', type: 'inspectionStandardSection', title: '■ 중간검사 기준서', hasDrawing: true },\n { id: 'inspectionData', type: 'stockInspectionTable', columns: ['검사항목', '겉모양/절곡상태', '길이', '너비', '간격', '판정'] },\n { id: 'finalJudgement', type: 'finalJudgementBox', options: ['합격', '불합격'] },\n { id: 'inspectorInfo', type: 'inspectorInfoTable' },\n ],\n inspectionItems: [\n { id: 'appearance', name: '겉모양/절곡상태', method: '육안검사', standard: '이상무' },\n { id: 'length', name: '길이', method: '버니어캘리퍼스', standard: '2500±3' },\n { id: 'width', name: '너비', method: '버니어캘리퍼스', standard: '120±1' },\n { id: 'interval', name: '간격', method: '버니어캘리퍼스', standard: '14±0.5' },\n ],\n approvalLine: [{ role: 'write', department: '판매/개발자' }, { role: 'review', department: '생산' }, { role: 'approve', department: '품질' }]\n },\n // 출고증 템플릿\n 'SHP': {\n title: '출 고 증',\n layout: 'shipment',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'shipmentHeader' },\n { id: 'approvalBox', type: 'approvalTable', roles: ['작성', '검토', '승인'] },\n { id: 'productInfo', type: 'shipmentProductInfo' },\n { id: 'orderInfo', type: 'shipmentOrderInfo' },\n { id: 'deliveryInfo', type: 'shipmentDeliveryInfo' },\n { id: 'itemTable', type: 'shipmentItemTable' },\n { id: 'remarks', type: 'remarksSection' },\n ],\n approvalLine: [{ role: 'write', department: '판매/전진' }, { role: 'review', department: '출하' }, { role: 'approve', department: '생산관리' }]\n },\n // 거래명세서 템플릿 (물류)\n 'TXS': {\n title: '거 래 명 세 서',\n layout: 'transactionStatement',\n showCompanyLogo: true,\n showApprovalBox: false,\n sections: [\n { id: 'header', type: 'titleHeader' },\n { id: 'tradeInfo', type: 'twoColumnTable' },\n { id: 'itemTable', type: 'productTable' },\n { id: 'summary', type: 'amountSummary' },\n ],\n approvalLine: []\n },\n // 납품확인서 템플릿\n 'DC': {\n title: '납 품 확 인 서',\n layout: 'deliveryConfirmation',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'kdHeader' },\n { id: 'approvalBox', type: 'approvalTable', roles: ['작성', '검토', '승인'] },\n { id: 'orderInfo', type: 'deliveryOrderInfo' },\n { id: 'deliveryInfo', type: 'deliveryDeliveryInfo' },\n { id: 'itemTable', type: 'deliveryItemTable' },\n { id: 'remarks', type: 'remarksSection' },\n { id: 'signature', type: 'deliverySignature' },\n ],\n approvalLine: [{ role: 'write', department: '판매/전진' }, { role: 'review', department: '출하' }, { role: 'approve', department: '품질' }]\n },\n // 출고증 템플릿 (작업일지 스타일)\n 'TS': {\n title: '출 고 증',\n layout: 'worklogStyle',\n showCompanyLogo: true,\n showApprovalBox: true,\n sections: [\n { id: 'header', type: 'kdHeader' },\n { id: 'approvalBox', type: 'approvalTable', roles: ['작성', '검토', '승인'] },\n { id: 'headerInfo', type: 'threeColumnHeader', columns: ['신청업체', '신청내용', '납품정보'] },\n { id: 'materialsTable', type: 'tsTable', title: '부자재' },\n { id: 'motorsTable', type: 'tsTable', title: '모터' },\n { id: 'slatTable', type: 'tsTable', title: '슬랫' },\n { id: 'foldingTable', type: 'tsTable', title: '절곡품', hasLotNo: true },\n { id: 'consumablesTable', type: 'tsTable', title: '소모품' },\n ],\n approvalLine: [{ role: 'write', department: '판매/전진' }, { role: 'review', department: '출하' }, { role: 'approve', department: '생산관리' }]\n },\n };\n\n // 문서 카테고리 정의\n const documentCategories = [\n { id: 'sales', code: 'SALES', name: '판매문서', icon: '📋', description: '견적서, 수주확인서 등', sortOrder: 1 },\n { id: 'production', code: 'PROD', name: '생산문서', icon: '🏭', description: '작업지시서, 발주서 등', sortOrder: 2 },\n { id: 'quality', code: 'QC', name: '품질문서', icon: '✅', description: '검사성적서, 부적합보고서 등', sortOrder: 3 },\n { id: 'logistics', code: 'LOGI', name: '물류문서', icon: '🚚', description: '입출고전표, 배송지시서 등', sortOrder: 4 },\n { id: 'accounting', code: 'ACC', name: '회계문서', icon: '💰', description: '세금계산서, 거래명세서 등', sortOrder: 5 },\n ];\n\n // 문서 유형 정의 (documentTemplateConfig에서 가져와서 연동)\n // 공정 등록 페이지의 작업일지 양식 드롭다운과 동일한 데이터 소스 사용\n const documentTypes = (documentTemplateConfig.documentTypes || []).map(doc => ({\n ...doc,\n hasTemplate: ['QT', 'QT-DTL', 'SO', 'SL', 'WO', 'WO-SCR', 'WO-SLT', 'WO-FLD', 'IQC', 'PQC', 'PQC-SCR', 'PQC-BND', 'PQC-SLT', 'PQC-JB', 'FQC', 'INV', 'WL-SCR', 'WL-SLT', 'WL-FLD', 'WL-STK', 'WL-PKG', 'DC', 'FQC-REQ', 'TS'].includes(doc.code),\n }));\n\n // 결재 역할\n const approvalRoles = [\n { id: 'write', name: '작성', sortOrder: 1 },\n { id: 'review', name: '검토', sortOrder: 2 },\n { id: 'approve', name: '승인', sortOrder: 3 },\n { id: 'confirm', name: '확인', sortOrder: 4 },\n ];\n\n // 회사 기본 정보\n const companyInfo = {\n name: '경동기업',\n nameEn: 'KYUNGDONG COMPANY',\n businessNo: '135-86-12345',\n ceo: '김 대 표',\n address: '경기도 안성시 공업용지 오성길 45-22',\n phone: '031-983-5130',\n fax: '031-983-5131',\n businessType: '제조업',\n businessItem: '방화셔터, 스크린셔터, 방화문',\n };\n const [activeTab, setActiveTab] = useState('template');\n const [selectedCategory, setSelectedCategory] = useState('all');\n const [selectedDocType, setSelectedDocType] = useState(null);\n const [showPreviewModal, setShowPreviewModal] = useState(false);\n const [showTemplateEditor, setShowTemplateEditor] = useState(false);\n const [showCreateModal, setShowCreateModal] = useState(false);\n const [editorMode, setEditorMode] = useState('view'); // 'view', 'edit', 'create'\n\n // 통합 문서 등록을 위한 섹션 타입 정의\n const sectionTypeOptions = [\n { id: 'header', name: '문서헤더', description: '문서 제목, 번호, 일자', icon: 'FileText' },\n { id: 'partyInfo', name: '당사자정보', description: '수요자/공급자 정보 테이블', icon: 'Users' },\n { id: 'infoTable', name: '정보테이블', description: '라벨-값 형식 테이블', icon: 'Table' },\n { id: 'amountBox', name: '금액박스', description: '총 금액 강조 표시', icon: 'DollarSign' },\n { id: 'productTable', name: '품목테이블', description: '품목/수량/단가/금액', icon: 'Package' },\n { id: 'inspectionTable', name: '검사테이블', description: '검사항목/기준/결과', icon: 'ShieldCheck' },\n { id: 'remarks', name: '비고사항', description: '비고/특이사항 영역', icon: 'Edit' },\n { id: 'signature', name: '서명/인감', description: '날짜, 회사명, 인감', icon: 'CheckCircle' },\n { id: 'approval', name: '결재란', description: '결재선 (작성/검토/승인)', icon: 'Users' },\n { id: 'judgement', name: '판정', description: '합격/불합격 판정', icon: 'CheckSquare' },\n { id: 'diagram', name: '도면/사진', description: '제품 도면 또는 사진', icon: 'Image' },\n ];\n\n // 템플릿 편집 상태\n const [templateForm, setTemplateForm] = useState({\n title: '',\n code: '',\n category: 'sales',\n description: '',\n paperSize: 'A4',\n orientation: 'portrait',\n showCompanyLogo: true,\n showApprovalBox: false,\n approvalLines: [],\n sections: [],\n inspectionItems: [\n { no: 1, item: '외관검사', standard: '육안검사', method: '목시', value: '', judgement: '' },\n { no: 2, item: '치수검사', standard: '도면 기준', method: '버니어캘리퍼스', value: '', judgement: '' },\n { no: 3, item: '재질검사', standard: '성적서 확인', method: '서류확인', value: '', judgement: '' },\n ],\n productItems: [\n { no: 1, name: '', spec: '', unit: 'EA', qty: '', price: '', amount: '', remark: '' },\n ],\n remarks: '',\n judgementOptions: ['합격', '불합격', '조건부합격'],\n });\n\n // 섹션 추가\n const handleAddSection = (sectionType) => {\n const newSection = {\n id: `section_${Date.now()}`,\n type: sectionType.id,\n typeName: sectionType.name,\n config: {},\n sortOrder: templateForm.sections.length + 1,\n };\n setTemplateForm(prev => ({\n ...prev,\n sections: [...prev.sections, newSection]\n }));\n };\n\n // 섹션 삭제\n const handleRemoveSection = (sectionId) => {\n setTemplateForm(prev => ({\n ...prev,\n sections: prev.sections.filter(s => s.id !== sectionId).map((s, i) => ({ ...s, sortOrder: i + 1 }))\n }));\n };\n\n // 섹션 순서 변경 (위로)\n const handleMoveSectionUp = (index) => {\n if (index === 0) return;\n const newSections = [...templateForm.sections];\n [newSections[index - 1], newSections[index]] = [newSections[index], newSections[index - 1]];\n setTemplateForm(prev => ({\n ...prev,\n sections: newSections.map((s, i) => ({ ...s, sortOrder: i + 1 }))\n }));\n };\n\n // 섹션 순서 변경 (아래로)\n const handleMoveSectionDown = (index) => {\n if (index === templateForm.sections.length - 1) return;\n const newSections = [...templateForm.sections];\n [newSections[index], newSections[index + 1]] = [newSections[index + 1], newSections[index]];\n setTemplateForm(prev => ({\n ...prev,\n sections: newSections.map((s, i) => ({ ...s, sortOrder: i + 1 }))\n }));\n };\n\n // 새 문서양식 생성 모달 열기\n const handleOpenCreateModal = () => {\n setTemplateForm({\n title: '',\n code: '',\n category: 'sales',\n description: '',\n paperSize: 'A4',\n orientation: 'portrait',\n showCompanyLogo: true,\n showApprovalBox: false,\n approvalLines: [],\n sections: [],\n inspectionItems: [],\n productItems: [],\n remarks: '',\n judgementOptions: ['합격', '불합격'],\n });\n setEditorMode('create');\n setShowCreateModal(true);\n };\n\n // 템플릿 저장\n const handleSaveTemplate = () => {\n console.log('저장할 템플릿:', templateForm);\n alert(`문서양식 \"${templateForm.title}\" 이(가) 저장되었습니다.`);\n setShowCreateModal(false);\n setEditorMode('view');\n };\n\n // 샘플 데이터 (미리보기용)\n const sampleData = {\n docNo: 'KD-PR-251209-01',\n docDate: '2025.12.09',\n customerName: '삼성물산(주)',\n customerCeo: '홍 길 동',\n customerBusinessNo: '123-45-67890',\n customerAddress: '서울시 강남구 삼성동 123',\n customerPhone: '02-1234-5678',\n siteName: '용신고등학교(4층)',\n siteAddress: '서울시 강남구 삼성동 456',\n itemName: '스크린 방화셔터',\n spec: '와이어 클라스 코팅직물',\n openSize: 'W3000 x H2500',\n makeSize: 'W3100 x H2600',\n caseSize: 'W3100 x D300 x H350',\n qty: '11',\n dueDate: '2025.12.20',\n totalAmount: '12,500,000',\n supplyAmount: '11,363,636',\n taxAmount: '1,136,364',\n lotNo: 'KD-WE-251015-01-(3)',\n lotSize: '11',\n inspectionDate: '2025.',\n orderNo: 'KD-TS-251209-01',\n orderDate: '2025.12.05',\n supplierName: '주일',\n inspector: '',\n // 중간검사 스크린용 샘플 데이터\n screenInspectionData: [\n { no: '01', processing: null, sewing: null, assembly: null, lengthSpec: '7,400', lengthVal: '', heightSpec: '2,950', heightVal: '', intervalStd: '400 이하', intervalOK: null, intervalNG: null, judgement: null },\n { no: '02', processing: null, sewing: null, assembly: null, lengthSpec: '4,700', lengthVal: '', heightSpec: '2,950', heightVal: '', intervalStd: '400 이하', intervalOK: null, intervalNG: null, judgement: null },\n { no: '03', processing: null, sewing: null, assembly: null, lengthSpec: '6,790', lengthVal: '', heightSpec: '2,950', heightVal: '', intervalStd: '400 이하', intervalOK: null, intervalNG: null, judgement: null },\n { no: '04', processing: null, sewing: null, assembly: null, lengthSpec: '3,700', lengthVal: '', heightSpec: '2,950', heightVal: '', intervalStd: '400 이하', intervalOK: null, intervalNG: null, judgement: null },\n { no: '05', processing: null, sewing: null, assembly: null, lengthSpec: '6,000', lengthVal: '', heightSpec: '2,950', heightVal: '', intervalStd: '400 이하', intervalOK: null, intervalNG: null, judgement: null },\n { no: '06', processing: null, sewing: null, assembly: null, lengthSpec: '7,300', lengthVal: '', heightSpec: '2,950', heightVal: '', intervalStd: '400 이하', intervalOK: null, intervalNG: null, judgement: null },\n { no: '07', processing: null, sewing: null, assembly: null, lengthSpec: '3,700', lengthVal: '', heightSpec: '2,950', heightVal: '', intervalStd: '400 이하', intervalOK: null, intervalNG: null, judgement: null },\n ],\n // 중간검사 절곡품용 샘플 데이터\n bendingInspectionData: {\n productName: 'KWE01',\n finishType: 'SUS마감',\n items: [\n { category: 'KWE01', name: '가이드레일', type: '벽면형', bendingOK: null, bendingNG: null, lengthSpec: '3,000', lengthVal: '', widthSpec: 'N/A', widthVal: 'N/A', points: [{ id: 1, spec: 30 }, { id: 2, spec: 80 }, { id: 3, spec: 45 }, { id: 4, spec: 40 }, { id: 5, spec: 34 }] },\n { category: 'KWE01', name: '케이스', type: '500*380', bendingOK: null, bendingNG: null, lengthSpec: '3,000', lengthVal: '', widthSpec: 'N/A', widthVal: 'N/A', points: [{ id: 1, spec: 380 }, { id: 2, spec: 50 }, { id: 3, spec: 240 }, { id: 4, spec: 50 }] },\n { category: 'KWE01', name: '하단마감재', type: '60x40', bendingOK: null, bendingNG: null, lengthSpec: '3,000', lengthVal: '', widthSpec: 'N/A', widthVal: 'N/A', points: [{ id: 1, spec: 60 }, { id: 2, spec: 64 }] },\n { category: 'KWE01', name: '하단 L-BAR', type: '17x60', bendingOK: null, bendingNG: null, lengthSpec: '3,000', lengthVal: '', widthSpec: 'N/A', widthVal: 'N/A', points: [{ id: 1, spec: 17 }] },\n { category: 'KWE01', name: '연기차단재', type: 'W50\\n(가이드레일용)', bendingOK: null, bendingNG: null, lengthSpec: '3,000', lengthVal: '', widthSpec: '① 50', widthVal: '', points: [{ id: 2, spec: 12 }] },\n { category: 'KWE01', name: '연기차단재', type: 'W80\\n(케이스용)', bendingOK: null, bendingNG: null, lengthSpec: '3,000', lengthVal: '', widthSpec: '① 80', widthVal: '', points: [{ id: 2, spec: 12 }] },\n ]\n },\n // 제품검사 샘플 데이터\n productInspectionData: {\n productName: '스크린 방화셔터',\n itemName: 'KWE01',\n inspectionItems: [\n {\n no: 1, category: '겉모양', items: [\n { name: '가공상태', result: null },\n { name: '재봉상태', result: null },\n { name: '조립상태', result: null },\n { name: '연기차단재', result: null },\n { name: '하단마감재', result: null },\n ]\n },\n { no: 2, category: '모터', items: [{ name: '', result: null }] },\n { no: 3, category: '재질', items: [{ name: '', result: null }] },\n {\n no: 4, category: '치수(오픈사이즈)', items: [\n { name: '길이', result: null, value: '' },\n { name: '높이', result: null, value: '' },\n { name: '가이드레일 홈간격', result: null, value: '' },\n { name: '하단마감재간격', result: null, value: '' },\n ]\n },\n { no: 5, category: '작동테스트', items: [{ name: '개폐성능', result: null }] },\n {\n no: 6, category: '내화시험', items: [\n { name: '비차열성', result: null },\n { name: '', result: null },\n { name: '', result: null },\n ]\n },\n { no: 7, category: '차연시험', items: [{ name: '공기누설량', result: null }] },\n {\n no: 8, category: '개폐시험', items: [\n { name: '', result: null },\n { name: '평균속도', result: null },\n { name: '', result: null },\n { name: '', result: null },\n { name: '', result: null },\n ]\n },\n { no: 9, category: '내충격시험', items: [{ name: '', result: null }] },\n ],\n },\n // 수입검사 샘플 데이터\n incomingInspectionData: {\n itemName: '전기 아연도금 강판\\n(KS D 3528, SECC) \"EGI 절곡판\"',\n supplierName: '지오TNS\\n(KG스틸)',\n spec: '1.55 * 1218 * 480',\n specDetail: { thickness: 1.55, width: 1218, length: 480 },\n lotNo: '250715-02',\n materialNo: 'PE02RB',\n lotSize: '200',\n lotUnit: '매',\n inspectionDate: '07/15',\n receiveDate: '2025-07-15',\n inspector: '노완호',\n approver: '노완호',\n approvalDate: '07/15',\n items: [\n { no: 1, category: '겉모양', standard: '사용상 해로운 결함이 없을 것', method: '육안검사', period: '', n1: 'OK', n2: 'OK', n3: 'OK', result: '적' },\n {\n no: 2, category: '치수', subItems: [\n { name: '두께', spec: 1.55, selectedRange: '1.25 이상 ~ 1.6 미만', tolerance: '± 0.10', n1: 1.528, n2: 1.533, n3: 1.521, result: '적' },\n { name: '너비', spec: 1219, selectedRange: '1250 미만', tolerance: '+7/-0', n1: 1222, n2: 1222, n3: 1222, result: '적' },\n { name: '길이', spec: 480, selectedRange: '1250 미만', tolerance: '+10/-0', n1: 480, n2: 480, n3: 480, result: '적' },\n ], method: '체크검사', period: 'n=3\\nc=0'\n },\n { no: 3, category: '인장강도 (N/㎟)', standard: '270 이상', method: '', period: '', n1: 313.8, n2: '', n3: '', result: '적' },\n { no: 4, category: '연신율\\n%', selectedRange: '두께 1.0 이상 ~ 1.6 미만', standard: '37 이상', method: '공급업체\\n밀시트', period: '입고시', n1: 46.5, n2: '', n3: '', result: '적' },\n { no: 5, category: '아연의 최소\\n부착량 (g/㎡)', standard: '한면 17 이상', method: '', period: '', n1: '17.21 / 17.17', n2: '', n3: '', result: '적' },\n ],\n remarks: [\n '# 1.55mm의 경우 KS F 4510에 따른 MIN 1.5의 기준에 따름',\n '# 두께의 경우 너비 1000 이상 ~ 1250 미만 기준에 따름',\n ],\n overallJudgement: '합격',\n },\n };\n\n // 필터링된 문서 유형\n const filteredDocTypes = selectedCategory === 'all'\n ? documentTypes\n : documentTypes.filter(dt => dt.category === selectedCategory);\n\n // 문서 유형 선택\n const handleSelectDocType = (docType) => {\n setSelectedDocType(docType);\n const template = documentTemplates[docType.code];\n if (template) {\n setTemplateForm({\n title: template.title,\n approvalLines: template.approvalLine || [],\n inspectionItems: [\n { no: 1, item: '외관검사', standard: '육안검사', method: '목시', value: '', judgement: '' },\n { no: 2, item: '치수검사', standard: '도면 기준', method: '버니어캘리퍼스', value: '', judgement: '' },\n { no: 3, item: '재질검사', standard: '성적서 확인', method: '서류확인', value: '', judgement: '' },\n ],\n productItems: [\n { no: 1, name: '', spec: '', unit: 'EA', qty: '', price: '', amount: '', remark: '' },\n ],\n remarks: '',\n judgementOptions: ['합격', '불합격', '조건부합격'],\n });\n }\n setShowPreviewModal(true);\n };\n\n // 결재자 추가\n const handleAddApprovalLine = () => {\n setTemplateForm(prev => ({\n ...prev,\n approvalLines: [...prev.approvalLines, { role: 'review', department: '', name: '' }]\n }));\n };\n\n // 결재자 삭제\n const handleRemoveApprovalLine = (index) => {\n setTemplateForm(prev => ({\n ...prev,\n approvalLines: prev.approvalLines.filter((_, i) => i !== index)\n }));\n };\n\n // 검사항목 추가\n const handleAddInspectionItem = () => {\n setTemplateForm(prev => ({\n ...prev,\n inspectionItems: [...prev.inspectionItems, {\n no: prev.inspectionItems.length + 1,\n item: '', standard: '', method: '', value: '', judgement: ''\n }]\n }));\n };\n\n // 검사항목 삭제\n const handleRemoveInspectionItem = (index) => {\n setTemplateForm(prev => ({\n ...prev,\n inspectionItems: prev.inspectionItems.filter((_, i) => i !== index).map((item, i) => ({ ...item, no: i + 1 }))\n }));\n };\n\n // 품목 추가\n const handleAddProductItem = () => {\n setTemplateForm(prev => ({\n ...prev,\n productItems: [...prev.productItems, {\n no: prev.productItems.length + 1,\n name: '', spec: '', unit: 'EA', qty: '', price: '', amount: '', remark: ''\n }]\n }));\n };\n\n // 품목 삭제\n const handleRemoveProductItem = (index) => {\n setTemplateForm(prev => ({\n ...prev,\n productItems: prev.productItems.filter((_, i) => i !== index).map((item, i) => ({ ...item, no: i + 1 }))\n }));\n };\n\n // 탭 정의\n const tabs = [\n { id: 'template', label: '문서유형', icon: 'FileText' },\n { id: 'preview', label: '미리보기', icon: 'Eye' },\n ];\n\n // 문서별 미리보기 렌더러\n const renderDocumentPreview = (docCode) => {\n const template = documentTemplates[docCode];\n if (!template) return
템플릿이 정의되지 않았습니다.
;\n\n // ========== 공통 렌더링 함수들 ==========\n\n // 1. KD 로고 헤더 공통 렌더러\n const renderKDHeader = (title, departmentName, approvalData = {}, options = {}) => (\n
\n \n \n {/* KD 로고 - 100% 공통 */}\n | \n KD \n 경동기업 \n {options.subTitle || 'KYUNGDONG COMPANY'} \n | \n {/* 제목 - 부서명 없으면 4행 병합, 있으면 2행 병합 */}\n \n {title} \n {options.subtitle && {options.subtitle} }\n | \n {/* 결재 라벨 */}\n \n 결 \n 재 \n | \n {/* 결재란 헤더 */}\n 작성 | \n 검토 | \n 승인 | \n
\n \n {/* 결재란 서명 영역 - 이름만 표시 */}\n | \n {approvalData.writer || ''} \n | \n \n {approvalData.reviewer || ''} \n | \n \n {approvalData.approver || ''} \n | \n
\n \n {/* 부서명 - 값이 있을 때만 렌더링 */}\n {departmentName && (\n | \n {departmentName}\n | \n )}\n {approvalData.writerDept || '판매/전진'} | \n {approvalData.reviewerDept || '생산'} | \n {approvalData.approverDept || '품질'} | \n
\n \n
\n );\n\n // 2. 신청업체/신청내용 테이블 공통 렌더러\n const renderRequestInfoTable = (data, options = {}) => (\n
\n \n \n | 신 청 업 체 | \n 신 청 내 용 | \n
\n \n | 발 주 일 | \n {data.orderDate} | \n 현 장 명 | \n {data.siteName} | \n
\n \n | 업 체 명 | \n {data.customerName} | \n 작업일자 | \n {data.workDate} | \n
\n \n | 담 당 자 | \n {data.managerName} | \n 제품 LOT NO. | \n {data.lotNo} | \n
\n \n | 연 락 처 | \n {data.phone || ''} | \n 생산담당자 | \n {data.productionManager || ''} | \n
\n {/* 제품명/마감유형 추가 행 (옵션) */}\n {options.showProductInfo && (\n \n | 제품명 | \n \n {data.productCode}\n {data.productName}\n | \n 마감유형 | \n \n {data.finishType}\n {data.finishSpec}\n | \n
\n )}\n \n
\n );\n\n // 3. 출고증 헤더 렌더러 (로트번호 포함)\n const renderShipmentHeader = (title, departmentName, data, approvalData = {}) => (\n
\n \n \n | \n KD \n 경동기업 \n KYUNGDONG COMPANY \n | \n \n {title} \n | \n 로트번호 | \n {data.lotNo} | \n
\n \n | \n 결 \n 재 \n | \n \n \n \n \n | 작성 | \n 검토 | \n 승인 | \n \n \n | {approvalData.writer || ''} | \n {approvalData.reviewer || ''} | \n {approvalData.approver || ''} | \n \n \n | {approvalData.writerDept || '판매/전진'} | \n {approvalData.reviewerDept || '출하'} | \n {approvalData.approverDept || '생산관리'} | \n \n \n \n | \n
\n \n | \n {departmentName}\n | \n
\n \n
\n );\n\n // ========== 문서별 미리보기 렌더링 ==========\n\n // 견적서 미리보기 (새로운 디자인 - 2025.02 업데이트)\n if (docCode === 'QT') {\n const qtSampleData = {\n docNo: 'KD-PR-250215-01',\n docDate: '2025년 02월 15일',\n customerName: '현대개발(주)',\n siteName: '연수 오피스텔',\n productName: 'ST-001',\n contactPerson: '홍길동',\n contactPhone: '010-1234-5678',\n totalAmount: 35000000,\n supplyAmount: 51600000,\n remarks: '오피스텔 신축 공사',\n items: [\n { no: 1, name: '가이드레일 상판', spec: '본관1층', qty: 100.00, unit: 'M', unitPrice: 85000, amount: 8500000 },\n { no: 2, name: '가이드레일 하판', spec: '본관1층', qty: 100.00, unit: 'M', unitPrice: 85000, amount: 8500000 },\n { no: 3, name: '셔터 세트', spec: '별관', qty: 30, unit: 'SET', unitPrice: 450000, amount: 13500000 },\n { no: 4, name: '알루미늄 프레임', spec: '지하주차장', qty: 80.00, unit: 'M', unitPrice: 120000, amount: 9600000 },\n { no: 5, name: 'DC 모터', spec: '전동식', qty: 50, unit: 'EA', unitPrice: 230000, amount: 11500000 },\n ]\n };\n return (\n
\n {/* 제목 */}\n
\n
견 적 서
\n
\n 문서번호: {qtSampleData.docNo} | 작성일자: {qtSampleData.docDate}\n
\n
\n\n
\n\n {/* 수요자 정보 */}\n
\n
\n \n \n | 수 요 자 | \n
\n \n \n \n | 업체명 | \n {qtSampleData.customerName} | \n
\n \n | 현장명 | \n {qtSampleData.siteName} | \n 담당자 | \n {qtSampleData.contactPerson} | \n
\n \n | 제품명 | \n {qtSampleData.productName} | \n 연락처 | \n {qtSampleData.contactPhone} | \n
\n \n
\n
\n\n {/* 공급자 정보 */}\n
\n
\n \n \n | 공 급 자 | \n
\n \n \n \n | 상호 | \n (주)염진건설 | \n 사업자등록번호 | \n 139-87-00353 | \n
\n \n | 대표자 | \n 김 용 진 | \n 업태 | \n 제조 | \n
\n \n | 종목 | \n 방창, 셔터, 금속창호 | \n
\n \n | 사업장주소 | \n 경기도 안성시 공업용지 오성길 45-22 | \n
\n \n | 전화 | \n 031-983-5130 | \n 팩스 | \n 02-6911-6315 | \n
\n \n
\n
\n\n {/* 총 견적금액 박스 */}\n
\n
총 견적금액
\n
₩ {qtSampleData.totalAmount.toLocaleString()}
\n
※ 부가가치세 별도
\n
\n\n {/* 세부산출내역 */}\n
\n
\n 세 부 산 출 내 역\n
\n
\n \n \n | No. | \n 품목명 | \n 규격 | \n 수량 | \n 단위 | \n 단가 | \n 금액 | \n
\n \n \n {qtSampleData.items.map((item) => (\n \n | {item.no} | \n {item.name} | \n {item.spec} | \n {item.qty.toLocaleString()} | \n {item.unit} | \n {item.unitPrice.toLocaleString()} | \n {item.amount.toLocaleString()} | \n
\n ))}\n \n \n \n | 공급가액 합계 | \n {qtSampleData.supplyAmount.toLocaleString()} | \n
\n \n
\n
\n\n {/* 비고사항 */}\n
\n
\n 비 고 사 항\n
\n
\n
{qtSampleData.remarks}
\n
\n
\n\n {/* 하단 서명 영역 */}\n
\n
\n
상기와 같이 견적합니다.
\n
{qtSampleData.docDate}
\n
공급자: (주)염진건설 (인)
\n
\n
\n (인감
날인)\n
\n
\n\n {/* 유의사항 */}\n
\n
【 유의사항 】
\n
1. 본 견적서는 {qtSampleData.docDate} 기준으로 작성되었으며, 자재 가격 변동 시 조정될 수 있습니다.
\n
2. 견적 유효기간은 발행일로부터 30일이며, 기간 경과 시 재견적이 필요합니다.
\n
3. 제작 사양 및 수량 변경 시 견적 금액이 변동될 수 있습니다.
\n
4. 현장 여건에 따라 추가 비용이 발생할 수 있습니다.
\n
\n\n {/* 문의 */}\n
\n 문의: {qtSampleData.contactPerson} | {qtSampleData.contactPhone}\n
\n
\n );\n }\n\n // 견적산출내역서 미리보기 (QT-DTL) - 2025.02 업데이트\n if (docCode === 'QT-DTL') {\n const qtDtlSampleData = {\n docNo: 'KD-PR-250215-01',\n docDate: '2025년 02월 15일',\n customerName: '현대개발(주)',\n siteName: '연수 오피스텔',\n productName: 'ST-001',\n contactPerson: '홍길동',\n contactPhone: '010-1234-5678',\n totalAmount: 35000000,\n supplyAmount: 51600000,\n remarks: '오피스텔 신축 공사',\n // 세부산출내역\n items: [\n { no: 1, name: '가이드레일 상판', spec: '본관1층', qty: 100.00, unit: 'M', unitPrice: 85000, amount: 8500000 },\n { no: 2, name: '가이드레일 하판', spec: '본관1층', qty: 100.00, unit: 'M', unitPrice: 85000, amount: 8500000 },\n { no: 3, name: '셔터 세트', spec: '별관', qty: 30, unit: 'SET', unitPrice: 450000, amount: 13500000 },\n { no: 4, name: '알루미늄 프레임', spec: '지하주차장', qty: 80.00, unit: 'M', unitPrice: 120000, amount: 9600000 },\n { no: 5, name: 'DC 모터', spec: '전동식', qty: 50, unit: 'EA', unitPrice: 230000, amount: 11500000 },\n ],\n // 소요자재내역 정보\n productType: '철재',\n productCode: '-',\n openSize: 'W - × H - (mm)',\n makeSize: 'W - × H - (mm)',\n qty: '80 SET',\n caseSize: '1219 × 550 (mm)',\n // 자재 목록\n materials: [\n { no: 1, name: '가이드레일', spec: '2438mm (EGI 1.6T, SUS 1.2T)', qty: 2, unit: '개' },\n { no: 2, name: '케이스', spec: '1219mm (EGI 1.6T)', qty: 1, unit: '개' },\n { no: 3, name: '모터', spec: '150KG', qty: 1, unit: '개' },\n { no: 4, name: '연동제어기', spec: '매입형', qty: 1, unit: '개' },\n { no: 5, name: '브라켓', spec: '백박스', qty: 1, unit: '개' },\n { no: 6, name: '엥클', spec: '-', qty: 4, unit: '개' },\n { no: 7, name: '연기차단재 (해당몰)', spec: '2438mm', qty: 2, unit: '개' },\n { no: 8, name: '연기차단재 (케이스용)', spec: '3000mm', qty: 4, unit: '개' },\n { no: 9, name: '참가시트', spec: '4500mm (5인치)', qty: 1, unit: '개' },\n { no: 10, name: '조인트바', spec: '300mm', qty: 5, unit: '개' },\n { no: 11, name: '엥클 (추가)', spec: '6000mm', qty: 4, unit: '개' },\n ]\n };\n return (\n
\n {/* 제목 */}\n
\n
견 적 산 출 내 역 서
\n
\n 문서번호: {qtDtlSampleData.docNo} | 작성일자: {qtDtlSampleData.docDate}\n
\n
\n\n
\n\n {/* 수요자 정보 */}\n
\n
\n \n \n | 수 요 자 | \n
\n \n \n \n | 업체명 | \n {qtDtlSampleData.customerName} | \n
\n \n | 현장명 | \n {qtDtlSampleData.siteName} | \n 담당자 | \n {qtDtlSampleData.contactPerson} | \n
\n \n | 제품명 | \n {qtDtlSampleData.productName} | \n 연락처 | \n {qtDtlSampleData.contactPhone} | \n
\n \n
\n
\n\n {/* 공급자 정보 */}\n
\n
\n \n \n | 공 급 자 | \n
\n \n \n \n | 상호 | \n (주)염진건설 | \n 사업자등록번호 | \n 139-87-00353 | \n
\n \n | 대표자 | \n 김 용 진 | \n 업태 | \n 제조 | \n
\n \n | 종목 | \n 방창, 셔터, 금속창호 | \n
\n \n | 사업장주소 | \n 경기도 안성시 공업용지 오성길 45-22 | \n
\n \n | 전화 | \n 031-983-5130 | \n 팩스 | \n 02-6911-6315 | \n
\n \n
\n
\n\n {/* 총 견적금액 박스 */}\n
\n
총 견적금액
\n
₩ {qtDtlSampleData.totalAmount.toLocaleString()}
\n
※ 부가가치세 별도
\n
\n\n {/* 세부산출내역 */}\n
\n
\n 세 부 산 출 내 역\n
\n
\n \n \n | No. | \n 품목명 | \n 규격 | \n 수량 | \n 단위 | \n 단가 | \n 금액 | \n
\n \n \n {qtDtlSampleData.items.map((item) => (\n \n | {item.no} | \n {item.name} | \n {item.spec} | \n {item.qty.toLocaleString()} | \n {item.unit} | \n {item.unitPrice.toLocaleString()} | \n {item.amount.toLocaleString()} | \n
\n ))}\n \n \n \n | 공급가액 합계 | \n {qtDtlSampleData.supplyAmount.toLocaleString()} | \n
\n \n
\n
\n\n {/* 소요자재내역 */}\n
\n
\n 소 요 자 재 내 역\n
\n
\n \n \n | 제품구분 | \n {qtDtlSampleData.productType} | \n 부호 | \n {qtDtlSampleData.productCode} | \n
\n \n | 오픈사이즈 | \n {qtDtlSampleData.openSize} | \n 제작사이즈 | \n {qtDtlSampleData.makeSize} | \n
\n \n | 수량 | \n {qtDtlSampleData.qty} | \n 케이스 | \n {qtDtlSampleData.caseSize} | \n
\n \n
\n\n {/* 자재 목록 테이블 */}\n
\n \n \n | No. | \n 자재명 | \n 규격 | \n 수량 | \n 단위 | \n
\n \n \n {qtDtlSampleData.materials.map((mat) => (\n \n | {mat.no} | \n {mat.name} | \n {mat.spec} | \n {mat.qty} | \n {mat.unit} | \n
\n ))}\n \n
\n
\n\n {/* 비고사항 */}\n
\n
\n 비 고 사 항\n
\n
\n
{qtDtlSampleData.remarks}
\n
\n
\n\n {/* 하단 서명 영역 */}\n
\n
\n
상기와 같이 견적합니다.
\n
{qtDtlSampleData.docDate}
\n
공급자: (주)염진건설 (인)
\n
\n
\n (인감
날인)\n
\n
\n\n {/* 유의사항 */}\n
\n
【 유의사항 】
\n
1. 본 견적서는 {qtDtlSampleData.docDate} 기준으로 작성되었으며, 자재 가격 변동 시 조정될 수 있습니다.
\n
2. 견적 유효기간은 발행일로부터 30일이며, 기간 경과 시 재견적이 필요합니다.
\n
3. 제작 사양 및 수량 변경 시 견적 금액이 변동될 수 있습니다.
\n
4. 현장 여건에 따라 추가 비용이 발생할 수 있습니다.
\n
\n\n {/* 문의 */}\n
\n 문의: {qtDtlSampleData.contactPerson} | {qtDtlSampleData.contactPhone}\n
\n
\n );\n }\n\n // 수주확인서 미리보기\n if (docCode === 'SO') {\n return (\n
\n
\n
\n {templateForm.approvalLines.map((line, i) => | {approvalRoles.find(r => r.id === line.role)?.name} | )}
\n {templateForm.approvalLines.map((_, i) => | )}
\n
\n
\n
{template.title}
\n
\n
\n
수 요 자
\n
\n | 상 호 | {sampleData.customerName} |
\n | 대표자 | {sampleData.customerCeo} |
\n | 주 소 | {sampleData.customerAddress} |
\n | 전 화 | {sampleData.customerPhone} |
\n
\n
\n
\n
공 급 자
\n
\n | 상 호 | {companyInfo.name} |
\n | 대표자 | {companyInfo.ceo} |
\n | 주 소 | {companyInfo.address} |
\n | 전 화 | {companyInfo.phone} |
\n
\n
\n
\n
\n \n | 현 장 명 | {sampleData.siteName} |
\n | 제 품 명 | {sampleData.itemName} | 규 격 | {sampleData.spec} |
\n | 납 기 일 | {sampleData.dueDate} | 금 액 | ₩ {sampleData.totalAmount} |
\n \n
\n
\n
\n );\n }\n\n // 작업지시서 미리보기\n if (docCode === 'WO') {\n return (\n
\n
\n
지시번호: {sampleData.orderNo}
\n
\n {templateForm.approvalLines.map((line, i) => | {approvalRoles.find(r => r.id === line.role)?.name} | )}
\n {templateForm.approvalLines.map((_, i) => | )}
\n
\n
\n
{template.title}
\n
\n \n | 수주번호 | {sampleData.orderNo} | 수주일자 | {sampleData.orderDate} |
\n | 거래처 | {sampleData.customerName} | 현장명 | {sampleData.siteName} |
\n | 제품명 | {sampleData.itemName} | 규격 | {sampleData.spec} |
\n | 지시수량 | {sampleData.qty} SET | 납기일 | {sampleData.dueDate} |
\n \n
\n
\n \n | 오픈사이즈 | {sampleData.openSize} | 제작사이즈 | {sampleData.makeSize} |
\n | 케이스 | {sampleData.caseSize} | 특이사항 | |
\n \n
\n
공정별 작업내용
\n
\n | 공정 | 작업내용 | 담당 | 시작일 | 완료일 |
\n \n {['커튼조립', '케이스조립', '모터장착', '포장'].map((process, i) => (\n | {process} | | | | |
\n ))}\n \n
\n
\n );\n }\n\n // 발주서 미리보기\n if (docCode === 'PO') {\n return (\n
\n
\n
\n {templateForm.approvalLines.map((line, i) => | {approvalRoles.find(r => r.id === line.role)?.name} | )}
\n {templateForm.approvalLines.map((_, i) => | )}
\n
\n
\n
{template.title}
\n
\n
수 신
\n
\n | 업 체 명 | | 담 당 자 | |
\n | 연 락 처 | | F A X | |
\n
\n
\n
\n \n | 납품현장 | {sampleData.siteName} | 납 기 일 | {sampleData.dueDate} |
\n | 현장주소 | {sampleData.siteAddress} |
\n \n
\n
발주 내역
\n
\n | NO | 품 명 | 규 격 | 단위 | 수량 | 비고 |
\n \n {[1, 2, 3, 4, 5].map(no => (\n | {no} | | | | | |
\n ))}\n \n
\n
\n
\n
{sampleData.docDate}
\n
{companyInfo.name}
\n
\n
\n
\n );\n }\n\n // 스크린 중간검사 성적서 미리보기 (PQC-SCR) - KD 헤더 스타일 적용\n if (docCode === 'PQC-SCR' || docCode === 'PQC') {\n return (\n
\n {/* KD 로고 헤더 + 결재란 - 작업일지 스타일 */}\n {renderKDHeader(<>스크린
중간검사 성적서>, '', {\n writer: '전진',\n writerDate: sampleData.inspectionDate,\n writerDept: '판매/전진',\n reviewerDept: '생산',\n approverDept: '품질'\n })}\n\n {/* 기본정보 테이블 */}\n
\n \n \n | 품 명 | \n 스크린 | \n 제품 LOT NO | \n {sampleData.lotNo} | \n
\n \n | 규 격 | \n {sampleData.spec} | \n 로 트 크 기 | \n {sampleData.lotSize} 개소 | \n
\n \n | 발 주 처 | \n {sampleData.supplierName} | \n 검 사 일 자 | \n {sampleData.inspectionDate} | \n
\n \n | 현 장 명 | \n {sampleData.siteName} | \n 검 사 자 | \n | \n
\n \n
\n\n {/* 중간검사 기준서 - 슬랫/조인트바와 동일한 구조 */}\n
\n \n {/* 헤더 행 */}\n \n \n 중간검사 기준서\n | \n 도해 | \n 검사항목 | \n 검사기준 | \n 검사방법 | \n 검사주기 | \n 관련규정 | \n
\n {/* 겉모양 - 가공상태 */}\n \n \n {/* 도해 다이어그램 - 스크린 이미지 */}\n \n {/* ① 앤드락 표시 - 상단 */}\n \n 1\n \n {/* 메인 스크린 본체 */}\n \n {/* 앤드락 라벨 */}\n 앤드락 \n {/* 내화실 폐쇄선 (점선) */}\n \n 내화실 \n 방화선 \n {/* ② 스크린원단 표시 */}\n \n 2\n 스크린원단\n \n \n {/* ③ 간격 표시 */}\n \n 3\n \n \n | \n 겉모양 | \n 가공상태 | \n 사용상 해로운 결함이 없을것 | \n 육안검사 | \n n = 1, c = 0 | \n KS F 4510 5.1항 | \n
\n {/* 겉모양 - 재봉상태 */}\n \n | 재봉상태 | \n 내화실에 의해 견고하게 접합되어야 함 | \n KS F 4510 9항 | \n
\n {/* 겉모양 - 조립상태 */}\n \n | 조립상태 | \n 앤드락이 견고하게 조립되어야 함 | \n KS F 4510 7항 표9 인용 | \n
\n {/* 치수 - 길이① */}\n \n 치수 (mm) | \n 길이 | \n \n ①\n | \n 도면치수 ± 4 | \n 체크검사 | \n
\n {/* 치수 - 높이② */}\n \n | 높이 | \n \n ②\n | \n 도면치수 + 제한없음 - 40 | \n 자체규정 | \n
\n {/* 치수 - 간격③ */}\n \n | 간격 | \n \n ③\n | \n 400 이하 | \n GONO 게이지 | \n
\n \n
\n\n {/* 중간검사 DATA */}\n
\n
중간검사 DATA
\n
\n \n \n 일련 번호 | \n 겉모양 | \n 치수 [mm] | \n 판정 | \n
\n \n | 가공상태 | \n 재봉상태 | \n 조립상태 | \n ① 길이 | \n ② 높이 | \n ③ 간격 | \n
\n \n | 도면치수 | \n 측정값 | \n 도면치수 | \n 측정값 | \n 기준치 | \n 측정값 | \n
\n \n \n {sampleData.screenInspectionData.map((row, i) => (\n \n | {row.no} | \n \n □ 양호□ 불량\n | \n \n □ 양호□ 불량\n | \n \n □ 양호□ 불량\n | \n {row.lengthSpec} | \n | \n {row.heightSpec} | \n | \n {row.intervalStd} | \n \n □ OK□ NG\n | \n \n □ 적합□ 부적합\n | \n
\n ))}\n {/* 부적합 내용 + 종합판정 행 */}\n \n | \n [부적합 내용] \n \n | \n \n 종합판정 \n \n \n \n | \n
\n \n
\n
\n\n {/* 문서번호 푸터 */}\n
\n KDQP-01-006\n KDPS-03-01\n
\n
\n );\n }\n\n // 절곡품 중간검사 성적서 미리보기 (PQC-BND) - KD 헤더 스타일 적용\n if (docCode === 'PQC-BND') {\n return (\n
\n {/* KD 로고 헤더 + 결재란 - 작업일지 스타일 */}\n {renderKDHeader(<>절곡품
중간검사 성적서>, '', {\n writer: '전진',\n writerDate: sampleData.inspectionDate,\n writerDept: '판매/전진',\n reviewerDept: '생산',\n approverDept: '품질'\n })}\n\n {/* 기본정보 테이블 */}\n
\n \n \n | 품 명 | \n 절곡품 | \n 제품 LOT NO | \n {sampleData.lotNo} | \n
\n \n | 규 격 | \n \n □ 철재☑ 스크린\n | \n 로 트 크 기 | \n {sampleData.lotSize} 개소 | \n
\n \n | 발 주 처 | \n {companyInfo.name} | \n 검 사 일 자 | \n {sampleData.inspectionDate} | \n
\n \n | 현 장 명 | \n {sampleData.siteName} | \n 검 사 자 | \n | \n
\n \n | 제 품 명 | \n {sampleData.bendingInspectionData.productName} | \n 마감유형 | \n {sampleData.bendingInspectionData.finishType} | \n
\n \n
\n\n {/* 중간검사 기준서 */}\n
\n \n {/* 가이드레일/케이스/하단마감재 섹션 */}\n \n \n 중간검사 기준서\n | \n \n 가이드레일 케이스 하단마감재\n | \n 도해 | \n 검사항목 | \n 검사기준 | \n 검사방법 | \n 검사주기 | \n 관련규정 | \n
\n \n \n 절곡품 제품검사 상세도면 참조\n | \n 겉모양 | \n 절곡상태 | \n 사용상 해로운 결함이 없을것 | \n 육안검사 | \n n = 1, c = 0 | \n KS F 4510 5.1항 | \n
\n \n 치수 (mm) | \n 길이 | \n 도면치수 ± 4 | \n 체크검사 | \n KS F 4510 7항 표9 | \n
\n \n | 간격 | \n 도면치수 ± 2 | \n KS F 4510 7항 표9 / 자체규정 | \n
\n {/* 연기차단재 섹션 */}\n \n | \n 연기차단재\n | \n 도해 | \n 검사항목 | \n 검사기준 | \n 검사방법 | \n 검사주기 | \n 관련규정 | \n
\n \n \n {/* 연기차단재 도해 */}\n \n 화이바 글라스 코팅직물 \n \n (연기차단재 프레임) \n (EGI 0.8T) \n 글라스 코팅직물 (너비 50/80) \n 2\n \n | \n 겉모양 | \n 절곡상태 | \n 원단이 프레임에서 빠지지 않을것 | \n 육안검사 | \n n = 1, c = 0 | \n KS F 4510 5.1항 | \n
\n \n 치수 (mm) | \n 길이 | \n 도면치수 ± 4 | \n 체크검사 | \n KS F 4510 7항 표9 인용 | \n
\n \n | 너비 | \n \n ①\n | \n W50 : 50 ± 5 W80 : 80 ± 5 | \n 자체규정 | \n
\n \n | 간격 | \n \n ②\n | \n 도면치수 ± 2 | \n
\n \n
\n\n {/* 중간검사 DATA */}\n
\n
중간검사 DATA
\n
\n \n \n | 분류 | \n 제품명 | \n 타입 | \n 겉모양 | \n 치수 [mm] | \n 판정 | \n
\n \n | 길이 | \n 너비 | \n 간격 | \n
\n \n | 절곡상태 | \n 도면치수 | \n 측정값 | \n 도면치수 | \n 측정값 | \n POINT | \n 도면치수 | \n 측정값 | \n
\n \n \n {/* 샘플 데이터 - 철재 가이드레일 */}\n \n \n 철재 \n (KQTS01)\n | \n 가이드레일 | \n 벽면형 | \n \n □ 양호□ 불량\n | \n 4,300 | \n | \n N/A | \n N/A | \n ① | \n 30 | \n | \n \n □ 적합□ 부적합\n | \n
\n \n | ② | \n 78 | \n | \n
\n \n | ③ | \n 25 | \n | \n
\n \n | ④ | \n 45 | \n | \n
\n {/* 빈 행 추가 */}\n {[1, 2].map(i => (\n \n | \n | \n | \n \n □ 양호□ 불량\n | \n | \n | \n | \n | \n | \n | \n | \n \n □ 적합□ 부적합\n | \n
\n ))}\n {/* 부적합 내용 + 종합판정 행 */}\n \n | \n [부적합 내용] \n \n | \n \n 종합판정 \n \n \n \n | \n
\n \n
\n
\n\n {/* 문서번호 푸터 */}\n
\n KDQP-01-007\n KDPS-10-01\n
\n
\n );\n }\n\n // 슬랫 중간검사성적서 미리보기 (PQC-SLT) - KD 헤더 스타일 적용\n if (docCode === 'PQC-SLT') {\n return (\n
\n {/* KD 로고 헤더 + 결재란 - 작업일지 스타일 */}\n {renderKDHeader(<>슬랫
중간검사 성적서>, '', {\n writer: '전진',\n writerDate: sampleData.inspectionDate,\n writerDept: '판매/전진',\n reviewerDept: '생산',\n approverDept: '품질'\n })}\n\n {/* 기본정보 테이블 */}\n
\n \n \n | 품 명 | \n 슬랫 | \n 제품 LOT NO | \n {sampleData.lotNo} | \n
\n \n | 규 격 | \n {sampleData.spec} | \n 로 트 크 기 | \n {sampleData.lotSize} 개소 | \n
\n \n | 발 주 처 | \n {sampleData.supplierName} | \n 검 사 일 자 | \n {sampleData.inspectionDate} | \n
\n \n | 현 장 명 | \n {sampleData.siteName} | \n 검 사 자 | \n | \n
\n \n
\n\n {/* 중간검사 기준서 - 첨부파일 양식 기준 */}\n
\n \n {/* 헤더 행 */}\n \n \n 중간검사 기준서\n | \n 도해 | \n 검사항목 | \n 검사기준 | \n 검사방법 | \n 검사주기 | \n 관련규정 | \n
\n {/* 겉모양 - 가공상태 */}\n \n \n {/* 도해 다이어그램 - 슬랫 이미지 */}\n \n {/* ① ② 표시 - 상단 */}\n \n 1\n 2\n \n {/* 슬랫 단면도 */}\n 조립상태 \n \n {/* ③ 표시 - 하단 */}\n \n 3\n \n \n | \n 겉모양 | \n 가공상태 | \n 사용상 해로운 결함이 없을것 | \n 육안검사 | \n n = 1, c = 0 | \n KS F 4510 5.1항 | \n
\n {/* 겉모양 - 조립상태 (위) */}\n \n | 조립상태 | \n 앤드락이 용접에 의해 견고하게 조립되어야 함 | \n KS F 4510 9항 | \n
\n {/* 겉모양 - 조립상태 (아래) */}\n \n 용접부위에 락카도색이 되어야 함 | \n 자체규정 | \n
\n {/* 치수 - 높이① */}\n \n 치수 (mm) | \n 높이 | \n \n ①\n | \n 16.5 ± 1 | \n 체크검사 | \n KS F 4510 7항 표9 | \n
\n {/* 치수 - 높이② */}\n \n | \n ②\n | \n 14.5 ± 1 | \n
\n {/* 치수 - 길이③ */}\n \n | 길이 | \n \n ③\n | \n 도면치수(앤드락제외) ± 4 | \n
\n \n
\n\n {/* 중간검사 DATA */}\n
\n
중간검사 DATA
\n
\n \n \n 일련 번호 | \n 겉모양 | \n 치수 [mm] | \n 판정 | \n
\n \n | 가공상태 | \n 조립상태 | \n ① 높이 | \n ② 높이 | \n ③ 길이 (앤드락제외) | \n
\n \n | 기준치 | \n 측정값 | \n 기준치 | \n 측정값 | \n 도면치수 | \n 측정값 | \n
\n \n \n \n | 01 | \n \n □ 양호□ 불량\n | \n \n □ 양호□ 불량\n | \n 16.5 ± 1 | \n | \n 14.5 ± 1 | \n | \n 4,510 | \n | \n \n □ 적합□ 부적합\n | \n
\n {[2, 3, 4, 5].map(i => (\n \n | {String(i).padStart(2, '0')} | \n \n □ 양호□ 불량\n | \n \n □ 양호□ 불량\n | \n | \n | \n | \n | \n | \n | \n \n □ 적합□ 부적합\n | \n
\n ))}\n {/* 부적합 내용 + 종합판정 행 */}\n \n | \n [부적합 내용] \n \n | \n \n 종합판정 \n \n \n \n | \n
\n \n
\n
\n\n {/* 문서번호 푸터 */}\n
\n KDQP-01-008\n KDPS-10-02\n
\n
\n );\n }\n\n // 조인트바 중간검사성적서 미리보기 (PQC-JB) - KD 헤더 스타일 적용\n if (docCode === 'PQC-JB') {\n return (\n
\n {/* KD 로고 헤더 + 결재란 - 작업일지 스타일 */}\n {renderKDHeader(<>조인트바
중간검사 성적서>, '', {\n writer: '전진',\n writerDate: sampleData.inspectionDate,\n writerDept: '판매/전진',\n reviewerDept: '생산',\n approverDept: '품질'\n })}\n\n {/* 기본정보 테이블 */}\n
\n \n \n | 품 명 | \n 조인트바 | \n 제품 LOT NO | \n {sampleData.lotNo} | \n
\n \n | 규 격 | \n {sampleData.spec} | \n 로 트 크 기 | \n {sampleData.lotSize} 개소 | \n
\n \n | 발 주 처 | \n {sampleData.supplierName} | \n 검 사 일 자 | \n {sampleData.inspectionDate} | \n
\n \n | 현 장 명 | \n {sampleData.siteName} | \n 검 사 자 | \n | \n
\n \n
\n\n {/* 중간검사 기준서 - 슬랫 양식과 동일한 구조 */}\n
\n \n {/* 헤더 행 */}\n \n \n 중간검사 기준서\n | \n 도해 | \n 검사항목 | \n 검사기준 | \n 검사방법 | \n 검사주기 | \n 관련규정 | \n
\n {/* 겉모양 - 가공상태 */}\n \n \n {/* 도해 다이어그램 - 조인트바 이미지 */}\n \n {/* 상단 슬랫 이미지 */}\n \n {/* 메인 슬랫 본체 */}\n \n {/* 하단 ③ 표시 */}\n \n 3\n \n \n | \n 겉모양 | \n 가공상태 | \n 사용상 해로운 결함이 없을것 | \n 육안검사 | \n n = 1, c = 0 | \n KS F 4510 5.1항 | \n
\n {/* 겉모양 - 조립상태 (위) */}\n \n | 조립상태 | \n 앤드락이 용접에 의해 견고하게 조립되어야 함 | \n KS F 4510 9항 | \n
\n {/* 겉모양 - 조립상태 (아래) */}\n \n 용접부위에 락카도색이 되어야 함 | \n 자체규정 | \n
\n {/* 치수 - 높이① */}\n \n 치수 (mm) | \n 높이 | \n \n ①\n | \n 16.5 ± 1 | \n 체크검사 | \n KS F 4510 7항 표9 | \n
\n {/* 치수 - 높이② */}\n \n | \n ②\n | \n 14.5 ± 1 | \n
\n {/* 치수 - 길이③ */}\n \n | 길이 | \n \n ③\n | \n 300(앤드락제외) ± 4 | \n
\n {/* 치수 - 간격④ */}\n \n | 간격 | \n \n ④\n | \n 150 ± 4 | \n 자체규정 | \n
\n \n
\n\n {/* 중간검사 DATA */}\n
\n
중간검사 DATA
\n\n {/* 중간검사 DATA 테이블 */}\n
\n \n {/* 1행: 일련번호 / 겉모양 / 치수 (mm) / 판정 */}\n \n 일련 번호 | \n 겉모양 | \n 치수 (mm) | \n 판정 | \n
\n {/* 2행: 가공상태/조립상태 + ①높이/②높이/③길이/④간격 */}\n \n | 가공상태 | \n 조립상태 | \n ① 높이 | \n ② 높이 | \n ③ 길이 (미미제외) | \n ④ 간격 | \n
\n {/* 3행: 기준치/측정값 */}\n \n | 기준치 | \n 측정값 | \n 기준치 | \n 측정값 | \n 기준치 | \n 측정값 | \n 기준치 | \n 측정값 | \n
\n \n \n {[1, 2, 3, 4, 5].map(i => (\n \n | {i} | \n □ 양호 □ 불량 | \n □ 양호 □ 불량 | \n 16.5 ± 1 | \n | \n 14.5 ± 1 | \n | \n 300 ± 4 | \n | \n 150 ± 4 | \n | \n □ 적합 □ 부적합 | \n
\n ))}\n {/* 부적합 내용 + 종합판정 행 */}\n \n | \n [부적합 내용] \n \n | \n \n 종합판정 \n \n \n \n | \n
\n \n
\n
\n\n {/* 문서번호 푸터 */}\n
\n KDQP-01-009\n KDPS-10-03\n
\n
\n );\n }\n\n // 제품검사성적서 미리보기 (FQC) - 작업일지 스타일 적용\n if (docCode === 'FQC') {\n return (\n
\n {/* 헤더 - KD 스타일 (작업일지 스타일) */}\n {renderKDHeader(<>제품검사성적서>, '', {\n writer: '김검사',\n reviewer: '',\n approver: '박승인',\n writerDept: '품질',\n reviewerDept: '',\n approverDept: '품질'\n })}\n\n {/* 기본정보 테이블 */}\n
\n \n \n | 상 품 명 | \n {sampleData.itemName || '자동방화셔터(인정)'} | \n 제품 LOT NO | \n KD-SA-251218-01 | \n
\n \n | 제 품 명 | \n WY-SC780 방화셔터 | \n 로 트 크 기 | \n EA | \n
\n \n | 발 주 처 | \n {sampleData.customerName || '(주)OO건설'} | \n 검 사 일 자 | \n 2023. . | \n
\n \n | 현 장 명 | \n {sampleData.siteName || 'OO아파트 신축공사'} | \n 검 사 자 | \n | \n
\n \n
\n\n {/* 제품사진 영역 */}\n
\n \n \n | 제품사진 | \n \n {/* 제품 도해 영역 - 첨부파일 기준 */}\n \n {/* 왼쪽: 방화셔터 전체 도해 */}\n \n {/* 셔터박스 */}\n \n {/* ① 번호 */}\n \n ①\n \n {/* 스크린 원단 영역 */}\n \n {/* 가로줄 패턴 */}\n {[...Array(12)].map((_, i) => (\n \n ))}\n \n {/* ③ 번호 */}\n \n ③\n \n {/* ④ 번호 */}\n \n ④\n \n {/* 하단 가이드레일 홈 간격 설명 */}\n \n 가이드레일 홈 간격 측정 \n \n ⓐ\n 높이 100 이내\n \n \n \n {/* 오른쪽: 가이드레일 상세 도해 */}\n \n {/* 가이드레일 벽면형 */}\n \n \n ②\n \n \n [단면도]\n \n 가이드레일 벽면형 \n \n {/* 가이드레일 측면형 */}\n \n \n ②\n \n \n [단면도]\n \n 가이드레일 측면형 \n \n \n \n | \n
\n \n
\n\n {/* 검사항목 테이블 - 검사항목(단일)/검사기준(colSpan=3) 구조로 재구성 */}\n
\n \n \n | No | \n 검사항목 | \n 검사기준 | \n 검사방법 | \n 검사주기 | \n 판정 | \n
\n \n \n {/* 1. 겉모양 - 검사기준: 가공상태/재봉상태/조립상태/연기차단재/하단마감재 */}\n \n | 1 | \n 겉모양 | \n 가공상태 | \n 사용상 해로운 결함이 없을 것 | \n 육안검사 | \n 전수검사 | \n □ 적합 □ 부적합 | \n
\n \n | 재봉상태 | \n 내화실에 의해 견고하게 접합되어야 함 | \n □ 적합 □ 부적합 | \n
\n \n | 조립상태 | \n 앤드락이 견고하게 조립되어야 함 | \n □ 적합 □ 부적합 | \n
\n \n | 연기차단재 | \n 연기차단재 설치여부(케이스 W80, 가이드레일 W50(양쪽설치)) | \n □ 적합 □ 부적합 | \n
\n \n | 하단마감재 | \n 내부 무게평철 설치 유무 | \n □ 적합 □ 부적합 | \n
\n\n {/* 2. 모터 */}\n \n | 2 | \n 모터 | \n 인정제품과 동일사양 | \n | \n | \n □ 적합 □ 부적합 | \n
\n\n {/* 3. 재질 */}\n \n | 3 | \n 재질 | \n WY-SC780 인쇄상태 확인 | \n □ 적합 □ 부적합 | \n
\n\n {/* 4. 치수(오픈사이즈) - 검사기준: 길이/높이/가이드레일홈간격/하단마감재간격 */}\n \n | 4 | \n 치수 (오픈사이즈) | \n 길이 | \n 발주치수 ± 30mm | \n 체크검사 | \n 전수검사 | \n 측정값( ) □ 적합 □ 부적합 | \n
\n \n | 높이 | \n 발주치수 ± 30mm | \n 측정값( ) □ 적합 □ 부적합 | \n
\n \n 가이드레일 홈간격 | \n 10 ± 5mm(측정부위 : ⓐ 높이 100 이내) | \n 측정값( ) □ 적합 □ 부적합 | \n
\n \n | 하단마감재간격 | \n 간격 (③+④) | \n 가이드레일과 하단마감재 틈새 25mm 이내 | \n 측정값( ) □ 적합 □ 부적합 | \n
\n\n {/* 5. 작동테스트 - 검사기준: 개폐성능 */}\n \n | 5 | \n 작동테스트 | \n 개폐성능 | \n 작동 유무 확인(일부 및 완전폐쇄) | \n | \n | \n □ 적합 □ 부적합 | \n
\n\n {/* 6. 내화시험 - 검사기준: 비차열 | 차염성 (별도 컬럼) */}\n \n | 6 | \n 내화시험 | \n 비 차 열 | \n 차 염 성 | \n 6mm 균열게이지 관통 후 150mm 이동 유무 | \n 공인 시험기관 시험성적서 | \n 1회 / 5년 | \n □ 적합 □ 부적합 | \n
\n \n | 25mm 균열게이지 관통 유무 | \n
\n \n | 10초 이상 지속되는 화염발생 유무 | \n
\n\n {/* 7. 차연시험 - 검사기준: 공기누설량 */}\n \n | 7 | \n 차연시험 | \n 공기누설량 | \n 25Pa 일때 공기누설량 0.9㎥/min·㎡ 이하 | \n
\n\n {/* 8. 개폐시험 - 검사기준: 평균속도 등 */}\n \n | 8 | \n 개폐시험 | \n 개폐의 원활한 작동 | \n
\n \n | 평균속도 | \n 전동개폐 2.5 ~ 6.5m/min | \n
\n \n | 자중강하 3 ~ 7m/min | \n
\n \n | 개폐 시 상부 및 하부 끝부분에서 자동정지 | \n
\n \n | 강하 중 임의의 위치에서 정지 | \n
\n\n {/* 9. 내충격시험 */}\n \n | 9 | \n 내충격시험 | \n 방화상 유해한 파괴, 박리 탈락 유무 | \n | \n | \n □ 적합 □ 부적합 | \n
\n \n
\n\n {/* 특이사항 및 종합판정 */}\n
\n \n \n | [특이사항] | \n \n 1. 내화시험, 차연시험, 개폐시험, 내충격시험의 경우 공인시험기관에서 시험을 진행하며, 그 시험성적서로 갈음한다. \n | \n \n 종합판정 \n | \n
\n \n
\n\n {/* 문서번호 푸터 */}\n
\n
\n );\n }\n\n // 수입검사성적서 미리보기 (IQC) - KD 헤더 스타일 적용\n if (docCode === 'IQC') {\n const iqcData = sampleData.incomingInspectionData;\n return (\n
\n {/* KD 로고 헤더 + 결재란 - 작업일지 스타일 */}\n {renderKDHeader('수 입 검 사 성 적 서', '품질관리부서', {\n writer: iqcData.inspector || '품질담당',\n writerDate: iqcData.inspectionDate,\n writerDept: '자재/입고',\n reviewerDept: '품질',\n approverDept: '품질팀장'\n })}\n\n {/* 입고일자 표시 */}\n
\n 입고일자: {iqcData.receiveDate}\n
\n\n {/* 기본정보 테이블 */}\n
\n \n \n | 품 명 | \n {iqcData.itemName} | \n 납품/제조업체 | \n {iqcData.supplierName} | \n
\n \n | 규 격 | \n {iqcData.spec} | \n 로트번호 | \n {iqcData.lotNo} | \n
\n \n | 자재번호 | \n {iqcData.materialNo} | \n 검사일자 | \n {iqcData.inspectionDate} | \n
\n \n | 로트크기 | \n {iqcData.lotSize} {iqcData.lotUnit} | \n 검사자 | \n {iqcData.inspector} | \n
\n \n
\n\n {/* 검사항목 테이블 - 첨부파일 기준 완전 구현 */}\n
\n \n \n | NO | \n 검사항목 | \n 검사기준 | \n 검사방식 | \n 검사주기 | \n 측정치 | \n 판정 (적/부) | \n
\n \n | \n | \n n1 양호/불량 | \n n2 양호/불량 | \n n3 양호/불량 | \n
\n \n \n {/* 1. 겉모양 */}\n \n | 1 | \n 겉모양 | \n 사용상 해로운 결함이 없을 것 | \n 육안검사 | \n | \n \n \n ✓OKNG\n \n | \n \n \n ✓OKNG\n \n | \n \n \n ✓OKNG\n \n | \n 적 | \n
\n\n {/* 2. 치수 - 두께 */}\n \n | 2 | \n 치수 | \n 두께 1.55 | \n \n 0.8 이상 \n ~ 1.0 미만\n ± 0.07\n | \n 체크검사 | \n n = 3 c = 0 | \n 1.528 | \n 1.533 | \n 1.521 | \n 적 | \n
\n \n \n 1.0 이상 \n ~ 1.25 미만\n ± 0.08\n | \n
\n \n \n ✓1.25 이상 \n ~ 1.6 미만\n ± 0.10\n | \n
\n \n \n 1.6 이상 \n ~ 2.0 미만\n ± 0.12\n | \n
\n\n {/* 치수 - 너비 */}\n \n 너비 1219 | \n \n ✓1250 미만\n + 7 - 0\n | \n | \n 1222 | \n 1222 | \n 1222 | \n 적 | \n
\n \n | \n
\n\n {/* 치수 - 길이 */}\n \n 길이 480 | \n \n ✓1250 미만\n + 10 - 0\n | \n | \n 480 | \n 480 | \n 480 | \n 적 | \n
\n \n \n 2000 이상 \n ~ 4000 미만\n + 15 - 0\n | \n
\n \n \n 4000 이상 \n ~ 6000 미만\n + 20 - 0\n | \n
\n \n | \n
\n\n {/* 3. 인장강도 */}\n \n | 3 | \n 인장강도 (N/㎟) | \n 270 이상 | \n | \n | \n 313.8 | \n 적 | \n
\n\n {/* 4. 연신율 */}\n \n | 4 | \n 연신율 % | \n \n 두께 0.6 이상 \n ~ 1.0 미만\n | \n 36 이상 | \n 공급업체 밀시트 | \n 입고시 | \n 46.5 | \n 적 | \n
\n \n \n ✓두께 1.0 이상 \n ~ 1.6 미만\n | \n 37 이상 | \n
\n \n \n 두께 1.6 이상 \n ~ 2.3 미만\n | \n 38 이상 | \n
\n\n {/* 5. 아연의 최소 부착량 */}\n \n | 5 | \n 아연의 최소 부착량 (g/㎡) | \n 한면 17 이상 | \n | \n | \n 17.21 / 17.17 | \n 적 | \n
\n \n
\n\n {/* 비고 영역 */}\n
\n {iqcData.remarks.map((remark, i) => (\n
{remark}
\n ))}\n
\n\n {/* 종합판정 */}\n
\n 종합판정\n
\n\n {/* 부적합 내용 및 판정 */}\n
\n
\n
\n
\n {iqcData.overallJudgement}\n
\n
\n
\n
\n );\n }\n\n // 거래명세서 미리보기 (심플 스타일 - 결재란 없음)\n if (docCode === 'SL') {\n return (\n
\n {/* 제목 */}\n
거 래 명 세 서
\n\n {/* 문서번호/발행일 */}\n
\n 수주번호: \n {sampleData.orderNo || 'KD-SO-250215-01'}\n |\n 발행일: \n {sampleData.issueDate || '2025-02-15'}\n
\n\n {/* 공급자/공급받는자 - 2열 테이블 */}\n
\n {/* 공급자 */}\n
\n
공급자
\n
\n \n \n | 상 호 | \n {sampleData.supplierName || '한국방화문(주)'} | \n
\n \n | 대표자 | \n {sampleData.supplierCeo || '홍길동'} | \n
\n \n | 사업자번호 | \n {sampleData.supplierBusinessNo || '123-45-67890'} | \n
\n \n | 주 소 | \n {sampleData.supplierAddress || '서울 강남구 테헤란로 123'} | \n
\n \n
\n
\n {/* 공급받는자 */}\n
\n
공급받는자
\n
\n \n \n | 상 호 | \n {sampleData.customerName || '삼성건설(주)'} | \n
\n \n | 담당자 | \n {sampleData.customerManager || '이영희'} | \n
\n \n | 연락처 | \n {sampleData.customerPhone || '010-2345-6789'} | \n
\n \n | 현장명 | \n {sampleData.siteName || '강남 타워 신축현장 (B동)'} | \n
\n \n
\n
\n
\n\n {/* 품목내역 제목 */}\n
\n 품 목 내 역\n
\n\n {/* 품목내역 테이블 */}\n
\n \n \n | 순번 | \n 품목코드 | \n 품명 | \n 규격 | \n 수량 | \n 단위 | \n 단가 | \n 공급가액 | \n
\n \n \n \n | 1 | \n FD-002 | \n 방화셔터 | \n 1200×2400 | \n 120 | \n SET | \n 400,000 | \n 48,000,000 | \n
\n {[2, 3].map(no => (\n \n | \n | \n | \n | \n | \n | \n | \n | \n
\n ))}\n \n
\n\n {/* 금액 요약 박스 */}\n
\n
\n
\n 공급가액\n 48,000,000원\n
\n
\n 할인율\n 0%\n
\n
\n 할인액\n -0원\n
\n
\n 할인 후 공급가액\n 48,000,000원\n
\n
\n 부가세 (10%)\n 4,800,000원\n
\n
\n 합계 금액\n ₩ 52,800,000\n
\n
\n
\n\n {/* 하단 증명 문구 */}\n
\n
위 금액을 거래하였음을 증명합니다.
\n
{sampleData.issueDate || '2025-02-15'}
\n
\n
\n
\n );\n }\n\n // ========== 제품검사신청서(FQC-REQ) 미리보기 ==========\n // 작업일지 스타일 적용 - 발주처에서 경동기업으로 신청하는 문서\n if (docCode === 'FQC-REQ') {\n return (\n
\n {/* 헤더 - KD 로고 + 제품검사요청서 제목 + 접수일 (작업일지 스타일) */}\n
\n \n \n | \n KD \n 경동기업 \n KYUNGDONG COMPANY \n | \n \n 자동방화셔터(인정제품) 제품검사요청서 \n | \n \n 접수일 \n 2024년 월 일 \n | \n
\n \n
\n\n {/* 기본정보 테이블 */}\n
\n \n \n | 발신처 | \n {sampleData.customerName || '(주)OO건설'} | \n 발주번호 | \n KD-PO-2024-001 | \n
\n \n | 현장명 | \n {sampleData.siteName || 'OO아파트 신축공사'} | \n
\n \n | 검사대상 | \n 자동방화셔터 | \n 수량 | \n 20 SET | \n
\n \n | 담당자 | \n 홍길동 | \n 연락처 | \n 010-0000-0000 | \n
\n \n
\n\n {/* 검사방문요청일 */}\n
\n
\n 검사방문요청일(전화예정):\n 2024년 월 일\n
\n
\n\n {/* 입력사항 섹션 */}\n
\n
\n 입력사항 (품질관리서 동일정보)\n
\n
\n \n {/* 1. 건축공사장 */}\n \n | 1 | \n 건축공사장 | \n \n \n \n \n | 현장명 | \n | \n \n \n | 대지위치 | \n | \n \n \n | 건축주 | \n | \n \n \n \n | \n
\n {/* 2. 자재유통업자 */}\n \n | 2 | \n 자재유통업자 | \n \n \n \n \n | 상호 | \n | \n \n \n | 대표자명 | \n | \n \n \n | 주소 | \n | \n \n \n | 연락처 | \n | \n \n \n \n | \n
\n {/* 3. 공사시공자 */}\n \n | 3 | \n 공사시공자 | \n \n \n \n \n | 상호 | \n | \n \n \n | 대표자명 | \n | \n \n \n | 주소 | \n | \n \n \n | 연락처 | \n | \n \n \n \n | \n
\n {/* 4. 공사감리자 */}\n \n | 4 | \n 공사감리자 | \n \n \n \n \n | 상호 | \n | \n \n \n | 대표자명 | \n | \n \n \n | 주소 | \n | \n \n \n | 연락처 | \n | \n \n \n \n | \n
\n \n
\n
\n\n {/* 검사요청시 필독 */}\n
\n
※ 검사요청시 필독
\n
\n - 제품검사요청서 접수 후 3일 이내 방문검사 실시
\n - 검사 불합격 시 재검사 비용은 신청자 부담
\n - 현장 접근성 및 검사환경 사전 확보 필요
\n - 검사 당일 담당자 현장 입회 필수
\n
\n
\n\n {/* 검사대상 사전 고지 정보 테이블 */}\n
\n
\n 검사대상 사전 고지 정보\n
\n
\n \n \n | No | \n 제품명 | \n 규격(W×H) | \n 수량 | \n 설치위치 | \n 비고 | \n
\n \n \n {[1, 2, 3, 4, 5, 6, 7, 8].map((no) => (\n \n | {no} | \n | \n | \n | \n | \n | \n
\n ))}\n \n
\n
\n\n {/* 신청인/접수인 서명란 */}\n
\n\n {/* 문서번호 */}\n
\n 문서번호: KD-SA-YYMMDD-##\n
\n
\n );\n }\n\n // ========== 작업일지 미리보기 ==========\n // 스크린 작업일지\n if (docCode === 'WL-SCR') {\n // 스크린 작업일지 샘플 데이터\n const scrSampleData = {\n orderDate: '2025년 10월 15일 수요일',\n siteName: '용신고등학교(4층)',\n customerName: '주일',\n workDate: '2025. .',\n managerName: '김영민과장',\n lotNo: 'KD-WE-251015-01-(3)',\n phone: '',\n productionManager: ''\n };\n\n return (\n
\n {/* 헤더 - 공통 함수 사용 */}\n {renderKDHeader('작 업 일 지', '스크린 생산부서', {\n writer: '전진',\n writerDate: '10/15',\n writerDept: '판매/전진'\n })}\n\n {/* 신청업체 / 신청내용 - 공통 함수 사용 */}\n {renderRequestInfoTable(scrSampleData)}\n\n {/* 작업 내역 */}\n
■ 작업 내역
\n
\n \n \n 일련 번호 | \n 입고 LOT NO. | \n 제품명 | \n 부호 | \n 제작사이즈(mm) | \n 나머지 높이 | \n 규격(매수) | \n
\n \n | 가로 | \n 세로 | \n 1180 | \n 900 | \n 600 | \n 400 | \n 300 | \n
\n \n \n {[\n { no: '01', lot: '', name: '와이어', code: '4층 FSS17', w: '7400', h: '2950', rest: '810', s1180: '2', s900: '1', s600: '', s400: '', s300: '' },\n { no: '02', lot: '', name: '와이어', code: '4층 FSS5', w: '4700', h: '2950', rest: '810', s1180: '2', s900: '1', s600: '', s400: '', s300: '' },\n { no: '03', lot: '', name: '와이어', code: '4층 FSS17A', w: '6790', h: '2950', rest: '810', s1180: '2', s900: '1', s600: '', s400: '', s300: '' },\n { no: '04', lot: '', name: '와이어', code: '4층 FSS3-1', w: '3700', h: '2950', rest: '810', s1180: '2', s900: '1', s600: '', s400: '', s300: '' },\n { no: '05', lot: '', name: '와이어', code: '4층 FSS7', w: '6000', h: '2950', rest: '810', s1180: '2', s900: '1', s600: '', s400: '', s300: '' },\n { no: '06', lot: '', name: '와이어', code: '4층 FSS16', w: '7300', h: '2950', rest: '810', s1180: '2', s900: '1', s600: '', s400: '', s300: '' },\n { no: '07', lot: '', name: '와이어', code: '4층 FSS3-2', w: '3700', h: '2950', rest: '810', s1180: '2', s900: '1', s600: '', s400: '', s300: '' },\n { no: '08', lot: '', name: '와이어', code: '4층 FSS6', w: '5900', h: '2950', rest: '810', s1180: '2', s900: '1', s600: '', s400: '', s300: '' },\n { no: '09', lot: '', name: '와이어', code: '4층 FSS3-3', w: '3910', h: '2950', rest: '810', s1180: '2', s900: '1', s600: '', s400: '', s300: '' },\n { no: '10', lot: '', name: '와이어', code: '4층 FSS17', w: '7400', h: '2950', rest: '810', s1180: '2', s900: '1', s600: '', s400: '', s300: '' },\n { no: '11', lot: '', name: '와이어', code: '4층 FSS15', w: '7000', h: '2950', rest: '810', s1180: '2', s900: '1', s600: '', s400: '', s300: '' },\n ].map((row, idx) => (\n \n | {row.no} | \n {row.lot} | \n {row.name} | \n {row.code} | \n {row.w} | \n {row.h} | \n {row.rest} | \n {row.s1180} | \n {row.s900} | \n {row.s600} | \n {row.s400} | \n {row.s300} | \n
\n ))}\n {/* 합계 행 */}\n \n | 합 계 | \n 22 | \n 11 | \n | \n | \n | \n
\n \n
\n\n {/* 내화실 입고 LOT.NO */}\n
\n \n \n | 내화실 입고 LOT.NO | \n | \n
\n \n
\n\n {/* 사용량 테이블 */}\n
\n \n \n | \n 사용량 \n (M) \n | \n 1220 | \n 127.60 | \n 400 | \n | \n \n 사용량 \n [ ㎡ ] \n | \n 213.09 | \n
\n \n | 900 | \n 63.8 | \n 300 | \n | \n
\n \n | 600 | \n | \n | \n
\n \n
\n
\n );\n }\n\n // 슬랫 작업일지\n if (docCode === 'WL-SLT') {\n // 슬랫 작업일지 샘플 데이터\n const sltSampleData = {\n orderDate: '2025년 6월 30일 월요일',\n siteName: '철재 스라트 전체표시(모터포함)',\n customerName: '경동기업',\n workDate: '06/30',\n managerName: '개발자',\n lotNo: 'KD-비인정',\n phone: '010-5123-8210',\n productionManager: '개발자'\n };\n\n return (\n
\n {/* 헤더 - 공통 함수 사용 */}\n {renderKDHeader('작 업 일 지', '슬랫 생산부서', {\n writer: '개발자',\n writerDate: '06/30',\n reviewer: '개발자',\n reviewerDate: '07/02',\n approver: '개발자',\n approverDate: '07/02',\n writerDept: '판매/개발자'\n })}\n\n {/* 신청업체 / 신청내용 - 공통 함수 사용 */}\n {renderRequestInfoTable(sltSampleData)}\n\n {/* 작업내역 */}\n
■ 작업내역
\n
\n \n \n 일련 번호 | \n 입고 LOT NO. | \n 방화유리 수량 | \n 품명 | \n 제작사이즈(mm) - 미미제외 | \n 조인트바 수량 | \n 코일 사용량 | \n 설치층/부호 | \n
\n \n | 가로 | \n 세로 | \n 매수(세로) | \n
\n \n \n {[\n { no: '1', lot: '250508-01', name: '', product: '슬랫\\n[EGI1.55T]', w: '3610', h: '4350', count: '61', joint: '5', coil: '225', floor: '/' },\n { no: '2', lot: '250425-01', name: '', product: '슬랫\\n[EGI1.55T]', w: '3610', h: '4350', count: '61', joint: '5', coil: '225', floor: '/' },\n ].map((row, idx) => (\n \n | {row.no} | \n {row.lot}i | \n {row.name} | \n {row.product} | \n {row.w} | \n {row.h} | \n {row.count} | \n {row.joint} | \n {row.coil} | \n {row.floor} | \n
\n ))}\n \n
\n\n {/* 합계 */}\n
\n \n \n | 생산량 합계 (㎡) | \n 450 | \n 조인트바 합계 | \n 10 | \n
\n \n
\n
\n );\n }\n\n // 절곡 작업일지 - 첨부파일 기준 완전 구현\n if (docCode === 'WL-FLD') {\n // 절곡 작업일지 샘플 데이터\n const fldSampleData = {\n orderDate: '2025년 10월 15일 수요일',\n siteName: '용신고등학교(4층)',\n customerName: '주일',\n workDate: '2025. .',\n managerName: '김영민과장',\n lotNo: 'KD-WE-251015-01-(3)',\n phone: '',\n productionManager: '',\n productCode: 'KWE01',\n productName: '와이어',\n finishType: 'SUS마감',\n finishSpec: '벽면형(120*70)'\n };\n\n return (\n
\n {/* 헤더 - 공통 함수 사용 */}\n {renderKDHeader('작 업 일 지', '절곡 생산부서', {\n writer: '전진',\n writerDate: '10/15',\n writerDept: '판매/전진'\n })}\n\n {/* 신청업체 / 신청내용 + 제품명/마감유형 - 공통 함수 사용 */}\n {renderRequestInfoTable(fldSampleData, { showProductInfo: true, siteNameColor: 'text-red-600' })}\n\n {/* 1. 작업 내역 - 절곡 상세도면 참조 */}\n
1. 작업 내역 - 절곡 상세도면 참조
\n\n {/* 1.1 벽면형 (120·70) */}\n
\n \n \n | 1.1 벽면형 (120·70) | \n
\n \n {/* 전개도 영역 */}\n \n \n {/* KSS01 도면 */}\n \n KSS01 \n \n \n \n \n {/* KSE01 도면 */}\n \n KSE01 \n \n \n \n \n \n | \n {/* 작업량 테이블 */}\n \n \n \n \n | 세부품명 | \n 재질 | \n 입고 & 생산 LOT NO | \n 작업량 | \n \n \n | 길이/규격 | \n 수량 | \n \n \n \n \n | ①마감재 | \n EGI1.15T | \n | \n L : 4,300 | \n 22 | \n \n \n | ②가이드레일 | \n SUS1.2T | \n | \n L : 4,000 | \n 44 | \n \n \n | ③C형 | \n EGI1.55T | \n | \n L : 3,500 | \n 22 | \n \n \n | ④D형 | \n EGI1.55T | \n | \n L : 3,000 | \n 22 | \n \n \n | ⑤별도마감재 | \n SUS1.2T | \n | \n L : 2,438 | \n 22 | \n \n \n | ⑥하부BASE | \n EGI1.55T | \n | \n 130·80 | \n 22 | \n \n \n \n | \n
\n \n
\n\n {/* 2. 하단마감재 [60·40] */}\n
\n \n \n | 2. 하단마감재 [60·40] | \n
\n \n {/* 전개도 영역 */}\n \n \n {/* KSS01 하단마감재 도면 */}\n \n KSS01 \n \n \n \n \n {/* KSE01 하단마감재 도면 */}\n \n KSE01 \n \n \n \n \n \n | \n {/* 작업량 테이블 */}\n \n \n \n \n | 세부품명 | \n 재질 | \n 입고 & 생산 LOT NO | \n 작업량 | \n \n \n | 길이/규격 | \n 수량 | \n \n \n \n \n | ①하단마감재 | \n EGI1.55T | \n | \n L : 4,000 | \n 11 | \n \n \n | ②하단보강엘바 | \n EGI1.55T | \n | \n L : 4,000 | \n 26 | \n \n \n | ③하단보강평철 | \n EGI1.15T | \n | \n L : 4,000 | \n 13 | \n \n \n | ④별도마감재 | \n SUS1.2T | \n | \n L : 4,000 | \n 11 | \n \n \n \n | \n
\n \n
\n\n {/* 3.1 케이스 [500*330] */}\n
\n \n \n | 3.1 케이스 [500*330] | \n
\n \n {/* 전개도 영역 */}\n | \n {/* 케이스 전개도 */}\n \n \n \n | \n {/* 작업량 테이블 */}\n \n \n \n \n | 세부품명 | \n 재질 | \n 입고 & 생산 LOT NO | \n 작업량 | \n \n \n | 길이/규격 | \n 수량 | \n \n \n \n \n | ①전면부 | \n EGI1.55T | \n | \n L : 4,150 | \n 1 | \n \n \n | L : 4,000 | \n 7 | \n \n \n | ②린텔부 | \n L : 3,500 | \n 5 | \n \n \n | L : 3,000 | \n 3 | \n \n \n | ③⑤하부점검구 | \n L : 2,438 | \n 3 | \n \n \n | L : 1,219 | \n | \n \n \n | ④후면코너부 | \n 상부덮개 [1219·389] | \n 59 | \n \n \n | ⑥상부덮개 | \n | \n | \n \n \n | ⑦측면부(마구리) | \n 마구리 505·335 | \n 22 | \n \n \n \n | \n
\n \n
\n\n {/* 4. 연기차단재 */}\n
\n \n \n | 4. 연기차단재 | \n
\n \n {/* 전개도 영역 */}\n | \n {/* 연기차단재 전개도 */}\n \n \n \n | \n {/* 작업량 테이블 */}\n \n \n \n \n | 세부품명 | \n 재질 | \n 입고 & 생산 LOT NO | \n 작업량 | \n \n \n | 길이/규격 | \n 수량 | \n \n \n \n \n 레일용 [W50] | \n EG0.8T + 화이바 글라스 코팅직물 | \n (원단 : ) | \n L : 4,300 | \n | \n \n \n | L : 4,000 | \n | \n \n \n | L : 3,500 | \n | \n \n \n | L : 3,000 | \n 44 | \n \n \n | L : 2,438 | \n | \n \n \n | \n | \n \n \n 케이스용 [W80] | \n EG0.8T + 화이바 글라스 코팅직물 | \n (원단 : ) | \n L : 3,000 | \n 47 | \n \n \n \n | \n
\n \n
\n\n {/* 생산량 합계 / 비고 - 두 줄 테이블 */}\n
\n \n {/* 첫째 줄: 생산량 합계 (kg) | SUS | 값 | 비고 | 비고내용 */}\n \n | \n 생산량 합계 \n (kg) \n | \n SUS | \n \n \n | \n 비 고 | \n | \n
\n {/* 둘째 줄: (생산량 합계 병합됨) | EGI | 값 | (비고 병합됨) | (비고내용 병합됨) */}\n \n | EGI | \n \n \n | \n
\n \n
\n
\n );\n }\n\n // 재고생산 작업일지 (중간검사성적서) - 첨부파일 기준 완전 구현\n if (docCode === 'WL-STK') {\n return (\n
\n {/* 헤더 - 공통 함수 사용 (부제목 포함) */}\n {renderKDHeader('절곡품 재고생산 작업일지', '재고생산 부서', {\n writer: '개발자',\n writerDate: '11/04',\n reviewer: '개발자',\n reviewerDate: '11/05',\n approver: '개발자',\n approverDate: '11/05',\n writerDept: '판매/개발자'\n }, { subtitle: '중간검사성적서', subTitle: 'KD FIRE DOOR COMPANY', logoColor: 'text-blue-700' })}\n\n {/* 제품 기본정보 테이블 */}\n
\n \n \n | 품명 | \n 케이스 - 전면부 | \n 생산 LOT NO | \n CF4A15-40 | \n
\n \n | 규격 | \n EGI 1.55T (W576) | \n 로트크기 | \n 16 EA | \n
\n \n | 길이 | \n 4,000 | \n 검사일자 | \n | \n
\n \n | 입고 LOT NO | \n 240927-01 | \n 검사자 | \n 개발자 | \n
\n \n
\n\n {/* 중간 검사 기준서 섹션 - 첨부파일 기준 (가로형 텍스트) */}\n
\n \n \n {/* 좌측 - 중간 검사 기준서 라벨 (가로형) */}\n | \n 중간 검사 \n 기준서 \n KDPS-20 \n | \n \n 케이스 \n 전면부 \n | \n {/* 도면 영역 */}\n \n {/* 케이스 전면부 도면 - SVG */}\n \n | \n
\n \n {/* 하단 검사기준 테이블 */}\n \n \n \n \n | 도해 | \n 검사항목 | \n 검사기준 | \n 검사방법 | \n 검사주기 | \n 관련규정 | \n \n \n \n \n | \n 절곡품 중간검사 상세도면 참조\n | \n 겉모양 | \n 절곡상태 | \n 사용상 해로운 결함이 없을것 | \n 육안검사 | \n n = 1, c = 0 | \n KS F 4510 5.1항 | \n \n \n 치수 (mm) | \n 길이 | \n 도면치수 ± 4 | \n 체크검사 | \n KS F 4510 7항 표9 | \n \n \n | 간격 | \n 도면치수 ± 2 | \n KS F 4510 7항 표9 / 자체규정 | \n \n \n \n | \n
\n \n
\n\n {/* 중간검사 DATA 섹션 - 첨부파일 기준 */}\n
\n \n \n | 중간검사 DATA | \n
\n \n | NO | \n 품명 | \n 겉모양 | \n 치수(mm) | \n 판정 (적/부) | \n
\n \n | 절곡상태 | \n 길이 | \n 너비 | \n 간격 | \n
\n \n | 도면치수 | \n 측정값 | \n 도면치수 | \n 측정값 | \n POINT | \n 도면치수 | \n 측정값 | \n
\n \n \n \n | 1 | \n 케이스-전면부 | \n \n \n \n ✓\n 양호\n \n \n \n 불량\n \n \n | \n 4000 | \n 4000 | \n N/A | \n N/A | \n ① | \n 380 | \n 380 | \n 적 | \n
\n {/* 빈 행들 */}\n {[2, 3, 4, 5].map((num) => (\n \n | {num} | \n | \n | \n | \n | \n | \n | \n | \n | \n | \n | \n
\n ))}\n {/* 부적합 내용 + 종합판정 행 */}\n \n | \n [부적합 내용] \n \n | \n \n 종합판정 \n \n \n \n | \n
\n \n
\n\n {/* 문서번호 푸터 */}\n
\n KDQP-01-009\n KDPS-10-03\n
\n
\n );\n }\n\n // 출고증 (SHP/TS) 미리보기 - 작업일지 스타일 적용\n if (docCode === 'SHP' || docCode === 'TS') {\n const shpSampleData = {\n lotNo: 'KD-WE-251015-01-(3)',\n productName: '국민방화스크린플러스셔터',\n productCode: 'KWE01',\n certNo: 'FDS-OTS23-0117-4',\n orderDate: '2025년 10월 15일 수요일',\n customerName: '주일',\n siteName: '용신고등학교(4층)',\n dueDate: '2025년 10월 30일 목요일',\n shipmentDate: '2025년 10월 29일 수요일',\n receiverName: '오천식반장',\n receiverPhone: '010-9414-7929',\n totalQty: 11,\n dispatchType: '상차',\n vehicleType: '선불',\n deliveryAddress: '경기도 용인시 처인구 고림동 320-6 용신고등학교',\n };\n return (\n
\n {/* 헤더 - 공통 함수 사용 */}\n {renderKDHeader('출 고 증', '출하 관리부서', {\n writer: '전진',\n writerDate: '10/29',\n writerDept: '판매/전진',\n reviewerDept: '출하',\n approverDept: '생산관리'\n })}\n\n {/* 로트번호 */}\n
\n \n \n | 제품 LOT NO. | \n {shpSampleData.lotNo} | \n
\n \n
\n\n {/* 전화 / 팩스 / 이메일 */}\n
\n 전화 : 031-938-5130 | 팩스 : 02-6911-6315 | 이메일 : kd5130@naver.com\n
\n\n {/* 상품명 / 제품명 / 인정번호 */}\n
\n \n \n | 상 품 명 | \n {shpSampleData.productName} | \n 제품명 | \n {shpSampleData.productCode} | \n 인정번호 | \n {shpSampleData.certNo} | \n
\n \n
\n\n {/* 신청업체 / 신청내용 / 납품정보 테이블 */}\n
\n \n \n | 신 청 업 체 | \n 신 청 내 용 | \n 납 품 정 보 | \n
\n \n | 발 주 일 | \n {shpSampleData.orderDate} | \n 현 장 명 | \n {shpSampleData.siteName} | \n 인수담당자 | \n {shpSampleData.receiverName} | \n
\n \n | 발 주 처 | \n {shpSampleData.customerName} | \n 납기요청일 | \n {shpSampleData.dueDate} | \n 인수자연락처 | \n {shpSampleData.receiverPhone} | \n
\n \n | 발주 담당자 | \n 김영민과장 | \n 출 고 일 | \n {shpSampleData.shipmentDate} | \n 배 송 방 법 | \n \n {shpSampleData.dispatchType}\n {shpSampleData.vehicleType}\n | \n
\n \n | 담당자 연락처 | \n | \n 셔터총수량 | \n {shpSampleData.totalQty} | \n 개소 | \n | \n
\n \n | 배송지 주소 | \n {shpSampleData.deliveryAddress} | \n
\n \n
\n\n {/* 1. 부자재 - 감기샤프트, 각파이프, 앵글 */}\n
1. 부자재 - 감기샤프트, 각파이프, 앵글
\n
\n \n \n {/* 감기샤프트 2인치 */}\n | \n 감기샤프트 \n 2인치 \n L : 300 \n | \n \n 수량 \n 11 \n | \n {/* 감기샤프트 4인치 */}\n \n 감기샤프트 \n 4인치 \n \n \n | L : 3,000 | \n | L : 4,500 | 3 | \n | L : 6,000 | 3 | \n \n \n | \n \n 수량 \n | \n {/* 감기샤프트 5인치 */}\n \n 감기샤프트 \n 5인치 \n \n \n | L : 6,000 | \n | L : 7,000 | 2 | \n | L : 8,200 | 3 | \n \n \n | \n \n 수량 \n | \n
\n \n {/* 각파이프 */}\n | \n 각파이프 \n (50*30*1.4T) \n L : 3,000 \n L : 6,000 \n | \n \n 수량 \n 56 \n | \n {/* 마철봉 */}\n \n 마철봉 \n (6mm) \n L : 3,000 \n | \n \n 수량 \n 27 \n | \n {/* 하단 무게평철 */}\n \n 하단 무게평철 \n [50*12T] \n L : 2,000 \n | \n \n 수량 \n 67 \n | \n
\n \n | \n ※ 별도 추가사항 - 부자재\n | \n {/* 앵글 */}\n \n 앵글 \n (40*40*3T) \n L : 380 \n L : 2,500 \n | \n \n 수량 \n 44 \n | \n
\n \n
\n\n {/* 2. 모터 */}\n
2. 모터
\n
\n \n \n {/* 2-1. 모터(220V 단상) */}\n | \n 2-1. 모터(220V 단상) \n \n \n \n | 모터 용량 | \n 수량 | \n 입고 LOT NO. | \n \n \n \n | KD-150K | | | \n | KD-300K | | | \n | KD-400K | | | \n \n \n | \n {/* 2-2. 모터(380V 삼상) */}\n \n 2-2. 모터(380V 삼상) \n \n \n \n | 모터 용량 | \n 수량 | \n 입고 LOT NO. | \n \n \n \n | KD-150K | 6 | | \n | KD-300K | 5 | | \n | KD-400K | | | \n \n \n | \n
\n \n {/* 2-3. 브라켓트 */}\n | \n 2-3. 브라켓트 \n \n \n \n | 브라켓트 | \n 수량 | \n 입고 LOT NO. | \n \n \n \n | 380*180 (2-4\") | 6 | | \n | 380*180 (2-5\") | 5 | | \n \n \n | \n {/* 2-4. 연동제어기 */}\n \n 2-4. 연동제어기 \n \n \n \n | 품명 | \n 수량 | \n 입고 LOT NO. | \n \n \n \n | 매립형 | | | \n | 노출형 | | | \n | 뒷박스 | | | \n \n \n | \n
\n \n | \n ※ 별도 추가사항 - 모터\n | \n
\n \n
\n\n {/* 하단 - 서명 영역 */}\n
\n
\n \n \n | 출고 담당 | \n | \n
\n \n | 인수 확인 | \n | \n
\n \n
\n
\n
\n );\n }\n\n // 납품확인서 (DC) 미리보기\n if (docCode === 'DC') {\n const dlvSampleData = {\n orderDate: '2025년 10월 15일 수요일',\n customerName: '주일',\n siteName: '용신고등학교(4층)',\n managerName: '김영민과장',\n phone: '010-1234-5678',\n deliveryDate: '2025년 10월 29일 수요일',\n lotNo: 'KD-WE-251015-01-(3)',\n receiverName: '오천식반장',\n receiverPhone: '010-9414-7929',\n deliveryAddress: '경기도 용인시 처인구 고림동 320-6 용신고등학교',\n };\n\n const dlvItems = [\n { no: 1, productName: '국민방화스크린플러스셔터', spec: 'W3000 x H3500', unit: 'SET', qty: 3, remark: '' },\n { no: 2, productName: '국민방화스크린플러스셔터', spec: 'W4500 x H4000', unit: 'SET', qty: 5, remark: '' },\n { no: 3, productName: '국민방화스크린플러스셔터', spec: 'W6000 x H3200', unit: 'SET', qty: 3, remark: '' },\n { no: 4, productName: '감기샤프트 4인치', spec: 'L4500', unit: 'EA', qty: 3, remark: '' },\n { no: 5, productName: '감기샤프트 4인치', spec: 'L6000', unit: 'EA', qty: 3, remark: '' },\n { no: 6, productName: '감기샤프트 5인치', spec: 'L7000', unit: 'EA', qty: 2, remark: '' },\n { no: 7, productName: '감기샤프트 5인치', spec: 'L8200', unit: 'EA', qty: 3, remark: '' },\n ];\n\n return (\n
\n {/* 헤더 - 공통 함수 사용 */}\n {renderKDHeader('납 품 확 인 서', '출하 관리부서', {\n writer: '전진',\n writerDate: '10/29',\n writerDept: '판매/전진',\n reviewerDept: '출하',\n approverDept: '품질'\n })}\n\n {/* 전화 / 팩스 / 이메일 */}\n
\n 전화 : 031-938-5130 | 팩스 : 02-6911-6315 | 이메일 : kd5130@naver.com\n
\n\n {/* 납품 정보 테이블 */}\n
\n \n \n | 발 주 정 보 | \n 납 품 정 보 | \n
\n \n | 발 주 일 | \n {dlvSampleData.orderDate} | \n 납 품 일 | \n {dlvSampleData.deliveryDate} | \n
\n \n | 발 주 처 | \n {dlvSampleData.customerName} | \n 현 장 명 | \n {dlvSampleData.siteName} | \n
\n \n | 담 당 자 | \n {dlvSampleData.managerName} | \n 인수담당자 | \n {dlvSampleData.receiverName} | \n
\n \n | 연 락 처 | \n {dlvSampleData.phone} | \n 인수자연락처 | \n {dlvSampleData.receiverPhone} | \n
\n \n | 제품 LOT NO. | \n {dlvSampleData.lotNo} | \n 납품지 주소 | \n {dlvSampleData.deliveryAddress} | \n
\n \n
\n\n {/* 납품 품목 테이블 */}\n
납품 품목
\n
\n \n \n | No | \n 품 명 | \n 규 격 | \n 단위 | \n 수량 | \n 비 고 | \n
\n \n \n {dlvItems.map((item, idx) => (\n \n | {item.no} | \n {item.productName} | \n {item.spec} | \n {item.unit} | \n {item.qty} | \n {item.remark} | \n
\n ))}\n {/* 빈 행 추가 (총 10행) */}\n {Array.from({ length: 10 - dlvItems.length }).map((_, idx) => (\n \n | {dlvItems.length + idx + 1} | \n | \n | \n | \n | \n | \n
\n ))}\n \n
\n\n {/* 특기사항 */}\n
특기사항
\n
\n \n \n | \n 위 물품을 상기와 같이 납품합니다.\n | \n
\n \n
\n\n {/* 하단 - 서명 영역 */}\n
\n {/* 납품자 서명 */}\n
\n \n \n | 납 품 자 | \n
\n \n | 회 사 명 | \n 경동기업 | \n
\n \n | 담 당 자 | \n 전진 | \n
\n \n | 서명/날인 | \n | \n
\n \n
\n\n {/* 인수자 서명 */}\n
\n \n \n | 인 수 자 | \n
\n \n | 회 사 명 | \n | \n
\n \n | 담 당 자 | \n | \n
\n \n | 서명/날인 | \n | \n
\n \n
\n
\n\n {/* 하단 안내문 */}\n
\n 상기 물품을 정히 인수하였음을 확인합니다.\n
\n
\n );\n }\n\n return
미리보기가 준비되지 않은 문서입니다.
;\n };\n\n return (\n
\n {/* 헤더 */}\n
\n\n {/* 카테고리 필터 */}\n
\n \n {documentCategories.map(cat => (\n \n ))}\n
\n\n {/* 문서 유형 목록 */}\n
\n \n {filteredDocTypes.map(docType => {\n const category = documentCategories.find(c => c.id === docType.category);\n const hasTemplate = documentTemplates[docType.code];\n return (\n
hasTemplate && handleSelectDocType(docType)}\n >\n
\n
\n
{docType.name}
\n
{docType.code}
\n
\n
\n \n {docType.isActive ? '활성' : '비활성'}\n \n {hasTemplate && (\n \n 템플릿\n \n )}\n
\n
\n
{docType.description}
\n
\n {category?.name}\n {hasTemplate && (\n 미리보기\n )}\n
\n
\n );\n })}\n
\n \n\n {/* 템플릿 상세 보기 */}\n {showTemplateEditor && selectedDocType && (\n
\n {/* 편집기 헤더 */}\n
\n
\n
\n
\n \n
{selectedDocType.name} 템플릿
\n ({selectedDocType.code})\n \n
\n
\n \n
\n
\n\n {/* 템플릿 정보 */}\n
\n \n
템플릿 정보
\n
\n
\n
\n
\n {documentTemplates[selectedDocType.code]?.title || selectedDocType.name}\n
\n
\n
\n
\n
\n {documentTemplates[selectedDocType.code]?.layout || '-'}\n
\n
\n
\n
\n
\n {documentTemplates[selectedDocType.code]?.showCompanyLogo ? '표시' : '미표시'}\n
\n
\n
\n
\n
\n {documentTemplates[selectedDocType.code]?.showApprovalBox ? '표시' : '미표시'}\n
\n
\n
\n
\n \n\n {/* 결재라인 설정 */}\n {documentTemplates[selectedDocType.code]?.showApprovalBox && (\n
\n \n
\n
\n
결재라인 설정
\n
문서 승인을 위한 결재라인을 구성합니다
\n
\n
\n
\n
\n {templateForm.approvalLines.map((line, index) => (\n
\n \n {index + 1}\n \n \n {\n const newLines = [...templateForm.approvalLines];\n newLines[index].department = e.target.value;\n setTemplateForm(prev => ({ ...prev, approvalLines: newLines }));\n }}\n placeholder=\"부서\"\n className=\"flex-1 px-3 py-2 border rounded-lg text-sm\"\n />\n \n
\n ))}\n
\n
\n \n )}\n\n {/* 섹션 구성 */}\n
\n \n
섹션 구성
\n
문서를 구성하는 섹션 목록입니다
\n
\n {documentTemplates[selectedDocType.code]?.sections?.map((section, index) => (\n
\n
\n {index + 1}\n \n
\n
{section.id}
\n
타입: {section.type}
\n
\n
\n )) || (\n
섹션 정보가 없습니다
\n )}\n
\n
\n \n\n {/* 미리보기 카드 */}\n
\n \n
문서 미리보기
\n
\n \n
\n
\n \n
\n )}\n\n {/* 필드구성 탭 */}\n {activeTab === 'field' && (\n
\n \n
\n\n {/* 필드 카테고리별 표시 */}\n
\n {config.fieldCategories.map(category => (\n
\n
\n \n \n \n {category.name}\n
\n
\n {config.commonFieldPool\n .filter(f => f.category === category.id)\n .map(field => (\n
\n
{field.fieldName}
\n
{field.fieldKey} ({field.fieldType})
\n
\n ))\n }\n
\n
\n ))}\n
\n
\n \n )}\n\n {/* 결재라인 탭 */}\n {activeTab === 'approval' && (\n
\n \n
\n
결재라인 프리셋
\n
\n
\n\n
\n {config.approvalPresets.map(preset => (\n
\n
{preset.name}
\n
{preset.description}
\n
\n {preset.lines.map((line, index) => (\n
\n \n {index + 1}\n \n {config.approvalRoles.find(r => r.id === line.role)?.name}\n {line.department && ({line.department})}\n
\n ))}\n
\n
\n ))}\n
\n
\n \n )}\n\n {/* 미리보기 탭 */}\n {activeTab === 'preview' && (\n
\n \n
\n
문서유형을 선택하고 편집기에서 미리보기를 확인하세요
\n
\n \n )}\n\n {/* 미리보기 모달 */}\n {showPreviewModal && selectedDocType && (\n
\n
\n
\n
\n
{selectedDocType.name} 미리보기
\n ({selectedDocType.code})\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n {renderDocumentPreview(selectedDocType.code)}\n
\n
\n
\n )}\n\n {/* 통합 문서양식 등록/편집 모달 */}\n {showCreateModal && (\n
\n
\n {/* 모달 헤더 */}\n
\n
\n
\n
\n
새 문서양식 등록
\n
섹션을 조합하여 새로운 문서 양식을 만듭니다
\n
\n
\n
\n
\n\n {/* 모달 본문 - 2컬럼 레이아웃 */}\n
\n {/* 왼쪽: 설정 영역 */}\n
\n {/* 기본 정보 */}\n
\n
\n \n 기본 정보\n
\n
\n
\n \n setTemplateForm(prev => ({ ...prev, code: e.target.value.toUpperCase() }))}\n placeholder=\"예: QT, SO, WO\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500\"\n />\n
\n
\n \n \n
\n
\n \n setTemplateForm(prev => ({ ...prev, title: e.target.value }))}\n placeholder=\"예: 견 적 서, 수입검사 성적서\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500\"\n />\n
\n
\n \n setTemplateForm(prev => ({ ...prev, description: e.target.value }))}\n placeholder=\"문서에 대한 간단한 설명\"\n className=\"w-full px-3 py-2 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500\"\n />\n
\n
\n
\n\n {/* 용지 설정 */}\n
\n
\n \n 용지 설정\n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n\n {/* 섹션 추가 */}\n
\n
\n \n 섹션 추가\n
\n
아래 섹션을 클릭하여 문서에 추가하세요
\n
\n {sectionTypeOptions.map(section => (\n
\n ))}\n
\n
\n\n {/* 구성된 섹션 목록 */}\n
\n
\n \n 구성된 섹션 ({templateForm.sections.length})\n
\n {templateForm.sections.length === 0 ? (\n
\n
\n
위에서 섹션을 선택하여 추가하세요
\n
\n ) : (\n
\n {templateForm.sections.map((section, index) => (\n
\n
\n {index + 1}\n \n
\n
{section.typeName}
\n
타입: {section.type}
\n
\n
\n \n \n \n
\n
\n ))}\n
\n )}\n
\n
\n\n {/* 오른쪽: 실시간 미리보기 */}\n
\n
\n \n 실시간 미리보기\n
\n
\n {/* 미리보기 헤더 */}\n {templateForm.title && (\n
\n
{templateForm.title}
\n
\n 문서번호: {templateForm.code || 'XX'}-YYMMDD-01\n
\n
\n )}\n\n {/* 섹션별 미리보기 */}\n {templateForm.sections.map((section, index) => (\n
\n
\n [{index + 1}] {section.typeName}\n
\n {/* 섹션 타입별 미리보기 */}\n {section.type === 'header' && (\n
문서 제목 영역
\n )}\n {section.type === 'partyInfo' && (\n
\n \n | 상호 | OOO건설 | 사업자번호 | 000-00-00000 |
\n | 대표자 | 홍길동 | 연락처 | 02-000-0000 |
\n \n
\n )}\n {section.type === 'infoTable' && (\n
\n )}\n {section.type === 'amountBox' && (\n
\n )}\n {section.type === 'productTable' && (\n
\n )}\n {section.type === 'inspectionTable' && (\n
\n | NO | 검사항목 | 검사기준 | 측정값 | 판정 |
\n | 1 | 외관검사 | 육안검사 | | □적합 □부적합 |
\n
\n )}\n {section.type === 'remarks' && (\n
\n )}\n {section.type === 'signature' && (\n
\n
\n
상기와 같이 확인합니다.
\n
2025년 00월 00일
\n
회사명: (주)OOO (인)
\n
\n
(인감)
\n
\n )}\n {section.type === 'approval' && (\n
\n )}\n {section.type === 'judgement' && (\n
\n □ 합격\n □ 불합격\n
\n )}\n {section.type === 'diagram' && (\n
\n [도면/사진 영역]\n
\n )}\n
\n ))}\n\n {templateForm.sections.length === 0 && !templateForm.title && (\n
\n
\n
문서 정보와 섹션을 추가하면
\n
여기에 미리보기가 표시됩니다
\n
\n )}\n
\n
\n
\n\n {/* 모달 푸터 */}\n
\n
\n {templateForm.sections.length > 0 && (\n 총 {templateForm.sections.length}개 섹션 구성됨\n )}\n
\n
\n \n \n
\n
\n
\n
\n )}\n
\n );\n};\n\n// ============ 단가 관리 (4탭 구조: 매입단가/매출단가/BOM제품단가/리포트) ============\n\nconst PriceList = ({ onNavigate }) => {\n const [search, setSearch] = useState('');\n const [activeMainTab, setActiveMainTab] = useState('purchase'); // purchase, sales, bom, report\n const [itemTypeFilter, setItemTypeFilter] = useState('전체');\n const [showModal, setShowModal] = useState(false);\n const [modalMode, setModalMode] = useState(''); // purchase-edit, sales-edit, sales-batch, version-new\n const [selectedItem, setSelectedItem] = useState(null);\n\n // ===== ① 매입단가 데이터 =====\n const [purchasePrices, setPurchasePrices] = useState([\n { id: 1, itemType: '부품', itemCode: 'GR-001', itemName: '가이드레일 130×80', spec: '130×80×2438', unit: 'EA', supplier: '철강공업', purchasePrice: 45000, validFrom: '2025-01-01', validTo: '2025-12-31', status: '활성', history: [{ date: '2025-01-01', price: 45000, supplier: '철강공업', by: '김관리' }, { date: '2024-07-01', price: 42000, supplier: '철강공업', by: '김관리' }] },\n { id: 2, itemType: '부품', itemCode: 'CASE-001', itemName: '케이스 철재', spec: '표준형', unit: 'EA', supplier: '케이스공업', purchasePrice: 35000, validFrom: '2025-01-01', validTo: null, status: '활성', history: [{ date: '2025-01-01', price: 35000, supplier: '케이스공업', by: '김관리' }] },\n { id: 3, itemType: '부품', itemCode: 'MOTOR-001', itemName: '모터 0.4KW', spec: '0.4KW', unit: 'EA', supplier: '모터공급사', purchasePrice: 120000, validFrom: '2025-01-01', validTo: null, status: '활성', history: [{ date: '2025-01-01', price: 120000, supplier: '모터공급사', by: '김관리' }, { date: '2024-06-01', price: 115000, supplier: '모터공급사', by: '이담당' }] },\n { id: 4, itemType: '부품', itemCode: 'CTL-001', itemName: '제어기 기본형', spec: '기본형', unit: 'EA', supplier: '제어기공급사', purchasePrice: 80000, validFrom: '2025-01-01', validTo: null, status: '활성', history: [] },\n { id: 5, itemType: '원자재', itemCode: 'STEEL-P01', itemName: '아연도금강판', spec: 't1.2×1219×2438', unit: 'EA', supplier: '포스코', purchasePrice: 32000, validFrom: '2025-01-01', validTo: null, status: '활성', history: [{ date: '2025-01-01', price: 32000, supplier: '포스코', by: '김관리' }] },\n { id: 6, itemType: '부자재', itemCode: 'BOLT-001', itemName: '볼트 M8×20', spec: 'M8×20', unit: 'EA', supplier: '나사공급사', purchasePrice: 120, validFrom: '2025-01-01', validTo: null, status: '활성', history: [] },\n { id: 7, itemType: '부품', itemCode: 'SLT-001', itemName: '슬랫 0.6T', spec: '0.6T×110', unit: 'M', supplier: '슬랫공업', purchasePrice: 8500, validFrom: '2025-01-01', validTo: null, status: '활성', history: [{ date: '2025-01-01', price: 8500, supplier: '슬랫공업', by: '김관리' }] },\n ]);\n\n // ===== ② 매출단가 데이터 (버전관리) =====\n const [salesSubTab, setSalesSubTab] = useState('items'); // items, versions, customer-groups\n const [selectedVersionId, setSelectedVersionId] = useState(1);\n\n const [salesPriceVersions, setSalesPriceVersions] = useState([\n { id: 1, versionNo: 'V2025-01', versionName: '2025년 상반기 단가표', status: '활성', createdAt: '2025-01-01', activatedAt: '2025-01-01', createdBy: '김관리', itemCount: 7, note: '2025년 상반기 공시가격 반영' },\n { id: 2, versionNo: 'V2024-02', versionName: '2024년 하반기 단가표', status: '보관', createdAt: '2024-07-01', activatedAt: '2024-07-01', archivedAt: '2024-12-31', createdBy: '김관리', itemCount: 6, note: '2024년 하반기 적용' },\n { id: 3, versionNo: 'V2025-02', versionName: '2025년 하반기 단가표(임시)', status: '임시', createdAt: '2025-06-01', createdBy: '이담당', itemCount: 4, note: '작성중 - 원자재 인상 반영 예정' },\n ]);\n\n const [salesPrices, setSalesPrices] = useState([\n { id: 1, versionId: 1, itemType: '부품', itemCode: 'GR-001', itemName: '가이드레일 130×80', spec: '130×80×2438', unit: 'EA', purchasePrice: 45000, processingCost: 5000, lossRate: 3, marginType: 'rate', marginValue: 20, sellingPrice: 60000, status: '활성' },\n { id: 2, versionId: 1, itemType: '부품', itemCode: 'CASE-001', itemName: '케이스 철재', spec: '표준형', unit: 'EA', purchasePrice: 35000, processingCost: 10000, lossRate: 2, marginType: 'rate', marginValue: 25, sellingPrice: 56250, status: '활성' },\n { id: 3, versionId: 1, itemType: '부품', itemCode: 'MOTOR-001', itemName: '모터 0.4KW', spec: '0.4KW', unit: 'EA', purchasePrice: 120000, processingCost: 10000, lossRate: 0, marginType: 'rate', marginValue: 15, sellingPrice: 149500, status: '활성' },\n { id: 4, versionId: 1, itemType: '부품', itemCode: 'CTL-001', itemName: '제어기 기본형', spec: '기본형', unit: 'EA', purchasePrice: 80000, processingCost: 5000, lossRate: 0, marginType: 'rate', marginValue: 20, sellingPrice: 102000, status: '활성' },\n { id: 5, versionId: 1, itemType: '원자재', itemCode: 'STEEL-P01', itemName: '아연도금강판', spec: 't1.2×1219×2438', unit: 'EA', purchasePrice: 32000, processingCost: 0, lossRate: 5, marginType: 'rate', marginValue: 15, sellingPrice: 38640, status: '활성' },\n { id: 6, versionId: 1, itemType: '부자재', itemCode: 'BOLT-001', itemName: '볼트 M8×20', spec: 'M8×20', unit: 'EA', purchasePrice: 120, processingCost: 0, lossRate: 0, marginType: 'amount', marginValue: 30, sellingPrice: 150, status: '활성' },\n { id: 7, versionId: 1, itemType: '부품', itemCode: 'SLT-001', itemName: '슬랫 0.6T', spec: '0.6T×110', unit: 'M', purchasePrice: 8500, processingCost: 500, lossRate: 2, marginType: 'rate', marginValue: 18, sellingPrice: 10827, status: '활성' },\n ]);\n\n // 거래처그룹 단가\n const [customerGroups, setCustomerGroups] = useState([\n { id: 1, groupCode: 'VIP', groupName: 'VIP 고객', discountRate: 10, description: '연간 거래액 1억 이상', customerCount: 5 },\n { id: 2, groupCode: 'GOLD', groupName: '골드 고객', discountRate: 7, description: '연간 거래액 5천만원 이상', customerCount: 12 },\n { id: 3, groupCode: 'SILVER', groupName: '실버 고객', discountRate: 5, description: '연간 거래액 1천만원 이상', customerCount: 28 },\n { id: 4, groupCode: 'NORMAL', groupName: '일반 고객', discountRate: 0, description: '일반 거래 고객', customerCount: 156 },\n ]);\n\n // ===== ③ BOM/제품단가 데이터 =====\n const [bomViewMode, setBomViewMode] = useState('detail'); // detail, compare\n const [selectedBomProduct, setSelectedBomProduct] = useState(null);\n\n const [bomProducts, setBomProducts] = useState([\n {\n id: 1, productCode: 'SCREEN-001', productName: '스크린 셔터 기본형', spec: '표준형', unit: 'SET', bomCost: 459000, processingCost: 85000, marginRate: 25, finalPrice: 680000, status: '활성',\n bom: [\n { partCode: 'GR-001', partName: '가이드레일 130×80', qty: 2, unit: 'EA', unitPrice: 45000, totalPrice: 90000 },\n { partCode: 'CASE-001', partName: '케이스 철재', qty: 1, unit: 'EA', unitPrice: 35000, totalPrice: 35000 },\n { partCode: 'MOTOR-001', partName: '모터 0.4KW', qty: 1, unit: 'EA', unitPrice: 120000, totalPrice: 120000 },\n { partCode: 'CTL-001', partName: '제어기 기본형', qty: 1, unit: 'EA', unitPrice: 80000, totalPrice: 80000 },\n { partCode: 'STEEL-P01', partName: '아연도금강판', qty: 4, unit: 'EA', unitPrice: 32000, totalPrice: 128000 },\n { partCode: 'BOLT-001', partName: '볼트 M8×20', qty: 50, unit: 'EA', unitPrice: 120, totalPrice: 6000 },\n ]\n },\n {\n id: 2, productCode: 'SCREEN-002', productName: '스크린 셔터 오픈형', spec: '오픈형', unit: 'SET', bomCost: 520000, processingCost: 95000, marginRate: 25, finalPrice: 768750, status: '활성',\n bom: [\n { partCode: 'GR-001', partName: '가이드레일 130×80', qty: 2, unit: 'EA', unitPrice: 45000, totalPrice: 90000 },\n { partCode: 'CASE-001', partName: '케이스 철재', qty: 1, unit: 'EA', unitPrice: 38000, totalPrice: 38000 },\n { partCode: 'MOTOR-001', partName: '모터 0.4KW', qty: 1, unit: 'EA', unitPrice: 135000, totalPrice: 135000 },\n { partCode: 'CTL-001', partName: '제어기 기본형', qty: 1, unit: 'EA', unitPrice: 85000, totalPrice: 85000 },\n { partCode: 'STEEL-P01', partName: '아연도금강판', qty: 5, unit: 'EA', unitPrice: 32000, totalPrice: 160000 },\n { partCode: 'BOLT-001', partName: '볼트 M8×20', qty: 100, unit: 'EA', unitPrice: 120, totalPrice: 12000 },\n ]\n },\n {\n id: 3, productCode: 'STEEL-001', productName: '철재 셔터 기본형', spec: '표준형', unit: 'SET', bomCost: 380000, processingCost: 70000, marginRate: 22, finalPrice: 548780, status: '활성',\n bom: [\n { partCode: 'GR-001', partName: '가이드레일 130×80', qty: 2, unit: 'EA', unitPrice: 45000, totalPrice: 90000 },\n { partCode: 'CASE-001', partName: '케이스 철재', qty: 1, unit: 'EA', unitPrice: 35000, totalPrice: 35000 },\n { partCode: 'MOTOR-001', partName: '모터 0.4KW', qty: 1, unit: 'EA', unitPrice: 120000, totalPrice: 120000 },\n { partCode: 'SLT-001', partName: '슬랫 0.6T', qty: 15, unit: 'M', unitPrice: 8500, totalPrice: 127500 },\n { partCode: 'BOLT-001', partName: '볼트 M8×20', qty: 60, unit: 'EA', unitPrice: 120, totalPrice: 7200 },\n ]\n },\n ]);\n\n // ===== ④ 리포트 데이터 =====\n const priceChangeReport = {\n currentMonth: '2025-01', previousMonth: '2024-12',\n summary: { totalItems: 7, increasedItems: 3, decreasedItems: 1, unchangedItems: 3, avgChangeRate: 3.8 },\n details: [\n { itemCode: 'GR-001', itemName: '가이드레일 130×80', prevPrice: 42000, currPrice: 45000, changeAmount: 3000, changeRate: 7.14, direction: 'up' },\n { itemCode: 'MOTOR-001', itemName: '모터 0.4KW', prevPrice: 115000, currPrice: 120000, changeAmount: 5000, changeRate: 4.35, direction: 'up' },\n { itemCode: 'STEEL-P01', itemName: '아연도금강판', prevPrice: 34000, currPrice: 32000, changeAmount: -2000, changeRate: -5.88, direction: 'down' },\n { itemCode: 'CASE-001', itemName: '케이스 철재', prevPrice: 35000, currPrice: 35000, changeAmount: 0, changeRate: 0, direction: 'unchanged' },\n { itemCode: 'CTL-001', itemName: '제어기 기본형', prevPrice: 78000, currPrice: 80000, changeAmount: 2000, changeRate: 2.56, direction: 'up' },\n { itemCode: 'BOLT-001', itemName: '볼트 M8×20', prevPrice: 120, currPrice: 120, changeAmount: 0, changeRate: 0, direction: 'unchanged' },\n { itemCode: 'SLT-001', itemName: '슬랫 0.6T', prevPrice: 8500, currPrice: 8500, changeAmount: 0, changeRate: 0, direction: 'unchanged' },\n ]\n };\n\n // ===== 탭/필터 정의 =====\n const mainTabs = [\n { id: 'purchase', label: '매입단가', icon: Download },\n { id: 'sales', label: '매출단가', icon: TrendingUp },\n { id: 'bom', label: 'BOM/제품단가', icon: Layers },\n { id: 'report', label: '리포트', icon: BarChart3 },\n ];\n const itemTypeTabs = ['전체', '제품', '부품', '부자재', '원자재', '소모품'];\n\n // 품목유형별 카운트\n const getTypeCount = (type) => {\n const data = activeMainTab === 'purchase' ? purchasePrices : salesPrices.filter(p => p.versionId === selectedVersionId);\n if (type === '전체') return data.length;\n return data.filter(p => p.itemType === type).length;\n };\n\n // 필터링 - 매입단가\n const filteredPurchase = purchasePrices\n .filter(p => itemTypeFilter === '전체' || p.itemType === itemTypeFilter)\n .filter(p => p.itemCode.toLowerCase().includes(search.toLowerCase()) || p.itemName.includes(search) || p.spec?.includes(search))\n .sort((a, b) => b.id - a.id); // ID 기준 내림차순 (최신 등록 최상단)\n\n // 필터링 - 매출단가\n const filteredSales = salesPrices\n .filter(p => p.versionId === selectedVersionId)\n .filter(p => itemTypeFilter === '전체' || p.itemType === itemTypeFilter)\n .filter(p => p.itemCode.toLowerCase().includes(search.toLowerCase()) || p.itemName.includes(search))\n .sort((a, b) => b.id - a.id); // ID 기준 내림차순 (최신 등록 최상단)\n\n // ===== 통계 =====\n const purchaseTotal = purchasePrices.length;\n const salesTotal = salesPrices.filter(p => p.versionId === selectedVersionId).length;\n const bomTotal = bomProducts.length;\n const activeVersion = salesPriceVersions.find(v => v.status === '활성');\n\n // ===== 핸들러 =====\n const openPurchaseEditModal = (item) => {\n setSelectedItem(item);\n setModalMode('purchase-edit');\n setShowModal(true);\n };\n\n const openSalesEditModal = (item) => {\n setSelectedItem(item);\n setModalMode('sales-edit');\n setShowModal(true);\n };\n\n const handleSavePurchasePrice = (updatedItem) => {\n const now = new Date().toISOString().slice(0, 10);\n const historyEntry = { date: now, price: updatedItem.purchasePrice, supplier: updatedItem.supplier, by: '시스템' };\n const newHistory = [...(updatedItem.history || [])];\n if (!newHistory.some(h => h.date === now && h.price === updatedItem.purchasePrice)) {\n newHistory.unshift(historyEntry);\n }\n setPurchasePrices(prev => prev.map(p => p.id === updatedItem.id ? { ...updatedItem, history: newHistory } : p));\n setShowModal(false);\n };\n\n const handleSaveSalesPrice = (updatedItem) => {\n setSalesPrices(prev => prev.map(p => p.id === updatedItem.id ? updatedItem : p));\n setShowModal(false);\n };\n\n // 버전 활성화\n const handleActivateVersion = (versionId) => {\n const now = new Date().toISOString().slice(0, 10);\n setSalesPriceVersions(prev => prev.map(v => ({\n ...v,\n status: v.id === versionId ? '활성' : (v.status === '활성' ? '보관' : v.status),\n activatedAt: v.id === versionId ? now : v.activatedAt,\n archivedAt: v.status === '활성' && v.id !== versionId ? now : v.archivedAt,\n })));\n setSelectedVersionId(versionId);\n };\n\n // 일괄 재계산\n const handleBatchRecalculate = () => {\n setSalesPrices(prev => prev.map(p => {\n if (p.versionId !== selectedVersionId || !p.purchasePrice) return p;\n const base = p.purchasePrice + (p.processingCost || 0);\n const lossApplied = Math.round(base * (1 + (p.lossRate || 0) / 100));\n const newSellingPrice = p.marginType === 'rate'\n ? Math.round(lossApplied * (1 + p.marginValue / 100))\n : lossApplied + p.marginValue;\n return { ...p, sellingPrice: newSellingPrice };\n }));\n };\n\n return (\n
\n {/* 헤더 */}\n
\n
\n \n 단가 목록\n
\n
\n {activeMainTab === 'purchase' && (\n
\n )}\n {activeMainTab === 'sales' && salesSubTab === 'items' && (\n
\n )}\n {activeMainTab === 'sales' && salesSubTab === 'versions' && (\n
\n )}\n
\n
\n\n {/* 통계 카드 */}\n
\n
\n
\n
\n
매입단가 품목
\n
{purchaseTotal}
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
매출단가 품목
\n
{salesTotal}
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
활성 버전
\n
{activeVersion?.versionNo || '-'}
\n
\n
\n \n
\n
\n
\n
\n\n {/* 검색 */}\n
\n
\n \n setSearch(e.target.value)}\n className=\"w-full pl-10 pr-4 py-2 border-0 text-sm focus:ring-0\"\n />\n
\n
\n\n {/* 메인 4탭 */}\n
\n
\n
\n {mainTabs.map(tab => (\n \n ))}\n
\n
\n\n {/* ========== ① 매입단가 탭 ========== */}\n {activeMainTab === 'purchase' && (\n <>\n {/* 품목유형 서브탭 (칩 스타일) */}\n
\n
\n {itemTypeTabs.map(tab => (\n \n ))}\n
\n
\n\n {/* 매입단가 테이블 */}\n
\n
\n \n \n | 번호 | \n 품목유형 | \n 품목코드 | \n 품목명 | \n 규격 | \n 단위 | \n 공급업체 | \n 매입단가 | \n 유효기간 | \n 상태 | \n 이력 | \n 작업 | \n
\n \n \n {filteredPurchase.map((item, idx) => (\n \n | {filteredPurchase.length - idx} | \n \n \n {item.itemType}\n \n | \n {item.itemCode} | \n {item.itemName} | \n {item.spec || '-'} | \n {item.unit} | \n {item.supplier || '-'} | \n \n {item.purchasePrice ? `${item.purchasePrice.toLocaleString()}원` : '-'}\n | \n \n {item.validFrom ? (\n \n {item.validFrom}~{item.validTo || '무기한'}\n \n ) : '-'}\n | \n \n \n {item.status}\n \n | \n \n {item.history && item.history.length > 0 ? (\n \n ) : '-'}\n | \n \n \n \n \n | \n
\n ))}\n {filteredPurchase.length === 0 && (\n \n | \n 등록된 매입단가가 없습니다.\n | \n
\n )}\n \n
\n
\n >\n )}\n\n {/* ② 매출단가 탭 */}\n {activeMainTab === 'sales' && (\n <>\n {/* 매출단가 서브탭 */}\n
\n {[\n { id: 'items', label: '품목별 단가' },\n { id: 'versions', label: '버전관리' },\n { id: 'groups', label: '거래처그룹' },\n ].map(tab => (\n \n ))}\n
\n\n {/* 품목별 단가 */}\n {salesSubTab === 'items' && (\n
\n
\n \n \n | 번호 | \n 품목코드 | \n 품목명 | \n 규격 | \n 단위 | \n 원가 | \n 마진유형 | \n 마진 | \n 판매단가 | \n 버전 | \n 작업 | \n
\n \n \n {filteredSales.map((item, idx) => (\n \n | {filteredSales.length - idx} | \n {item.itemCode} | \n {item.itemName} | \n {item.spec || '-'} | \n {item.unit} | \n \n {item.costPrice?.toLocaleString()}원\n | \n \n \n {item.marginType === 'rate' ? '율(%)' : '액(원)'}\n \n | \n \n {item.marginType === 'rate'\n ? `${item.marginValue}%`\n : `${item.marginValue?.toLocaleString()}원`}\n | \n \n {item.sellingPrice?.toLocaleString()}원\n | \n \n {item.versionNo}\n | \n \n \n | \n
\n ))}\n {filteredSales.length === 0 && (\n \n | \n 등록된 매출단가가 없습니다.\n | \n
\n )}\n \n
\n
\n )}\n\n {/* 버전관리 */}\n {salesSubTab === 'versions' && (\n
\n
\n \n \n | 버전번호 | \n 버전명 | \n 상태 | \n 품목수 | \n 적용기간 | \n 생성일 | \n 생성자 | \n 작업 | \n
\n \n \n {[...salesPriceVersions].sort((a, b) => b.id - a.id).map((ver) => (\n \n | {ver.versionNo} | \n {ver.versionName} | \n \n \n {ver.status}\n \n | \n {ver.itemCount}건 | \n \n {ver.validFrom}~{ver.validTo || '무기한'}\n | \n {ver.createdAt} | \n {ver.createdBy} | \n \n \n {ver.status === '임시' && (\n \n )}\n \n \n | \n
\n ))}\n \n
\n
\n )}\n\n {/* 거래처그룹 */}\n {salesSubTab === 'groups' && (\n
\n
\n \n \n | 그룹명 | \n 할인율 | \n 거래처수 | \n 설명 | \n 작업 | \n
\n \n \n {[...customerGroups].sort((a, b) => b.id - a.id).map((group) => (\n \n | {group.groupName} | \n \n -{group.discountRate}%\n | \n {group.customerCount}개사 | \n {group.description || '-'} | \n \n \n | \n
\n ))}\n \n
\n
\n )}\n >\n )}\n\n {/* ③ BOM/제품단가 탭 */}\n {activeMainTab === 'bom' && (\n
\n {/* 좌측: 제품 목록 */}\n
\n
제품 목록
\n
\n {[...bomProducts].sort((a, b) => b.id - a.id).map((product) => (\n
\n ))}\n
\n
\n\n {/* 우측: BOM 상세/비교 */}\n
\n {/* 보기모드 선택 */}\n
\n \n \n
\n\n {selectedBomProduct ? (\n <>\n {/* BOM 상세 */}\n {bomViewMode === 'detail' && (\n
\n
\n
{selectedBomProduct.productName}
\n
{selectedBomProduct.productCode}
\n
\n 총 원가: {selectedBomProduct.totalCost?.toLocaleString()}원\n 판매가: {selectedBomProduct.sellingPrice?.toLocaleString()}원\n 마진: {((selectedBomProduct.sellingPrice - selectedBomProduct.totalCost) / selectedBomProduct.sellingPrice * 100).toFixed(1)}%\n
\n
\n
\n \n \n | 부품코드 | \n 부품명 | \n 수량 | \n 단가 | \n 금액 | \n
\n \n \n {selectedBomProduct.bom?.map((part, idx) => (\n \n | {part.partCode} | \n {part.partName} | \n {part.qty} | \n {part.unitPrice?.toLocaleString()}원 | \n {(part.qty * part.unitPrice)?.toLocaleString()}원 | \n
\n ))}\n \n
\n
\n )}\n\n {/* 부품 비교 */}\n {bomViewMode === 'compare' && (\n
\n 부품별 가격 비교 기능 (구현 예정)\n
\n )}\n >\n ) : (\n
\n 좌측에서 제품을 선택하세요\n
\n )}\n
\n
\n )}\n\n {/* ④ 리포트 탭 */}\n {activeMainTab === 'report' && (\n
\n
\n
\n {priceChangeReport.currentMonth} 대비 {priceChangeReport.previousMonth} 단가 변동 현황\n
\n
\n \n \n 상승: {priceChangeReport.summary.increased}건\n \n \n \n 하락: {priceChangeReport.summary.decreased}건\n \n \n -\n 유지: {priceChangeReport.summary.unchanged}건\n \n
\n
\n\n
\n
\n \n \n | 품목코드 | \n 품목명 | \n 이전단가 | \n 현재단가 | \n 변동 | \n 변동율 | \n
\n \n \n {priceChangeReport.details.map((item, idx) => (\n \n | {item.itemCode} | \n {item.itemName} | \n {item.prevPrice?.toLocaleString()}원 | \n {item.currPrice?.toLocaleString()}원 | \n \n {item.changeRate > 0 ? (\n \n ) : item.changeRate < 0 ? (\n \n ) : (\n -\n )}\n | \n \n 0 ? 'text-red-600' : item.changeRate < 0 ? 'text-blue-600' : 'text-gray-500'\n }`}>\n {item.changeRate > 0 ? '+' : ''}{item.changeRate}%\n \n | \n
\n ))}\n \n
\n
\n
\n )}\n
\n\n {/* 모달 */}\n {showModal && modalMode === 'edit' && selectedItem && (\n
setShowModal(false)}\n onSave={handleSaveBasePrice}\n />\n )}\n\n {showModal && modalMode === 'detail' && selectedItem && (\n setShowModal(false)}\n onEdit={() => { setModalMode('edit'); }}\n />\n )}\n\n {showModal && modalMode === 'customer' && (\n p.status === '활성')}\n onClose={() => setShowModal(false)}\n onSave={handleSaveCustomerPrice}\n />\n )}\n \n );\n};\n\n// 기본단가 수정 모달\nconst PriceEditModal = ({ item, onClose, onSave }) => {\n const [formData, setFormData] = useState({\n validFrom: item.validFrom || new Date().toISOString().slice(0, 10),\n validTo: item.validTo || '',\n supplier: item.supplier || '',\n purchaseDate: item.purchaseDate || '',\n author: item.author || '',\n purchasePrice: item.purchasePrice || 0,\n unit: item.unit || 'EA',\n lossRate: item.lossRate || 0,\n processingCost: item.processingCost || 0,\n roundingRule: item.roundingRule || '반올림',\n roundingUnit: item.roundingUnit || '1원 단위',\n marginRate: item.marginRate || 0,\n sellingPrice: item.sellingPrice || 0,\n note: item.note || '',\n qtyPricing: item.qtyPricing || [],\n });\n\n // 수량별 단가 추가\n const addQtyPricing = () => {\n setFormData(prev => ({\n ...prev,\n qtyPricing: [...prev.qtyPricing, { minQty: 1, maxQty: 10, price: prev.sellingPrice }]\n }));\n };\n\n // 수량별 단가 삭제\n const removeQtyPricing = (idx) => {\n setFormData(prev => ({\n ...prev,\n qtyPricing: prev.qtyPricing.filter((_, i) => i !== idx)\n }));\n };\n\n // 수량별 단가 수정\n const updateQtyPricing = (idx, field, value) => {\n setFormData(prev => ({\n ...prev,\n qtyPricing: prev.qtyPricing.map((qp, i) => i === idx ? { ...qp, [field]: value } : qp)\n }));\n };\n\n // 원가 계산\n const subtotal = formData.purchasePrice + formData.processingCost;\n const lossAppliedCost = Math.round(subtotal * (1 + formData.lossRate / 100));\n\n // 마진 계산\n const margin = formData.sellingPrice - lossAppliedCost;\n const calculatedMarginRate = lossAppliedCost > 0 ? ((margin / lossAppliedCost) * 100) : 0;\n\n // 마진율로 판매단가 자동 계산\n const handleMarginRateChange = (rate) => {\n const newSellingPrice = Math.round(lossAppliedCost * (1 + rate / 100));\n setFormData(prev => ({\n ...prev,\n marginRate: rate,\n sellingPrice: newSellingPrice,\n }));\n };\n\n // 판매단가로 마진율 자동 계산\n const handleSellingPriceChange = (price) => {\n const newMarginRate = lossAppliedCost > 0 ? ((price - lossAppliedCost) / lossAppliedCost * 100) : 0;\n setFormData(prev => ({\n ...prev,\n sellingPrice: price,\n marginRate: parseFloat(newMarginRate.toFixed(1)),\n }));\n };\n\n // 저장\n const handleSave = () => {\n const now = new Date().toISOString().slice(0, 10);\n const historyEntry = item.status === '활성'\n ? { date: now, action: '단가 변경', oldPrice: item.sellingPrice, newPrice: formData.sellingPrice, by: formData.author || '시스템' }\n : { date: now, action: '단가 등록', oldPrice: null, newPrice: formData.sellingPrice, by: formData.author || '시스템' };\n\n onSave({\n ...item,\n ...formData,\n status: '활성',\n history: [...(item.history || []), historyEntry],\n });\n };\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n
\n \n
\n
단가 {item.status === '활성' ? '수정' : '등록'}
\n
\n
\n
\n\n {/* 품목 정보 */}\n
\n
\n
\n
\n
\n
품목코드\n
{item.itemCode}
\n
\n
\n
품목명\n
{item.itemName}
\n
\n
\n
품목유형\n
{item.itemType}
\n
\n
\n
\n
\n
\n\n {/* 단가 정보 */}\n
\n {/* 유효기간 */}\n
\n
\n 유효기간\n
\n
\n
\n \n setFormData(prev => ({ ...prev, validFrom: e.target.value }))}\n className=\"w-full border rounded-lg px-3 py-2 text-sm\"\n />\n
\n
\n
\n
setFormData(prev => ({ ...prev, validTo: e.target.value }))}\n className=\"w-full border rounded-lg px-3 py-2 text-sm\"\n placeholder=\"무기한\"\n />\n
비워두면 무기한
\n
\n
\n
\n\n {/* 매입 정보 */}\n
\n
\n 매입 정보\n
\n\n
\n\n
\n
\n
\n
\n setFormData(prev => ({ ...prev, purchasePrice: parseInt(e.target.value) || 0 }))}\n className=\"flex-1 border rounded-lg px-3 py-2 text-sm text-right\"\n />\n 원\n
\n
\n
\n
\n
\n setFormData(prev => ({ ...prev, processingCost: parseInt(e.target.value) || 0 }))}\n className=\"flex-1 border rounded-lg px-3 py-2 text-sm text-right\"\n />\n 원\n
\n
\n
\n\n
\n
\n
\n
\n setFormData(prev => ({ ...prev, lossRate: parseFloat(e.target.value) || 0 }))}\n className=\"flex-1 border rounded-lg px-3 py-2 text-sm text-right\"\n step=\"0.1\"\n />\n %\n
\n
제조 손실율
\n
\n
\n \n \n
\n
\n\n {/* 원가 계산 */}\n
\n
\n \n 원가 계산\n
\n
\n
\n
입고가\n
{formData.purchasePrice.toLocaleString()}원
\n
\n
\n
+ 가공비\n
{formData.processingCost.toLocaleString()}원
\n
\n
\n
= 소계\n
{subtotal.toLocaleString()}원
\n
\n
\n
LOSS 적용\n
{lossAppliedCost.toLocaleString()}원
\n
\n
\n
\n
\n\n {/* 판매 정보 */}\n
\n
\n 판매 정보\n
\n\n
\n
\n
\n
\n handleMarginRateChange(parseFloat(e.target.value) || 0)}\n className=\"flex-1 border rounded-lg px-3 py-2 text-sm text-right\"\n step=\"0.1\"\n />\n %\n
\n
입력 시 판매단가 자동 계산
\n
\n
\n
\n
\n handleSellingPriceChange(parseInt(e.target.value) || 0)}\n className=\"flex-1 border rounded-lg px-3 py-2 text-sm text-right\"\n />\n 원\n
\n
입력 시 마진율 자동 계산
\n
\n
\n\n {/* 마진 계산 */}\n
\n
\n 마진\n \n {margin.toLocaleString()}원 ({calculatedMarginRate.toFixed(1)}%)\n \n
\n
\n
\n\n {/* 수량별 단가 */}\n
\n
\n
\n 수량별 단가 (물량 할인)\n
\n
\n
\n\n {formData.qtyPricing.length === 0 ? (\n
\n 수량별 단가가 없습니다. 기본 판매단가가 적용됩니다.\n
\n ) : (\n
\n )}\n
\n\n {/* 비고 */}\n
\n \n
\n
\n\n {/* 푸터 */}\n
\n \n \n
\n
\n
\n );\n};\n\n// 기본단가 상세보기 모달 (이력 포함)\nconst PriceDetailModal = ({ item, onClose, onEdit }) => {\n return (\n
\n
\n {/* 헤더 */}\n
\n
단가 상세
\n \n \n\n
\n {/* 품목 정보 */}\n
\n
\n
\n
품목코드\n
{item.itemCode}
\n
\n
\n
품목명\n
{item.itemName}
\n
\n
\n
품목유형\n
{item.itemType}
\n
\n
\n
공급업체\n
{item.supplier || '-'}
\n
\n
\n
\n\n {/* 가격 정보 */}\n
\n
\n
입고가\n
{item.purchasePrice?.toLocaleString()}원
\n
\n
\n
가공비\n
{item.processingCost?.toLocaleString()}원
\n
\n
\n
판매단가\n
{item.sellingPrice?.toLocaleString()}원
\n
\n
\n
마진율\n
{item.marginRate?.toFixed(1)}%
\n
\n
\n\n {/* 유효기간 */}\n
\n
유효기간\n
{item.validFrom} ~ {item.validTo || '무기한'}
\n
\n\n {/* 수량별 단가 */}\n {item.qtyPricing?.length > 0 && (\n
\n
수량별 단가
\n
\n \n \n | 수량 범위 | \n 적용단가 | \n 할인율 | \n
\n \n \n {item.qtyPricing.map((qp, idx) => (\n \n | {qp.minQty} ~ {qp.maxQty}개 | \n {qp.price.toLocaleString()}원 | \n \n -{((item.sellingPrice - qp.price) / item.sellingPrice * 100).toFixed(1)}%\n | \n
\n ))}\n \n
\n
\n )}\n\n {/* 변경 이력 */}\n
\n
\n 단가 변경 이력\n
\n {item.history?.length > 0 ? (\n
\n {item.history.map((h, idx) => (\n
\n
\n
\n
\n {h.action}\n {h.date}\n
\n {h.oldPrice !== null && (\n
\n {h.oldPrice?.toLocaleString()}원 → {h.newPrice?.toLocaleString()}원\n
\n )}\n {h.oldPrice === null && (\n
{h.newPrice?.toLocaleString()}원
\n )}\n
{h.by}
\n
\n
\n ))}\n
\n ) : (\n
변경 이력이 없습니다.
\n )}\n
\n
\n\n {/* 푸터 */}\n
\n \n \n
\n
\n
\n );\n};\n\n// 거래처별 단가 등록/수정 모달\nconst CustomerPriceModal = ({ item, basePrices, onClose, onSave }) => {\n const [formData, setFormData] = useState({\n id: item?.id || null,\n customerId: item?.customerId || '',\n customerName: item?.customerName || '',\n itemCode: item?.itemCode || '',\n itemName: item?.itemName || '',\n unit: item?.unit || 'EA',\n basePrice: item?.basePrice || 0,\n discountType: item?.discountType || 'rate', // rate 또는 fixed\n discountRate: item?.discountRate || 0,\n discountAmount: item?.discountAmount || 0,\n sellingPrice: item?.sellingPrice || 0,\n validFrom: item?.validFrom || new Date().toISOString().slice(0, 10),\n validTo: item?.validTo || '',\n note: item?.note || '',\n });\n\n // 품목 선택 시\n const handleItemSelect = (itemCode) => {\n const selected = basePrices.find(p => p.itemCode === itemCode);\n if (selected) {\n setFormData(prev => ({\n ...prev,\n itemCode: selected.itemCode,\n itemName: selected.itemName,\n unit: selected.unit,\n basePrice: selected.sellingPrice,\n sellingPrice: selected.sellingPrice,\n }));\n }\n };\n\n // 할인율 변경 시\n const handleDiscountRateChange = (rate) => {\n const newPrice = Math.round(formData.basePrice * (1 - rate / 100));\n setFormData(prev => ({\n ...prev,\n discountRate: rate,\n sellingPrice: newPrice,\n }));\n };\n\n // 적용단가 직접 변경 시\n const handleSellingPriceChange = (price) => {\n const newRate = formData.basePrice > 0 ? ((formData.basePrice - price) / formData.basePrice * 100) : 0;\n setFormData(prev => ({\n ...prev,\n sellingPrice: price,\n discountRate: parseFloat(newRate.toFixed(1)),\n }));\n };\n\n // 저장\n const handleSave = () => {\n onSave({\n ...formData,\n status: '활성',\n });\n };\n\n // 샘플 거래처 목록\n const customers = [\n { id: 'C001', name: '삼성전자' },\n { id: 'C002', name: 'LG전자' },\n { id: 'C003', name: '현대건설' },\n { id: 'C004', name: 'SK하이닉스' },\n { id: 'C005', name: '포스코' },\n ];\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n 거래처별 단가 {item ? '수정' : '등록'}\n
\n \n \n\n
\n {/* 거래처 선택 */}\n
\n \n \n
\n\n {/* 품목 선택 */}\n
\n \n \n
\n\n {/* 기본단가 표시 */}\n {formData.basePrice > 0 && (\n
\n
\n 기본 판매단가\n {formData.basePrice.toLocaleString()}원\n
\n
\n )}\n\n {/* 할인 설정 */}\n
\n
\n
\n
\n handleDiscountRateChange(parseFloat(e.target.value) || 0)}\n className=\"flex-1 border rounded-lg px-3 py-2 text-sm text-right\"\n step=\"0.1\"\n />\n %\n
\n
\n
\n
\n
\n handleSellingPriceChange(parseInt(e.target.value) || 0)}\n className=\"flex-1 border rounded-lg px-3 py-2 text-sm text-right\"\n />\n 원\n
\n
\n
\n\n {/* 할인 금액 표시 */}\n {formData.basePrice > 0 && formData.discountRate > 0 && (\n
\n
\n 할인 금액\n \n -{(formData.basePrice - formData.sellingPrice).toLocaleString()}원\n \n
\n
\n )}\n\n {/* 유효기간 */}\n
\n
\n \n setFormData(prev => ({ ...prev, validFrom: e.target.value }))}\n className=\"w-full border rounded-lg px-3 py-2 text-sm\"\n />\n
\n
\n
\n
setFormData(prev => ({ ...prev, validTo: e.target.value }))}\n className=\"w-full border rounded-lg px-3 py-2 text-sm\"\n />\n
비워두면 무기한
\n
\n
\n\n {/* 비고 */}\n
\n \n
\n
\n\n {/* 푸터 */}\n
\n \n \n
\n
\n
\n );\n};\n\n// 이전 패널 컴포넌트들 (하위 호환용)\nconst PriceDetailPanel = ({ price, onEdit, onClose }) => null;\nconst PriceFormPanel = ({ formData, setFormData, onSave, onCancel, isEdit }) => null;\n\n// 견적 목록\nconst QuoteList = ({ quotes = sampleQuotesData, onNavigate, onCreateOrder, onUpdateQuote, onDeleteQuotes }) => {\n const [search, setSearch] = useState('');\n const [activeTab, setActiveTab] = useState('all');\n\n // 체크박스 선택 상태\n const [selectedIds, setSelectedIds] = useState([]);\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n // 수주전환 모달 상태\n const [showConvertModal, setShowConvertModal] = useState(false);\n const [selectedQuote, setSelectedQuote] = useState(null);\n const [convertForm, setConvertForm] = useState({\n shipDate: '',\n shipDateUndecided: false,\n dueDate: '',\n dueDateUndecided: false,\n deliveryMethod: '상차',\n freightCost: '선불',\n receiverName: '',\n receiverPhone: '',\n postalCode: '',\n deliveryAddress: '',\n deliveryAddressDetail: '',\n note: '',\n });\n\n // 배송방식 옵션 (선불/착불 구분)\n const deliveryMethodOptions = [\n { id: 'pickup-prepaid', label: '상차(선불)', category: '상차' },\n { id: 'pickup-collect', label: '상차(착불)', category: '상차' },\n { id: 'direct', label: '직접배차', category: '직접' },\n { id: 'self', label: '직접수령', category: '직접' },\n { id: 'kyungdong-prepaid', label: '경동화물(선불)', category: '화물' },\n { id: 'kyungdong-collect', label: '경동화물(착불)', category: '화물' },\n { id: 'kyungdong-parcel-prepaid', label: '경동택배(선불)', category: '택배' },\n { id: 'kyungdong-parcel-collect', label: '경동택배(착불)', category: '택배' },\n { id: 'daesin-prepaid', label: '대신화물(선불)', category: '화물' },\n { id: 'daesin-collect', label: '대신화물(착불)', category: '화물' },\n { id: 'daesin-parcel-prepaid', label: '대신택배(선불)', category: '택배' },\n { id: 'daesin-parcel-collect', label: '대신택배(착불)', category: '택배' },\n ];\n\n const getStatusCount = (status) => {\n if (status === 'all') return quotes.length;\n if (status === 'editing') return quotes.filter(q => q.status.includes('수정')).length;\n return quotes.filter(q => q.status === status).length;\n };\n\n const tabs = [\n { id: 'all', label: '전체', count: getStatusCount('all') },\n { id: '최초작성', label: '최초작성', count: getStatusCount('최초작성') },\n { id: 'editing', label: '수정중', count: getStatusCount('editing') },\n { id: '최종확정', label: '최종확정', count: getStatusCount('최종확정') },\n { id: '수주전환', label: '수주전환', count: getStatusCount('수주전환') },\n ];\n\n const statusFilter = {\n all: () => true,\n '최초작성': (q) => q.status === '최초작성',\n editing: (q) => q.status.includes('수정'),\n '최종확정': (q) => q.status === '최종확정',\n '수주전환': (q) => q.status === '수주전환',\n };\n\n const filtered = quotes\n .filter(statusFilter[activeTab])\n .filter(q =>\n q.quoteNo.toLowerCase().includes(search.toLowerCase()) ||\n q.customerName.includes(search) ||\n q.siteName.includes(search) ||\n q.productName.includes(search)\n )\n .sort((a, b) => b.id - a.id); // ID 기준 내림차순 (최신 등록 최상단)\n\n // 전체 선택/해제\n const handleSelectAll = (e) => {\n if (e.target.checked) {\n setSelectedIds(filtered.map(q => q.id));\n } else {\n setSelectedIds([]);\n }\n };\n\n // 개별 선택\n const handleSelectOne = (id, e) => {\n e.stopPropagation();\n setSelectedIds(prev =>\n prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]\n );\n };\n\n // 삭제 확인\n const handleDeleteConfirm = () => {\n if (onDeleteQuotes && selectedIds.length > 0) {\n onDeleteQuotes(selectedIds);\n }\n setSelectedIds([]);\n setShowDeleteModal(false);\n };\n\n // 통계 계산\n const thisMonthQuotes = quotes.filter(q => {\n const date = new Date(q.quoteDate);\n const now = new Date();\n return date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear();\n });\n\n const thisMonthAmount = thisMonthQuotes.reduce((sum, q) => sum + q.totalAmount, 0);\n const progressAmount = quotes.filter(q => !['최종확정', '수주전환'].includes(q.status))\n .reduce((sum, q) => sum + q.totalAmount, 0);\n const thisWeekNew = quotes.filter(q => {\n const date = new Date(q.quoteDate);\n const now = new Date();\n const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);\n return date >= weekAgo;\n }).length;\n const conversionRate = quotes.length > 0\n ? ((quotes.filter(q => q.status === '수주전환').length / quotes.length) * 100).toFixed(1)\n : 0;\n\n // 수주전환 모달 열기\n const handleOpenConvertModal = (quote, e) => {\n e?.stopPropagation();\n setSelectedQuote(quote);\n // 견적 정보로 기본값 설정\n setConvertForm({\n shipDate: '',\n shipDateUndecided: false,\n dueDate: '',\n dueDateUndecided: false,\n deliveryMethod: '상차',\n freightCost: '선불',\n receiverName: '',\n receiverPhone: '010-0000-0000',\n postalCode: '',\n deliveryAddress: '',\n deliveryAddressDetail: '',\n note: '',\n });\n setShowConvertModal(true);\n };\n\n // 수주전환 실행 - 실제 수주 데이터 생성\n const handleConvertToOrder = () => {\n if (!selectedQuote) return;\n\n // 수주번호 생성 - 채번관리 규칙 적용 (제품유형에 따라 스크린/슬랫/절곡 구분)\n const productType = selectedQuote.productType || selectedQuote.productName || '';\n const newOrderNo = generateNumber('수주번호', productType, []);\n\n // 새 수주 데이터 생성\n const newOrder = {\n id: Date.now(),\n orderNo: newOrderNo,\n quoteNo: selectedQuote.quoteNo,\n orderDate: new Date().toISOString().split('T')[0],\n customerName: selectedQuote.customerName,\n siteName: selectedQuote.siteName,\n siteAddress: selectedQuote.siteAddress || convertForm.deliveryAddress,\n manager: selectedQuote.manager,\n contact: selectedQuote.contact,\n productName: selectedQuote.productName,\n qty: selectedQuote.qty,\n totalAmount: selectedQuote.finalAmount || selectedQuote.totalAmount,\n status: '수주등록',\n dueDate: convertForm.dueDateUndecided ? '미정' : convertForm.dueDate,\n shipDate: convertForm.shipDateUndecided ? '미정' : convertForm.shipDate,\n deliveryMethod: convertForm.deliveryMethod,\n freightCost: convertForm.freightCost,\n receiverName: convertForm.receiverName,\n receiverPhone: convertForm.receiverPhone,\n deliveryAddress: convertForm.deliveryAddress,\n deliveryAddressDetail: convertForm.deliveryAddressDetail,\n note: convertForm.note,\n creditGrade: selectedQuote.creditGrade,\n items: selectedQuote.items || [],\n calculatedItems: selectedQuote.calculatedItems || [],\n bomData: selectedQuote.bomData || {},\n splits: [],\n history: [{\n id: Date.now(),\n changedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n changeType: '수주등록',\n description: `견적(${selectedQuote.quoteNo})에서 수주전환`,\n changedBy: '현재 사용자',\n }],\n };\n\n // 수주 생성 콜백 호출\n if (onCreateOrder) {\n onCreateOrder(newOrder);\n }\n\n // 견적 상태 업데이트\n if (onUpdateQuote) {\n onUpdateQuote({\n ...selectedQuote,\n status: '수주전환',\n convertedOrderNo: newOrderNo,\n });\n }\n\n alert(`수주전환 완료!\\n\\n수주번호: ${newOrderNo}\\n발주처: ${selectedQuote.customerName}\\n현장: ${selectedQuote.siteName}\\n금액: ${(selectedQuote.finalAmount || selectedQuote.totalAmount).toLocaleString()}원`);\n\n setShowConvertModal(false);\n setSelectedQuote(null);\n\n // 수주 목록으로 이동\n onNavigate('orders');\n };\n\n // 신용등급 배지\n const CreditGradeBadge = ({ grade }) => {\n const colors = {\n 'A': 'bg-green-100 text-green-700',\n 'B': 'bg-yellow-100 text-yellow-700',\n 'C': 'bg-red-100 text-red-700',\n };\n const labels = { 'A': '우량', 'B': '관리', 'C': '위험' };\n return (\n
\n {grade}{labels[grade] ? `(${labels[grade]})` : ''}\n \n );\n };\n\n return (\n
\n
onNavigate('quote-create')}>\n 견적 등록\n \n }\n />\n\n {/* 리포트 카드 (4개, 한 줄) */}\n \n \n \n \n \n
\n\n {/* 검색 */}\n \n\n \n\n {/* 테이블 */}\n \n {/* 선택 삭제 버튼 (2개 이상 선택 시) */}\n {selectedIds.length >= 2 && (\n
\n \n {selectedIds.length}개 항목 선택됨\n \n \n
\n )}\n
\n\n {filtered.length === 0 && (\n
\n 검색 결과가 없습니다.\n
\n )}\n
\n\n {/* 수주전환 모달 (스크린샷 형태) */}\n {showConvertModal && selectedQuote && (\n \n
\n {/* 모달 헤더 */}\n
\n
\n
수주전환
\n
배송 정보를 입력하여 수주로 전환합니다
\n
\n
\n
\n\n {/* 모달 본문 - 배송 정보 입력 */}\n
\n {/* 출고예정일 */}\n
\n
\n
\n
\n
{selectedQuote.customerName}
\n
\n
\n\n {/* 납품요청일 */}\n
\n
\n
\n
\n
{selectedQuote.siteName}
\n
\n
\n\n {/* 배송방식 */}\n
\n \n \n
\n\n {/* 운임비용 */}\n
\n \n \n
\n\n {/* 수신(반장/업체) */}\n
\n \n setConvertForm({ ...convertForm, receiverName: e.target.value })}\n />\n
\n\n {/* 수신처 연락처 */}\n
\n \n setConvertForm({ ...convertForm, receiverPhone: e.target.value })}\n />\n
\n\n {/* 수신처 주소 */}\n
\n\n {/* 비고 */}\n
\n \n setConvertForm({ ...convertForm, note: e.target.value })}\n />\n
\n
\n\n {/* 모달 푸터 */}\n
\n \n \n
\n
\n
\n )}\n\n {/* 삭제 확인 모달 */}\n {showDeleteModal && (\n \n
\n
\n
\n
\n 선택한 {selectedIds.length}개의 견적을 삭제하시겠습니까?\n
\n
\n
⚠️ 주의
\n
삭제된 견적은 복구할 수 없습니다.
\n
\n
\n
\n \n \n
\n
\n
\n )}\n \n );\n};\n\n// ============================================================\n// 견적 산출내역서 컴포넌트 (스크린샷 기반)\n// ============================================================\nconst QuoteCalculationSheet = ({ quote }) => {\n // 품목별 세부 산출 데이터 생성 (실제로는 견적 데이터에서 가져옴)\n const generateCalculationItems = (item) => {\n const width = item?.width || 4300;\n const height = item?.height || 3000;\n const area = (width * height) / 1000000; // m²\n\n // 철재스라트 기준 산출 항목\n return [\n { id: 1, name: '검사비', qty: 1, unit: 'SET', formula: '', areaM2: '', areaMmPrice: '', unitPrice: 50000, amount: 50000 },\n { id: 2, name: '스라트', qty: 1, unit: 'SET', formula: '', areaM2: area.toFixed(2), areaMmPrice: 45000, unitPrice: Math.round(area * 45000), amount: Math.round(area * 45000) },\n { id: 3, name: '조인트바', qty: 5, unit: 'SET', formula: '300', areaM2: '', areaMmPrice: '', unitPrice: 5000, amount: 25000 },\n { id: 4, name: '모터', qty: 1, unit: 'SET', formula: '400KG', areaM2: '', areaMmPrice: '', unitPrice: 330000, amount: 330000 },\n { id: 5, name: '매립/노출 연동제어기(뒷박스 포함)', qty: 1, unit: 'SET', formula: '', areaM2: '', areaMmPrice: '', unitPrice: 140000, amount: 140000 },\n { id: 6, name: '케이스', qty: 1, unit: 'SET', formula: '650*550', areaM2: '4.54', areaMmPrice: 74412, unitPrice: 337830, amount: 337830 },\n { id: 7, name: '케이스용 연기차단재', qty: 1, unit: '식', formula: 'W80', areaM2: '4.54', areaMmPrice: 8590, unitPrice: 38999, amount: 38999 },\n { id: 8, name: '케이스 마구리', qty: 1, unit: 'SET', formula: '655*555', areaM2: '', areaMmPrice: '', unitPrice: 26704, amount: 26704 },\n { id: 9, name: '모터 받침용 앵글', qty: 4, unit: 'EA', formula: '50*50*530', areaM2: '', areaMmPrice: '', unitPrice: 4000, amount: 16000 },\n { id: 10, name: '가이드레일', qty: 1, unit: 'SET', formula: '벽면형(130*75) S', areaM2: '3.25', areaMmPrice: 74988, unitPrice: 243711, amount: 243711 },\n { id: 11, name: '레일용 연기차단재', qty: 1, unit: 'SET', formula: 'W50', areaM2: '3.25', areaMmPrice: 5080, unitPrice: 16510, amount: 16510 },\n { id: 12, name: '하장바', qty: 1, unit: 'SET', formula: 'SUS마감', areaM2: '4.30', areaMmPrice: '', unitPrice: 13330, amount: 57319 },\n { id: 13, name: '감기샤프트', qty: 1, unit: '식', formula: '4500x1EA', areaM2: '', areaMmPrice: '', unitPrice: 41000, amount: 41000 },\n { id: 14, name: '각파이프(6000)', qty: 4, unit: 'EA', formula: '6000x4EA', areaM2: '', areaMmPrice: '', unitPrice: 14000, amount: 56000 },\n ];\n };\n\n // 소요자재 내역 데이터\n const materialSpecs = {\n body: {\n finishType: '철재',\n installType: 'SUS마감',\n guideRail: '벽면형(130*75)',\n openSize: { width: 4300, height: 3000 },\n makeSize: { width: 4410, height: 3350 },\n qty: 1,\n caseSize: '650*550'\n },\n motor: {\n kg: 400,\n selected: true,\n controller: { buried: true, exposed: false, backBox: true },\n bracket: 1,\n angle: 4\n },\n cutting: {\n guideRail: [\n { size: 2438, qty: 0 },\n { size: 3000, qty: 0 },\n { size: 3500, qty: 2 },\n { size: 4000, qty: 0 },\n { size: 4300, qty: 0 },\n ],\n case: [\n { size: 1219, qty: 1 },\n { size: 2438, qty: 0 },\n { size: 3000, qty: 0 },\n { size: 3500, qty: 1 },\n { size: 4000, qty: 0 },\n { size: 4150, qty: 0 },\n ],\n caseExtra: { topCover: 4, end: 2 },\n bottomFinish: { type: 'SUS 1.2T', size3000: 2, size4000: 0 },\n smokeBlock: {\n rail: [\n { size: 2438, qty: 0 },\n { size: 3000, qty: 0 },\n { size: 3500, qty: 2 },\n { size: 4000, qty: 0 },\n { size: 4300, qty: 0 },\n ],\n case: [{ size: 3000, qty: 4 }]\n }\n },\n subMaterial: {\n shaft: { inch: 4, size: 4500, qty: 1 },\n jointBar: { size: 300, qty: 5 },\n squarePipe: { size: 6000, qty: 4 },\n angle: { size: 2500, qty: 0 }\n }\n };\n\n const items = quote?.items || [{ width: 4300, height: 3000, productName: '철재스라트' }];\n const firstItem = items[0];\n const calculationItems = generateCalculationItems(firstItem);\n const totalAmount = calculationItems.reduce((sum, item) => sum + item.amount, 0);\n\n return (\n
\n {/* 견적 산출내역서 헤더 */}\n
\n \n
\n
\n
작성일자
\n
{quote?.quoteDate || '2025-07-17'}
\n
\n
\n
(철재스라트) 견적 산출내역서
\n \n
\n
견적번호
\n
{quote?.quoteNo || 'KD-PR-250717-02'}
\n
\n
\n
\n\n {/* 업체/공급자 정보 */}\n \n
\n
업체정보
\n
\n
업체명: {quote?.customerName || '성지금속 주식회사 (귀하)'}
\n
담당자: {quote?.manager || '박성근'}
\n
제품명: 자동방화셔터 철재인정제품
\n
연락처: {quote?.contact || '02-2625-5600'}
\n
현장명: {quote?.siteName || '인천 롯데백화점'}
\n
\n
\n
\n
공급자
\n
\n
상호: (주)경동기업
\n
대표자: 이 경 호
\n
등록번호: 139-87-00333
\n
업태: 제조업
\n
사업장주소: 경기도 김포시 통진읍 용정로 45-22
\n
종목: 방화셔터, 금속창호
\n
TEL: 031-983-5130
\n
FAX: 02-6911-6315
\n
\n
\n
\n\n {/* 총 금액 */}\n \n ( ₩ \n {totalAmount.toLocaleString()}\n ) \n (VAT 별도)\n
\n\n {/* 제품 요약 */}\n \n
\n \n \n | 일련번호 | \n 종류 | \n 부호 | \n 제품명 | \n 오픈사이즈 | \n 수량 | \n 단위 | \n 단가 | \n 합계금액 | \n
\n \n | \n | \n | \n | \n 가로 | \n 세로 | \n | \n | \n | \n | \n
\n \n \n \n | 1 | \n 철재 | \n | \n KQTS01 | \n {firstItem?.width || 4300} | \n {firstItem?.height || 3000} | \n 1 | \n SET | \n {totalAmount.toLocaleString()} | \n {totalAmount.toLocaleString()} | \n
\n \n | 소계 | \n 1 | \n | \n {totalAmount.toLocaleString()} | \n
\n \n
\n
\n \n\n {/* 세부 산출내역서 */}\n
\n \n
\n \n \n | 일련번호 | \n 항목 | \n 수량 | \n 단위 | \n 산출식 | \n 면적(m²) 길이(mm) | \n 면적(m²) 길이(mm) 단가 | \n 단가 | \n 합계 | \n
\n \n \n {calculationItems.map((item, idx) => (\n \n | {idx === 0 ? '1' : ''} | \n {item.name} | \n {item.qty} | \n {item.unit} | \n {item.formula} | \n {item.areaM2} | \n {item.areaMmPrice ? item.areaMmPrice.toLocaleString() : ''} | \n {item.unitPrice.toLocaleString()} | \n {item.amount.toLocaleString()} | \n
\n ))}\n \n | 소계 | \n {totalAmount.toLocaleString()} | \n
\n \n | 전체 합계 | \n {totalAmount.toLocaleString()} | \n
\n \n
\n
\n \n\n {/* 소요자재 내역 */}\n
\n {/* 2.1 본체 (철재스라트) */}\n \n
\n 2.1\n 본체(철재스라트)\n
\n
\n
\n \n \n | 마감유형 | \n 설치유형 | \n 가로 | \n 세로 | \n 가로 | \n 세로 | \n 제작사이즈 | \n 수량 | \n 케이스 | \n
\n \n \n \n | {materialSpecs.body.finishType} | \n {materialSpecs.body.installType} | \n {materialSpecs.body.guideRail} | \n {materialSpecs.body.openSize.width} | \n {materialSpecs.body.openSize.height} | \n {materialSpecs.body.makeSize.width} | \n {materialSpecs.body.makeSize.height} | \n {materialSpecs.body.qty} | \n | \n {materialSpecs.body.caseSize} | \n
\n \n
\n
\n
\n\n {/* 2.2 모터 */}\n \n
\n 2.2\n 모터\n
\n
\n
\n \n \n | 모터종류(KG) | \n 연동제어기 | \n 브라켓트 | \n 앵글 | \n
\n \n | 150 | \n 300 | \n 400 | \n 500 | \n 600 | \n 800 | \n 1000 | \n | \n 매립 | \n 노출 | \n 뒷박스 | \n 수량 | \n 수량 | \n
\n \n \n \n | \n | \n 1 | \n | \n | \n | \n | \n | \n 1 | \n | \n 1 | \n 1 | \n 4 | \n
\n \n
\n
\n
\n\n {/* 2.3 절곡 */}\n \n
\n 2.3\n 절곡\n
\n
\n {/* (1) 가이드레일 */}\n
\n
(1) 가이드레일
(EGI 1.6T/ SUS 1.2T)
\n
\n \n \n | 사이즈 | \n 수량 | \n
\n \n \n {materialSpecs.cutting.guideRail.map((item, idx) => (\n \n | {item.size} | \n {item.qty || ''} | \n
\n ))}\n \n
\n
\n\n {/* (2) 케이스 */}\n
\n
(2) 케이스
(EGI 1.6T)
\n
\n \n \n | 사이즈 | \n 수량 | \n
\n \n \n {materialSpecs.cutting.case.map((item, idx) => (\n \n | {item.size} | \n {item.qty || ''} | \n
\n ))}\n \n | 상부덮개 | \n {materialSpecs.cutting.caseExtra.topCover} | \n
\n \n | 마구리 | \n {materialSpecs.cutting.caseExtra.end} | \n
\n \n
\n
\n\n {/* (3) 하단마감재 */}\n
\n
(3) 하단마감재
(SUS 1.2T)
\n
\n \n \n | 분류 | \n 3000 | \n 4000 | \n
\n \n \n \n 하단마감재 (SUS 1.2T) | \n {materialSpecs.cutting.bottomFinish.size3000} | \n {materialSpecs.cutting.bottomFinish.size4000 || ''} | \n
\n \n
\n
\n\n {/* (4) 연기차단재 */}\n
\n
(4) 연기차단재
\n
\n \n \n | 분류 | \n 사이즈 | \n 수량 | \n
\n \n \n {materialSpecs.cutting.smokeBlock.rail.filter(r => r.qty > 0).map((item, idx) => (\n \n | 레일용 | \n {item.size} | \n {item.qty} | \n
\n ))}\n {materialSpecs.cutting.smokeBlock.case.map((item, idx) => (\n \n | 케이스용 | \n {item.size} | \n {item.qty} | \n
\n ))}\n \n
\n
\n
\n
\n\n {/* 2.4 부자재 */}\n \n
\n 2.4\n 부자재\n
\n
\n
\n \n \n | 감기샤프트 | \n
\n \n | 4인치 | \n 5인치 | \n 6인치 | \n 8인치 | \n
\n \n | 3000 | \n 4500 | \n 6000 | \n 6000 | \n 7000 | \n 8200 | \n 3000 | \n 6000 | \n 7000 | \n 8000 | \n 8200 | \n
\n \n \n \n | \n 1 | \n | \n | \n | \n | \n | \n | \n | \n | \n | \n
\n \n
\n
\n
\n
\n
\n \n \n | 조인트바 | \n
\n \n | 300 | \n | \n
\n \n \n \n | {materialSpecs.subMaterial.jointBar.qty} | \n | \n
\n \n
\n
\n
\n
\n \n \n | 각파이프 | \n
\n \n | 3000 | \n 6000 | \n
\n \n \n \n | \n {materialSpecs.subMaterial.squarePipe.qty} | \n
\n \n
\n
\n
\n
\n \n \n | 앵글 | \n
\n \n | 2500 | \n
\n \n \n \n | {materialSpecs.subMaterial.angle.qty || ''} | \n
\n \n
\n
\n
\n
\n \n\n {/* 인쇄 버튼 */}\n
\n
\n
\n
\n
\n );\n};\n\n// ============================================================\n// 발주서 컴포넌트\n// ============================================================\nconst QuotePurchaseOrder = ({ quote }) => {\n return (\n
\n
\n \n
\n
발 주 서
\n
Purchase Order
\n
\n\n
\n
\n
발주처
\n
\n
업체명: (주)경동기업
\n
담당자: 판매부
\n
연락처: 031-983-5130
\n
FAX: 02-6911-6315
\n
\n
\n
\n
수신처
\n
\n
업체명: {quote?.customerName || '성지금속 주식회사'}
\n
담당자: {quote?.manager || '박성근'}
\n
연락처: {quote?.contact || '02-2625-5600'}
\n
\n
\n
\n\n
\n
\n
발주번호: {quote?.quoteNo?.replace('PR', 'PO') || 'KD-PO-250717-02'}
\n
발주일: {quote?.quoteDate || '2025-07-17'}
\n
납기일: {quote?.dueDate || '2025-08-15'}
\n
\n
\n\n
\n
\n \n \n | No | \n 품목명 | \n 규격 | \n 수량 | \n 단위 | \n 단가 | \n 금액 | \n
\n \n \n {(quote?.items || [{ productName: '철재스라트 셔터', width: 4300, height: 3000, qty: 1, unitPrice: 2053623, amount: 2053623 }]).map((item, idx) => (\n \n | {idx + 1} | \n {item.productName} | \n {item.width}×{item.height} | \n {item.qty} | \n SET | \n {(item.unitPrice || item.amount)?.toLocaleString()} | \n {item.amount?.toLocaleString()} | \n
\n ))}\n \n \n \n | 합계 | \n \n {(quote?.totalAmount || 2053623).toLocaleString()}원\n | \n
\n \n | VAT (10%) | \n \n {Math.round((quote?.totalAmount || 2053623) * 0.1).toLocaleString()}원\n | \n
\n \n | 총액 | \n \n {Math.round((quote?.totalAmount || 2053623) * 1.1).toLocaleString()}원\n | \n
\n \n
\n
\n\n
\n
특이사항
\n
\n - ★ 해당 견적서의 유효기간은 발행일 기준 1개월입니다.
\n - ★ 견적금액의 50%를 입금하시면 발주가 진행됩니다.
\n
\n
\n\n
\n
결제방법: 계좌이체
\n
계좌정보: 국민은행 796801-00-039630
\n
담당자: 손금주
\n
연락처: 070-4351-5275
\n
\n
\n \n\n
\n
\n
\n
\n
\n );\n};\n\n// 견적 상세\nconst QuoteDetail = ({ quote, orders = [], onNavigate, onBack, onConvertToOrder, onUpdateQuote, onCreateOrder }) => {\n // 문서 다이얼로그 상태\n const [showQuoteSheet, setShowQuoteSheet] = useState(false);\n const [showCalculationSheet, setShowCalculationSheet] = useState(false);\n const [showPurchaseOrder, setShowPurchaseOrder] = useState(false);\n\n const [showItemAddModal, setShowItemAddModal] = useState(false);\n const [showConvertModal, setShowConvertModal] = useState(false);\n const [newItem, setNewItem] = useState({\n category: '스크린',\n productName: '스크린 셔터 (표준형)',\n floor: '',\n location: '',\n width: '',\n height: '',\n qty: 1,\n });\n const [convertForm, setConvertForm] = useState({\n shipDate: '',\n shipDateUndecided: false,\n dueDate: '',\n dueDateUndecided: false,\n deliveryMethod: '상차',\n freightCost: '선불',\n receiverName: '',\n receiverPhone: '',\n deliveryAddress: '',\n deliveryAddressDetail: '',\n note: '',\n });\n\n // ★ 수주전환 완료 확인 다이얼로그 상태\n const [showConvertCompleteDialog, setShowConvertCompleteDialog] = useState(false);\n const [convertCompleteInfo, setConvertCompleteInfo] = useState({ orderNo: '', customerName: '', siteName: '', totalAmount: 0 });\n\n // 연관 수주 찾기\n const relatedOrders = orders.filter(o => o.quoteId === quote.id || o.quoteNo === quote.quoteNo);\n\n // 수주된 품목 / 미수주 품목 분리\n const orderedItems = quote.items?.filter(item => item.orderStatus === 'ordered') || [];\n const pendingItems = quote.items?.filter(item => item.orderStatus !== 'ordered') || [];\n\n const handleConvertToOrder = () => {\n if (quote.status === '수주전환' && pendingItems.length === 0) {\n alert('모든 품목이 이미 수주로 전환되었습니다.');\n return;\n }\n // 수주전환 모달 열기\n setConvertForm({\n shipDate: '',\n shipDateUndecided: false,\n dueDate: '',\n dueDateUndecided: false,\n deliveryMethod: '상차',\n freightCost: '선불',\n receiverName: '',\n receiverPhone: '',\n deliveryAddress: quote.deliveryAddress || '',\n deliveryAddressDetail: '',\n note: '',\n });\n setShowConvertModal(true);\n };\n\n // 수주전환 실행\n const handleConvertConfirm = () => {\n // 수주번호 생성 - 채번관리 규칙 적용\n const productType = quote.productType || quote.productName || '';\n const newOrderNo = generateNumber('수주번호', productType, []);\n\n // 새 수주 데이터 생성\n const newOrder = {\n id: Date.now(),\n orderNo: newOrderNo,\n quoteNo: quote.quoteNo,\n quoteId: quote.id,\n orderDate: new Date().toISOString().split('T')[0],\n customerName: quote.customerName,\n siteName: quote.siteName,\n siteAddress: quote.deliveryAddress || convertForm.deliveryAddress,\n manager: quote.manager,\n contact: quote.contact,\n productName: quote.productName,\n qty: quote.qty,\n totalAmount: quote.finalAmount || quote.totalAmount,\n status: '수주등록',\n dueDate: convertForm.dueDateUndecided ? '미정' : convertForm.dueDate,\n shipDate: convertForm.shipDateUndecided ? '미정' : convertForm.shipDate,\n deliveryMethod: convertForm.deliveryMethod,\n freightCost: convertForm.freightCost,\n receiverName: convertForm.receiverName,\n receiverPhone: convertForm.receiverPhone,\n deliveryAddress: convertForm.deliveryAddress,\n deliveryAddressDetail: convertForm.deliveryAddressDetail,\n note: convertForm.note,\n creditGrade: quote.creditGrade,\n items: quote.items || [],\n history: [{\n id: Date.now(),\n changedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n changeType: '수주등록',\n description: `견적(${quote.quoteNo})에서 수주전환`,\n changedBy: '현재 사용자',\n }],\n };\n\n // 수주 생성 콜백 호출\n if (onCreateOrder) {\n onCreateOrder(newOrder);\n }\n\n // 견적 상태 업데이트\n if (onUpdateQuote) {\n onUpdateQuote({\n ...quote,\n status: '수주전환',\n convertedOrderNo: newOrderNo,\n });\n }\n\n // ★ 확인 다이얼로그 표시 (alert 대신)\n setConvertCompleteInfo({\n orderNo: newOrderNo,\n customerName: quote.customerName,\n siteName: quote.siteName,\n totalAmount: quote.finalAmount || quote.totalAmount,\n });\n setShowConvertModal(false);\n setShowConvertCompleteDialog(true);\n };\n\n // 추가분 수주전환\n const handleConvertPendingItems = () => {\n if (pendingItems.length === 0) {\n alert('수주 대기 중인 품목이 없습니다.');\n return;\n }\n // 추가분만 선택해서 수주 생성\n onNavigate('order-create-additional', { quote, pendingItems, relatedOrders });\n };\n\n // 품목 추가\n const handleAddItem = () => {\n // 간단한 자동 계산 (실제로는 복잡한 로직)\n const unitPrice = newItem.category === '스크린'\n ? Math.round((newItem.width * newItem.height) / 1000 * 0.5)\n : 450000;\n const amount = unitPrice * newItem.qty;\n\n const addedItem = {\n id: Date.now(),\n ...newItem,\n width: parseInt(newItem.width) || 0,\n height: parseInt(newItem.height) || 0,\n unitPrice,\n amount,\n orderStatus: 'pending',\n orderId: null,\n };\n\n onUpdateQuote?.({\n ...quote,\n items: [...(quote.items || []), addedItem],\n totalAmount: (quote.totalAmount || 0) + amount,\n });\n\n setShowItemAddModal(false);\n setNewItem({\n category: '스크린',\n productName: '스크린 셔터 (표준형)',\n floor: '',\n location: '',\n width: '',\n height: '',\n qty: 1,\n });\n };\n\n return (\n
\n {/* 상단 헤더 - 타이틀 위, 버튼 아래 */}\n
\n {/* 타이틀 영역 */}\n
\n
\n \n 견적 상세\n
\n
\n {quote.quoteNo}\n \n
\n
\n {/* 버튼 영역 - 좌측: 문서버튼 / 우측: 액션버튼 */}\n
\n {/* 좌측: 문서 버튼 */}\n
\n {tabs.map((tab) => (\n \n ))}\n
\n {/* 우측: 액션 버튼 */}\n
\n
\n {/* 최초작성 상태 → 최종확정 버튼 */}\n {(quote.status === '최초작성' || quote.status?.includes('수정')) && (\n
\n )}\n {/* 최종확정 상태 → 수주전환 버튼 */}\n {quote.status === '최종확정' && relatedOrders.length === 0 && (\n
\n )}\n {/* 추가분 수주전환 */}\n {relatedOrders.length > 0 && pendingItems.length > 0 && (\n
\n )}\n
\n
\n
\n\n {/* 산출내역서 탭 */}\n {activeTab === 'calculation' && (\n
\n )}\n\n {/* 발주서 탭 */}\n {activeTab === 'order' && (\n
\n )}\n\n {/* 견적서 탭 */}\n {activeTab === 'quote' && (\n <>\n {/* 기본 정보 */}\n
\n \n
\n
\n
{quote.quoteNo}
\n
\n
\n
\n
{quote.createdBy}
\n
\n
\n
\n
{quote.customerName}
\n
\n
\n
\n
{quote.manager}
\n
\n
\n
\n
{quote.contact}
\n
\n
\n
\n
{quote.siteName}
\n
\n
\n
\n
{quote.siteCode}
\n
\n
\n
\n
\n
{quote.quoteDate}
\n
\n
\n
\n
{quote.dueDate}
\n
\n
\n {quote.note && (\n \n )}\n \n\n {/* 품목 내역 (수주 상태 포함) */}\n
\n 품목 내역\n \n \n }>\n {quote.items && quote.items.length > 0 ? (\n
\n {/* 수주 완료 품목 */}\n {orderedItems.length > 0 && (\n
\n
\n 수주 완료 ({orderedItems.length}건)\n
\n
\n {orderedItems.map((item) => (\n
\n
\n
\n
\n
{item.productName}
\n
\n
\n
\n
{item.floor}/{item.location}
\n
\n
\n
\n
{item.width}×{item.height}
\n
\n
\n
\n
\n
{item.amount?.toLocaleString()}원
\n
\n
\n
\n
\n {relatedOrders.find(o => o.id === item.orderId)?.orderNo || '-'}\n
\n
\n
\n
\n ))}\n
\n
\n )}\n\n {/* 미수주 품목 */}\n {pendingItems.length > 0 && (\n
\n
\n 수주 대기 ({pendingItems.length}건)\n
\n
\n {pendingItems.map((item) => (\n
\n
\n
\n
\n
{item.productName}
\n
\n
\n
\n
{item.floor || '-'}/{item.location || '-'}
\n
\n
\n
\n
{item.width}×{item.height}
\n
\n
\n
\n
\n
{item.amount?.toLocaleString()}원
\n
\n
\n
\n ))}\n
\n
\n )}\n\n
\n
\n 총 견적 금액\n {quote.totalAmount?.toLocaleString()}원\n
\n
\n
\n ) : (\n
\n
\n
등록된 품목이 없습니다.
\n
\n
\n )}\n \n >\n )}\n\n {/* 수주이력 탭 */}\n {activeTab === 'orderHistory' && (\n
\n {relatedOrders.length > 0 ? (\n \n {/* 수주 목록 */}\n
\n \n \n | 로트번호 | \n 유형 | \n 수주일 | \n 금액 | \n 상태 | \n | \n
\n \n \n {relatedOrders.map(order => (\n \n | \n {order.orderNo}\n | \n \n \n | \n {order.orderDate} | \n {order.totalAmount?.toLocaleString()}원 | \n | \n \n \n | \n
\n ))}\n \n \n \n | 합계 | \n \n {relatedOrders.reduce((sum, o) => sum + (o.totalAmount || 0), 0).toLocaleString()}원\n | \n | \n
\n \n
\n\n {/* 미수주 품목 안내 */}\n {pendingItems.length > 0 && (\n
\n
\n
\n
\n
미수주 품목 {pendingItems.length}건\n
({pendingItems.reduce((sum, i) => sum + (i.amount || 0), 0).toLocaleString()}원)\n
\n
\n
\n
\n )}\n
\n ) : (\n \n
\n
연관된 수주가 없습니다.
\n {quote.status === '최종확정' && (\n
\n )}\n
\n )}\n \n )}\n\n {/* 품목 추가 모달 */}\n {showItemAddModal && (\n
\n
\n
\n
품목 추가
\n
견적에 품목을 추가합니다.
\n
\n\n
\n
\n
\n \n \n
\n
\n \n setNewItem(prev => ({ ...prev, productName: e.target.value }))}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n
\n
\n\n {newItem.category !== '부자재' && (\n
\n )}\n\n
\n
\n\n
\n
\n
\n
\n
\n
\n )}\n\n {/* 수주전환 모달 */}\n {showConvertModal && (\n
\n
\n
\n
수주전환
\n \n \n\n
\n
\n\n
\n\n
\n
\n \n \n
\n
\n \n \n
\n
\n\n
\n\n
\n \n setConvertForm(prev => ({ ...prev, deliveryAddress: e.target.value }))}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 mb-2\"\n placeholder=\"기본 주소\"\n />\n setConvertForm(prev => ({ ...prev, deliveryAddressDetail: e.target.value }))}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n placeholder=\"상세 주소\"\n />\n
\n\n
\n \n
\n
\n\n
\n \n \n
\n
\n
\n )}\n\n {/* ★ 수주전환 완료 확인 다이얼로그 */}\n {showConvertCompleteDialog && (\n
\n
\n {/* 헤더 */}\n
\n
\n
\n \n
\n
\n
수주전환 완료
\n
수주가 성공적으로 생성되었습니다.
\n
\n
\n
\n\n {/* 내용 */}\n
\n
\n
\n 수주번호\n {convertCompleteInfo.orderNo}\n
\n
\n 발주처\n {convertCompleteInfo.customerName}\n
\n
\n 현장\n {convertCompleteInfo.siteName}\n
\n
\n 금액\n {convertCompleteInfo.totalAmount?.toLocaleString()}원\n
\n
\n
\n\n {/* 버튼 */}\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// 견적 등록\nconst QuoteCreate = ({ onNavigate, onBack, onSave }) => {\n const [formData, setFormData] = useState({\n quoteDate: new Date().toISOString().split('T')[0],\n createdBy: '박판매',\n customerName: '',\n siteName: '',\n manager: '',\n contact: '',\n dueDate: '',\n wingIndex: 50, // ★ 마구리 날개지수 (기본값 50)\n inspectionFee: 50000, // ★ 검사비 (기본값 50,000원)\n note: '',\n });\n\n // 유효성 검사 규칙\n const validationRules = {\n customerName: { required: true, label: '발주처', message: '발주처를 선택해주세요.' },\n siteName: { required: true, label: '현장명', message: '현장을 선택해주세요.' },\n dueDate: { required: true, label: '납기일', message: '납기일을 선택해주세요.' },\n };\n\n const { errors, touched, hasError, getFieldError, validateForm, handleBlur, clearFieldError } = useFormValidation(formData, validationRules);\n\n const [items, setItems] = useState([{\n id: Date.now(),\n floor: '',\n location: '',\n category: '',\n productName: '',\n width: '',\n height: '',\n guideType: '백면',\n motorPower: '220V',\n controller: '단독제어',\n qty: 1,\n }]);\n\n // 산출된 품목 리스트 (수식 기반)\n const [calculatedItems, setCalculatedItems] = useState([]);\n const [showCalculatedItems, setShowCalculatedItems] = useState(false);\n\n const categories = ['스크린', '슬랫', '철재'];\n const products = {\n '스크린': ['스크린 셔터 (표준형)', '스크린 셔터 (방풍형)', '스크린 셔터 (방충형)'],\n '슬랫': ['슬랫 셔터 (일반)', '슬랫 셔터 (단열)'],\n '철재': ['철재 셔터 (일반)', '철재 셔터 (강화)'],\n };\n const guideTypes = ['백면', '측면', '양측'];\n const motorPowers = ['220V', '380V'];\n const controllers = ['없음', '단독제어', '연동제어', '중앙제어'];\n\n // 샘플 발주처 데이터\n const sampleCustomers = [\n { id: 1, name: '오정건설주식회사', manager: '이담당', contact: '010-1111-2222' },\n { id: 2, name: '서울건축', manager: '김담당', contact: '010-2222-3333' },\n { id: 3, name: '인천건설', manager: '최담당', contact: '010-3333-4444' },\n ];\n\n // 단가 테이블 (품목코드별 단가)\n const priceTable = {\n 'BP-GR-001': { name: '가이드레일', basePrice: 45000, pricePerMm: 15 },\n 'BP-GB-001': { name: '하부베이스', basePrice: 25000, pricePerMm: 8 },\n 'BP-CS-001': { name: '케이스', basePrice: 80000, pricePerMm: 25 },\n 'BP-SC-001': { name: '측면덮개', basePrice: 15000, pricePerMm: 0 },\n 'BP-TC-001': { name: '상부덮개', basePrice: 20000, pricePerMm: 0 },\n 'BP-BF-001': { name: '하단마감재', basePrice: 30000, pricePerMm: 10 },\n 'BP-BB-001': { name: '하단보강빔바', basePrice: 25000, pricePerMm: 8 },\n 'BP-SH-001': { name: '샤프트', basePrice: 35000, pricePerMm: 12 },\n 'BP-MT-001': { name: '모터', basePrice: 250000, pricePerMm: 0 },\n 'BP-CT-001': { name: '제어기', basePrice: 150000, pricePerMm: 0 },\n 'BP-SR-001': { name: '스크린 원단', basePrice: 0, pricePerSqm: 85000 },\n 'BP-SB-001': { name: '연기차단재', basePrice: 20000, pricePerMm: 5 },\n };\n\n const handleChange = (field, value) => {\n setFormData(prev => ({ ...prev, [field]: value }));\n };\n\n const handleCustomerSelect = (customer) => {\n setFormData(prev => ({\n ...prev,\n customerName: customer.name,\n manager: customer.manager,\n contact: customer.contact,\n }));\n };\n\n const handleItemChange = (itemId, field, value) => {\n setItems(prev => prev.map(item =>\n item.id === itemId ? { ...item, [field]: value } : item\n ));\n // 입력 변경 시 산출 결과 초기화\n setShowCalculatedItems(false);\n };\n\n const handleAddItem = () => {\n setItems(prev => [...prev, {\n id: Date.now(),\n floor: '',\n location: '',\n category: '',\n productName: '',\n width: '',\n height: '',\n guideType: '백면',\n motorPower: '220V',\n controller: '단독제어',\n qty: 1,\n }]);\n };\n\n const handleRemoveItem = (itemId) => {\n if (items.length === 1) {\n alert('최소 1개의 견적 항목이 필요합니다.');\n return;\n }\n setItems(prev => prev.filter(item => item.id !== itemId));\n };\n\n const handleDuplicateItem = (itemId) => {\n const item = items.find(i => i.id === itemId);\n if (item) {\n setItems(prev => [...prev, { ...item, id: Date.now() }]);\n }\n };\n\n // 수식 기반 자동 견적 산출\n const calculateQuote = () => {\n const allCalculatedItems = [];\n let totalProductAmount = 0;\n\n // 각 견적 항목에 대해 수식 실행\n const calculatedItemsDetails = items.map((item, itemIdx) => {\n const W0 = parseInt(item.width) || 2000;\n const H0 = parseInt(item.height) || 2500;\n const QTY = parseInt(item.qty) || 1;\n const GT = item.guideType || '백면';\n const PC = item.category || '스크린';\n\n // 계산된 변수들\n const W1_스크린 = W0 + 140;\n const H1_스크린 = H0 + 350;\n const W1_철재 = W0 + 110;\n const H1_철재 = H0 + 350;\n const AREA = (W1_스크린 * H1_스크린) / 1000000;\n const GR_L = H0 + 100;\n const GR_BASE = W0 + 60;\n const CASE_L = W0 + 100;\n const SHAFT_L = W0 + 200;\n const SHAFT_D = W0 > 3000 ? 60 : (W0 > 2000 ? 50 : 40);\n const BF_L = W0;\n\n // 품목 산출\n const outputItems = [];\n\n // 1. 스크린 원단 (스크린 카테고리인 경우)\n if (PC === '스크린' || PC === '슬랫') {\n const screenPrice = Math.round(AREA * priceTable['BP-SR-001'].pricePerSqm);\n outputItems.push({\n id: `${item.id}-SR`,\n itemCode: 'BP-SR-001',\n itemName: '스크린 원단',\n spec: `${W1_스크린} x ${H1_스크린}mm`,\n length: null,\n area: AREA,\n qty: QTY,\n unit: '㎡',\n unitPrice: priceTable['BP-SR-001'].pricePerSqm,\n amount: screenPrice * QTY,\n process: '스크린',\n floor: item.floor,\n location: item.location,\n });\n }\n\n // 2. 가이드레일 (절곡)\n const grPrice = priceTable['BP-GR-001'].basePrice + (GR_L * priceTable['BP-GR-001'].pricePerMm / 1000);\n outputItems.push({\n id: `${item.id}-GR`,\n itemCode: 'BP-GR-001',\n itemName: '가이드레일',\n spec: `${GR_L}mm`,\n length: GR_L,\n qty: QTY * 2,\n unit: 'EA',\n unitPrice: Math.round(grPrice),\n amount: Math.round(grPrice * QTY * 2),\n process: '절곡',\n floor: item.floor,\n location: item.location,\n });\n\n // 3. 하부베이스 (절곡)\n const gbPrice = priceTable['BP-GB-001'].basePrice + (GR_BASE * priceTable['BP-GB-001'].pricePerMm / 1000);\n outputItems.push({\n id: `${item.id}-GB`,\n itemCode: 'BP-GB-001',\n itemName: '하부베이스',\n spec: `${GR_BASE}mm`,\n length: GR_BASE,\n qty: QTY * 2,\n unit: 'EA',\n unitPrice: Math.round(gbPrice),\n amount: Math.round(gbPrice * QTY * 2),\n process: '절곡',\n floor: item.floor,\n location: item.location,\n });\n\n // 4. 케이스 (절곡)\n const csPrice = priceTable['BP-CS-001'].basePrice + (CASE_L * priceTable['BP-CS-001'].pricePerMm / 1000);\n outputItems.push({\n id: `${item.id}-CS`,\n itemCode: 'BP-CS-001',\n itemName: '케이스',\n spec: `${CASE_L}mm`,\n length: CASE_L,\n qty: QTY,\n unit: 'EA',\n unitPrice: Math.round(csPrice),\n amount: Math.round(csPrice * QTY),\n process: '절곡',\n floor: item.floor,\n location: item.location,\n });\n\n // 5. 측면덮개 (절곡)\n const sideCoverQty = GT === '백면' ? 2 : 4;\n outputItems.push({\n id: `${item.id}-SC`,\n itemCode: 'BP-SC-001',\n itemName: '측면덮개',\n spec: '-',\n length: null,\n qty: sideCoverQty * QTY,\n unit: 'EA',\n unitPrice: priceTable['BP-SC-001'].basePrice,\n amount: priceTable['BP-SC-001'].basePrice * sideCoverQty * QTY,\n process: '절곡',\n floor: item.floor,\n location: item.location,\n });\n\n // 6. 상부덮개 (절곡)\n outputItems.push({\n id: `${item.id}-TC`,\n itemCode: 'BP-TC-001',\n itemName: '상부덮개',\n spec: '-',\n length: null,\n qty: QTY,\n unit: 'EA',\n unitPrice: priceTable['BP-TC-001'].basePrice,\n amount: priceTable['BP-TC-001'].basePrice * QTY,\n process: '절곡',\n floor: item.floor,\n location: item.location,\n });\n\n // 7. 하단마감재 (절곡)\n const bfPrice = priceTable['BP-BF-001'].basePrice + (BF_L * priceTable['BP-BF-001'].pricePerMm / 1000);\n outputItems.push({\n id: `${item.id}-BF`,\n itemCode: 'BP-BF-001',\n itemName: '하단마감재',\n spec: `${BF_L}mm`,\n length: BF_L,\n qty: QTY,\n unit: 'EA',\n unitPrice: Math.round(bfPrice),\n amount: Math.round(bfPrice * QTY),\n process: '절곡',\n floor: item.floor,\n location: item.location,\n });\n\n // 8. 하단보강빔바 (절곡)\n const bbPrice = priceTable['BP-BB-001'].basePrice + (BF_L * priceTable['BP-BB-001'].pricePerMm / 1000);\n outputItems.push({\n id: `${item.id}-BB`,\n itemCode: 'BP-BB-001',\n itemName: '하단보강빔바',\n spec: `${BF_L}mm`,\n length: BF_L,\n qty: QTY * 2,\n unit: 'EA',\n unitPrice: Math.round(bbPrice),\n amount: Math.round(bbPrice * QTY * 2),\n process: '절곡',\n floor: item.floor,\n location: item.location,\n });\n\n // 9. 샤프트 (절곡)\n const shPrice = priceTable['BP-SH-001'].basePrice + (SHAFT_L * priceTable['BP-SH-001'].pricePerMm / 1000);\n outputItems.push({\n id: `${item.id}-SH`,\n itemCode: 'BP-SH-001',\n itemName: '샤프트',\n spec: `Ø${SHAFT_D} x ${SHAFT_L}mm`,\n length: SHAFT_L,\n diameter: SHAFT_D,\n qty: QTY,\n unit: 'EA',\n unitPrice: Math.round(shPrice),\n amount: Math.round(shPrice * QTY),\n process: '절곡',\n floor: item.floor,\n location: item.location,\n });\n\n // 10. 연기차단재 (스크린)\n const sbPrice = priceTable['BP-SB-001'].basePrice + (GR_L * priceTable['BP-SB-001'].pricePerMm / 1000);\n outputItems.push({\n id: `${item.id}-SB`,\n itemCode: 'BP-SB-001',\n itemName: '연기차단재',\n spec: `${GR_L}mm`,\n length: GR_L,\n qty: QTY * 2,\n unit: 'EA',\n unitPrice: Math.round(sbPrice),\n amount: Math.round(sbPrice * QTY * 2),\n process: '스크린',\n floor: item.floor,\n location: item.location,\n });\n\n // 11. 모터\n outputItems.push({\n id: `${item.id}-MT`,\n itemCode: 'BP-MT-001',\n itemName: '모터',\n spec: item.motorPower,\n length: null,\n qty: QTY,\n unit: 'EA',\n unitPrice: priceTable['BP-MT-001'].basePrice,\n amount: priceTable['BP-MT-001'].basePrice * QTY,\n process: '전기',\n floor: item.floor,\n location: item.location,\n });\n\n // 12. 제어기 (있는 경우만)\n if (item.controller && item.controller !== '없음') {\n outputItems.push({\n id: `${item.id}-CT`,\n itemCode: 'BP-CT-001',\n itemName: '제어기',\n spec: item.controller,\n length: null,\n qty: QTY,\n unit: 'EA',\n unitPrice: priceTable['BP-CT-001'].basePrice,\n amount: priceTable['BP-CT-001'].basePrice * QTY,\n process: '전기',\n floor: item.floor,\n location: item.location,\n });\n }\n\n // 항목별 합계\n const itemTotal = outputItems.reduce((sum, o) => sum + o.amount, 0);\n\n // 검사비는 기본정보에서 가져옴 (전체 견적에 1회 적용)\n const inspectionAmount = itemIdx === 0 ? (formData.inspectionFee || 0) : 0;\n\n allCalculatedItems.push(...outputItems);\n totalProductAmount += itemTotal;\n\n return {\n ...item,\n calculatedVariables: { W0, H0, W1_스크린, H1_스크린, W1_철재, H1_철재, AREA, GR_L, CASE_L, SHAFT_L, SHAFT_D },\n outputItems,\n itemTotal,\n inspectionAmount,\n wingIndex: formData.wingIndex, // 기본정보에서 가져옴\n totalAmount: itemTotal + inspectionAmount,\n };\n });\n\n // items 업데이트\n setItems(calculatedItemsDetails.map(d => ({\n ...d,\n unitPrice: d.itemTotal,\n amount: d.totalAmount,\n })));\n\n setCalculatedItems(allCalculatedItems);\n setShowCalculatedItems(true);\n };\n\n const handleSubmit = () => {\n // 유효성 검사 실행\n if (!validateForm()) {\n return;\n }\n\n if (!showCalculatedItems) {\n alert('먼저 견적을 산출해주세요.');\n return;\n }\n\n // 견적번호 생성 - 채번관리 규칙 적용\n const quoteNo = generateNumber('견적번호', null, [], new Date(formData.quoteDate));\n\n const totalAmount = items.reduce((sum, item) => sum + (item.amount || 0), 0);\n\n // BOM 데이터 생성 (기존 형식 유지)\n const bomData = generateBomData(items);\n\n const newQuote = {\n id: Date.now(),\n quoteNo,\n ...formData,\n status: '최초작성',\n totalAmount,\n qty: items.reduce((sum, item) => sum + (item.qty || 0), 0),\n productName: items[0]?.productName || '-',\n productCode: items[0]?.productCode || '-',\n items,\n // 수식 기반 산출 품목 리스트 추가\n calculatedItems,\n bomData,\n };\n\n onSave?.(newQuote);\n onBack();\n };\n\n // 공정별 품목 그룹화\n const groupedByProcess = calculatedItems.reduce((acc, item) => {\n const process = item.process || '기타';\n if (!acc[process]) acc[process] = [];\n acc[process].push(item);\n return acc;\n }, {});\n\n const processColors = {\n '스크린': 'bg-green-100 text-green-700',\n '절곡': 'bg-orange-100 text-orange-700',\n '전기': 'bg-blue-100 text-blue-700',\n '기타': 'bg-gray-100 text-gray-700',\n };\n\n return (\n
\n
\n
\n
\n \n \n
\n
\n\n {/* 기본 정보 */}\n
\n \n
\n handleChange('quoteDate', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-gray-50\"\n readOnly\n />\n \n
\n \n \n
\n \n \n
\n \n \n
\n handleChange('manager', val)}\n placeholder=\"담당자명\"\n />\n \n
\n handleChange('contact', val)}\n placeholder=\"010-0000-0000\"\n />\n \n
\n {\n handleChange('dueDate', e.target.value);\n clearFieldError('dueDate');\n }}\n onBlur={() => handleBlur('dueDate')}\n className={getInputClassName(hasError('dueDate'))}\n />\n \n
\n \n handleChange('note', val)}\n placeholder=\"특이사항을 입력하세요\"\n />\n \n
\n
\n \n\n {/* 자동 견적 산출 - 입력 */}\n
\n \n {items.map((item, idx) => (\n
\n {/* 모바일/태블릿: 견적번호+버튼 상단, 필드 아래 / 데스크탑: 한 줄 */}\n\n {/* 견적번호 + 버튼 (모바일/태블릿에서만 표시) */}\n
\n
\n 견적 {idx + 1}\n \n
\n \n \n
\n
\n\n {/* 입력 필드들 - 반응형 그리드 */}\n
\n {/* 견적 번호 (데스크탑에서만 표시) */}\n
\n \n 견적 {idx + 1}\n \n
\n
\n handleItemChange(item.id, 'floor', val)}\n placeholder=\"1층\"\n />\n \n
\n handleItemChange(item.id, 'location', val)}\n placeholder=\"A\"\n />\n \n
\n \n \n
\n \n \n
\n handleItemChange(item.id, 'width', val)}\n placeholder=\"2000\"\n />\n \n
\n handleItemChange(item.id, 'height', val)}\n placeholder=\"2500\"\n />\n \n
\n \n \n
\n \n \n
\n \n \n
\n handleItemChange(item.id, 'qty', parseInt(e.target.value) || 1)}\n min=\"1\"\n className=\"w-full px-2 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm\"\n />\n \n {/* 복사/삭제 버튼 (데스크탑에서만 표시) */}\n
\n \n \n
\n
\n\n {item.amount && (\n
\n \n 산출 품목: {item.outputItems?.length || 0}개\n \n \n {item.amount.toLocaleString()}원\n \n
\n )}\n
\n ))}\n\n
\n
\n \n\n {/* 자동 견적 산출 버튼 */}\n
\n\n {/* 산출 결과 - 공정별 품목 리스트 */}\n {showCalculatedItems && calculatedItems.length > 0 && (\n
\n \n {/* 공정별 그룹 */}\n {Object.entries(groupedByProcess).map(([process, processItems]) => (\n
\n
\n
\n {process} 공정\n ({processItems.length}개 품목)\n
\n
\n {processItems.reduce((sum, i) => sum + i.amount, 0).toLocaleString()}원\n \n
\n
\n \n \n | 품목 | \n 규격 | \n 수량 | \n 단가 | \n 금액 | \n 위치 | \n
\n \n \n {processItems.map(item => (\n \n | {item.itemName} | \n {item.spec} | \n {item.qty} {item.unit} | \n {item.unitPrice.toLocaleString()} | \n {item.amount.toLocaleString()} | \n {item.floor} {item.location} | \n
\n ))}\n \n
\n
\n ))}\n\n {/* 합계 */}\n
\n
\n
총 견적금액\n
({calculatedItems.length}개 품목)
\n
\n
\n {items.reduce((sum, item) => sum + (item.amount || 0), 0).toLocaleString()}원\n \n
\n
\n \n )}\n
\n );\n};\n\n// ============ 수주 관리 ============\n\n// 수주 목록\nconst OrderList = ({ orders, shipments = [], workOrders = [], onNavigate, onCreateWorkOrders, onDeleteOrders, onCancelOrder }) => {\n const [search, setSearch] = useState('');\n const [activeTab, setActiveTab] = useState('all');\n const [showWOCreateModal, setShowWOCreateModal] = useState(false);\n const [selectedOrderForWO, setSelectedOrderForWO] = useState(null);\n const { isMobile, isTablet, isDesktop } = useResponsive();\n\n // 체크박스 선택 상태\n const [selectedIds, setSelectedIds] = useState([]);\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n // 수주 취소 모달 상태\n const [showCancelModal, setShowCancelModal] = useState(false);\n const [cancelTargetOrder, setCancelTargetOrder] = useState(null);\n const [cancelReason, setCancelReason] = useState('');\n const [cancelReasonDetail, setCancelReasonDetail] = useState('');\n\n // 전체 선택/해제\n const handleSelectAll = (e) => {\n if (e.target.checked) {\n setSelectedIds(filtered.map(o => o.id));\n } else {\n setSelectedIds([]);\n }\n };\n\n // 개별 선택\n const handleSelectOne = (id, e) => {\n e.stopPropagation();\n setSelectedIds(prev =>\n prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]\n );\n };\n\n // 삭제 확인\n const handleDeleteConfirm = () => {\n if (onDeleteOrders && selectedIds.length > 0) {\n onDeleteOrders(selectedIds);\n }\n setSelectedIds([]);\n setShowDeleteModal(false);\n };\n\n // 수주에 연결된 작업지시 찾기\n const getWorkOrdersByOrder = (order) => {\n return workOrders.filter(wo => wo.orderNo === order.orderNo);\n };\n\n // 작업지시 클릭 핸들러 (수주 -> 바로 작업지시)\n const handleWorkOrderClick = (e, order) => {\n e.stopPropagation();\n const existingWOs = getWorkOrdersByOrder(order);\n if (existingWOs.length > 0) {\n // 이미 작업지시가 있으면 작업지시 관리 페이지로 이동\n onNavigate('work-order-list');\n } else if (order.status === '수주확정') {\n // 수주확정 상태이면 작업지시 생성 모달 표시\n setSelectedOrderForWO(order);\n setShowWOCreateModal(true);\n }\n };\n\n // 작업지시 생성 확인 (공정별 자동 생성)\n const handleConfirmCreateWO = () => {\n if (selectedOrderForWO && onCreateWorkOrders) {\n onCreateWorkOrders(selectedOrderForWO);\n }\n setShowWOCreateModal(false);\n setSelectedOrderForWO(null);\n };\n\n const tabs = [\n { id: 'all', label: '전체', count: orders.length },\n { id: 'registered', label: '수주등록', count: orders.filter(o => o.status === '수주등록').length },\n { id: 'confirmed', label: '수주확정', count: orders.filter(o => o.status === '수주확정').length },\n { id: 'production-complete', label: '생산지시완료', count: orders.filter(o => o.status === '생산지시완료').length },\n { id: 'cancelled', label: '취소', count: orders.filter(o => o.status === '취소').length },\n ];\n\n const statusFilter = {\n all: () => true,\n registered: (o) => o.status === '수주등록',\n confirmed: (o) => o.status === '수주확정',\n 'production-complete': (o) => o.status === '생산지시완료',\n cancelled: (o) => o.status === '취소',\n };\n\n // 취소 가능 여부 확인 (생산지시완료 전까지만 취소 가능)\n const canCancelOrder = (order) => {\n const nonCancellableStatuses = ['생산지시완료', '출하대기', '출하완료', '취소'];\n return !nonCancellableStatuses.includes(order.status);\n };\n\n // 취소 버튼 클릭 핸들러\n const handleCancelClick = (e, order) => {\n e.stopPropagation();\n if (!canCancelOrder(order)) {\n alert('생산지시 완료 후에는 취소할 수 없습니다.');\n return;\n }\n setCancelTargetOrder(order);\n setCancelReason('');\n setCancelReasonDetail('');\n setShowCancelModal(true);\n };\n\n // 취소 확인 핸들러\n const handleCancelConfirm = () => {\n if (!cancelReason) {\n alert('취소 사유를 선택해주세요.');\n return;\n }\n if (cancelTargetOrder && onCancelOrder) {\n onCancelOrder(cancelTargetOrder.id, {\n reason: cancelReason,\n reasonDetail: cancelReasonDetail,\n cancelledAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n cancelledBy: '현재 사용자',\n });\n }\n setShowCancelModal(false);\n setCancelTargetOrder(null);\n setCancelReason('');\n setCancelReasonDetail('');\n };\n\n const filtered = orders\n .filter(statusFilter[activeTab])\n .filter(o =>\n o.orderNo.toLowerCase().includes(search.toLowerCase()) ||\n o.customerName.includes(search) ||\n o.siteName.includes(search)\n )\n .sort((a, b) => b.id - a.id); // ID 기준 내림차순 (최신 등록 최상단)\n\n // 출하진행률 계산 헬퍼 (출하 데이터에서 직접 조회)\n const getShipmentPercentByOrder = (order) => {\n if (!order.splits || order.splits.length === 0) {\n return order.status === '출하완료' ? 100 : 0;\n }\n\n const completed = order.splits.filter(split => {\n // 해당 분할의 출하 상태를 shipments에서 찾아서 확인\n const shipment = shipments.find(s => s.splitNo === split.splitNo);\n return shipment?.status === '배송완료';\n }).length;\n\n return Math.round((completed / order.splits.length) * 100);\n };\n\n // 수주관리 핵심 통계\n const thisMonthAmount = orders.reduce((sum, o) => sum + o.totalAmount, 0);\n // 생산지시 대기: 분할 완료됐는데 생산지시 안 된 건\n const productionPending = orders.filter(o =>\n o.splits?.length > 0 &&\n o.splits.some(s => !s.productionStatus || s.productionStatus === '미지시')\n ).length;\n // 출하 대기: 출하진행 0% 건수 (shipments 데이터 기준)\n const shipmentPending = orders.filter(o => {\n if (o.status === '출하완료') return false;\n if (!o.splits || o.splits.length === 0) return false;\n return getShipmentPercentByOrder(o) === 0;\n }).length;\n const needSplit = orders.filter(o =>\n (!o.splits || o.splits.length === 0) && o.status === '수주확정'\n ).length;\n\n // 분할 정보 표시 헬퍼\n const getSplitInfo = (order) => {\n if (!order.splits || order.splits.length === 0) return '-';\n return `${order.splits.length}차분할`;\n };\n\n // 출하진행 표시 헬퍼 (텍스트용)\n const getShipmentProgress = (order) => {\n if (!order.splits || order.splits.length === 0) {\n return order.status === '출하완료' ? '완료' : '-';\n }\n const completed = order.splits.filter(split => {\n const shipment = shipments.find(s => s.splitNo === split.splitNo);\n return shipment?.status === '배송완료';\n }).length;\n const total = order.splits.length;\n if (completed === 0) return '-';\n if (completed === total) return '완료';\n return `${completed}/${total}`;\n };\n\n // 생산지시 상태 표시 헬퍼\n const getProductionStatus = (order) => {\n if (!order.splits || order.splits.length === 0) {\n return order.status === '수주확정' ? '대기' : '-';\n }\n const instructed = order.splits.filter(s => s.productionStatus && s.productionStatus !== '미지시').length;\n const total = order.splits.length;\n if (instructed === 0) return '대기';\n if (instructed === total) return '완료';\n return `${instructed}/${total}`;\n };\n\n // 주문과 연결된 출하 정보 가져오기 (출고예정일, 배송방식, 출하상태)\n const getShipmentInfoByOrder = (order) => {\n // 분할이 있으면 첫 번째 분할의 출하 정보 반환\n if (order.splits && order.splits.length > 0) {\n const shipment = shipments.find(s => s.splitNo === order.splits[0].splitNo || s.lotNo === order.orderNo);\n if (shipment) {\n return {\n shipmentDate: shipment.shipmentDate,\n deliveryMethod: shipment.deliveryMethod,\n status: shipment.status\n };\n }\n }\n // 분할이 없으면 로트번호로 직접 조회\n const shipment = shipments.find(s => s.lotNo === order.orderNo);\n if (shipment) {\n return {\n shipmentDate: shipment.shipmentDate,\n deliveryMethod: shipment.deliveryMethod,\n status: shipment.status\n };\n }\n // 출하 데이터가 없으면 수주 데이터의 scheduledShipDate, deliveryMethod 사용\n return {\n shipmentDate: order.scheduledShipDate || null,\n deliveryMethod: order.deliveryMethod || null,\n status: null\n };\n };\n\n return (\n
\n {/* 헤더 */}\n
onNavigate('order-create')}>\n 수주 등록\n \n }\n />\n\n {/* 리포트 카드 (4개, 한 줄) */}\n \n \n \n \n \n
\n\n {/* 검색바 (전체 너비, 한 줄) */}\n \n\n {/* 탭 필터 - 모바일에서는 스크롤 */}\n {!isMobile && (\n \n )}\n\n {/* 데스크탑: 테이블 뷰 */}\n {isDesktop && (\n \n {/* 선택 삭제 버튼 (2개 이상 선택 시) */}\n {selectedIds.length >= 2 && (\n
\n \n {selectedIds.length}개 항목 선택됨\n \n \n
\n )}\n
\n
\n )}\n\n {/* 태블릿: 카드 그리드 (3열) */}\n {isTablet && (\n \n {filtered.map((order) => (\n onNavigate('order-detail', order)}\n />\n ))}\n
\n )}\n\n {/* 모바일: 카드 리스트 (1열) */}\n {isMobile && (\n \n {filtered.map((order) => (\n onNavigate('order-detail', order)}\n />\n ))}\n
\n )}\n\n {/* 검색 결과 없음 */}\n {filtered.length === 0 && (\n \n )}\n\n {/* 작업지시 생성 모달 */}\n {showWOCreateModal && selectedOrderForWO && (\n \n
\n
\n
\n \n 작업지시 자동 생성\n
\n \n
\n
\n
\n
수주번호
\n
{selectedOrderForWO.orderNo}
\n
발주처
\n
{selectedOrderForWO.customerName}
\n
현장명
\n
{selectedOrderForWO.siteName}
\n
납기일
\n
{selectedOrderForWO.dueDate}
\n
\n
\n
\n 이 수주에 대한 작업지시서를 공정별로 자동 생성합니다.
\n BOM 품목 기준으로 각 공정(스크린, 절곡, 샤프트, 조립, 검사)에 맞는 작업지시서가 생성됩니다.\n
\n
\n \n 생성 완료 후 작업지시 관리 페이지로 자동 이동합니다.\n
\n
\n
\n \n \n
\n
\n
\n )}\n\n {/* 삭제 확인 모달 */}\n {showDeleteModal && (\n \n
\n
\n
\n
\n 선택한 {selectedIds.length}개의 수주를 삭제하시겠습니까?\n
\n
\n
⚠️ 주의
\n
삭제된 수주는 복구할 수 없습니다. 관련된 작업지시, 출하정보도 함께 삭제될 수 있습니다.
\n
\n
\n
\n \n \n
\n
\n
\n )}\n\n {/* 수주 취소 모달 */}\n {showCancelModal && cancelTargetOrder && (\n \n
\n
\n
\n \n 수주 취소\n
\n \n
\n {/* 취소 대상 정보 */}\n
\n
\n
수주번호
\n
{cancelTargetOrder.orderNo}
\n
발주처
\n
{cancelTargetOrder.customerName}
\n
현장명
\n
{cancelTargetOrder.siteName}
\n
현재 상태
\n
\n
\n
\n\n {/* 취소 사유 선택 */}\n
\n \n \n
\n\n {/* 상세 사유 입력 */}\n
\n \n
\n\n {/* 경고 메시지 */}\n
\n
취소 시 유의사항
\n
\n - 취소된 수주는 목록에서 '취소' 상태로 표시됩니다
\n - 취소 후에는 수정이 불가능합니다
\n - 관련된 생산지시가 있는 경우 먼저 생산지시를 취소해야 합니다
\n
\n
\n
\n
\n \n \n
\n
\n
\n )}\n \n );\n};\n\n// 문서 발송 섹션 컴포넌트\nconst DocumentSendSection = ({ order }) => {\n const [showSendModal, setShowSendModal] = useState(false);\n const [selectedDocType, setSelectedDocType] = useState('');\n const [sendMethod, setSendMethod] = useState('이메일');\n const [recipient, setRecipient] = useState(order.contact || '');\n const [recipientName, setRecipientName] = useState(order.manager || '');\n\n const documentTypes = [\n { id: 'contract', name: '수주계약서', description: '로트번호, 현장명, 주문품목 상세내역(개소별 사이즈&품목별 금액)' },\n { id: 'statement', name: '거래명세서', description: '로트번호, 현장명, 개소별 금액(할인율/금액 적용)' },\n { id: 'taxInvoice', name: '세금계산서', description: '전자세금계산서 발행용' },\n ];\n\n const sendMethods = [\n { id: '이메일', icon: '📧', placeholder: 'example@company.com' },\n { id: '팩스', icon: '📠', placeholder: '02-1234-5678' },\n { id: '카카오톡', icon: '💬', placeholder: '010-1234-5678' },\n ];\n\n const handleOpenSendModal = (docType) => {\n setSelectedDocType(docType);\n setShowSendModal(true);\n };\n\n const handleSend = () => {\n alert(`${selectedDocType} 문서가 ${sendMethod}로 발송되었습니다.\\n수신: ${recipientName} (${recipient})`);\n setShowSendModal(false);\n };\n\n return (\n
\n {/* 문서 발송 버튼 */}\n
\n \n {documentTypes.map(doc => (\n
handleOpenSendModal(doc.name)}\n >\n
\n {doc.name}\n \n
\n
{doc.description}
\n
\n \n \n
\n
\n ))}\n
\n \n * PDF 형식으로 이메일 / 팩스 / 카카오톡(이미지) 발송\n
\n \n\n {/* 발송 이력 */}\n
\n {order.documentHistory?.length > 0 ? (\n \n \n \n | 발송일시 | \n 문서종류 | \n 발송방법 | \n 수신자 | \n 발송자 | \n 상태 | \n
\n \n \n {order.documentHistory.map(doc => (\n \n | {doc.sentAt} | \n {doc.docType} | \n \n \n {doc.sentMethod}\n \n | \n {doc.recipient} | \n {doc.sentBy} | \n \n \n {doc.status}\n \n | \n
\n ))}\n \n
\n ) : (\n \n )}\n \n\n {/* 발송 모달 */}\n {showSendModal && (\n
\n
\n
\n
{selectedDocType} 발송
\n \n \n\n
\n {/* 문서 정보 */}\n
\n
\n 로트번호: {order.orderNo}\n
\n
\n 현장: {order.siteName}\n
\n
\n\n {/* 발송 방법 선택 */}\n
\n
\n
\n {sendMethods.map(method => (\n
\n ))}\n
\n
\n\n {/* 수신자 정보 */}\n
\n \n setRecipientName(e.target.value)}\n className=\"w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500\"\n placeholder=\"담당자명\"\n />\n
\n\n
\n \n setRecipient(e.target.value)}\n className=\"w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500\"\n placeholder={sendMethods.find(m => m.id === sendMethod)?.placeholder}\n />\n
\n\n {/* 미리보기 버튼 */}\n
\n
\n\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// 수주 상세\nconst OrderDetail = ({ order, productionOrders = [], customers = [], onNavigate, onBack, onUpdate, onCreateWorkOrder, onCreateProductionOrder, onCancelOrder }) => {\n const [activeDocTab, setActiveDocTab] = useState('contract'); // 계약서, 거래명세서, 발주서\n const [showSplitModal, setShowSplitModal] = useState(false);\n const [showProductionModal, setShowProductionModal] = useState(false);\n const [showPOCreateModal, setShowPOCreateModal] = useState(false);\n // ★ 문서 출력 모달 상태\n const [showDocumentModal, setShowDocumentModal] = useState(false);\n const [documentType, setDocumentType] = useState('contract'); // contract, statement, purchaseOrder\n const [showProductionSheet, setShowProductionSheet] = useState(false);\n const [sheetTitle, setSheetTitle] = useState('수주서');\n // ★ 생산지시 완료 확인 다이얼로그 상태\n const [showProductionCompleteDialog, setShowProductionCompleteDialog] = useState(false);\n const [productionCompleteInfo, setProductionCompleteInfo] = useState({ workOrders: [], splitNo: null });\n const [splitForm, setSplitForm] = useState({\n splitType: '개소별',\n dueDate: '',\n selectedItems: [],\n });\n // ★ 수주 취소 모달 상태\n const [showCancelModal, setShowCancelModal] = useState(false);\n const [cancelReason, setCancelReason] = useState('');\n const [cancelReasonDetail, setCancelReasonDetail] = useState('');\n\n // order가 없으면 early return (모든 hooks 이후에 배치)\n if (!order) {\n return (\n
\n );\n }\n\n // 해당 수주의 생산지시 조회\n const relatedPO = productionOrders.find(po => po.orderNo === order.orderNo);\n\n // ★ 거래처 정보 조회 (결제조건 확인용)\n const customer = customers.find(c => c.name === order?.customerName) || {};\n const paymentTerms = customer.paymentTerms || '월말마감익월25일';\n const isPrePayment = paymentTerms === '입금후출고';\n const isPaid = order?.paymentStatus === '입금완료' || order?.accountingStatus === '입금확인';\n\n // ★ 문서 발행 가능 여부 체크\n const canIssueStatement = () => {\n // 입금후출고 조건이면 입금 확인 후에만 거래명세서 발행 가능\n if (isPrePayment && !isPaid) {\n return { allowed: false, reason: '입금 확인 후 발행 가능합니다.' };\n }\n return { allowed: true, reason: '' };\n };\n\n const canIssueTaxInvoice = () => {\n // 입금후출고 조건이면 입금 확인 후에만 세금계산서 발행 가능\n if (isPrePayment && !isPaid) {\n return { allowed: false, reason: '입금 확인 후 발행 가능합니다.' };\n }\n return { allowed: true, reason: '' };\n };\n\n // ★ 문서 탭 클릭 핸들러\n const handleDocTabClick = (tabId) => {\n setActiveDocTab(tabId);\n setDocumentType(tabId);\n setShowDocumentModal(true);\n };\n\n // 문서 탭 (계약서, 거래명세서, 발주서)\n const docTabs = [\n { id: 'contract', label: '계약서' },\n { id: 'statement', label: '거래명세서' },\n { id: 'purchaseOrder', label: '발주서' },\n ];\n\n // 분할에 포함된 아이템 정보 연결\n const getSplitItems = (split) => {\n return (order.items || []).filter(item => split.itemIds?.includes(item.id));\n };\n\n // 잔여수량 계산 (분할되지 않은 품목)\n const getUnassignedItems = () => {\n const assignedIds = (order.splits || []).flatMap(s => s.itemIds || []);\n return (order.items || []).filter(item => !assignedIds.includes(item.id));\n };\n\n // 분할 추가 핸들러\n const handleAddSplit = () => {\n if (splitForm.selectedItems.length === 0) {\n alert('분할할 품목을 선택해주세요.');\n return;\n }\n if (!splitForm.dueDate) {\n alert('출고예정일을 입력해주세요.');\n return;\n }\n\n const newSplitOrder = (order.splits?.length || 0) + 1;\n const newSplit = {\n id: Date.now(),\n splitNo: `${order.orderNo}-${String(newSplitOrder).padStart(2, '0')}`,\n splitOrder: newSplitOrder,\n splitType: splitForm.splitType,\n dueDate: splitForm.dueDate,\n itemIds: splitForm.selectedItems,\n productionStatus: '작업대기',\n shipmentStatus: '출고대기',\n productionOrderNo: null,\n totalQty: splitForm.selectedItems.length,\n completedQty: 0,\n remainingQty: splitForm.selectedItems.length,\n };\n\n onUpdate?.({\n ...order,\n splits: [...(order.splits || []), newSplit],\n });\n\n setShowSplitModal(false);\n setSplitForm({ splitType: '개소별', dueDate: '', selectedItems: [] });\n };\n\n // 품목 선택 토글\n const toggleItemSelection = (itemId) => {\n setSplitForm(prev => ({\n ...prev,\n selectedItems: prev.selectedItems.includes(itemId)\n ? prev.selectedItems.filter(id => id !== itemId)\n : [...prev.selectedItems, itemId]\n }));\n };\n\n // 분할단위 생산지시\n const handleProductionOrder = (split) => {\n // ※ C등급 경리승인 체크는 출하 시점에서 진행 (생산은 허용)\n\n // 분할에 포함된 품목 가져오기\n const splitItems = (order.items || []).filter(item => (split.itemIds || []).includes(item.id));\n const splitItemCount = splitItems.length;\n\n // 품목 카테고리 분석\n const inferCategory = (item) => {\n if (item.category) return item.category;\n const name = (item.productName || '').toLowerCase();\n if (name.includes('슬랫') || name.includes('slat')) return '슬랫';\n if (name.includes('철재') || name.includes('steel')) return '철재';\n return '스크린';\n };\n\n const categories = [...new Set(splitItems.map(item => inferCategory(item)))];\n const hasScreen = categories.includes('스크린');\n const hasSlat = categories.includes('슬랫') || categories.includes('철재');\n\n // 공정별 stepStatus 생성\n const getStepStatus = (processType) => {\n const steps = {\n '스크린': ['원단절단', '미싱', '앤드락작업', '중간검사', '포장'],\n '슬랫': ['코일절단', '중간검사', '미미작업', '포장'],\n '절곡': ['절단', '절곡', '중간검사', '포장'],\n };\n const stepStatus = {};\n (steps[processType] || []).forEach(step => {\n stepStatus[step] = { status: '대기' };\n });\n return stepStatus;\n };\n\n // ★ 공정별 기본 담당자 배정 (팀 단위 배정 지원)\n const getDefaultAssignee = (processType) => {\n const processTeams = {\n '스크린': { assignee: '스크린팀', assignees: ['김스크린', '이스크린', '박스크린'] },\n '슬랫': { assignee: '슬랫팀', assignees: ['김슬랫', '이슬랫', '박슬랫'] },\n '절곡': { assignee: '절곡팀', assignees: ['김절곡', '이절곡'] },\n '재고생산': { assignee: '김생산', assignees: ['김생산'] },\n };\n return processTeams[processType] || { assignee: '김생산', assignees: ['김생산'] };\n };\n\n const dateCode = new Date().toISOString().slice(2, 10).replace(/-/g, '');\n const workOrders = [];\n const workOrderNos = [];\n let seqNum = 1;\n\n // 1. 스크린 공정 작업지시\n if (hasScreen) {\n const screenItems = splitItems.filter(item => inferCategory(item) === '스크린');\n const screenWoNo = `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`;\n workOrderNos.push(screenWoNo);\n\n const screenAssignee = getDefaultAssignee('스크린');\n workOrders.push({\n id: Date.now(),\n workOrderNo: screenWoNo,\n orderNo: order.orderNo,\n splitNo: split.splitNo,\n orderDate: new Date().toISOString().split('T')[0],\n processType: '스크린',\n customerName: order.customerName,\n siteName: order.siteName,\n dueDate: split.dueDate || order.dueDate,\n status: '작업대기',\n priority: '일반',\n assignee: screenAssignee.assignees.join(', '), // 팀 전체 자동 배정\n assignees: screenAssignee.assignees, // 배열로도 저장\n totalQty: screenItems.reduce((sum, item) => sum + (item.qty || 1), 0),\n completedQty: 0,\n currentStep: null,\n stepStatus: getStepStatus('스크린'),\n movedToShippingArea: false,\n items: screenItems.map(item => ({ ...item, category: inferCategory(item), lotNo: null, materialLotNo: null })),\n issues: [],\n createdAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n createdBy: '현재 사용자',\n });\n }\n\n // 2. 슬랫 공정 작업지시\n if (hasSlat) {\n const slatItems = splitItems.filter(item => {\n const cat = inferCategory(item);\n return cat === '슬랫' || cat === '철재';\n });\n const slatWoNo = `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`;\n workOrderNos.push(slatWoNo);\n\n const slatAssignee = getDefaultAssignee('슬랫');\n workOrders.push({\n id: Date.now() + 1,\n workOrderNo: slatWoNo,\n orderNo: order.orderNo,\n splitNo: split.splitNo,\n orderDate: new Date().toISOString().split('T')[0],\n processType: '슬랫',\n customerName: order.customerName,\n siteName: order.siteName,\n dueDate: split.dueDate || order.dueDate,\n status: '작업대기',\n priority: '일반',\n assignee: slatAssignee.assignees.join(', '), // 팀 전체 자동 배정\n assignees: slatAssignee.assignees, // 배열로도 저장\n totalQty: slatItems.reduce((sum, item) => sum + (item.qty || 1), 0),\n completedQty: 0,\n currentStep: null,\n stepStatus: getStepStatus('슬랫'),\n movedToShippingArea: false,\n items: slatItems.map(item => ({ ...item, category: inferCategory(item), lotNo: null, materialLotNo: null })),\n issues: [],\n createdAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n createdBy: '현재 사용자',\n });\n }\n\n // 3. 절곡 공정 작업지시 - bomData 활용 (분할 품목 수 비율로 계산)\n const bendWoNo = `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`;\n workOrderNos.push(bendWoNo);\n\n // 분할 비율 계산 (전체 품목 대비 분할 품목 비율)\n const totalItemCount = (order.items || []).length;\n const splitRatio = splitItemCount / totalItemCount;\n\n // BOM 데이터에서 절곡물 품목 추출 (분할 비율 적용)\n const bomData = order.bomData || {};\n const bendingItems = [];\n\n // 가이드레일\n if (bomData.guideRails?.items) {\n bomData.guideRails.items.forEach(rail => {\n rail.lengths?.forEach(len => {\n const adjustedQty = Math.ceil(len.qty * splitRatio);\n if (adjustedQty > 0) {\n bendingItems.push({\n id: Date.now() + bendingItems.length,\n partType: '가이드레일',\n partName: `가이드레일 ${rail.type} ${rail.spec}`,\n partCode: rail.code || '',\n spec: `${len.length}mm`,\n length: len.length,\n qty: adjustedQty,\n unit: 'EA',\n material: 'EGI 1.55T',\n status: '대기',\n });\n }\n });\n });\n }\n\n // 케이스\n if (bomData.cases?.items) {\n bomData.cases.items.forEach(caseItem => {\n const adjustedQty = Math.ceil(caseItem.qty * splitRatio);\n if (adjustedQty > 0) {\n bendingItems.push({\n id: Date.now() + bendingItems.length,\n partType: '케이스',\n partName: `케이스 ${bomData.cases.mainSpec}`,\n spec: `${caseItem.length}mm`,\n length: caseItem.length,\n qty: adjustedQty,\n unit: 'EA',\n material: 'EGI 1.55T',\n status: '대기',\n });\n }\n });\n }\n\n // 하단마감재\n if (bomData.bottomFinish?.items) {\n bomData.bottomFinish.items.forEach(finish => {\n finish.lengths?.forEach(len => {\n const adjustedQty = Math.ceil(len.qty * splitRatio);\n if (adjustedQty > 0) {\n bendingItems.push({\n id: Date.now() + bendingItems.length,\n partType: '하단마감재',\n partName: `${finish.name} ${finish.spec}`,\n spec: `${len.length}mm`,\n length: len.length,\n qty: adjustedQty,\n unit: 'EA',\n material: 'EGI 1.55T',\n status: '대기',\n });\n }\n });\n });\n }\n\n const bendAssignee = getDefaultAssignee('절곡');\n workOrders.push({\n id: Date.now() + 2,\n workOrderNo: bendWoNo,\n orderNo: order.orderNo,\n splitNo: split.splitNo,\n orderDate: new Date().toISOString().split('T')[0],\n processType: '절곡',\n customerName: order.customerName,\n siteName: order.siteName,\n dueDate: split.dueDate || order.dueDate,\n status: '작업대기',\n priority: '일반',\n assignee: bendAssignee.assignees.join(', '), // 팀 전체 자동 배정\n assignees: bendAssignee.assignees, // 배열로도 저장\n totalQty: bendingItems.length,\n completedQty: 0,\n currentStep: null,\n stepStatus: getStepStatus('절곡'),\n movedToShippingArea: false,\n items: splitItems.map(item => ({ ...item, category: inferCategory(item), lotNo: null, materialLotNo: null })),\n bendingItems: bendingItems,\n bomData: bomData,\n issues: [],\n createdAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n createdBy: '현재 사용자',\n });\n\n // 모든 작업지시 생성\n workOrders.forEach(wo => {\n onCreateWorkOrder?.(wo);\n });\n\n // 분할 상태 업데이트\n const updatedSplits = (order.splits || []).map(s =>\n s.id === split.id\n ? { ...s, productionOrderNo: workOrderNos.join(', '), productionStatus: '작업지시' }\n : s\n );\n\n // 변경 이력 추가\n const newHistory = {\n id: Date.now(),\n changedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n changeType: '생산지시',\n description: `분할 ${split.splitNo} 생산지시 (${workOrders.map(wo => `${wo.processType}:${wo.workOrderNo}`).join(', ')})`,\n changedBy: '현재 사용자',\n };\n\n onUpdate?.({\n ...order,\n splits: updatedSplits,\n status: order.status === '수주확정' ? '생산중' : order.status,\n changeHistory: [...(order.changeHistory || []), newHistory],\n });\n\n // ★ 확인 다이얼로그 표시 (alert 대신)\n setProductionCompleteInfo({ workOrders, splitNo: split.splitNo, itemCount: splitItems.length });\n setShowProductionCompleteDialog(true);\n };\n\n // 전체 생산지시 (분할 없이 전체 품목 한번에) - 공정별 자동 분리\n const handleFullProductionOrder = () => {\n // ※ C등급 경리승인 체크는 출하 시점에서 진행 (생산은 허용)\n\n const dateCode = new Date().toISOString().slice(2, 10).replace(/-/g, '');\n const items = order.items || [];\n const calculatedItems = order.calculatedItems || [];\n\n // 품목명에서 카테고리 추정 함수\n const inferCategory = (item) => {\n if (item.category) return item.category;\n const name = (item.productName || '').toLowerCase();\n if (name.includes('슬랫') || name.includes('slat')) return '슬랫';\n if (name.includes('철재') || name.includes('steel')) return '철재';\n return '스크린'; // 기본값\n };\n\n // 품목 카테고리 분석\n const categories = [...new Set(items.map(item => inferCategory(item)))];\n const hasScreen = categories.includes('스크린');\n const hasSlat = categories.includes('슬랫') || categories.includes('철재');\n\n // 공정별 stepStatus 생성 함수\n const getStepStatus = (processType) => {\n const steps = {\n '스크린': ['원단절단', '미싱', '앤드락작업', '중간검사', '포장'],\n '슬랫': ['코일절단', '중간검사', '미미작업', '포장'],\n '절곡': ['절단', '절곡', '중간검사', '포장'],\n '전기': ['배선', '검사', '포장'],\n };\n const stepStatus = {};\n (steps[processType] || []).forEach(step => {\n stepStatus[step] = { status: '대기' };\n });\n return stepStatus;\n };\n\n // 생성할 작업지시 목록\n const workOrders = [];\n const workOrderNos = [];\n let seqNum = 1;\n\n // ★ calculatedItems가 있는 경우 공정별로 분류하여 작업지시 생성\n if (calculatedItems.length > 0) {\n // 공정별 품목 그룹화\n const itemsByProcess = calculatedItems.reduce((acc, item) => {\n const process = item.process || '기타';\n if (!acc[process]) acc[process] = [];\n acc[process].push(item);\n return acc;\n }, {});\n\n // 각 공정별 작업지시 생성\n Object.entries(itemsByProcess).forEach(([process, processItems]) => {\n const woNo = `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`;\n workOrderNos.push(woNo);\n\n const processAssignee = getDefaultAssignee(process);\n workOrders.push({\n id: Date.now() + workOrders.length,\n workOrderNo: woNo,\n orderNo: order.orderNo,\n splitNo: null,\n orderDate: new Date().toISOString().split('T')[0],\n processType: process,\n customerName: order.customerName,\n siteName: order.siteName,\n dueDate: order.dueDate,\n status: '작업대기',\n priority: '일반',\n assignee: processAssignee.assignees.join(', '), // 공정별 팀 자동 배정\n assignees: processAssignee.assignees, // 배열로도 저장\n totalQty: processItems.reduce((sum, item) => sum + (item.qty || 1), 0),\n completedQty: 0,\n currentStep: null,\n stepStatus: getStepStatus(process),\n movedToShippingArea: false,\n // 완제품 정보 (참조용)\n items: items.map(item => ({ ...item, category: inferCategory(item), lotNo: null, materialLotNo: null })),\n // ★ 견적에서 산출된 품목 리스트 (작업자가 실제로 만들어야 할 부품)\n calculatedItems: processItems.map(item => ({\n ...item,\n status: '대기',\n completedQty: 0,\n })),\n // 절곡 공정인 경우 bendingItems 형식으로도 제공\n bendingItems: process === '절곡' ? processItems.map(item => ({\n id: item.id,\n partType: item.itemName,\n partName: item.itemName,\n partCode: item.itemCode,\n spec: item.spec,\n length: item.length,\n qty: item.qty,\n unit: item.unit,\n material: 'EGI 1.55T',\n status: '대기',\n floor: item.floor,\n location: item.location,\n })) : undefined,\n issues: [],\n createdAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n createdBy: '현재 사용자',\n });\n });\n } else {\n // 기존 로직: calculatedItems가 없는 경우 bomData 활용\n\n // 1. 메인 공정 작업지시 (스크린 또는 슬랫)\n if (hasScreen) {\n const screenItems = items.filter(item => inferCategory(item) === '스크린');\n const screenWoNo = `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`;\n workOrderNos.push(screenWoNo);\n\n const screenAssignee2 = getDefaultAssignee('스크린');\n workOrders.push({\n id: Date.now(),\n workOrderNo: screenWoNo,\n orderNo: order.orderNo,\n splitNo: null,\n orderDate: new Date().toISOString().split('T')[0],\n processType: '스크린',\n customerName: order.customerName,\n siteName: order.siteName,\n dueDate: order.dueDate,\n status: '작업대기',\n priority: '일반',\n assignee: screenAssignee2.assignees.join(', '), // 팀 자동 배정\n assignees: screenAssignee2.assignees,\n totalQty: screenItems.reduce((sum, item) => sum + (item.qty || 1), 0),\n completedQty: 0,\n currentStep: null,\n stepStatus: getStepStatus('스크린'),\n movedToShippingArea: false,\n items: screenItems.map(item => ({ ...item, category: inferCategory(item), lotNo: null, materialLotNo: null })),\n issues: [],\n createdAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n createdBy: '현재 사용자',\n });\n }\n\n if (hasSlat) {\n const slatItems = items.filter(item => {\n const cat = inferCategory(item);\n return cat === '슬랫' || cat === '철재';\n });\n const slatWoNo = `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`;\n workOrderNos.push(slatWoNo);\n\n const slatAssignee2 = getDefaultAssignee('슬랫');\n workOrders.push({\n id: Date.now() + 1,\n workOrderNo: slatWoNo,\n orderNo: order.orderNo,\n splitNo: null,\n orderDate: new Date().toISOString().split('T')[0],\n processType: '슬랫',\n customerName: order.customerName,\n siteName: order.siteName,\n dueDate: order.dueDate,\n status: '작업대기',\n priority: '일반',\n assignee: slatAssignee2.assignees.join(', '), // 팀 자동 배정\n assignees: slatAssignee2.assignees,\n totalQty: slatItems.reduce((sum, item) => sum + (item.qty || 1), 0),\n completedQty: 0,\n currentStep: null,\n stepStatus: getStepStatus('슬랫'),\n movedToShippingArea: false,\n items: slatItems.map(item => ({ ...item, category: inferCategory(item), lotNo: null, materialLotNo: null })),\n issues: [],\n createdAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n createdBy: '현재 사용자',\n });\n }\n\n // 2. 절곡 공정 작업지시 (모든 제품에 필요) - bomData 활용\n const bendWoNo = `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`;\n workOrderNos.push(bendWoNo);\n\n // BOM 데이터에서 절곡물 품목 추출\n const bomData = order.bomData || {};\n const bendingItems = [];\n\n // 가이드레일\n if (bomData.guideRails?.items) {\n bomData.guideRails.items.forEach(rail => {\n rail.lengths?.forEach(len => {\n if (len.qty > 0) {\n bendingItems.push({\n id: Date.now() + bendingItems.length,\n partType: '가이드레일',\n partName: `가이드레일 ${rail.type} ${rail.spec}`,\n partCode: rail.code || '',\n spec: `${len.length}mm`,\n length: len.length,\n qty: len.qty,\n unit: 'EA',\n material: 'EGI 1.55T',\n status: '대기',\n });\n }\n });\n });\n // 연기차단재\n if (bomData.guideRails.smokeBarrier?.lengths) {\n bomData.guideRails.smokeBarrier.lengths.forEach(len => {\n if (len.qty > 0) {\n bendingItems.push({\n id: Date.now() + bendingItems.length,\n partType: '연기차단재',\n partName: `연기차단재 ${bomData.guideRails.smokeBarrier.spec}`,\n spec: `${len.length}mm`,\n length: len.length,\n qty: len.qty,\n unit: 'EA',\n material: bomData.guideRails.smokeBarrier.material,\n status: '대기',\n });\n }\n });\n }\n }\n\n // 케이스\n if (bomData.cases?.items) {\n bomData.cases.items.forEach(caseItem => {\n if (caseItem.qty > 0) {\n bendingItems.push({\n id: Date.now() + bendingItems.length,\n partType: '케이스',\n partName: `케이스 ${bomData.cases.mainSpec}`,\n spec: `${caseItem.length}mm`,\n length: caseItem.length,\n qty: caseItem.qty,\n unit: 'EA',\n material: 'EGI 1.55T',\n status: '대기',\n });\n }\n });\n // 측면덮개\n if (bomData.cases.sideCover?.qty > 0) {\n bendingItems.push({\n id: Date.now() + bendingItems.length,\n partType: '케이스',\n partName: `측면덮개 ${bomData.cases.sideCover.spec}`,\n spec: '',\n qty: bomData.cases.sideCover.qty,\n unit: 'EA',\n material: 'EGI 1.55T',\n status: '대기',\n });\n }\n // 상부덮개\n if (bomData.cases.topCover?.qty > 0) {\n bendingItems.push({\n id: Date.now() + bendingItems.length,\n partType: '케이스',\n partName: '상부덮개',\n spec: '',\n qty: bomData.cases.topCover.qty,\n unit: 'EA',\n material: 'EGI 1.55T',\n status: '대기',\n });\n }\n }\n\n // 하단마감재\n if (bomData.bottomFinish?.items) {\n bomData.bottomFinish.items.forEach(finish => {\n finish.lengths?.forEach(len => {\n if (len.qty > 0) {\n bendingItems.push({\n id: Date.now() + bendingItems.length,\n partType: '하단마감재',\n partName: `${finish.name} ${finish.spec}`,\n spec: `${len.length}mm`,\n length: len.length,\n qty: len.qty,\n unit: 'EA',\n material: 'EGI 1.55T',\n status: '대기',\n });\n }\n });\n });\n }\n\n const bendAssignee2 = getDefaultAssignee('절곡');\n workOrders.push({\n id: Date.now() + 2,\n workOrderNo: bendWoNo,\n orderNo: order.orderNo,\n splitNo: null,\n orderDate: new Date().toISOString().split('T')[0],\n processType: '절곡',\n customerName: order.customerName,\n siteName: order.siteName,\n dueDate: order.dueDate,\n status: '작업대기',\n priority: '일반',\n assignee: bendAssignee2.assignees.join(', '), // 팀 자동 배정\n assignees: bendAssignee2.assignees,\n totalQty: bendingItems.length,\n completedQty: 0,\n currentStep: null,\n stepStatus: getStepStatus('절곡'),\n movedToShippingArea: false,\n items: items.map(item => ({ ...item, category: inferCategory(item), lotNo: null, materialLotNo: null })),\n bendingItems: bendingItems,\n bomData: bomData,\n issues: [],\n createdAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n createdBy: '현재 사용자',\n });\n }\n\n // 변경 이력 추가\n const newHistory = {\n id: Date.now(),\n changedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n changeType: '생산지시',\n description: `전체 생산지시 완료 (작업지시: ${workOrders.map(wo => `${wo.processType}(${wo.workOrderNo})`).join(', ')})`,\n changedBy: '현재 사용자',\n };\n\n // 모든 작업지시 생성\n workOrders.forEach(wo => {\n onCreateWorkOrder?.(wo);\n });\n\n // 수주 상태 업데이트\n onUpdate?.({\n ...order,\n status: '생산중',\n workOrderNo: workOrderNos.join(', '),\n productionOrderedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n productionOrderedBy: '현재 사용자',\n changeHistory: [...(order.changeHistory || []), newHistory],\n });\n\n setShowProductionModal(false);\n\n // ★ 확인 다이얼로그 표시 (alert 대신)\n setProductionCompleteInfo({ workOrders, splitNo: null, itemCount: (order.items || []).length });\n setShowProductionCompleteDialog(true);\n };\n\n // 금액 계산\n const subtotal = order.items?.reduce((sum, item) => sum + (item.amount || 0), 0) || order.totalAmount || 0;\n const discountRate = order.discountRate || 0;\n const discountAmount = Math.round(subtotal * discountRate / 100);\n const finalTotal = subtotal - discountAmount;\n\n return (\n
\n {/* 상단 헤더 - 타이틀 아래에 버튼 영역 */}\n
\n {/* 타이틀 */}\n
\n \n 수주 상세\n
\n {/* 버튼 영역 - 좌측: 문서버튼 / 우측: 액션버튼 */}\n
\n {/* 좌측: 문서 버튼 */}\n
\n {docTabs.map((tab) => (\n \n ))}\n
\n {/* 우측: 액션 버튼 */}\n
\n \n {order.status !== '출하완료' && order.status !== '취소' && (\n \n )}\n {/* 생산지시 생성 버튼 (생산지시가 없는 경우) - 출하완료/취소/완료 제외, 생산지시 없으면 표시 */}\n {!relatedPO && !order.workOrderNo && !['출하완료', '취소', '완료'].includes(order.status) && (\n \n )}\n {/* 생산지시 보기 버튼 (생산지시가 있는 경우) */}\n {relatedPO && (\n \n )}\n {/* 분할 생산지시 버튼 (생산지시 없고 분할도 없는 경우) */}\n {!relatedPO && !order.workOrderNo && !['출하완료', '취소', '완료'].includes(order.status) && (order.splits?.length || 0) === 0 && (\n \n )}\n {/* 취소 버튼 - 취소 가능한 상태에서만 표시 (생산지시완료 전까지) */}\n {!['생산지시완료', '출하대기', '출하완료', '취소'].includes(order.status) && (\n \n )}\n
\n
\n
\n\n {/* 기본 정보 섹션 */}\n
\n
\n
기본 정보
\n \n
\n
\n
\n 발주처\n {order.customerName}\n
\n
\n 현장명\n {order.siteName}\n
\n
\n 담당자\n {order.manager || '-'}\n
\n
\n 연락처\n {order.contact || '-'}\n
\n
\n
\n
\n\n {/* 수주/배송 정보 섹션 */}\n
\n
\n
수주/배송 정보
\n \n
\n
\n
\n 수주일자\n {order.orderDate || order.createdAt?.split(' ')[0] || '-'}\n
\n
\n 출고예정일\n {order.shippingDate || order.dueDate || '-'}\n
\n
\n 납품요청일\n {order.deliveryRequestDate || order.dueDate || '-'}\n
\n
\n 배송방식\n {order.deliveryMethod || '-'}\n
\n
\n 운임비용\n {order.freightCost || '-'}\n
\n
\n 수신(반장/업체)\n {order.receiverName || '-'}\n
\n
\n 수신처 연락처\n {order.receiverPhone || '-'}\n
\n
\n 수신처 주소\n \n {order.deliveryAddress || (order.deliveryZipcode ? `(${order.deliveryZipcode}) ${order.deliveryAddressBase || ''} ${order.deliveryAddressDetail || ''}` : '-')}\n \n
\n
\n 비고\n {order.deliveryNote || order.note || '-'}\n
\n
\n
\n
\n\n {/* 제품 내역 섹션 */}\n
\n
\n
제품 내역
\n \n
\n
\n \n \n | 순번 | \n 품목코드 | \n 품목명 | \n 층 | \n 부호 | \n 규격 | \n 수량 | \n 단위 | \n 단가 | \n 금액 | \n
\n \n \n {(order.items || []).map((item, idx) => (\n \n | {idx + 1} | \n {item.productCode} | \n {item.productName} | \n {item.floor || '-'} | \n {item.location || '-'} | \n {item.spec} | \n {item.qty} | \n {item.unit} | \n {(item.unitPrice || 0).toLocaleString()} | \n {(item.amount || 0).toLocaleString()} | \n
\n ))}\n \n
\n
\n\n {/* 합계 영역 */}\n
\n
\n
\n
\n 소계\n {subtotal.toLocaleString()}원\n
\n
\n 할인율\n {discountRate}%\n
\n
\n 총금액\n {finalTotal.toLocaleString()}원\n
\n
\n
\n
\n
\n\n\n {/* 분할 추가 모달 */}\n {showSplitModal && (\n
\n
\n
\n
분할 추가
\n
{(order.splits || []).length + 1}차 분할을 생성합니다.
\n
\n\n
\n {/* 분할 유형 선택 */}\n
\n
\n
\n {['개소별', '층별', '자재별'].map(type => (\n \n ))}\n
\n
\n\n {/* 출고예정일 */}\n
\n \n setSplitForm(prev => ({ ...prev, dueDate: e.target.value }))}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n
\n\n {/* 품목 선택 */}\n
\n
\n
\n
\n {getUnassignedItems().length === 0 && (\n
\n )}\n
\n
\n\n {/* 분할 정보 요약 */}\n {splitForm.selectedItems.length > 0 && (\n
\n
분할 정보 요약
\n
\n
\n
분할번호\n
{order.orderNo}-{String((order.splits || []).length + 1).padStart(2, '0')}
\n
\n
\n
분할 유형\n
{splitForm.splitType}
\n
\n
\n
선택 품목\n
{splitForm.selectedItems.length}건
\n
\n
\n
\n )}\n
\n\n
\n
\n
\n
\n
\n
\n )}\n\n {/* 수주서/생산지시서 출력 모달 */}\n {showProductionSheet && (\n
setShowProductionSheet(false)}\n />\n )}\n\n {/* 생산지시 상세 모달 (탭 기반) */}\n {showProductionModal && (\n setShowProductionModal(false)}\n onConfirm={handleFullProductionOrder}\n onPreview={() => {\n setShowProductionModal(false);\n setSheetTitle('생산지시서');\n setShowProductionSheet(true);\n }}\n />\n )}\n\n {/* 새로운 생산지시 생성 모달 */}\n {showPOCreateModal && (\n \n
\n
\n
\n \n 생산지시 생성\n
\n
수주를 생산지시로 전환합니다
\n
\n
\n
\n
\n
\n 수주번호\n {order.orderNo}\n
\n
\n 현장명\n {order.siteName}\n
\n
\n 수량\n {order.qty || order.items?.length || 0}개\n
\n
\n 납기\n {order.dueDate}\n
\n
\n
\n\n
\n
\n \n BOM 기반 공정 분류\n
\n
\n 수주 품목이 아래 공정으로 자동 분류됩니다:\n
\n
\n
\n 1\n 스크린 공정\n
\n
\n 2\n 절곡 공정\n
\n
\n 3\n 샤프트 공정\n
\n
\n 4\n 조립 공정\n
\n
\n
\n\n
\n 생산지시 생성 후, 생산관리에서 작업지시서를 발행하여 생산팀에 전달할 수 있습니다.\n
\n
\n
\n \n \n
\n
\n
\n )}\n\n {/* ★ 문서 출력 모달 (계약서/거래명세서/발주서) */}\n {showDocumentModal && (\n \n
\n {/* 모달 헤더 */}\n
\n
\n {documentType === 'contract' && '계약서'}\n {documentType === 'statement' && '거래명세서'}\n {documentType === 'purchaseOrder' && '발주서'}\n
\n \n \n\n {/* 액션 버튼 */}\n
\n
\n
\n {(documentType === 'contract' || documentType === 'purchaseOrder') && (\n <>\n
\n
\n >\n )}\n
\n
\n
\n
\n\n {/* 결제조건 안내 (입금후출고 조건일 때) */}\n {isPrePayment && documentType === 'statement' && !isPaid && (\n
\n
\n
\n
결제조건: {paymentTerms}\n
\n
\n 이 거래처는 입금 확인 후 거래명세서를 발행해야 합니다. 현재 입금 상태를 확인해주세요.\n
\n
\n )}\n\n {/* 문서 미리보기 영역 */}\n
\n
\n {/* ========== 계약서 ========== */}\n {documentType === 'contract' && (\n
\n {/* 제목 */}\n
\n
계 약 서
\n
\n 수주번호: {order.orderNo} | 계약일자: {order.orderDate || order.createdAt?.split(' ')[0]}\n
\n
\n\n {/* 제품명 */}\n
\n
제품명
\n
{order.items?.[0]?.productName || '방화문 세트'}
\n
\n\n {/* 수주물목 (개소별 사이즈) */}\n
\n
수주물목 (개소별 사이즈)
\n
\n \n \n | 품목코드 | \n 품명 | \n 규격 | \n 수량 | \n 단위 | \n
\n \n \n {(order.items || []).slice(0, 5).map((item, idx) => (\n \n | {item.productCode} | \n {item.productName} | \n {item.spec} | \n {item.qty} | \n {item.unit} | \n
\n ))}\n \n
\n
\n\n {/* 발주처 정보 / 당사 정보 */}\n
\n
\n
발주처정보
\n
\n
업체명{order.customerName}
\n
현장명{order.siteName}
\n
담당자{order.manager || '-'}
\n
연락처{order.contact || '-'}
\n
\n
\n
\n
당사정보
\n
\n
업체명한국방화문(주)
\n
대표자홍길동
\n
사업자번호123-45-67890
\n
연락처02-1234-5678
\n
주소서울특별시 강남구 테헤란로 123
\n
\n
\n
\n\n {/* 총 계약 금액 */}\n
\n
총 계약 금액
\n
₩ {finalTotal.toLocaleString()}
\n
(부가세 포함)
\n
\n\n {/* 금액 상세 */}\n
\n \n \n | 공급가액 | \n {subtotal.toLocaleString()}원 | \n 할인율 | \n {discountRate}% | \n
\n \n | 할인액 | \n -{Math.round(subtotal * discountRate / 100).toLocaleString()}원 | \n 할인 후 공급가액 | \n {Math.round(subtotal * (1 - discountRate / 100)).toLocaleString()}원 | \n
\n \n | 부가세(10%) | \n {Math.round(subtotal * (1 - discountRate / 100) * 0.1).toLocaleString()}원 | \n 합계 | \n {finalTotal.toLocaleString()}원 | \n
\n \n
\n\n {/* 특이사항 */}\n
\n
특이사항
\n
{order.note || '-'}
\n
\n
\n )}\n\n {/* ========== 거래명세서 ========== */}\n {documentType === 'statement' && (\n
\n {/* 제목 */}\n
\n
거 래 명 세 서
\n
\n 수주번호: {order.orderNo} | 발행일: {new Date().toISOString().split('T')[0]}\n
\n
\n\n {/* 공급자 / 공급받는자 */}\n
\n
\n
공급자
\n
\n
상호한국방화문(주)
\n
대표자홍길동
\n
사업자번호123-45-67890
\n
주소서울 강남구 테헤란로 123
\n
\n
\n
\n
공급받는자
\n
\n
상호{order.customerName}
\n
담당자{order.manager || '-'}
\n
연락처{order.contact || '-'}
\n
현장명{order.siteName}
\n
\n
\n
\n\n {/* 품목 내역 */}\n
\n
품목내역
\n
\n \n \n | 순번 | \n 품목코드 | \n 품명 | \n 규격 | \n 수량 | \n 단위 | \n 단가 | \n 공급가액 | \n
\n \n \n {(order.items || []).map((item, idx) => (\n \n | {idx + 1} | \n {item.productCode} | \n {item.productName} | \n {item.spec} | \n {item.qty} | \n {item.unit} | \n {(item.unitPrice || 0).toLocaleString()} | \n {(item.amount || 0).toLocaleString()} | \n
\n ))}\n \n
\n
\n\n {/* 금액 상세 */}\n
\n
공급가액{subtotal.toLocaleString()}원
\n
할인율{discountRate}%
\n
할인액-{Math.round(subtotal * discountRate / 100).toLocaleString()}원
\n
할인 후 공급가액{Math.round(subtotal * (1 - discountRate / 100)).toLocaleString()}원
\n
부가세 (10%){Math.round(subtotal * (1 - discountRate / 100) * 0.1).toLocaleString()}원
\n
\n 합계 금액\n ₩ {finalTotal.toLocaleString()}\n
\n
\n\n {/* 증명 문구 */}\n
\n
위 금액을 거래하였음을 증명합니다.
\n
{new Date().toISOString().split('T')[0]}
\n
\n
\n
\n )}\n\n {/* ========== 발주서 ========== */}\n {documentType === 'purchaseOrder' && (\n
\n {/* 헤더 */}\n
\n
발 주 서
\n
\n
\n \n \n | 로트번호 | \n {order.orderNo} | \n
\n \n | 결재 | \n \n \n 작성\n 검토\n 승인\n \n | \n
\n \n | \n \n 전진\n 회계\n 생산\n \n | \n
\n \n
\n
\n
\n\n {/* 신청업체 / 신청내용 */}\n
\n
\n \n \n | 신청업체 | \n 발주처 | \n {order.customerName} | \n 발주일 | \n {order.orderDate || order.createdAt?.split(' ')[0]} | \n
\n \n | 담당자 | \n {order.manager || '-'} | \n 연락처 | \n {order.contact || '-'} | \n
\n \n | FAX | \n - | \n 설치개소(총) | \n {order.items?.length || 0}개소 | \n
\n \n
\n\n
\n \n \n | 신청내용 | \n 현장명 | \n {order.siteName} | \n
\n \n | 납기요청일 | \n {order.dueDate} | \n
\n \n | 출고일 | \n {order.shippingDate || '-'} | \n 배송방법 | \n {order.deliveryMethod || '상차'} | \n
\n \n | 납품주소 | \n {order.deliveryAddress || '-'} | \n
\n \n
\n
\n\n {/* 부자재 목록 */}\n
\n
■ 부자재
\n
\n \n \n | 구분 | \n 품명 | \n 규격 | \n 길이(mm) | \n 수량 | \n 비고 | \n
\n \n \n {(order.items || []).slice(0, 5).map((item, idx) => (\n \n | {idx + 1} | \n {item.productName} | \n {item.floor || '-'} | \n {item.spec?.split('×')[0] || '-'} | \n {item.qty} | \n - | \n
\n ))}\n \n
\n
\n\n {/* 특이사항 / 유의사항 */}\n
\n
\n
【특이사항】
\n
{order.note || '-'}
\n
\n
\n
【 유의사항 】
\n
\n - • 발주서 승인 완료 후 작업을 진행해주시기 바랍니다.
\n - • 납기 엄수 부탁드리며, 품질 기준에 맞춰 납품해주시기 바랍니다.
\n - • 기타 문의사항은 담당자에게 연락 부탁드립니다.
\n
\n
문의: 홍길동 | 010-1234-5678
\n
\n
\n
\n )}\n
\n
\n
\n
\n )}\n\n {/* ★ 생산지시 완료 확인 다이얼로그 */}\n {showProductionCompleteDialog && (\n \n
\n {/* 헤더 */}\n
\n
\n
\n \n
\n
\n
생산지시 완료
\n
작업지시가 성공적으로 생성되었습니다.
\n
\n
\n
\n\n {/* 내용 */}\n
\n {productionCompleteInfo.splitNo && (\n
\n 분할번호: {productionCompleteInfo.splitNo}\n
\n )}\n\n
\n
\n 생성된 작업지시 ({productionCompleteInfo.workOrders?.length || 0}건)\n
\n
\n {productionCompleteInfo.workOrders?.map((wo, idx) => (\n
\n \n {wo.processType}:\n {wo.workOrderNo}\n
\n ))}\n
\n
\n\n {productionCompleteInfo.itemCount && (\n
\n 품목 수: {productionCompleteInfo.itemCount}건\n
\n )}\n
\n\n {/* 버튼 */}\n
\n \n \n
\n
\n
\n )}\n\n {/* 수주 취소 모달 */}\n {showCancelModal && (\n \n
\n
\n
\n \n 수주 취소\n
\n \n
\n {/* 취소 대상 정보 */}\n
\n
\n
수주번호
\n
{order.orderNo}
\n
발주처
\n
{order.customerName}
\n
현장명
\n
{order.siteName}
\n
현재 상태
\n
\n
\n
\n\n {/* 취소 사유 선택 */}\n
\n \n \n
\n\n {/* 상세 사유 입력 */}\n
\n \n
\n\n {/* 경고 메시지 */}\n
\n
취소 시 유의사항
\n
\n - 취소된 수주는 목록에서 '취소' 상태로 표시됩니다
\n - 취소 후에는 수정이 불가능합니다
\n - 관련된 생산지시가 있는 경우 먼저 생산지시를 취소해야 합니다
\n
\n
\n
\n
\n \n \n
\n
\n
\n )}\n \n );\n};\n\n// 견적 선택 모달\nconst QuoteSelectModal = ({ isOpen, onClose, onSelect, quotes }) => {\n const [search, setSearch] = useState('');\n const [page, setPage] = useState(1);\n const itemsPerPage = 10;\n\n // 샘플 견적 데이터 (실제로는 props로 받음)\n const sampleQuotes = [\n {\n id: 1, quoteNo: 'KD-PR-250110-01', customerName: '서울건축', siteName: '강남 오피스텔 A동', totalAmount: 52000000, status: '최종확정', dueDate: '2025-02-28', deliveryAddress: '서울시 강남구 테헤란로 123', manager: '이담당', contact: '010-1234-5678', items: [\n { id: 1, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'A-01', spec: '7660×2550', qty: 1, unit: 'EA', unitPrice: 9000000, amount: 9000000 },\n { id: 2, productCode: 'SCR-001', productName: '스크린 셔터 (표준형)', floor: '1층', location: 'A-02', spec: '6500×2400', qty: 1, unit: 'EA', unitPrice: 8500000, amount: 8500000 },\n ]\n },\n { id: 2, quoteNo: 'KD-PR-250115-01', customerName: '인천건설', siteName: '송도 오피스텔 B동', totalAmount: 28000000, status: '최종확정', dueDate: '2025-03-15', deliveryAddress: '인천시 연수구 송도동 45', manager: '최담당', contact: '010-2345-6789', items: [] },\n { id: 3, quoteNo: 'KD-PR-250118-01', customerName: '(주)서울인테리어', siteName: '해운대 주상복합', totalAmount: 75000000, status: '최종확정', dueDate: '2025-04-01', deliveryAddress: '부산시 해운대구 우동 123', manager: '박담당', contact: '010-3456-7890', items: [] },\n { id: 4, quoteNo: 'KD-PR-250120-01', customerName: '대전건설', siteName: '유성 오피스텔', totalAmount: 32000000, status: '견적발송', dueDate: '2025-04-15', deliveryAddress: '대전시 유성구 봉명동 56', manager: '김담당', contact: '010-4567-8901', items: [] },\n { id: 5, quoteNo: 'KD-PR-250122-01', customerName: '광주건설', siteName: '상무 주상복합', totalAmount: 45000000, status: '최종확정', dueDate: '2025-05-01', deliveryAddress: '광주시 서구 치평동 78', manager: '정담당', contact: '010-5678-9012', items: [] },\n { id: 6, quoteNo: 'KD-PR-250125-01', customerName: '울산건설', siteName: '삼산 오피스텔', totalAmount: 38000000, status: '최종확정', dueDate: '2025-05-15', deliveryAddress: '울산시 남구 삼산동 90', manager: '조담당', contact: '010-6789-0123', items: [] },\n { id: 7, quoteNo: 'KD-PR-250128-01', customerName: '제주건설', siteName: '연동 주상복합', totalAmount: 55000000, status: '최종확정', dueDate: '2025-06-01', deliveryAddress: '제주시 연동 123', manager: '한담당', contact: '010-7890-1234', items: [] },\n { id: 8, quoteNo: 'KD-PR-250130-01', customerName: '세종건설', siteName: '세종 아파트', totalAmount: 62000000, status: '최종확정', dueDate: '2025-06-15', deliveryAddress: '세종시 보람동 123', manager: '강담당', contact: '010-8901-2345', items: [] },\n ];\n\n const allQuotes = quotes?.length > 0 ? quotes : sampleQuotes;\n\n // 최종확정 상태만 필터링\n const filteredQuotes = allQuotes\n .filter(q => q.status === '최종확정')\n .filter(q =>\n search === '' ||\n q.quoteNo.toLowerCase().includes(search.toLowerCase()) ||\n q.customerName.includes(search) ||\n q.siteName.includes(search)\n )\n .sort((a, b) => new Date(b.quoteDate) - new Date(a.quoteDate)); // 최신순 정렬\n\n // 페이지네이션\n const totalPages = Math.ceil(filteredQuotes.length / itemsPerPage);\n const paginatedQuotes = filteredQuotes.slice(\n (page - 1) * itemsPerPage,\n page * itemsPerPage\n );\n\n const handleSelect = (quote) => {\n onSelect(quote);\n onClose();\n };\n\n if (!isOpen) return null;\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n
\n
\n
견적 선택
\n
확정된 견적 중 선택하세요
\n
\n
\n
\n
\n\n {/* 검색 */}\n
\n
{ setSearch(val); setPage(1); }}\n />\n \n 확정된 견적 {filteredQuotes.length}건\n
\n \n\n {/* 목록 */}\n
\n {paginatedQuotes.length === 0 ? (\n
\n
\n
확정된 견적이 없습니다
\n
견적관리에서 견적을 확정해주세요.
\n
\n ) : (\n
\n \n \n | 견적번호 | \n 발주처 | \n 현장명 | \n 담당자 | \n 금액 | \n 납기일 | \n 선택 | \n
\n \n \n {paginatedQuotes.map(quote => (\n \n | \n {quote.quoteNo}\n | \n {quote.customerName} | \n {quote.siteName} | \n {quote.manager} | \n \n {(quote.totalAmount / 10000).toLocaleString()}만원\n | \n {quote.dueDate} | \n \n \n | \n
\n ))}\n \n
\n )}\n
\n\n {/* 페이지네이션 */}\n {totalPages > 1 && (\n
\n \n \n {page} / {totalPages}\n \n \n
\n )}\n
\n
\n );\n};\n\n// 수주 등록\nconst OrderCreate = ({ quotes = [], fromQuote = null, additionalItems = null, relatedOrders = [], isAdditional = false, onNavigate, onBack, onSave }) => {\n const [selectedQuote, setSelectedQuote] = useState(fromQuote);\n const [showQuotePanel, setShowQuotePanel] = useState(false);\n const [quoteSearch, setQuoteSearch] = useState('');\n\n // 직접 입력용 품목 상태\n const [directItems, setDirectItems] = useState([]);\n\n // fromQuote가 있을 때 초기 품목 자동 설정\n useEffect(() => {\n if (fromQuote?.items?.length > 0 && directItems.length === 0) {\n setDirectItems(fromQuote.items.map(item => ({\n id: item.id,\n productCode: `PRD-${item.id}`,\n productName: item.productName,\n category: item.category || '스크린', // 카테고리 유지 (생산지시 공정 분리에 필요)\n floor: item.floor || '',\n location: item.location || '',\n spec: item.width && item.height ? `${item.width}×${item.height}` : (item.spec || ''),\n width: item.width || 0,\n height: item.height || 0,\n qty: item.qty || 1,\n unit: 'EA',\n unitPrice: item.unitPrice || 0,\n amount: item.amount || 0,\n })));\n }\n }, [fromQuote]);\n\n // 추가분인 경우 초기 데이터 설정\n const initialDueDate = fromQuote?.dueDate || '';\n const initialAmount = isAdditional && additionalItems\n ? additionalItems.reduce((sum, item) => sum + (item.amount || 0), 0)\n : fromQuote?.totalAmount || 0;\n\n const [formData, setFormData] = useState({\n orderDate: new Date().toISOString().split('T')[0],\n dueDate: initialDueDate,\n deliveryMethod: '직접배차',\n receiverName: fromQuote?.manager || '',\n receiverPhone: fromQuote?.contact || '',\n deliveryAddress: fromQuote?.deliveryAddress || '',\n note: isAdditional ? '추가 발주' : (fromQuote?.note || ''),\n // 기본 정보\n customerName: fromQuote?.customerName || '',\n customerId: fromQuote?.customerId || '',\n siteName: fromQuote?.siteName || '',\n manager: fromQuote?.manager || '',\n contact: fromQuote?.contact || '',\n totalAmount: initialAmount,\n });\n\n // 유효성 검사 규칙\n const validationRules = {\n customerName: { required: true, label: '발주처', message: '발주처를 입력해주세요.' },\n siteName: { required: true, label: '현장명', message: '현장명을 입력해주세요.' },\n dueDate: { required: true, label: '납기일', message: '납기일을 선택해주세요.' },\n };\n\n const { errors, touched, hasError, getFieldError, validateForm, handleBlur, clearFieldError } = useFormValidation(formData, validationRules);\n\n // 견적 필터링\n const availableQuotes = sampleQuotesData.filter(q => q.status === '최종확정');\n const filteredQuotes = availableQuotes\n .filter(q =>\n q.quoteNo.toLowerCase().includes(quoteSearch.toLowerCase()) ||\n q.customerName.includes(quoteSearch) ||\n q.siteName.includes(quoteSearch)\n )\n .sort((a, b) => new Date(b.quoteDate) - new Date(a.quoteDate)); // 최신순 정렬\n\n // 배송방식 옵션 (선불/착불 구분)\n const deliveryMethodOptions = [\n { id: 'pickup-prepaid', label: '상차(선불)', category: '상차' },\n { id: 'pickup-collect', label: '상차(착불)', category: '상차' },\n { id: 'direct', label: '직접배차', category: '직접' },\n { id: 'self', label: '직접수령', category: '직접' },\n { id: 'kyungdong-prepaid', label: '경동화물(선불)', category: '화물' },\n { id: 'kyungdong-collect', label: '경동화물(착불)', category: '화물' },\n { id: 'kyungdong-parcel-prepaid', label: '경동택배(선불)', category: '택배' },\n { id: 'kyungdong-parcel-collect', label: '경동택배(착불)', category: '택배' },\n { id: 'daesin-prepaid', label: '대신화물(선불)', category: '화물' },\n { id: 'daesin-collect', label: '대신화물(착불)', category: '화물' },\n { id: 'daesin-parcel-prepaid', label: '대신택배(선불)', category: '택배' },\n { id: 'daesin-parcel-collect', label: '대신택배(착불)', category: '택배' },\n ];\n\n // 추가분 수주번호 생성\n const generateAdditionalOrderNo = () => {\n if (isAdditional && relatedOrders.length > 0) {\n const baseOrderNo = relatedOrders[0].orderNo;\n const existingAdditionals = relatedOrders.filter(o => o.orderType === 'additional');\n const suffix = String.fromCharCode(65 + existingAdditionals.length); // A, B, C...\n return `${baseOrderNo}-${suffix}`;\n }\n return null;\n };\n\n // 견적 선택 시 폼 데이터 채우기\n const handleQuoteSelect = (quote) => {\n setSelectedQuote(quote);\n setShowQuotePanel(false);\n // 폼 데이터 자동 채우기\n setFormData(prev => ({\n ...prev,\n dueDate: quote.dueDate || prev.dueDate,\n deliveryAddress: quote.deliveryAddress || prev.deliveryAddress,\n receiverName: quote.manager || prev.receiverName,\n receiverPhone: quote.contact || prev.receiverPhone,\n customerName: quote.customerName,\n customerId: quote.customerId,\n siteName: quote.siteName,\n manager: quote.manager,\n contact: quote.contact,\n totalAmount: quote.totalAmount,\n }));\n // 품목도 채우기\n if (quote.items?.length > 0) {\n setDirectItems(quote.items.map(item => ({\n id: item.id,\n productCode: `PRD-${item.id}`,\n productName: item.productName,\n category: item.category || '스크린', // 카테고리 유지 (생산지시 공정 분리에 필요)\n floor: item.floor || '',\n location: item.location || '',\n spec: item.width && item.height ? `${item.width}×${item.height}` : (item.spec || ''),\n width: item.width || 0,\n height: item.height || 0,\n qty: item.qty || 1,\n unit: 'EA',\n unitPrice: item.unitPrice || 0,\n amount: item.amount || 0,\n })));\n }\n };\n\n // 견적 연결 해제\n const handleClearQuote = () => {\n setSelectedQuote(null);\n };\n\n const handleChange = (field, value) => {\n setFormData(prev => ({ ...prev, [field]: value }));\n };\n\n // 품목 상세 편집 모달 상태\n const [showItemModal, setShowItemModal] = useState(false);\n const [editingItem, setEditingItem] = useState(null);\n const [itemForm, setItemForm] = useState({\n productCode: '',\n productName: '국민방화스크린셔터',\n floor: '',\n location: '',\n openWidth: '',\n openHeight: '',\n qty: 1,\n unitPrice: 8000000,\n guideRailType: '백면형',\n guideRailSpec: '120-70',\n finish: 'SUS마감',\n });\n\n // 품목 추가 모달 열기\n const handleAddItem = () => {\n setEditingItem(null);\n setItemForm({\n productCode: '',\n productName: '국민방화스크린셔터',\n floor: '',\n location: '',\n openWidth: '',\n openHeight: '',\n qty: 1,\n unitPrice: 8000000,\n guideRailType: '백면형',\n guideRailSpec: '120-70',\n finish: 'SUS마감',\n });\n setShowItemModal(true);\n };\n\n // 품목 수정 모달 열기\n const handleEditItem = (item) => {\n setEditingItem(item);\n const spec = item.productionSpec || {};\n setItemForm({\n productCode: item.productCode || '',\n productName: item.productName || '국민방화스크린셔터',\n floor: item.floor || '',\n location: item.location || '',\n openWidth: spec.openWidth || item.spec?.split('×')[0] || '',\n openHeight: spec.openHeight || item.spec?.split('×')[1] || '',\n qty: item.qty || 1,\n unitPrice: item.unitPrice || 8000000,\n guideRailType: spec.guideRailType || '백면형',\n guideRailSpec: spec.guideRailSpec || '120-70',\n finish: spec.finish || 'SUS마감',\n });\n setShowItemModal(true);\n };\n\n // 품목 저장 (추가/수정)\n const handleSaveItem = () => {\n const openW = parseInt(itemForm.openWidth) || 0;\n const openH = parseInt(itemForm.openHeight) || 0;\n\n // 제작사이즈 자동 계산\n const prodWidth = openW + 140;\n const prodHeight = Math.max(openH + 400, 2950);\n const shaft = openW > 6000 ? 5 : 4;\n const capacity = openW > 6000 ? 300 : 160;\n\n const newItem = {\n id: editingItem?.id || Date.now(),\n productCode: itemForm.productCode || `SCR-${Date.now()}`,\n productName: itemForm.productName,\n floor: itemForm.floor,\n location: itemForm.location,\n spec: `${openW}×${openH}`,\n qty: parseInt(itemForm.qty) || 1,\n unit: 'EA',\n unitPrice: parseInt(itemForm.unitPrice) || 0,\n amount: (parseInt(itemForm.qty) || 1) * (parseInt(itemForm.unitPrice) || 0),\n productionSpec: {\n type: '와이어',\n drawingNo: `${itemForm.floor} ${itemForm.location}`,\n openWidth: openW,\n openHeight: openH,\n prodWidth,\n prodHeight,\n guideRailType: itemForm.guideRailType,\n guideRailSpec: itemForm.guideRailSpec,\n shaft,\n caseSpec: '500-330',\n motorBracket: '380-180',\n capacity,\n finish: itemForm.finish,\n },\n };\n\n if (editingItem) {\n setDirectItems(prev => prev.map(item => item.id === editingItem.id ? newItem : item));\n } else {\n setDirectItems(prev => [...prev, newItem]);\n }\n\n setShowItemModal(false);\n };\n\n // 품목 삭제\n const handleRemoveItem = (itemId) => {\n setDirectItems(prev => prev.filter(item => item.id !== itemId));\n };\n\n // 기존 handleItemChange는 간단한 필드용으로 유지\n const handleItemChange = (itemId, field, value) => {\n setDirectItems(prev => prev.map(item => {\n if (item.id === itemId) {\n const updated = { ...item, [field]: value };\n if (field === 'qty' || field === 'unitPrice') {\n updated.amount = (updated.qty || 0) * (updated.unitPrice || 0);\n }\n return updated;\n }\n return item;\n }));\n };\n\n // 총액 계산\n const calculateTotal = () => {\n return directItems.reduce((sum, item) => sum + (item.amount || 0), 0);\n };\n\n // 날짜를 YYMMDD 형식으로 변환\n const formatDateCode = (date) => {\n const d = new Date(date);\n const yy = String(d.getFullYear()).slice(-2);\n const mm = String(d.getMonth() + 1).padStart(2, '0');\n const dd = String(d.getDate()).padStart(2, '0');\n return `${yy}${mm}${dd}`;\n };\n\n const handleSubmit = () => {\n // 유효성 검사 실행\n if (!validateForm()) {\n return;\n }\n\n // 수주번호 생성\n let orderNo;\n if (isAdditional && relatedOrders.length > 0) {\n // 추가분 수주번호: 원 수주번호-A, B, C...\n orderNo = generateAdditionalOrderNo();\n } else {\n // 신규 수주번호: KD-SO-YYMMDD-순번\n const dateCode = formatDateCode(formData.orderDate);\n const seq = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0');\n orderNo = `KD-SO-${dateCode}-${seq}`;\n }\n\n // 품목 결정 (제작 스펙 자동 생성 포함)\n let orderItems;\n if (isAdditional && additionalItems) {\n orderItems = additionalItems.map((item, idx) => {\n const spec = item.width && item.height ? `${item.width}×${item.height}` : '-';\n const newItem = {\n id: item.id,\n productCode: item.productCode || `PRD-${item.id}`,\n productName: item.productName,\n category: item.category || '스크린', // 카테고리 유지 (생산지시 공정 분리에 필요)\n floor: item.floor || '-',\n location: item.location || '-',\n spec,\n width: item.width || 0,\n height: item.height || 0,\n qty: item.qty,\n unit: 'EA',\n unitPrice: item.unitPrice || 0,\n amount: item.amount || 0,\n };\n // 제작 스펙 자동 생성\n newItem.productionSpec = generateProductionSpec(newItem, idx);\n return newItem;\n });\n } else {\n orderItems = directItems.map((item, idx) => ({\n ...item,\n category: item.category || '스크린', // 카테고리 유지\n productionSpec: generateProductionSpec(item, idx),\n }));\n }\n\n // 모터 스펙 및 BOM 자동 생성\n const motorSpec = generateMotorSpec(orderItems);\n const bomData = generateBomData(orderItems);\n\n // 견적에서 산출된 품목 리스트 (있으면 사용)\n const calculatedItems = selectedQuote?.calculatedItems || fromQuote?.calculatedItems || [];\n\n const newOrder = {\n id: Date.now(),\n orderNo,\n orderDate: formData.orderDate,\n quoteId: selectedQuote?.id || fromQuote?.id || null,\n quoteNo: selectedQuote?.quoteNo || fromQuote?.quoteNo || null,\n orderType: isAdditional ? 'additional' : (selectedQuote ? 'from-quote' : 'direct'),\n parentOrderNo: isAdditional ? relatedOrders[0]?.orderNo : null,\n customerId: formData.customerId || null,\n customerName: formData.customerName,\n creditGrade: selectedQuote?.creditGrade || fromQuote?.creditGrade || initialCustomers.find(c => c.id === formData.customerId)?.creditGrade || 'B',\n siteName: formData.siteName,\n siteCode: `PJ-${String(new Date().getFullYear()).slice(-2)}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}-${String(Math.floor(Math.random() * 99) + 1).padStart(2, '0')}`,\n manager: formData.manager,\n contact: formData.contact,\n dueDate: formData.dueDate,\n status: '수주등록',\n paymentStatus: '미입금',\n accountingStatus: '미확인',\n accountingConfirmedBy: null,\n accountingConfirmedAt: null,\n deliveryMethod: formData.deliveryMethod,\n totalAmount: calculateTotal() || formData.totalAmount,\n deliveryAddress: formData.deliveryAddress,\n receiverName: formData.receiverName,\n receiverPhone: formData.receiverPhone,\n items: orderItems,\n motorSpec, // 모터/전장품 스펙\n bomData, // 절곡물 BOM\n calculatedItems, // ★ 견적에서 산출된 품목 리스트 (공정별 작업지시에 사용)\n splits: [],\n documentHistory: [],\n changeHistory: [],\n createdAt: formData.orderDate,\n createdBy: '현재 사용자',\n note: formData.note,\n };\n\n onSave?.(newOrder);\n // 저장 후 상세 페이지로 이동 (생산지시 바로 가능하도록)\n onNavigate?.('order-detail', newOrder);\n };\n\n const canSubmit = isAdditional\n ? true // 추가분은 이미 데이터가 있음\n : (formData.customerName && formData.siteName);\n\n // ���고예정일/납품요청일 미정 체크박스 상태\n const [shippingDateUndecided, setShippingDateUndecided] = useState(false);\n const [deliveryDateUndecided, setDeliveryDateUndecided] = useState(false);\n // 운임비용 상태\n const [freightCost, setFreightCost] = useState('선불');\n // 배송지 주소 분리\n const [deliveryZipcode, setDeliveryZipcode] = useState('');\n const [deliveryAddressBase, setDeliveryAddressBase] = useState('');\n const [deliveryAddressDetail, setDeliveryAddressDetail] = useState('');\n\n return (\n
\n {/* 상단 헤더 */}\n
\n
\n
\n {isAdditional ? '수주 등록 (추가분)' : '수주 등록'}\n
\n {isAdditional && relatedOrders.length > 0 && (\n \n 원 수주: {relatedOrders[0]?.orderNo}\n \n )}\n \n
\n \n \n
\n
\n\n {/* 추가분 수주인 경우 별도 UI */}\n {isAdditional ? (\n
\n {/* 견적 정보 (읽기 전용) */}\n
\n \n
\n \n {fromQuote?.quoteNo}\n
\n
\n
\n
발주처\n
{fromQuote?.customerName}
\n
\n
\n
현장명\n
{fromQuote?.siteName}
\n
\n
\n
담당자\n
{fromQuote?.manager}
\n
\n
\n
추가분 금액\n
\n {additionalItems?.reduce((sum, i) => sum + (i.amount || 0), 0).toLocaleString()}원\n
\n
\n
\n
\n \n\n {/* 추가분 품목 */}\n
\n \n
\n \n \n | 품목명 | \n 층/부호 | \n 규격 | \n 수량 | \n 금액 | \n
\n \n \n {additionalItems?.map(item => (\n \n | {item.productName} | \n {item.floor || '-'}/{item.location || '-'} | \n {item.width && item.height ? `${item.width}×${item.height}` : '-'} | \n {item.qty} | \n {item.amount?.toLocaleString()}원 | \n
\n ))}\n \n \n \n | 합계 | \n \n {additionalItems?.reduce((sum, i) => sum + (i.amount || 0), 0).toLocaleString()}원\n | \n
\n \n
\n
\n \n\n {/* 배송 정보 */}\n
\n \n
\n \n handleChange('dueDate', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n
\n
\n
\n
\n {deliveryMethodOptions.map(option => (\n \n ))}\n
\n
\n
\n \n handleChange('deliveryAddress', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n
\n
\n \n handleChange('receiverName', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n
\n
\n \n handleChange('receiverPhone', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n
\n
\n \n
\n
\n \n
\n ) : (\n /* ========== 일반 수주 등록 (스크린샷 UI) ========== */\n
\n {/* 견적 불러오기 섹션 */}\n
\n
\n
견적 불러오기
\n \n
\n
\n
\n {selectedQuote ? (\n
\n
\n {selectedQuote.quoteNo}\n \n
\n
\n {selectedQuote.customerName} / {selectedQuote.siteName} / {selectedQuote.totalAmount?.toLocaleString()}원\n
\n
\n ) : (\n
확정된 견적을 선택하면 정보가 자동으로 채워집니다\n )}\n
\n
\n {selectedQuote && (\n \n )}\n \n
\n
\n
\n
\n\n {/* 기본 정보 섹션 */}\n
\n
\n
기본 정보
\n \n
\n
\n
\n
\n {selectedQuote ? (\n
{formData.customerName}
\n ) : (\n
\n )}\n {hasError('customerName') &&
{getFieldError('customerName')}
}\n
\n
\n
\n
{\n handleChange('siteName', e.target.value);\n clearFieldError('siteName');\n }}\n onBlur={() => handleBlur('siteName')}\n className={`${getInputClassName(hasError('siteName'))} ${selectedQuote ? 'bg-gray-50' : ''}`}\n placeholder=\"현장명 입력\"\n readOnly={!!selectedQuote}\n />\n {hasError('siteName') &&
{getFieldError('siteName')}
}\n
\n
\n \n handleChange('manager', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n placeholder=\"담당자 이름\"\n />\n
\n
\n \n handleChange('contact', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n placeholder=\"010-0000-0000\"\n />\n
\n
\n
\n
\n\n {/* 수주/배송 정보 섹션 */}\n
\n
\n
수주/배송 정보
\n \n
\n
\n {/* 출고예정일 */}\n
\n {/* 납품요청일 */}\n
\n
\n \n \n
\n
{\n handleChange('dueDate', e.target.value);\n clearFieldError('dueDate');\n }}\n onBlur={() => handleBlur('dueDate')}\n disabled={deliveryDateUndecided}\n className={`${getInputClassName(hasError('dueDate'))} ${deliveryDateUndecided ? 'bg-gray-100 text-gray-400' : ''}`}\n />\n {hasError('dueDate') &&
{getFieldError('dueDate')}
}\n
\n {/* 배송방식 */}\n
\n \n \n
\n {/* 운임비용 */}\n
\n \n \n
\n {/* 수신(반장/업체) */}\n
\n \n handleChange('receiverName', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n placeholder=\"수신(반장/업체) 이름\"\n />\n
\n {/* 수신처 연락처 */}\n
\n \n handleChange('receiverPhone', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n placeholder=\"010-0000-0000\"\n />\n
\n {/* 수신처 주소 */}\n
\n {/* 비고 */}\n
\n \n
\n
\n
\n
\n\n {/* 품목 내역 섹션 */}\n
\n
\n
\n
\n \n \n | 순번 | \n 품목코드 | \n 품명 | \n 층 | \n 부호 | \n 규격 | \n 수량 | \n 단위 | \n 단가 | \n 금액 | \n
\n \n \n {directItems.length === 0 ? (\n \n | \n 품목을 추가해주세요\n | \n
\n ) : (\n directItems.map((item, idx) => (\n \n | {idx + 1} | \n {item.productCode || '-'} | \n {item.productName} | \n {item.floor || '-'} | \n {item.location || '-'} | \n {item.spec || `${item.productionSpec?.openWidth || '-'}×${item.productionSpec?.openHeight || '-'}`} | \n {item.qty} | \n {item.unit || 'EA'} | \n {(item.unitPrice || 0).toLocaleString()} | \n {(item.amount || 0).toLocaleString()} | \n
\n ))\n )}\n \n
\n
\n {/* 합계 영역 */}\n
\n
\n
\n
\n 소계\n {calculateTotal().toLocaleString()}원\n
\n
\n 할인율\n 0 %\n
\n
\n 총금액\n {calculateTotal().toLocaleString()}원\n
\n
\n
\n
\n
\n\n {/* 품목 추가/수정 모달 */}\n {showItemModal && (\n
\n
\n
\n
\n
{editingItem ? '품목 수정' : '품목 추가'}
\n \n
\n
\n\n
\n {/* 위치 정보 */}\n
\n \n setItemForm(prev => ({ ...prev, floor: e.target.value }))}\n placeholder=\"예: 4층\"\n />\n \n \n setItemForm(prev => ({ ...prev, location: e.target.value }))}\n placeholder=\"예: FSS1\"\n />\n \n \n setItemForm(prev => ({ ...prev, productName: e.target.value }))}\n />\n \n
\n\n {/* 오픈 사이즈 (고객 제공) */}\n
\n
오픈사이즈 (고객 제공 치수)
\n
\n \n setItemForm(prev => ({ ...prev, openWidth: e.target.value }))}\n placeholder=\"예: 7260\"\n />\n \n \n setItemForm(prev => ({ ...prev, openHeight: e.target.value }))}\n placeholder=\"예: 2600\"\n />\n \n
\n
\n\n {/* 제작 사이즈 (자동 계산) */}\n {itemForm.openWidth && itemForm.openHeight && (\n
\n
제작사이즈 (자동 계산)
\n
\n
\n
가로
\n
{(parseInt(itemForm.openWidth) + 140).toLocaleString()}
\n
\n
\n
세로
\n
{Math.max(parseInt(itemForm.openHeight) + 400, 2950).toLocaleString()}
\n
\n
\n
샤프트
\n
{parseInt(itemForm.openWidth) > 6000 ? '5\"' : '4\"'}
\n
\n
\n
모터용량
\n
{parseInt(itemForm.openWidth) > 6000 ? '300K' : '160K'}
\n
\n
\n
\n )}\n\n {/* 스펙 옵션 */}\n
\n \n \n \n \n \n setItemForm(prev => ({ ...prev, unitPrice: e.target.value }))}\n />\n \n
\n
\n\n
\n \n \n
\n
\n
\n )}\n
\n )}\n\n {/* 견적 선택 모달 */}\n {showQuotePanel && (\n
\n
\n
\n
견적 선택
\n \n \n
\n
\n \n setQuoteSearch(e.target.value)} placeholder=\"견적번호, 거래처, 현장명 검색...\" className=\"w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n
\n
전환 가능한 견적 {availableQuotes.length}건 (최종확정 상태)
\n
\n {filteredQuotes.length === 0 ? (\n
\n ) : (\n filteredQuotes.map((quote) => (\n
handleQuoteSelect(quote)} className={`p-4 hover:bg-blue-50 cursor-pointer transition-colors ${selectedQuote?.id === quote.id ? 'bg-blue-50 border-l-4 border-l-blue-500' : ''}`}>\n
\n
\n
{quote.quoteNo}
\n
{quote.customerName}
\n
{quote.siteName}
\n
\n
\n
{quote.totalAmount?.toLocaleString()}원
\n
{quote.items?.length || 0}개 품목
\n
\n
\n
\n ))\n )}\n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 수주 수정\nconst OrderEdit = ({ order, onNavigate, onBack, onSave }) => {\n const [formData, setFormData] = useState({\n dueDate: order?.dueDate || '',\n shippingDate: order?.shippingDate || '',\n deliveryMethod: order?.deliveryMethod || '상차',\n freightCost: order?.freightCost || '선불',\n receiverName: order?.receiverName || '',\n receiverPhone: order?.receiverPhone || '',\n deliveryAddress: order?.deliveryAddress || '',\n note: order?.note || '',\n });\n\n // 출고예정일/납품요청일 미정 상태\n const [shippingDateUndecided, setShippingDateUndecided] = useState(!order?.shippingDate);\n const [deliveryDateUndecided, setDeliveryDateUndecided] = useState(!order?.dueDate);\n\n // 품목 목록 상태\n const [items, setItems] = useState(order?.items || []);\n const [showItemModal, setShowItemModal] = useState(false);\n const [editingItem, setEditingItem] = useState(null);\n const [itemForm, setItemForm] = useState({\n productCode: '',\n productName: '',\n floor: '',\n location: '',\n spec: '',\n qty: 1,\n unit: 'EA',\n unitPrice: 0,\n });\n\n // 생산 시작 후에는 품목 수정 불가\n const isProductionStarted = order?.status === '생산중' || order?.status === '생산완료';\n const canEditItems = order?.status === '수주확정';\n\n const unitTypes = ['EA', 'SET', 'M', 'M2'];\n\n const handleChange = (field, value) => {\n setFormData(prev => ({ ...prev, [field]: value }));\n };\n\n // 품목 추가/수정 모달 열기\n const handleAddItem = () => {\n if (!canEditItems) {\n alert('생산이 시작된 수주는 품목을 수정할 수 없습니다.');\n return;\n }\n setEditingItem(null);\n setItemForm({\n productCode: '',\n productName: '',\n floor: '',\n location: '',\n spec: '',\n qty: 1,\n unit: 'EA',\n unitPrice: 0,\n });\n setShowItemModal(true);\n };\n\n const handleEditItem = (item, index) => {\n if (!canEditItems) {\n alert('생산이 시작된 수주는 품목을 수정할 수 없습니다.');\n return;\n }\n setEditingItem(index);\n setItemForm({\n productCode: item.productCode || '',\n productName: item.productName || '',\n floor: item.floor || '',\n location: item.location || '',\n spec: item.spec || '',\n qty: item.qty || 1,\n unit: item.unit || 'EA',\n unitPrice: item.unitPrice || 0,\n });\n setShowItemModal(true);\n };\n\n const handleSaveItem = () => {\n if (!itemForm.productName || !itemForm.floor || !itemForm.location) {\n alert('품목명, 층, 부호는 필수입니다.');\n return;\n }\n\n const qty = parseInt(itemForm.qty) || 1;\n const unitPrice = parseInt(itemForm.unitPrice) || 0;\n\n const newItem = {\n id: editingItem !== null ? items[editingItem].id : Date.now(),\n productCode: itemForm.productCode || `ITEM-${String(items.length + 1).padStart(3, '0')}`,\n productName: itemForm.productName,\n floor: itemForm.floor,\n location: itemForm.location,\n spec: itemForm.spec,\n qty: qty,\n unit: itemForm.unit,\n unitPrice: unitPrice,\n amount: qty * unitPrice,\n };\n\n if (editingItem !== null) {\n setItems(prev => prev.map((item, idx) => idx === editingItem ? newItem : item));\n } else {\n setItems(prev => [...prev, newItem]);\n }\n setShowItemModal(false);\n setEditingItem(null);\n };\n\n const handleDeleteItem = (index) => {\n if (!canEditItems) {\n alert('생산이 시작된 수주는 품목을 삭제할 수 없습니다.');\n return;\n }\n if (confirm('이 품목을 삭제하시겠습니까?')) {\n setItems(prev => prev.filter((_, idx) => idx !== index));\n }\n };\n\n // 합계 계산\n const totalQty = items.reduce((sum, item) => sum + (item.qty || 0), 0);\n const totalAmount = items.reduce((sum, item) => sum + (item.amount || 0), 0);\n\n const handleSubmit = () => {\n if (items.length === 0) {\n alert('최소 1개 이상의 품목이 필요합니다.');\n return;\n }\n\n const history = {\n id: Date.now(),\n changedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n changeType: '수주정보수정',\n description: '수주 정보가 수정되었습니다.',\n changedBy: '현재 사용자',\n };\n\n onSave?.({\n ...order,\n ...formData,\n items: items,\n totalQty: totalQty,\n totalAmount: totalAmount,\n changeHistory: [...(order.changeHistory || []), history],\n });\n onBack();\n };\n\n if (!order) return null;\n\n return (\n
\n {/* 헤더 */}\n
\n
\n
수주 수정
\n {order.orderNo}\n \n \n
\n \n \n
\n
\n\n {/* 상태별 안내 메시지 */}\n {isProductionStarted && (\n
\n
\n
\n
생산이 시작된 수주입니다
\n
배송 정보(납기일, 배송방식, 수령인 등)만 수정 가능합니다. 품목 변경이 필요한 경우 생산팀에 문의하세요.
\n
\n
\n )}\n\n {/* 기본 정보 (읽기 전용) */}\n
\n
기본 정보
\n
\n
\n
\n
{order.orderNo}
\n
\n
\n
\n
{order.quoteNo || '-'}
\n
\n
\n
\n
{order.salesperson || '김판매'}
\n
\n
\n
\n
{order.customerName}
\n
\n
\n
\n
{order.siteName}
\n
\n
\n
\n
{order.manager}
\n
\n
\n
\n
{order.contact}
\n
\n
\n
\n\n {/* 수주/배송 정보 */}\n
\n
수주/배송 정보
\n
\n {/* 출고예정일 */}\n
\n\n {/* 납품요청일 */}\n
\n\n {/* 배송방식 */}\n
\n \n \n
\n\n {/* 운임비용 */}\n
\n \n \n
\n\n {/* 수신(반장/업체) */}\n
\n \n handleChange('receiverName', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n placeholder=\"수신(반장/업체) 이름\"\n />\n
\n\n {/* 수신처 연락처 */}\n
\n \n handleChange('receiverPhone', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n placeholder=\"010-0000-0000\"\n />\n
\n\n {/* 수신처 주소 - 전체 너비 */}\n
\n\n {/* 비고 - 전체 너비 */}\n
\n \n
\n
\n
\n\n {/* 품목 내역 */}\n
\n
\n
품목 내역
\n {canEditItems ? (\n
\n ) : (\n
생산 시작 후 수정 불가\n )}\n
\n\n
\n
\n \n \n | No | \n 품목코드 | \n 품목명 | \n 층 | \n 부호 | \n 규격(mm) | \n 수량 | \n 단위 | \n 단가 | \n 금액 | \n {canEditItems && 관리 | }\n
\n \n \n {items.map((item, idx) => (\n \n | {idx + 1} | \n {item.productCode} | \n {item.productName} | \n {item.floor} | \n {item.location} | \n {item.spec || `${item.productionSpec?.openWidth || '-'}×${item.productionSpec?.openHeight || '-'}`} | \n {item.qty} | \n {item.unit || 'EA'} | \n {item.unitPrice?.toLocaleString()} | \n {item.amount?.toLocaleString()} | \n {canEditItems && (\n \n \n \n \n \n | \n )}\n
\n ))}\n \n
\n
\n\n {items.length === 0 && (\n
\n
\n
등록된 품목이 없습니다
\n {canEditItems && (\n
\n )}\n
\n )}\n\n {/* 합계 영역 */}\n {items.length > 0 && (\n
\n
\n
\n
\n 소계\n {totalAmount.toLocaleString()}원\n
\n
\n 할인율\n 0%\n
\n
\n 총금액\n {totalAmount.toLocaleString()}원\n
\n
\n
\n
\n )}\n
\n\n {/* 품목 추가/수정 모달 */}\n {showItemModal && (\n
\n
\n
\n
{editingItem !== null ? '품목 수정' : '품목 추가'}
\n \n \n
\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n\n// ============ 작업지시 관리 ============\n\n// 작업지시 목록\n// ============ 제품 이력 추적 (Traceability) ============\nconst ProductTraceability = ({ workOrders, workResults, onNavigate }) => {\n const [searchType, setSearchType] = useState('lot'); // lot, workOrder, order, product\n const [searchValue, setSearchValue] = useState('');\n const [traceResult, setTraceResult] = useState(null);\n const [selectedNode, setSelectedNode] = useState(null);\n\n // 샘플 추적 데이터\n const sampleTraceData = {\n 'KD-WE-250215-01-절곡': {\n lotNo: 'KD-WE-250215-01-절곡',\n productName: 'KD 웨이브 블라인드',\n workOrderNo: 'KD-KD-WO-2502-0012-01',\n orderNo: 'KD-TS-250200-12',\n customer: '삼성SDI 기흥캠퍼스',\n productionDate: '2025-02-15',\n status: '출하완료',\n timeline: [\n {\n step: 1, stage: '원자재 입고', date: '2025-02-10', lotNo: 'RM-250210-001',\n details: { materialName: '슬랫(웨이브)', supplier: '삼성SDI', qty: '100EA', iqcResult: '합격', inspector: '김품질' }\n },\n {\n step: 2, stage: '수입검사(IQC)', date: '2025-02-10', lotNo: 'IQC-250210-001',\n details: { inspectionType: '수입검사', result: '합격', defectQty: 0, inspector: '김품질', checkItems: ['외관검사', '치수검사', '성적서 확인'] }\n },\n {\n step: 3, stage: '자재 출고', date: '2025-02-12', lotNo: 'MO-250212-001',\n details: { fromLocation: '원자재창고', toLocation: '생산라인', qty: '50EA', issuedBy: '박자재' }\n },\n {\n step: 4, stage: '절곡 작업', date: '2025-02-15', lotNo: 'KD-WE-250215-01-절곡',\n details: { processName: '절곡', workOrderNo: 'KD-KD-WO-2502-0012-01', worker: '김생산', goodQty: 48, defectQty: 2, machine: '절곡기-01' }\n },\n {\n step: 5, stage: '중간검사(PQC)', date: '2025-02-15', lotNo: 'PQC-250215-001',\n details: { inspectionType: '중간검사', result: '합격', sampleQty: 5, defectQty: 0, inspector: '이품질', checkItems: ['외관', '치수', '동작'] }\n },\n {\n step: 6, stage: '포밍 작업', date: '2025-02-16', lotNo: 'KD-WE-250216-01-포밍',\n details: { processName: '포밍', workOrderNo: 'KD-KD-WO-2502-0012-02', worker: '박생산', goodQty: 48, defectQty: 0, machine: '포밍기-02' }\n },\n {\n step: 7, stage: '최종검사(FQC)', date: '2025-02-17', lotNo: 'FQC-250217-001',\n details: { inspectionType: '최종검사', result: '합격', totalQty: 48, passQty: 48, failQty: 0, inspector: '최품질' }\n },\n {\n step: 8, stage: '포장/라벨링', date: '2025-02-17', lotNo: 'PKG-250217-001',\n details: { packagingType: '골판지박스', labelInfo: 'KD-WE-250215', palletNo: 'PLT-001', packedBy: '정포장' }\n },\n {\n step: 9, stage: '출하', date: '2025-02-18', lotNo: 'SHP-250218-001',\n details: { shipmentNo: 'SH-2502-0008', deliveryAddress: '경기도 용인시 기흥구', carrier: '삼성물류', driverName: '김운송' }\n }\n ],\n inputMaterials: [\n { materialCode: 'SLT-WV-01', materialName: '슬랫(웨이브)', lotNo: 'RM-250210-001', qty: 50, supplier: '삼성SDI' },\n { materialCode: 'END-CAP-01', materialName: '엔드캡', lotNo: 'RM-250208-003', qty: 100, supplier: '부품공업' },\n { materialCode: 'CORD-01', materialName: '조작줄', lotNo: 'RM-250209-002', qty: 50, supplier: '로프텍' }\n ],\n relatedWorkOrders: [\n { woNo: 'KD-KD-WO-2502-0012-01', processName: '절곡', status: '완료', completedQty: 48 },\n { woNo: 'KD-KD-WO-2502-0012-02', processName: '포밍', status: '완료', completedQty: 48 },\n { woNo: 'KD-KD-WO-2502-0012-03', processName: '포장', status: '완료', completedQty: 48 }\n ],\n qualityRecords: [\n { type: 'IQC', date: '2025-02-10', result: '합격', inspector: '김품질' },\n { type: 'PQC', date: '2025-02-15', result: '합격', inspector: '이품질' },\n { type: 'FQC', date: '2025-02-17', result: '합격', inspector: '최품질' }\n ]\n }\n };\n\n const handleSearch = () => {\n if (!searchValue.trim()) {\n alert('검색어를 입력하세요.');\n return;\n }\n // 샘플 데이터에서 검색 (실제로는 API 호출)\n const found = sampleTraceData[searchValue] || sampleTraceData['KD-WE-250215-01-절곡'];\n setTraceResult(found);\n setSelectedNode(null);\n };\n\n const getStageColor = (stage) => {\n const colors = {\n '원자재 입고': 'bg-blue-100 border-blue-500 text-blue-700',\n '수입검사(IQC)': 'bg-green-100 border-green-500 text-green-700',\n '자재 출고': 'bg-yellow-100 border-yellow-500 text-yellow-700',\n '절곡 작업': 'bg-purple-100 border-purple-500 text-purple-700',\n '중간검사(PQC)': 'bg-green-100 border-green-500 text-green-700',\n '포밍 작업': 'bg-purple-100 border-purple-500 text-purple-700',\n '최종검사(FQC)': 'bg-green-100 border-green-500 text-green-700',\n '포장/라벨링': 'bg-orange-100 border-orange-500 text-orange-700',\n '출하': 'bg-red-100 border-red-500 text-red-700',\n };\n return colors[stage] || 'bg-gray-100 border-gray-500 text-gray-700';\n };\n\n const getStageIcon = (stage) => {\n const icons = {\n '원자재 입고': '📦',\n '수입검사(IQC)': '🔍',\n '자재 출고': '📤',\n '절곡 작업': '⚙️',\n '중간검사(PQC)': '✅',\n '포밍 작업': '⚙️',\n '최종검사(FQC)': '🏆',\n '포장/라벨링': '📋',\n '출하': '🚚',\n };\n return icons[stage] || '📍';\n };\n\n return (\n
\n {/* 페이지 헤더 */}\n
\n
\n
제품 이력 추적
\n
원자재 입고부터 출하까지 전체 제조 이력을 추적합니다
\n
\n
\n\n {/* 검색 영역 */}\n
\n
\n
\n \n \n
\n
\n \n setSearchValue(e.target.value)}\n placeholder=\"예: KD-WE-250215-01-절곡\"\n className=\"w-full border rounded px-3 py-2 text-sm\"\n onKeyPress={(e) => e.key === 'Enter' && handleSearch()}\n />\n
\n
\n
\n
\n * 샘플 데이터: \"KD-WE-250215-01-절곡\" 을 입력하여 테스트할 수 있습니다.\n
\n
\n\n {/* 검색 결과 */}\n {traceResult && (\n
\n {/* 제품 정보 요약 */}\n
\n
\n 제품 정보\n
\n
\n
\n
LOT 번호
\n
{traceResult.lotNo}
\n
\n
\n
제품명
\n
{traceResult.productName}
\n
\n
\n
수주번호
\n
{traceResult.orderNo}
\n
\n
\n
거래처
\n
{traceResult.customer}
\n
\n
\n
작업지시 번호
\n
{traceResult.workOrderNo}
\n
\n
\n
생산일자
\n
{traceResult.productionDate}
\n
\n
\n
현재 상태
\n
{traceResult.status}\n
\n
\n
\n\n {/* 제조 이력 타임라인 */}\n
\n
\n 제조 이력 타임라인\n
\n
\n {/* 타임라인 라인 */}\n
\n\n
\n {traceResult.timeline.map((item, idx) => (\n
setSelectedNode(selectedNode === idx ? null : idx)}\n >\n {/* 단계 번호 */}\n
\n {getStageIcon(item.stage)}\n {item.step}\n
\n\n {/* 내용 */}\n
\n
\n {item.stage}\n {item.date}\n
\n
LOT: {item.lotNo}
\n\n {/* 상세 정보 (선택 시 표시) */}\n {selectedNode === idx && item.details && (\n
\n {Object.entries(item.details).map(([key, value]) => (\n
\n {key}:\n {Array.isArray(value) ? value.join(', ') : value}\n
\n ))}\n
\n )}\n
\n\n {/* 펼침 아이콘 */}\n
\n
\n ))}\n
\n
\n
\n\n {/* 투입 원자재 정보 */}\n
\n
\n
\n 투입 원자재\n
\n
\n \n \n | 자재코드 | \n 자재명 | \n 입고 LOT | \n 수량 | \n
\n \n \n {traceResult.inputMaterials.map((mat, idx) => (\n \n | {mat.materialCode} | \n {mat.materialName} | \n {mat.lotNo} | \n {mat.qty} | \n
\n ))}\n \n
\n
\n\n {/* 관련 작업지시 */}\n
\n
\n 관련 작업지시\n
\n
\n \n \n | 작업지시 번호 | \n 공정 | \n 상태 | \n 완료수량 | \n
\n \n \n {traceResult.relatedWorkOrders.map((wo, idx) => (\n \n | {wo.woNo} | \n {wo.processName} | \n \n {wo.status}\n | \n {wo.completedQty} | \n
\n ))}\n \n
\n
\n
\n\n {/* 품질 검사 기록 */}\n
\n
\n 품질 검사 기록\n
\n
\n {traceResult.qualityRecords.map((qr, idx) => (\n
\n
\n {qr.type}\n {qr.result}\n
\n
\n
검사일: {qr.date}
\n
검사자: {qr.inspector}
\n
\n
\n ))}\n
\n
\n\n {/* 인쇄/내보내기 버튼 */}\n
\n
\n
\n
\n
\n
\n )}\n\n {/* 검색 전 안내 */}\n {!traceResult && (\n
\n
\n
제품 이력을 검색하세요
\n
LOT 번호, 작업지시 번호, 수주 번호로 제품의 전체 제조 이력을 추적할 수 있습니다.
\n
\n
추적 가능 정보:
\n
\n - • 원자재 입고 LOT 및 공급업체
\n - • 수입검사(IQC) 결과
\n - • 공정별 작업 이력 및 작업자
\n - • 중간검사(PQC) / 최종검사(FQC) 결과
\n - • 포장 및 출하 정보
\n
\n
\n
\n )}\n
\n );\n};\n\n// ============ 생산관리 현황판 (관리자용) ============\nconst ProductionStatusBoard = ({ workOrders, workResults, onNavigate }) => {\n const [selectedFactory, setSelectedFactory] = useState('all');\n\n const factories = [\n { id: 'all', name: '전체', icon: Factory },\n { id: '스크린', name: '스크린공장', icon: Factory },\n { id: '슬랫', name: '슬랫공장', icon: Factory },\n { id: '절곡', name: '절곡공장', icon: Factory },\n ];\n\n // 공장별 현황 계산\n const getFactoryStats = (factoryId) => {\n const filtered = factoryId === 'all'\n ? workOrders\n : workOrders.filter(wo => wo.processType === factoryId);\n\n return {\n total: filtered.length,\n waiting: filtered.filter(wo => wo.status === '작업대기').length,\n working: filtered.filter(wo => wo.status === '작업중').length,\n complete: filtered.filter(wo => wo.status === '작업완료').length,\n urgent: filtered.filter(wo => wo.priority === '긴급').length,\n delayed: filtered.filter(wo => {\n const dueDate = new Date(wo.dueDate);\n return dueDate < new Date() && wo.status !== '작업완료';\n }).length,\n };\n };\n\n // 오늘 작업 현황\n const todayStats = {\n started: workResults.filter(wr => wr.workDate === new Date().toISOString().split('T')[0]).length,\n completed: workResults.filter(wr =>\n wr.workDate === new Date().toISOString().split('T')[0] && wr.status === '작업완료'\n ).length,\n };\n\n // 작업자별 현황\n const workerStats = {};\n workOrders.forEach(wo => {\n if (wo.assignee) {\n if (!workerStats[wo.assignee]) {\n workerStats[wo.assignee] = { total: 0, working: 0, complete: 0 };\n }\n workerStats[wo.assignee].total++;\n if (wo.status === '작업중') workerStats[wo.assignee].working++;\n if (wo.status === '작업완료') workerStats[wo.assignee].complete++;\n }\n });\n\n // 긴급 작업 목록\n const urgentWorks = workOrders\n .filter(wo => wo.priority === '긴급' && wo.status !== '작업완료')\n .slice(0, 5);\n\n // 지연 작업 목록\n const delayedWorks = workOrders\n .filter(wo => {\n const dueDate = new Date(wo.dueDate);\n return dueDate < new Date() && wo.status !== '작업완료';\n })\n .slice(0, 5);\n\n const currentStats = getFactoryStats(selectedFactory);\n\n return (\n
\n
\n \n \n \n }\n />\n\n {/* 공장 선택 탭 */}\n
\n {factories.map(factory => (\n \n ))}\n
\n\n {/* 현황 카드 */}\n
\n
\n
전체 작업
\n
{currentStats.total}건
\n
\n
\n
작업대기
\n
{currentStats.waiting}건
\n
\n
\n
작업중
\n
{currentStats.working}건
\n
\n
\n
작업완료
\n
{currentStats.complete}건
\n
\n
\n
긴급
\n
{currentStats.urgent}건
\n
\n
\n
지연
\n
{currentStats.delayed}건
\n
\n
\n\n
\n {/* 긴급 작업 */}\n
\n {urgentWorks.length === 0 ? (\n \n ) : (\n \n {urgentWorks.map(wo => (\n
onNavigate('work-order-detail', wo)}\n className=\"p-3 bg-red-50 rounded-lg border border-red-200 cursor-pointer hover:bg-red-100\"\n >\n
\n {wo.workOrderNo}\n \n
\n
{wo.customerName} / {wo.siteName}
\n
납기: {wo.dueDate}
\n
\n ))}\n
\n )}\n \n\n {/* 지연 작업 */}\n
\n {delayedWorks.length === 0 ? (\n \n ) : (\n \n {delayedWorks.map(wo => (\n
onNavigate('work-order-detail', wo)}\n className=\"p-3 bg-orange-50 rounded-lg border border-orange-200 cursor-pointer hover:bg-orange-100\"\n >\n
\n {wo.workOrderNo}\n \n {Math.ceil((new Date() - new Date(wo.dueDate)) / (1000 * 60 * 60 * 24))}일 지연\n \n
\n
{wo.customerName}
\n
담당: {wo.assignee || '미지정'}
\n
\n ))}\n
\n )}\n \n\n {/* 작업자별 현황 */}\n
\n {Object.keys(workerStats).length === 0 ? (\n \n ) : (\n \n {Object.entries(workerStats).map(([worker, stats]) => (\n
\n
\n {worker}\n {stats.total}건 배정\n
\n
\n \n 작업중 {stats.working}\n \n \n 완료 {stats.complete}\n \n
\n
\n ))}\n
\n )}\n \n
\n
\n );\n};\n\n// ============ 작업자 전용 화면 (반응형 대시보드) ============\n// ============================================================\n// 작업자 대시보드 - 모바일/데스크탑 반응형\n// ============================================================\nconst WorkerTaskView = ({ workOrders, workResults, onNavigate, onBack, onStartWork, onCompleteWork, onReportIssue, onUpdateItemStatus, onPrintWorkLog, onMaterialInput }) => {\n // 공정관리에서 사용중인 공정 목록 동적으로 가져오기\n const activeProcesses = sampleProcesses.filter(p => p.isActive);\n\n // ★ 작업자/팀 선택 상태 - 개인 또는 공정팀 선택 가능\n const [viewMode, setViewMode] = useState('team'); // 'individual' 또는 'team'\n const [selectedWorker, setSelectedWorker] = useState('전체'); // 개인 선택 시 사용\n const [selectedTeam, setSelectedTeam] = useState('전체'); // 팀 선택 시 사용\n const [sortOption, setSortOption] = useState('priority'); // 정렬 옵션: priority, dueDate, recent\n\n // 작업자 목록 - 공정관리에서 배정된 작업자 목록 동적으로 가져옴\n const allAssignedWorkers = activeProcesses.flatMap(p => p.assignedWorkers || []);\n const workers = ['전체', ...new Set(allAssignedWorkers)];\n // 팀/공정 목록 - 공정관리에서 동적으로 가져옴\n const teams = ['전체', ...activeProcesses.map(p => `${p.processName}팀`)];\n\n // 공정명과 팀명 매핑 (동적 생성)\n const teamProcessMap = activeProcesses.reduce((acc, p) => {\n acc[`${p.processName}팀`] = p.processName;\n return acc;\n }, {});\n\n const [showCompleteToast, setShowCompleteToast] = useState(null);\n const [showIssueModal, setShowIssueModal] = useState(false);\n const [selectedTask, setSelectedTask] = useState(null);\n const [quickInputTask, setQuickInputTask] = useState(null);\n const [quickResult, setQuickResult] = useState({ goodQty: 0, defectQty: 0 });\n // ★ 개별 수량 처리를 위한 상태\n const [expandedTask, setExpandedTask] = useState(null); // 펼쳐진 작업 (공정 상세 보기)\n const [stepProgress, setStepProgress] = useState({}); // { taskId: { stepCode: [true, true, false] } }\n\n // ★ 공정 단계명 매핑 (stepStatus 키 → processWorkflows stepName 정규화)\n // E2E 데이터의 stepStatus 키와 processWorkflows의 stepName 간 불일치 해결\n const stepNameMapping = {\n // 스크린 공정\n '원단절단': '원단 절단',\n '원단 절단': '원단 절단',\n '앤드락작업': '앤드락 작업',\n '앤드락 작업': '앤드락 작업',\n '미싱': '미싱',\n '중간검사': '중간검사',\n '포장': '포장',\n // 슬랫 공정\n '코일절단': '코일 절단',\n '코일 절단': '코일 절단',\n '미미작업': '미미작업',\n // 절곡 공정\n '절곡판/코일 절단': '절곡판/코일 절단',\n 'V컷팅': 'V컷팅',\n '절곡': '절곡',\n // 재고생산 공정\n '준비': '준비',\n '작업': '작업',\n '검사': '검사',\n };\n\n // ★ stepStatus에서 stepProgress 초기화 (작업지시 데이터 → 작업자 화면 진행상태 연동)\n // 작업 선택 시 또는 컴포넌트 마운트 시 stepStatus 데이터를 stepProgress로 변환\n const initializeStepProgressFromStatus = (task) => {\n if (!task || !task.stepStatus) return;\n\n const steps = getTaskSteps(task);\n const totalQty = task.totalQty || 1;\n const newProgress = {};\n\n steps.forEach(step => {\n // stepStatus 키 찾기 (정규화된 이름으로 검색)\n const statusKey = Object.keys(task.stepStatus).find(key =>\n stepNameMapping[key] === step.stepName || key === step.stepName\n );\n\n if (statusKey && task.stepStatus[statusKey]) {\n const stepData = task.stepStatus[statusKey];\n if (stepData.status === '완료') {\n // 완료된 단계는 모든 항목 체크\n newProgress[step.stepCode] = Array(totalQty).fill(true);\n } else if (stepData.status === '진행중') {\n // 진행중인 단계는 완료된 수량만큼 체크\n const completedCount = task.completedQty || 0;\n newProgress[step.stepCode] = Array(totalQty).fill(false).map((_, i) => i < completedCount);\n } else {\n // 대기 상태는 빈 배열\n newProgress[step.stepCode] = Array(totalQty).fill(false);\n }\n }\n });\n\n setStepProgress(prev => ({\n ...prev,\n [task.id]: newProgress\n }));\n };\n\n // ★ 작업 펼치기 시 stepStatus에서 초기화\n const handleExpandTask = (taskId) => {\n if (expandedTask === taskId) {\n setExpandedTask(null);\n } else {\n setExpandedTask(taskId);\n // 아직 초기화되지 않은 경우에만 stepStatus에서 로드\n if (!stepProgress[taskId]) {\n const task = workOrders.find(wo => wo.id === taskId);\n initializeStepProgressFromStatus(task);\n }\n }\n };\n // ★ 작업일지 모달 상태\n const [showWorkLogModal, setShowWorkLogModal] = useState(false);\n const [workLogTask, setWorkLogTask] = useState(null);\n // ★ 작업일지 양식보기 모달 상태 (데이터 연동)\n const [showWorkLogDataPreview, setShowWorkLogDataPreview] = useState(false);\n const [workLogDataPreviewTask, setWorkLogDataPreviewTask] = useState(null);\n\n // ★ 담당자 확인 함수\n const isAssignedToWorker = (wo, worker) => {\n if (worker === '전체') return true;\n if (wo.assignees && Array.isArray(wo.assignees)) return wo.assignees.includes(worker);\n if (wo.assignee) return wo.assignee.split(', ').map(a => a.trim()).includes(worker);\n return false;\n };\n\n // ★ 팀/공정 확인 함수 - 동적 매핑 사용\n const isInTeam = (wo, team) => {\n if (team === '전체') return true;\n // 위에서 정의한 동적 teamProcessMap 사용\n return wo.processType === teamProcessMap[team];\n };\n\n // 내 작업 목록 (미완료만) - 개인/팀 모드에 따라 필터링\n const myTasks = workOrders\n .filter(wo => {\n if (wo.status === '작업완료') return false;\n if (viewMode === 'individual') {\n return isAssignedToWorker(wo, selectedWorker);\n } else {\n return isInTeam(wo, selectedTeam);\n }\n })\n .sort((a, b) => {\n // 작업중인 것은 항상 최상단\n if (a.status === '작업중' && b.status !== '작업중') return -1;\n if (a.status !== '작업중' && b.status === '작업중') return 1;\n\n // 정렬 옵션에 따른 정렬\n if (sortOption === 'priority') {\n // 우선순위순 (숫자가 낮을수록 높은 우선순위)\n const aPriority = a.priorityLevel || a.workSequence || 5;\n const bPriority = b.priorityLevel || b.workSequence || 5;\n if (aPriority !== bPriority) return aPriority - bPriority;\n // 같은 우선순위면 긴급 우선\n if (a.priority === '긴급' && b.priority !== '긴급') return -1;\n if (a.priority !== '긴급' && b.priority === '긴급') return 1;\n return new Date(a.dueDate) - new Date(b.dueDate);\n } else if (sortOption === 'dueDate') {\n // 납기일순\n return new Date(a.dueDate) - new Date(b.dueDate);\n } else {\n // 최신등록순 (ID 내림차순)\n return b.id - a.id;\n }\n });\n\n // 오늘 완료한 작업\n const todayCompleted = workOrders.filter(wo => {\n if (wo.status !== '작업완료') return false;\n if (!wo.completedAt) return false;\n const today = new Date().toDateString();\n return new Date(wo.completedAt).toDateString() === today;\n });\n\n // 공정별 통계 - 공정관리에서 동적으로 가져옴\n const processStats = activeProcesses.reduce((acc, p) => {\n acc[p.processName] = myTasks.filter(t => t.processType === p.processName);\n return acc;\n }, {});\n\n // 긴급 작업 수\n const urgentCount = myTasks.filter(t => t.priority === '긴급').length;\n const inProgressCount = myTasks.filter(t => t.status === '작업중').length;\n\n // 빠른 완료 처리\n const handleQuickComplete = (task) => {\n // ★ 자재투입이 필요한데 투입하지 않은 경우 경고\n if (needsMaterialInput(task)) {\n const confirmProceed = window.confirm(\n `⚠️ 자재 투입이 필요합니다!\\n\\n` +\n `작업지시: ${task.workOrderNo}\\n` +\n `공정: ${task.processType}\\n\\n` +\n `자재 투입 없이 완료 처리하시겠습니까?\\n` +\n `(LOT 추적이 불가능해집니다)`\n );\n if (!confirmProceed) {\n // 자재투입 모달 열기\n onMaterialInput?.(task);\n return;\n }\n }\n\n onStartWork?.(task);\n onCompleteWork?.({\n ...task,\n status: '작업완료',\n goodQty: task.totalQty || 0,\n defectQty: 0,\n note: '빠른 완료',\n completedAt: new Date().toISOString(),\n });\n setShowCompleteToast({ taskNo: task.workOrderNo, qty: task.totalQty });\n setTimeout(() => setShowCompleteToast(null), 2000);\n };\n\n // 실적 입력 후 완료\n const submitQuickResult = (task) => {\n onStartWork?.(task);\n onCompleteWork?.({\n ...task,\n status: '작업완료',\n goodQty: quickResult.goodQty,\n defectQty: quickResult.defectQty,\n completedAt: new Date().toISOString(),\n });\n setQuickInputTask(null);\n setQuickResult({ goodQty: 0, defectQty: 0 });\n setShowCompleteToast({ taskNo: task.workOrderNo, qty: quickResult.goodQty });\n setTimeout(() => setShowCompleteToast(null), 2000);\n };\n\n // ★ 자재 투입 필요 여부 확인 함수\n // 공정별로 필요한 자재가 정의되어 있고, 해당 자재가 투입되지 않은 경우 true 반환\n const needsMaterialInput = (task) => {\n // 자재투입이 필요한 공정 유형\n const processesRequiringMaterial = ['스크린', '슬랫', '절곡'];\n if (!processesRequiringMaterial.includes(task.processType)) return false;\n\n // 작업중 또는 대기 상태인 경우에만 자재투입 필요\n if (task.status === '작업완료') return false;\n\n // 이미 자재가 투입된 경우 (materialInputs 배열에 데이터가 있음)\n if (task.materialInputs && task.materialInputs.length > 0) return false;\n\n return true;\n };\n\n // ★ 자재 투입 상태 확인 함수\n const getMaterialInputStatus = (task) => {\n // 자재투입이 필요없는 공정\n const processesRequiringMaterial = ['스크린', '슬랫', '절곡'];\n if (!processesRequiringMaterial.includes(task.processType)) {\n return { status: 'not-required', label: '해당없음', color: 'gray' };\n }\n\n // 이미 자재가 투입된 경우\n if (task.materialInputs && task.materialInputs.length > 0) {\n return { status: 'completed', label: '투입완료', color: 'green', count: task.materialInputs.length };\n }\n\n // 자재 투입 필요\n return { status: 'required', label: '투입필요', color: 'red' };\n };\n\n // 공정타입별 색상\n const getProcessColor = (type) => {\n const colors = {\n '스크린': { bg: 'bg-blue-500', light: 'bg-blue-100', text: 'text-blue-700' },\n '슬랫': { bg: 'bg-purple-500', light: 'bg-purple-100', text: 'text-purple-700' },\n '절곡': { bg: 'bg-orange-500', light: 'bg-orange-100', text: 'text-orange-700' },\n '재고생산': { bg: 'bg-green-500', light: 'bg-green-100', text: 'text-green-700' },\n };\n return colors[type] || { bg: 'bg-gray-500', light: 'bg-gray-100', text: 'text-gray-700' };\n };\n\n // ★ 공정별 작업 단계 정의 (processMasterConfig에서 가져옴)\n const processWorkflows = {\n SCREEN: {\n code: 'SCREEN', name: '스크린',\n steps: [\n { stepNo: 1, stepCode: 'SCR-MAT', stepName: '자재투입', isQCStep: false },\n { stepNo: 2, stepCode: 'SCR-CNT', stepName: '절단매수확인', isQCStep: false },\n { stepNo: 3, stepCode: 'SCR-CUT', stepName: '원단 절단', isQCStep: false },\n { stepNo: 4, stepCode: 'SCR-CHK', stepName: '절단 Check', isQCStep: true },\n { stepNo: 5, stepCode: 'SCR-SEW', stepName: '미싱', isQCStep: false },\n { stepNo: 6, stepCode: 'SCR-END', stepName: '앤드락 작업', isQCStep: false },\n { stepNo: 7, stepCode: 'SCR-QC', stepName: '중간검사', isQCStep: true },\n { stepNo: 8, stepCode: 'SCR-PACK', stepName: '포장', isQCStep: false },\n ]\n },\n SLAT: {\n code: 'SLAT', name: '슬랫',\n steps: [\n { stepNo: 1, stepCode: 'SLT-CUT', stepName: '코일 절단', isQCStep: false },\n { stepNo: 2, stepCode: 'SLT-FIN', stepName: '미미작업', isQCStep: false },\n { stepNo: 3, stepCode: 'SLT-QC1', stepName: '중간검사', isQCStep: true },\n { stepNo: 4, stepCode: 'SLT-PACK', stepName: '포장', isQCStep: false },\n ]\n },\n FOLD: {\n code: 'FOLD', name: '절곡',\n steps: [\n { stepNo: 1, stepCode: 'FLD-CUT', stepName: '절곡판/코일 절단', isQCStep: false },\n { stepNo: 2, stepCode: 'FLD-VCUT', stepName: 'V컷팅', isQCStep: false },\n { stepNo: 3, stepCode: 'FLD-BEND', stepName: '절곡', isQCStep: false },\n { stepNo: 4, stepCode: 'FLD-QC', stepName: '중간검사', isQCStep: true },\n { stepNo: 5, stepCode: 'FLD-PACK', stepName: '포장', isQCStep: false },\n ]\n },\n STOCK: {\n code: 'STOCK', name: '재고생산',\n steps: [\n { stepNo: 1, stepCode: 'STK-PREP', stepName: '준비', isQCStep: false },\n { stepNo: 2, stepCode: 'STK-WORK', stepName: '작업', isQCStep: false },\n { stepNo: 3, stepCode: 'STK-QC', stepName: '검사', isQCStep: true },\n { stepNo: 4, stepCode: 'STK-PACK', stepName: '포장', isQCStep: false },\n ]\n }\n };\n\n // ★ 공정 타입명 → 코드 변환\n const getProcessCode = (typeName) => {\n const map = { '스크린': 'SCREEN', '슬랫': 'SLAT', '절곡': 'FOLD', '재고생산': 'STOCK' };\n return map[typeName] || 'STOCK';\n };\n\n // ★ 작업의 공정 단계 가져오기\n const getTaskSteps = (task) => {\n const code = getProcessCode(task.processType);\n return processWorkflows[code]?.steps || [];\n };\n\n // ★ 개별 수량 체크 토글\n const toggleItemCheck = (taskId, stepCode, itemIndex) => {\n setStepProgress(prev => {\n const taskProgress = prev[taskId] || {};\n const stepChecks = taskProgress[stepCode] || [];\n const newChecks = [...stepChecks];\n newChecks[itemIndex] = !newChecks[itemIndex];\n return {\n ...prev,\n [taskId]: {\n ...taskProgress,\n [stepCode]: newChecks\n }\n };\n });\n };\n\n // ★ 단계 전체 완료 여부 확인\n const isStepComplete = (taskId, stepCode, totalQty) => {\n const checks = stepProgress[taskId]?.[stepCode] || [];\n return checks.filter(Boolean).length >= totalQty;\n };\n\n // ★ 모든 단계 완료 여부 확인\n const isAllStepsComplete = (task) => {\n const steps = getTaskSteps(task);\n return steps.every(step => isStepComplete(task.id, step.stepCode, task.totalQty || 0));\n };\n\n // ★ 단계 완료 개수 가져오기\n const getStepCompletedCount = (taskId, stepCode) => {\n const checks = stepProgress[taskId]?.[stepCode] || [];\n return checks.filter(Boolean).length;\n };\n\n // ★ 검사 요청 처리 (QC Step)\n const requestInspection = (task, step) => {\n alert(`${step.stepName} 검사 요청이 품질팀에 전송되었습니다.`);\n // QC 단계는 자동 완료 처리\n const totalQty = task.totalQty || 0;\n setStepProgress(prev => ({\n ...prev,\n [task.id]: {\n ...prev[task.id],\n [step.stepCode]: Array(totalQty).fill(true)\n }\n }));\n };\n\n\n\n // ★ 개별 항목 상세 정보 생성 (전개도/치수 정보)\n const getItemDetails = (task, itemIndex) => {\n // 실제로는 작업지시에 연결된 BOM/전개도 데이터에서 가져옴\n const baseW = task.productionSizeW || 2500;\n const baseH = task.productionSizeH || 3000;\n const variation = (itemIndex % 3) * 100; // 항목별 사이즈 차이\n\n return {\n itemNo: itemIndex + 1,\n // 층/호수/부호 정보\n floor: `${Math.floor((itemIndex / 2) + 1)}층`,\n unit: `${(itemIndex % 10) + 1}호`,\n code: ['A', 'B', 'C', 'D'][itemIndex % 4],\n // 전개 치수 정보\n width: baseW + variation,\n height: baseH + (variation * 1.2),\n // 규격\n spec: `W${baseW + variation} × H${Math.floor(baseH + (variation * 1.2))}`,\n // 자재 정보\n material: task.processType === '슬랫' ? '슬랫 코일' :\n task.processType === '스크린' ? '스크린 원단' : '절곡판',\n lotNo: task.assignedLots?.[0]?.lotNo || `LOT-${task.processType?.substring(0, 2)}-2025-${String(itemIndex + 1).padStart(3, '0')}`,\n // 비고\n note: itemIndex === 0 ? '선행 생산' : '',\n };\n };\n\n // ============ 대시보드 렌더링 ============\n return (\n
\n {/* 상단 헤더 - 심플하게 */}\n
\n\n {/* 메인 대시보드 - 전체 폭 사용 */}\n
\n {/* 상단 통계 카드 - 모바일 2열, 데스크탑 4열 */}\n \n
\n
할일
\n
{myTasks.length}건
\n
\n
\n
작업중
\n
{inProgressCount}건
\n
\n
\n
완료
\n
{todayCompleted.length}건
\n
\n
0 ? 'bg-gray-100 border-gray-400' : 'bg-white border-gray-200'}`}>\n
긴급
\n
0 ? 'text-gray-900' : 'text-gray-400'}`}>{urgentCount}건
\n
\n
\n\n {/* 작업 목록 */}\n \n
\n
내 작업 목록
\n {/* 정렬 옵션 */}\n \n \n\n {myTasks.length === 0 ? (\n
\n
모든 작업 완료
\n
현재 배정된 작업이 없습니다.
\n
\n ) : (\n
\n {myTasks.map((task) => {\n const isUrgent = task.priority === '긴급';\n const isWorking = task.status === '작업중';\n const priorityLevel = task.priorityLevel || task.workSequence || 5;\n\n return (\n
\n
\n {/* 1순위: 제품명 + 수량 (한 줄 좌우 배치) */}\n
\n
\n {task.productName || '제품'}\n
\n
\n {task.totalQty || 0}EA\n
\n
\n\n {/* 2순위: 상태/우선순위/납기 (중요) */}\n
\n
\n {/* 우선순위 */}\n \n {priorityLevel}순위\n \n {isUrgent && (\n \n 긴급\n \n )}\n {isWorking && (\n \n 작업중\n \n )}\n
\n {/* 납기일 - 우측 */}\n
\n
\n\n {/* 3순위: 공정/작업지시번호 */}\n
\n \n {task.processType}\n \n {task.workOrderNo}\n
\n\n {/* 4순위: 발주처/현장 (작게) */}\n
\n {task.customerName}\n {task.siteName && ·}\n {task.siteName && {task.siteName}}\n
\n\n {/* 담당자 */}\n {(task.assignedWorkers?.length > 0 || task.assignee) && (\n
\n 담당: {task.assignedWorkers?.length > 0\n ? task.assignedWorkers.length > 2\n ? `${task.assignedWorkers.slice(0, 2).join(', ')} 외 ${task.assignedWorkers.length - 2}명`\n : task.assignedWorkers.join(', ')\n : task.assignee\n }\n
\n )}\n\n {/* 지시사항 표시 */}\n {task.instruction && (\n
\n 지시: {task.instruction}\n
\n )}\n\n {/* 액션 버튼들 - 터치 친화적 반응형 */}\n
\n {/* 메인 액션: 전량완료 (가장 중요) */}\n
\n {/* 보조 액션: 2열 배치 */}\n
\n \n \n
\n {/* 기타 액션: 2열 배치 */}\n
\n \n \n
\n
\n
\n\n {/* 공정 단계별 개별 체크 영역 */}\n {expandedTask === task.id && (\n
\n {/* 투입 자재 LOT 정보 */}\n {task.materialInputs && task.materialInputs.length > 0 && (\n
\n
\n 투입 자재 ({task.materialInputs.length}건)\n
\n
\n {task.materialInputs.map((input, idx) => (\n
\n {input.materialName || input.materialCode} {input.qty}{input.unit}\n {input.lotNo}\n
\n ))}\n
\n
\n )}\n\n {/* 자재 미투입 경고 */}\n {needsMaterialInput(task) && (\n
\n
자재 투입 필요
\n
\n
\n )}\n\n
\n
공정 단계 ({getTaskSteps(task).length}단계)
\n \n {getTaskSteps(task).filter(s => isStepComplete(task.id, s.stepCode, task.totalQty)).length} / {getTaskSteps(task).length} 완료\n \n \n\n
\n {getTaskSteps(task).map((step, stepIdx) => {\n const completedCount = getStepCompletedCount(task.id, step.stepCode);\n const totalQty = task.totalQty || 0;\n const isComplete = completedCount >= totalQty;\n const prevStep = getTaskSteps(task)[stepIdx - 1];\n const isPrevComplete = !prevStep || isStepComplete(task.id, prevStep.stepCode, totalQty);\n\n return (\n
\n {/* 단계 헤더 */}\n
\n
\n \n {step.stepNo}\n \n \n {step.stepName}\n \n {step.isQCStep && (\n 검사\n )}\n
\n
\n {completedCount}/{totalQty}\n \n
\n\n {/* 개별 체크박스 또는 검사 요청 버튼 */}\n {isPrevComplete && !isComplete && (\n step.isQCStep ? (\n // QC 단계: 검사 요청 버튼\n
\n ) : (\n // 작업 단계: 개별 항목 카드 + 체크박스\n
\n {Array.from({ length: totalQty }, (_, i) => {\n const isChecked = stepProgress[task.id]?.[step.stepCode]?.[i] || false;\n const itemDetail = getItemDetails(task, i);\n return (\n
\n
\n {/* 체크 버튼 */}\n
\n\n {/* 항목 상세 정보 */}\n
\n
\n #{itemDetail.itemNo}\n \n {itemDetail.floor} {itemDetail.unit}-{itemDetail.code}\n \n {itemDetail.note && (\n \n {itemDetail.note}\n \n )}\n
\n\n {/* 전개도 치수 */}\n
\n
\n 규격:\n {itemDetail.spec}\n
\n
\n 자재:\n {itemDetail.material}\n
\n
\n LOT:\n {itemDetail.lotNo}\n
\n
\n
\n\n {/* 완료 표시 */}\n {isChecked && (\n
완료\n )}\n
\n
\n );\n })}\n
\n )\n )}\n\n {/* 완료 상태 표시 */}\n {isComplete && (\n
완료\n )}\n
\n );\n })}\n
\n\n {/* 전체 완료 버튼 */}\n {isAllStepsComplete(task) && (\n
\n )}\n
\n )}\n
\n );\n })}\n
\n )}\n
\n\n {/* 오늘 완료한 작업 */}\n {todayCompleted.length > 0 && (\n \n
오늘 완료 ({todayCompleted.length}건)
\n
\n {todayCompleted.slice(0, 3).map((task) => (\n
\n {task.workOrderNo}\n {task.customerName}\n {task.totalQty}EA\n
\n ))}\n {todayCompleted.length > 3 && (\n
외 {todayCompleted.length - 3}건
\n )}\n
\n
\n )}\n \n\n {/* 빠른 실적 입력 모달 - 바텀시트 스타일 */}\n {quickInputTask && (\n
\n
\n
\n
실적 입력
\n \n \n\n
\n {/* 작업 정보 */}\n
\n
{quickInputTask.workOrderNo}
\n
{quickInputTask.customerName}
\n
목표: {quickInputTask.totalQty}EA
\n
\n\n {/* 수량 입력 */}\n
\n\n {/* 빠른 선택 버튼 */}\n
\n \n {quickInputTask.totalQty > 1 && (\n \n )}\n
\n
\n\n
\n \n \n
\n
\n
\n )}\n\n {/* 완료 토스트 */}\n {showCompleteToast && (\n
\n
\n \n {showCompleteToast.taskNo} 완료! ({showCompleteToast.qty}EA)\n
\n
\n )}\n\n {/* 이슈 보고 모달 */}\n {showIssueModal && selectedTask && (\n
setShowIssueModal(false)}\n onSubmit={(issue) => {\n onReportIssue?.(selectedTask, issue);\n setShowIssueModal(false);\n }}\n />\n )}\n\n {/* 작업일지 출력 모달 - 공정별 전용 템플릿 (데이터 맵핑) */}\n {showWorkLogModal && workLogTask && (\n {\n setShowWorkLogModal(false);\n setWorkLogTask(null);\n }}\n />\n )}\n\n {/* 작업일지 양식보기 모달 - 실제 데이터 연동 */}\n {showWorkLogDataPreview && workLogDataPreviewTask && (\n r.workOrderNo === workLogDataPreviewTask.workOrderNo) || []}\n order={null}\n onClose={() => {\n setShowWorkLogDataPreview(false);\n setWorkLogDataPreviewTask(null);\n }}\n />\n )}\n \n );\n};\n\n// ★ 입고 로트 입력 모달\nconst MaterialLotInputModal = ({ task, onClose, onSubmit }) => {\n const [materialName, setMaterialName] = useState('');\n const [lotNo, setLotNo] = useState('');\n const [qty, setQty] = useState('');\n const [unit, setUnit] = useState('EA');\n\n // 공정별 자재 프리셋\n const materialPresets = {\n '스크린': [\n { name: '방충망 원단 (1016)', unit: 'm' },\n { name: '방충망 원단 (1270)', unit: 'm' },\n { name: '방충망 원단 (1524)', unit: 'm' },\n { name: '앤드락', unit: 'EA' },\n ],\n '슬랫': [\n { name: '슬랫 코일 (백색)', unit: 'm' },\n { name: '슬랫 코일 (아이보리)', unit: 'm' },\n { name: '미미 부품', unit: 'EA' },\n ],\n '절곡': [\n { name: '철판 (1.0t)', unit: '매' },\n { name: '철판 (1.2t)', unit: '매' },\n { name: '철판 (1.5t)', unit: '매' },\n ],\n };\n\n const presets = materialPresets[task.processType] || [];\n\n const handlePresetSelect = (preset) => {\n setMaterialName(preset.name);\n setUnit(preset.unit);\n };\n\n const handleSubmit = () => {\n if (!materialName || !lotNo || !qty) {\n alert('자재명, 로트번호, 수량을 모두 입력해주세요.');\n return;\n }\n onSubmit({\n materialName,\n lotNo,\n qty: parseInt(qty) || 0,\n unit,\n });\n };\n\n return (\n
\n
\n
\n
📦 입고 로트 등록
\n \n \n\n
\n {/* 작업 정보 */}\n
\n
{task.workOrderNo}
\n
{task.customerName} · {task.processType}
\n
\n\n {/* 자재 프리셋 */}\n {presets.length > 0 && (\n
\n
\n
\n {presets.map((preset, idx) => (\n \n ))}\n
\n
\n )}\n\n {/* 자재명 입력 */}\n
\n \n setMaterialName(e.target.value)}\n className=\"w-full px-3 py-3 border rounded-lg focus:ring-2 focus:ring-amber-500 text-base\"\n placeholder=\"자재명을 입력하세요\"\n />\n
\n\n {/* 로트번호 입력 */}\n
\n \n setLotNo(e.target.value.toUpperCase())}\n className=\"w-full px-3 py-3 border rounded-lg focus:ring-2 focus:ring-amber-500 text-base font-mono\"\n placeholder=\"예: LOT-241210-001\"\n />\n
\n\n {/* 수량 입력 */}\n
\n
\n \n setQty(e.target.value)}\n className=\"w-full px-3 py-3 border rounded-lg focus:ring-2 focus:ring-amber-500 text-base text-center font-bold\"\n placeholder=\"0\"\n />\n
\n
\n \n \n
\n
\n
\n\n
\n
\n
\n );\n};\n\n// ★ 작업일지 미리보기/출력 모달 (공정별 전용 템플릿 적용 - ISO 인증용)\nconst WorkLogPreviewModal = ({ task, workLogData, onClose }) => {\n const handlePrint = () => {\n window.print();\n };\n\n // 공정 타입에 따른 문서 코드 매핑\n const getDocCode = (processType) => {\n const codeMap = {\n '스크린': 'WL-SCR',\n '절곡': 'WL-FLD',\n '슬랫': 'WL-SLT',\n '포밍': 'WL-STK',\n '포장': 'WL-PKG',\n };\n return codeMap[processType] || 'WL-GEN';\n };\n\n const docCode = getDocCode(task.processType);\n\n // ===== 스크린 작업일지: 망규격 × 폭규격 매트릭스 테이블 =====\n const ScreenWorkLogTable = () => {\n // 매트릭스 데이터 (documentTemplateConfig 기준)\n const rowHeaders = ['1016', '1270', '1524', '1780']; // 망규격 (세로)\n const colHeaders = ['900', '1000', '1100', '1200', '1300', '1400', '1500']; // 폭규격 (가로)\n\n // 샘플 데이터 (실제로는 workLogData.screenProduction에서 가져옴)\n const screenData = workLogData.screenProduction || {};\n\n // 행/열 합계 계산\n const rowTotals = rowHeaders.map(row =>\n colHeaders.reduce((sum, col) => sum + (screenData[`${row}_${col}`] || 0), 0)\n );\n const colTotals = colHeaders.map(col =>\n rowHeaders.reduce((sum, row) => sum + (screenData[`${row}_${col}`] || 0), 0)\n );\n const grandTotal = rowTotals.reduce((sum, val) => sum + val, 0);\n\n return (\n
\n
작 업 내 역 (망규격 × 폭규격)
\n
\n
\n \n \n | 망규격 ↓ / 폭규격 → | \n {colHeaders.map(col => (\n {col} | \n ))}\n 합계 | \n
\n \n \n {rowHeaders.map((row, rowIdx) => (\n \n | {row} | \n {colHeaders.map(col => (\n \n {screenData[`${row}_${col}`] || '-'}\n | \n ))}\n \n {rowTotals[rowIdx] || '-'}\n | \n
\n ))}\n \n | 합계 | \n {colTotals.map((total, idx) => (\n \n {total || '-'}\n | \n ))}\n \n {grandTotal} 매\n | \n
\n \n
\n
\n
\n );\n };\n\n // ===== 절곡 작업일지: 부품별 도면 포함 테이블 =====\n const FoldWorkLogTable = () => {\n const parts = workLogData.foldParts || [\n { partCode: 'FLD-001', partName: '앙카브라켓', spec: '100×50×3T', material: 'STS304', qty: 120, unit: 'EA' },\n { partCode: 'FLD-002', partName: '고정클립', spec: '80×30×2T', material: 'STS304', qty: 240, unit: 'EA' },\n { partCode: 'FLD-003', partName: '연결브라켓', spec: '150×80×3T', material: 'STS304', qty: 80, unit: 'EA' },\n ];\n const totalQty = parts.reduce((sum, p) => sum + p.qty, 0);\n\n return (\n
\n
부 품 작 업 내 역
\n
\n \n \n | NO | \n 부품코드 | \n 부품명 | \n 도면 | \n 규격 | \n 재질 | \n 수량 | \n 단위 | \n
\n \n \n {parts.map((part, idx) => (\n \n | {idx + 1} | \n {part.partCode} | \n {part.partName} | \n \n \n 📐 도면 이미지\n \n | \n {part.spec} | \n {part.material} | \n {part.qty} | \n {part.unit} | \n
\n ))}\n \n | 총 생산량: | \n {totalQty} | \n EA | \n
\n \n
\n
\n );\n };\n\n // ===== 슬랫 작업일지: LOT별 생산내역 테이블 =====\n const SlatWorkLogTable = () => {\n const lots = workLogData.slatLots || [\n { lotNo: 'SLT-241210-01', spec: '100mm', color: '화이트', productionQty: 500, defectQty: 5 },\n { lotNo: 'SLT-241210-02', spec: '100mm', color: '아이보리', productionQty: 300, defectQty: 3 },\n { lotNo: 'SLT-241210-03', spec: '80mm', color: '화이트', productionQty: 400, defectQty: 2 },\n ];\n const totalProduction = lots.reduce((sum, l) => sum + l.productionQty, 0);\n const totalDefect = lots.reduce((sum, l) => sum + l.defectQty, 0);\n const totalGood = totalProduction - totalDefect;\n const yieldRate = totalProduction > 0 ? Math.round((totalGood / totalProduction) * 100) : 0;\n\n return (\n
\n
L O T 별 생 산 내 역
\n
\n \n \n | NO | \n LOT번호 | \n 규격 | \n 색상 | \n 생산량 | \n 불량 | \n 양품 | \n 양품율 | \n
\n \n \n {lots.map((lot, idx) => {\n const good = lot.productionQty - lot.defectQty;\n const rate = lot.productionQty > 0 ? Math.round((good / lot.productionQty) * 100) : 0;\n return (\n \n | {idx + 1} | \n {lot.lotNo} | \n {lot.spec} | \n {lot.color} | \n {lot.productionQty} | \n {lot.defectQty} | \n {good} | \n {rate}% | \n
\n );\n })}\n \n | 합 계: | \n {totalProduction} | \n {totalDefect} | \n {totalGood} | \n {yieldRate}% | \n
\n \n
\n
\n );\n };\n\n // ===== 포밍 작업일지: 재고부품 생산내역 =====\n const FormingWorkLogTable = () => {\n const items = workLogData.formingItems || [\n { itemCode: 'STK-001', itemName: '슬랫 캡', spec: '100mm', qty: 200, unit: 'EA' },\n { itemCode: 'STK-002', itemName: '바텀레일', spec: '1200mm', qty: 50, unit: 'EA' },\n ];\n const totalQty = items.reduce((sum, i) => sum + i.qty, 0);\n\n return (\n
\n
포 밍 생 산 내 역
\n
\n \n \n | NO | \n 품목코드 | \n 품목명 | \n 규격 | \n 수량 | \n 단위 | \n
\n \n \n {items.map((item, idx) => (\n \n | {idx + 1} | \n {item.itemCode} | \n {item.itemName} | \n {item.spec} | \n {item.qty} | \n {item.unit} | \n
\n ))}\n \n | 총 생산량: | \n {totalQty} | \n EA | \n
\n \n
\n
\n );\n };\n\n // ===== 포장 작업일지: 박스별 수량 테이블 =====\n const PackagingWorkLogTable = () => {\n const boxes = workLogData.packagingBoxes || [\n { boxNo: 1, boxType: '대', itemName: '슬랫 100mm', qty: 50, note: '' },\n { boxNo: 2, boxType: '중', itemName: '고정클립', qty: 100, note: '' },\n { boxNo: 3, boxType: '소', itemName: '연결브라켓', qty: 40, note: '취급주의' },\n ];\n const totalBoxes = boxes.length;\n\n return (\n
\n
포 장 내 역
\n
\n \n \n | 박스번호 | \n 규격 | \n 내용물 | \n 수량 | \n 비고 | \n
\n \n \n {boxes.map(box => (\n \n | {box.boxNo} | \n {box.boxType} | \n {box.itemName} | \n {box.qty} | \n {box.note} | \n
\n ))}\n \n | 총 박스 수: | \n {totalBoxes} | \n 박스 | \n
\n \n
\n
\n );\n };\n\n // 공정별 전용 테이블 렌더링\n const renderProcessSpecificTable = () => {\n switch (docCode) {\n case 'WL-SCR': return
;\n case 'WL-FLD': return
;\n case 'WL-SLT': return
;\n case 'WL-STK': return
;\n case 'WL-PKG': return
;\n default: return null;\n }\n };\n\n // 자재 사용 내역 타이틀 (공정별)\n const getMaterialTitle = () => {\n const titles = {\n 'WL-SCR': '원단 사용내역',\n 'WL-FLD': '철판 사용내역',\n 'WL-SLT': '슬랫 자재 사용내역',\n 'WL-STK': '포밍 자재 사용내역',\n 'WL-PKG': '포장재 사용내역',\n };\n return titles[docCode] || '자재 사용 내역';\n };\n\n return (\n
\n
\n {/* 모달 헤더 */}\n
\n
\n
📋 {task.processType} 작업일지
\n {docCode}\n \n
\n
\n\n {/* 작업일지 미리보기 (ISO 인증용 양식) */}\n
\n {/* 문서 헤더 */}\n
\n
{task.processType} 작 업 일 지
\n
{workLogData.workLogNo}
\n
\n\n {/* 결재라인 (우측 상단) - APR-2LINE 스타일 */}\n
\n
\n \n \n {workLogData.approvalLine.roles.map(role => (\n | \n {role.label}\n | \n ))}\n
\n \n \n \n {workLogData.approvalLine.roles.map(role => (\n \n {role.signed ? (\n \n {role.name}\n {role.date} \n \n ) : (\n (인)\n )}\n | \n ))}\n
\n \n
\n
\n\n {/* 기본 정보 테이블 (WL-HEADER-INFO 블록) */}\n
\n \n \n | 신청업체 | \n {workLogData.customerName} | \n 작성일 | \n {workLogData.workDate} | \n
\n \n | 현장명 | \n {workLogData.siteName} | \n 작성자 | \n {workLogData.writer} | \n
\n \n | 작업지시번호 | \n {workLogData.workOrderNo} | \n
\n \n | 신청내용 | \n {workLogData.requestContent} | \n
\n \n
\n\n {/* ★ 공정별 전용 작업내역 테이블 (ISO 인증용 핵심 블록) */}\n {renderProcessSpecificTable()}\n\n {/* 공정 진행 상태 (비 ISO 공정은 범용 테이블 사용) */}\n {!renderProcessSpecificTable() && (\n <>\n
\n
\n \n 공정 진행 현황 ({workLogData.progress.completed}/{workLogData.progress.total} 완료)\n
\n
\n {workLogData.progress.steps.map((step, idx) => (\n
\n {idx + 1}. {step.name}\n {step.type === 'inspection' && 품질팀}\n {step.status.status === 'completed' && }\n {step.status.status === 'waiting' && }\n
\n ))}\n
\n
\n\n {/* 생산 실적 (범용) */}\n
\n \n \n | 지시 수량 | \n 양품 수량 | \n 불량 수량 | \n 양품율 | \n
\n \n \n \n | {workLogData.production.totalQty} | \n {workLogData.production.goodQty} | \n {workLogData.production.defectQty} | \n \n {workLogData.production.totalQty > 0\n ? Math.round((workLogData.production.goodQty / workLogData.production.totalQty) * 100)\n : 0}%\n | \n
\n \n
\n >\n )}\n\n {/* 생산실적 요약 (WL-SUMMARY 블록) */}\n {renderProcessSpecificTable() && (\n
\n
생 산 실 적 요 약
\n
\n
\n
총 생산량
\n
{workLogData.production.totalQty}
\n
\n
\n
양품 수량
\n
{workLogData.production.goodQty}
\n
\n
\n
불량 수량
\n
{workLogData.production.defectQty}
\n
\n
\n
양품율
\n
\n {workLogData.production.totalQty > 0\n ? Math.round((workLogData.production.goodQty / workLogData.production.totalQty) * 100)\n : 0}%\n
\n
\n
\n
\n )}\n\n {/* 자재 사용 내역 (WL-MATERIAL-USAGE 블록) */}\n
\n
{getMaterialTitle()}
\n {workLogData.materialUsage.length > 0 ? (\n
\n \n \n | NO | \n 자재명 | \n 입고 로트번호 | \n 사용량 | \n 투입시간 | \n 비고 | \n
\n \n \n {workLogData.materialUsage.map((material, idx) => (\n \n | {idx + 1} | \n {material.materialName} | \n {material.lotNo} | \n {material.qty} {material.unit} | \n \n {new Date(material.inputAt).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' })}\n | \n {material.remarks || ''} | \n
\n ))}\n \n
\n ) : (\n
\n 투입된 자재가 없습니다\n
\n )}\n
\n\n {/* 특이사항 (RMK-STANDARD 블록) */}\n
\n
특 이 사 항
\n
\n {workLogData.remarks || 특이사항 없음}\n
\n
\n
\n\n {/* 모달 푸터 */}\n
\n
\n
\n );\n};\n\n// 이슈 보고 모달\nconst IssueReportModal = ({ task, onClose, onSubmit }) => {\n const [issueType, setIssueType] = useState('');\n const [description, setDescription] = useState('');\n\n // 이슈 유형 - 아이콘 없이 기본 항목으로\n const issueTypes = [\n { id: 'defect', label: '불량품 발생' },\n { id: 'no-stock', label: '재고 없음' },\n { id: 'delay', label: '일정 지연' },\n { id: 'equipment', label: '설비 문제' },\n { id: 'other', label: '기타' },\n ];\n\n const handleSubmit = () => {\n if (!issueType) {\n alert('이슈 유형을 선택해주세요.');\n return;\n }\n onSubmit({\n type: issueType,\n description,\n reportedAt: new Date().toISOString(),\n taskId: task.id,\n });\n };\n\n return (\n
\n
\n
\n
이슈 보고
\n \n \n\n
\n
\n
작업: {task.workOrderNo}
\n
{task.customerName}
\n
\n\n
\n
\n
\n {issueTypes.map(type => (\n
\n ))}\n
\n
\n\n
\n \n
\n
\n\n
\n \n \n
\n
\n
\n );\n};\n\n// ============ 생산지시 상세 모달 (스텝/위자드 기반) ============\nconst ProductionOrderModal = ({ order, onClose, onConfirm, onPreview, inventory = [] }) => {\n const [currentStep, setCurrentStep] = useState(0);\n const [priority, setPriority] = useState('일반');\n const [note, setNote] = useState('');\n const [showDetailTab, setShowDetailTab] = useState('screen'); // 상세 탭 (스크린/모터/절곡)\n\n // 품목에서 카테고리 추정\n const inferCategory = (item) => {\n if (item.category) return item.category;\n const name = (item.productName || '').toLowerCase();\n if (name.includes('슬랫') || name.includes('slat')) return '슬랫';\n if (name.includes('철재') || name.includes('steel')) return '철재';\n return '스크린';\n };\n\n // 품목 카테고리 분석\n const items = order.items || [];\n const categories = [...new Set(items.map(item => inferCategory(item)))];\n const hasScreen = categories.includes('스크린');\n const hasSlat = categories.includes('슬랫') || categories.includes('철재');\n\n // 생성될 작업지시 미리보기\n const previewWorkOrders = [];\n const dateCode = new Date().toISOString().slice(2, 10).replace(/-/g, '');\n let seqNum = 1;\n\n if (hasScreen) {\n const screenItems = items.filter(item => inferCategory(item) === '스크린');\n previewWorkOrders.push({\n processType: '스크린',\n workOrderNo: `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`,\n steps: ['원단절단', '미싱', '앤드락작업', '중간검사', '포장'],\n itemCount: screenItems.length,\n totalQty: screenItems.reduce((sum, item) => sum + (item.qty || 1), 0),\n });\n }\n\n if (hasSlat) {\n const slatItems = items.filter(item => ['슬랫', '철재'].includes(inferCategory(item)));\n previewWorkOrders.push({\n processType: '슬랫',\n workOrderNo: `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`,\n steps: ['코일절단', '중간검사', '미미작업', '포장'],\n itemCount: slatItems.length,\n totalQty: slatItems.reduce((sum, item) => sum + (item.qty || 1), 0),\n });\n }\n\n // 절곡 공정 (항상 필요)\n previewWorkOrders.push({\n processType: '절곡',\n workOrderNo: `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`,\n steps: ['절단', '절곡', '중간검사', '포장'],\n itemCount: items.length,\n totalQty: items.length,\n });\n\n // 자재 소요량 계산\n const materialRequirements = [\n { code: 'SCR-MAT-001', name: '스크린 원단', unit: '㎡', required: hasScreen ? items.filter(i => inferCategory(i) === '스크린').length * 15 : 0, stock: 500 },\n { code: 'SCR-MAT-002', name: '앤드락', unit: 'EA', required: hasScreen ? items.filter(i => inferCategory(i) === '스크린').length * 2 : 0, stock: 800 },\n { code: 'SCR-MAT-003', name: '하단바', unit: 'EA', required: hasScreen ? items.filter(i => inferCategory(i) === '스크린').length : 0, stock: 120 },\n { code: 'SLT-MAT-001', name: '슬랫 코일', unit: 'KG', required: hasSlat ? items.filter(i => ['슬랫', '철재'].includes(inferCategory(i))).length * 25 : 0, stock: 1500 },\n { code: 'SLT-MAT-002', name: '미미자재', unit: 'EA', required: hasSlat ? items.filter(i => ['슬랫', '철재'].includes(inferCategory(i))).length * 4 : 0, stock: 600 },\n { code: 'BND-MAT-001', name: '철판', unit: 'KG', required: items.length * 30, stock: 2000 },\n { code: 'BND-MAT-002', name: '가이드레일', unit: 'M', required: items.length * 6, stock: 300 },\n { code: 'BND-MAT-003', name: '케이스', unit: 'EA', required: items.length, stock: 100 },\n ].filter(m => m.required > 0);\n\n // 재고에서 실제 수량 반영\n const materialsWithStock = materialRequirements.map(m => {\n const inventoryItem = inventory.find(i => i.materialCode === m.code);\n return {\n ...m,\n stock: inventoryItem?.stock ?? m.stock,\n isShort: (inventoryItem?.stock ?? m.stock) < m.required,\n };\n });\n\n // 품목에서 productionSpec 추출\n const screenItems = order.items?.map((item, idx) => {\n const spec = item.productionSpec || {};\n return {\n seq: String(idx + 1).padStart(2, '0'),\n type: spec.type || '와이어',\n drawingNo: spec.drawingNo || `${item.floor} ${item.location}`,\n openWidth: spec.openWidth || parseInt(item.spec?.split('×')[0]) || 0,\n openHeight: spec.openHeight || parseInt(item.spec?.split('×')[1]) || 0,\n prodWidth: spec.prodWidth || (parseInt(item.spec?.split('×')[0]) || 0) + 140,\n prodHeight: spec.prodHeight || Math.max((parseInt(item.spec?.split('×')[1]) || 0) + 400, 2950),\n guideRailType: spec.guideRailType || '백면형',\n guideRailSpec: spec.guideRailSpec || '120-70',\n shaft: spec.shaft || (parseInt(item.spec?.split('×')[0]) > 6000 ? 5 : 4),\n caseSpec: spec.caseSpec || '500-330',\n motorBracket: spec.motorBracket || '380-180',\n capacity: spec.capacity || (parseInt(item.spec?.split('×')[0]) > 6000 ? 300 : 160),\n finish: spec.finish || 'SUS마감',\n };\n }) || [];\n\n // 모터 스펙 (있으면 사용, 없으면 자동 계산)\n const motorSpec = order.motorSpec || {\n motors220V: [\n { model: 'KD-150K', qty: 0 },\n { model: 'KD-300K', qty: 0 },\n { model: 'KD-400K', qty: 0 },\n ],\n motors380V: [\n { model: 'KD-150K', qty: screenItems.filter(i => i.capacity <= 160).length },\n { model: 'KD-300K', qty: screenItems.filter(i => i.capacity > 160 && i.capacity <= 300).length },\n { model: 'KD-400K', qty: screenItems.filter(i => i.capacity > 300).length },\n ],\n brackets: [\n { spec: '380-180 [2-4\"]', qty: screenItems.filter(i => i.shaft === 4).length },\n { spec: '380-180 [2-5\"]', qty: screenItems.filter(i => i.shaft === 5).length },\n ],\n heatSinks: [\n { spec: '40-60, L-380', qty: screenItems.length * 4 },\n ],\n controllers: [\n { type: '매입형', qty: 0 },\n { type: '노출형', qty: 0 },\n { type: '핫라스', qty: 0 },\n ],\n };\n\n // BOM 데이터 (있으면 사용, 없으면 자동 계산)\n const bomData = order.bomData || {\n guideRails: {\n items: [\n { type: '백면형', spec: '120-70', code: 'KSE01/KWE01', lengths: [{ length: 3000, qty: screenItems.length * 2 }] },\n { type: '하부BASE', spec: '130-80', code: '', lengths: [{ length: 0, qty: screenItems.length * 2 }] },\n ],\n smokeBarrier: { spec: 'W80', lengths: [{ length: 2950, qty: screenItems.length * 2 }] },\n },\n cases: {\n mainSpec: '500-330',\n items: [\n { length: 4000, qty: Math.ceil(screenItems.length * 0.6) },\n { length: 3500, qty: Math.ceil(screenItems.length * 0.3) },\n { length: 3000, qty: Math.ceil(screenItems.length * 0.1) },\n ],\n sideCover: { spec: '500-355', qty: screenItems.length * 2 },\n smokeBarrier: { spec: 'W80', length: 3000, qty: Math.ceil(screenItems.length * 1.5) },\n },\n bottomFinish: {\n items: [\n { name: '하단마감재', spec: '50-40', lengths: [{ length: 4000, qty: screenItems.length }] },\n { name: '하단보강빔바', spec: '80-17', lengths: [{ length: 4000, qty: screenItems.length * 2 }] },\n { name: '하단부재횡철', spec: '50-12T', lengths: [{ length: 2000, qty: screenItems.length * 4 }] },\n ],\n },\n };\n\n // 스텝 정의 (4단계로 간소화)\n const steps = [\n { id: 'info', label: '기본정보', icon: FileText, description: '우선순위 및 메모' },\n { id: 'preview', label: '작업지시 확인', icon: ClipboardList, description: '생성될 작업지시' },\n { id: 'material', label: '자재확인', icon: Box, description: '자재 재고 확인' },\n { id: 'detail', label: '상세확인', icon: Package, description: '품목/BOM 상세' },\n ];\n\n // 스텝 이동\n const goNext = () => setCurrentStep(prev => Math.min(prev + 1, steps.length - 1));\n const goPrev = () => setCurrentStep(prev => Math.max(prev - 1, 0));\n const isLastStep = currentStep === steps.length - 1;\n\n // 백면형 가이드레일 전개도 SVG\n const GuideRailFrontSVG = () => (\n
\n );\n\n // 측면형 가이드레일 전개도 SVG\n const GuideRailSideSVG = () => (\n
\n );\n\n // 케이스(셔터박스) 전개도 SVG\n const CaseBoxSVG = () => (\n
\n );\n\n const hasShortage = materialsWithStock.some(m => m.isShort);\n\n return (\n
\n
\n {/* 헤더 - 고정 */}\n
\n
\n
\n
\n
생산지시
\n
{order.orderNo} · {order.siteName}
\n
\n
\n
\n {/* 요약 정보 */}\n
\n
\n {previewWorkOrders.length}개 작업지시\n
\n {hasShortage && (\n
\n )}\n
\n
\n
\n
\n\n {/* 탭 네비게이션 - 고정 */}\n
\n {tabs.map((tab) => (\n \n ))}\n
\n\n {/* 탭 콘텐츠 - 스크롤 */}\n
\n {/* 기본정보 탭 */}\n {activeTab === 'info' && (\n
\n {/* 신용등급 안내 */}\n
\n
\n \n \n {order.creditGrade === 'A' ? '자동 진행 거래처' :\n order.creditGrade === 'B' ? '입금확인 후 출고 거래처' :\n '출고 시 경리승인 필요 거래처'}\n \n
\n
\n {order.creditGrade === 'A' && '생산지시 후 자동으로 생산이 진행되고, 완료 후 바로 출고됩니다.'}\n {order.creditGrade === 'B' && '생산지시 후 생산이 진행되며, 출고 시 입금확인이 필요합니다.'}\n {order.creditGrade === 'C' && (\n order.accountingStatus === '회계확인완료'\n ? '✅ 경리승인 완료. 생산지시 가능하며, 출고 시 입금확인이 필요합니다.'\n : '⚠️ 생산지시는 가능하나, 출고 시 경리승인이 필요합니다.'\n )}\n
\n
\n\n {/* 수주 정보 */}\n
\n
\n \n
\n 발주일\n {order.orderDate}\n
\n
\n 발주처\n {order.customerName}\n
\n
\n 발주담당자\n {order.manager || '-'}\n
\n
\n 연락처\n {order.contact || '-'}\n
\n
\n \n\n
\n \n
\n 현장명\n {order.siteName}\n
\n
\n 납기요청일\n {order.dueDate}\n
\n
\n 인수담당자\n {order.receiverName || '-'}\n
\n
\n 배송방법\n {order.deliveryMethod || '상차'}\n
\n
\n \n
\n\n {/* 배송지 */}\n
\n {order.deliveryAddress || '-'}
\n \n
\n )}\n\n {/* 작업지시 미리보기 탭 */}\n {activeTab === 'preview' && (\n
\n
\n
\n 생성될 작업지시 ({previewWorkOrders.length}건)\n
\n \n\n {/* 작업지시 카드들 */}\n
\n {previewWorkOrders.map((wo, idx) => (\n
\n
\n {wo.processType} 공정\n {wo.workOrderNo}\n
\n
\n {/* 수량 정보 */}\n
\n
\n
품목 수
\n
{wo.itemCount}건
\n
\n
\n
총 수량
\n
{wo.totalQty}EA
\n
\n
\n\n {/* 작업 단계 */}\n
\n
작업 단계
\n
\n {wo.steps.map((step, sidx) => (\n \n {sidx + 1}. {step}\n \n ))}\n
\n
\n
\n
\n ))}\n
\n\n {/* 옵션 설정 */}\n
\n \n
\n
\n
\n {['긴급', '높음', '일반', '낮음'].map(p => (\n \n ))}\n
\n
\n
\n \n
\n
\n \n
\n )}\n\n {/* 자재 소요량 탭 */}\n {activeTab === 'material' && (\n
\n
\n
\n \n 자재 소요량 확인\n
\n {hasShortage && (\n
\n )}\n
\n\n
\n \n
\n \n \n | 자재코드 | \n 자재명 | \n 단위 | \n 소요량 | \n 현재고 | \n 예상잔고 | \n 상태 | \n
\n \n \n {materialsWithStock.map((m, idx) => (\n \n | {m.code} | \n {m.name} | \n {m.unit} | \n {m.required.toLocaleString()} | \n {m.stock.toLocaleString()} | \n \n {(m.stock - m.required).toLocaleString()}\n | \n \n {m.isShort ? (\n \n 부족\n \n ) : (\n \n 충분\n \n )}\n | \n
\n ))}\n \n
\n
\n \n\n {hasShortage && (\n
\n
\n
\n
\n
재고 부족 경고
\n
\n 일부 자재의 재고가 부족합니다. 생산지시는 가능하지만, 자재 입고 후 생산을 진행하시기 바랍니다.\n
\n
\n
\n
\n
\n )}\n
\n )}\n\n {/* 스크린 품목 탭 */}\n {activeTab === 'screen' && (\n
\n
\n
1. 스크린 ({screenItems.length}개)
\n
* 가이드레일 너비 120° 기준
\n
\n
\n
\n \n \n | 일련번호 | \n 종류 | \n 도면부호 | \n 오픈사이즈(mm) | \n 제작사이즈(mm) | \n 가이드레일 유형 | \n 샤프트 (인치) | \n 케이스 (규격) | \n 모터 | \n 마감 | \n
\n \n | 가로 | \n 세로 | \n (오픈+140) | \n (오픈+350) | \n 브라켓 | \n 용량(KG) | \n
\n \n \n {screenItems.map((item, idx) => (\n \n | {item.seq} | \n {item.type} | \n {item.drawingNo} | \n {item.openWidth?.toLocaleString()} | \n {item.openHeight?.toLocaleString()} | \n {item.prodWidth?.toLocaleString()} | \n {item.prodHeight?.toLocaleString()} | \n {item.guideRailType} ({item.guideRailSpec}) | \n {item.shaft} | \n {item.caseSpec} | \n {item.motorBracket} | \n {item.capacity} | \n {item.finish} | \n
\n ))}\n \n
\n
\n
\n )}\n\n {/* 모터/전장품 탭 */}\n {activeTab === 'motor' && (\n
\n
2. 모터
\n\n
\n {/* 2-1. 모터(220V 단상) */}\n
\n \n \n \n | 모터 용량 | \n 수량 | \n
\n \n \n {motorSpec.motors220V?.map((motor, idx) => (\n \n | {motor.model} | \n {motor.qty || '-'} | \n
\n ))}\n \n
\n \n\n {/* 2-2. 모터(380V 삼상) */}\n
\n \n \n \n | 모터 용량 | \n 수량 | \n
\n \n \n {motorSpec.motors380V?.map((motor, idx) => (\n \n | {motor.model} | \n {motor.qty || '-'} | \n
\n ))}\n \n
\n \n\n {/* 2-3. 브라켓 */}\n
\n \n \n \n | 브라켓 | \n 수량 | \n
\n \n \n {motorSpec.brackets?.map((bracket, idx) => (\n \n | {bracket.spec} | \n {bracket.qty || '-'} | \n
\n ))}\n \n | 방열등 | \n {motorSpec.heatSinks?.[0]?.qty || screenItems.length * 4} | \n
\n \n
\n \n\n {/* 2-4. 연동제어기 */}\n
\n \n \n \n | 품명 | \n 수량 | \n
\n \n \n {motorSpec.controllers?.map((ctrl, idx) => (\n \n | {ctrl.type} | \n {ctrl.qty || '-'} | \n
\n ))}\n \n
\n \n
\n\n
* 별도 추가사항
\n
\n )}\n\n {/* 절곡물 BOM 탭 */}\n {activeTab === 'bom' && (\n
\n
3. 절곡물
\n\n {/* 3-1. 가이드레일 */}\n
\n
\n 3-1. 가이드레일\n - EGI 1.55T + 마감재 EGI 1.15T + 별도마감재 SUS 1.15T\n
\n\n
\n {/* 백면형 */}\n
\n
백면형 (120-70)
\n
\n
\n
\n ① 마감재 ② 가이드레일 ③ C형 ④ D형\n
\n
\n \n \n | 코드 | \n 길이 | \n 수량 | \n
\n \n \n {bomData.guideRails?.items?.filter(i => i.type === '백면형').map((item, idx) => (\n item.lengths?.map((len, lidx) => (\n 0 ? 'bg-blue-50' : ''}>\n | {item.code || '-'} | \n L: {len.length?.toLocaleString()} | \n {len.qty || '-'} | \n
\n ))\n ))}\n \n
\n
\n
\n\n {/* 측면형 */}\n
\n
측면형 (120-120)
\n
\n
\n
\n ① ② 마감재 ③ ④ 가이드레일 ⑤ C형 ⑥ D형\n
\n
\n \n \n | 코드 | \n 길이 | \n 수량 | \n
\n \n \n {bomData.guideRails?.items?.filter(i => i.type === '측면형').map((item, idx) => (\n item.lengths?.map((len, lidx) => (\n 0 ? 'bg-blue-50' : ''}>\n | {item.code || '-'} | \n L: {len.length?.toLocaleString()} | \n {len.qty || '-'} | \n
\n ))\n ))}\n \n
\n
\n
\n
\n\n {/* 하부BASE & 연기차단재 */}\n
\n
\n \n \n {bomData.guideRails?.items?.filter(i => i.type === '하부BASE').map((item, idx) => (\n \n | 하부BASE | \n {item.lengths?.[0]?.qty || '-'}개 | \n
\n ))}\n \n
\n \n\n
\n \n
원 0.8T 화이바글라스코팅직물
\n
* 가이드레일 양쪽에 설치
\n
\n \n \n | 규격(L) | \n 수량 | \n
\n \n \n {bomData.guideRails?.smokeBarrier?.lengths?.map((len, idx) => (\n \n | {len.length?.toLocaleString()} | \n {len.qty} | \n
\n ))}\n \n
\n
\n \n
\n
\n\n {/* 3-2. 케이스(셔터박스) */}\n
\n
\n 3-2. 케이스(셔터박스)\n - EGI 1.55T\n
\n\n
\n
\n
규격: {bomData.cases?.mainSpec} (150,300,400/K용)
\n
\n
\n
\n \n \n | 규격 | \n 길이 | \n 수량 | \n
\n \n \n {bomData.cases?.items?.map((item, idx) => (\n 0 ? 'bg-pink-50' : ''}>\n | {bomData.cases?.mainSpec} | \n L: {item.length?.toLocaleString()} | \n {item.qty || '-'} | \n
\n ))}\n \n
\n
\n
\n\n
\n
\n \n
규격: {bomData.cases?.sideCover?.spec}
\n
{bomData.cases?.sideCover?.qty}개
\n
\n \n
\n \n
규격: 1219-389
\n
{bomData.cases?.topCover?.qty || 0}개
\n
\n \n
\n \n
전면부, 린텔부 양쪽에 설치
\n
L: {bomData.cases?.smokeBarrier?.length?.toLocaleString()} × {bomData.cases?.smokeBarrier?.qty}개
\n
\n \n
\n
\n
\n\n {/* 3-3. 하단마감재 */}\n
\n
\n 3-3. 하단마감재\n - 하단마감재(EGI 1.55T) + 하단보강빔바(EGI 1.55T) + 하단 무게평철(50-12T)\n
\n\n
\n
\n \n \n | 구성품 | \n 길이 (mm) | \n 수량 | \n 구성품 | \n 길이 (mm) | \n 수량 | \n
\n \n \n \n 하단마감재 (50-40) | \n L: 4,000 | \n {bomData.bottomFinish?.items?.[0]?.lengths?.[0]?.qty || 11} | \n 하단보강빔바 (80-17) | \n L: 4,000 | \n {bomData.bottomFinish?.items?.[1]?.lengths?.[0]?.qty || 28} | \n
\n \n | L: 3,000 | \n {bomData.bottomFinish?.items?.[0]?.lengths?.[1]?.qty || 8} | \n L: 3,000 | \n {bomData.bottomFinish?.items?.[1]?.lengths?.[1]?.qty || 12} | \n
\n \n | 하단보강철 | \n L: 4,000 | \n {bomData.bottomFinish?.items?.[2]?.lengths?.[0]?.qty || 13} | \n 하단 부재횡철 (50-12T) | \n L: 2,000 | \n {bomData.bottomFinish?.items?.[3]?.lengths?.[0]?.qty || 67} | \n
\n \n
\n
\n
\n
\n )}\n
\n\n {/* 하단 버튼 - 고정 */}\n
\n \n \n \n
\n
\n
\n );\n};\n\n// ============ 생산지시 생성 페이지 (전체 페이지 형태 - 섹션 기반) ============\nconst ProductionOrderCreatePage = ({ order, onBack, onConfirm, onNavigate, inventory = [] }) => {\n const [priority, setPriority] = useState('일반');\n const [note, setNote] = useState('');\n // ★ 생산지시 완료 확인 다이얼로그 상태\n const [showCompleteDialog, setShowCompleteDialog] = useState(false);\n const [completeInfo, setCompleteInfo] = useState({ workOrders: [] });\n\n // ★ 생산지시 확정 핸들러 (다이얼로그 표시)\n const handleConfirm = () => {\n // 상위 컴포넌트에 확정 요청\n onConfirm?.(order);\n // 다이얼로그 표시\n setCompleteInfo({ workOrders: previewWorkOrders, itemCount: items.length });\n setShowCompleteDialog(true);\n };\n\n // ★ 공정관리 연동: classifyItemToProcess를 사용하여 품목을 공정별로 자동 분류\n const classifyItem = (item) => {\n const result = classifyItemToProcess(item);\n return result.processName || '미분류';\n };\n\n const items = order?.items || [];\n\n // ★ 공정별 품목 그룹핑 (공정관리 규칙 기반)\n const itemsByProcess = items.reduce((acc, item) => {\n const processName = classifyItem(item);\n if (!acc[processName]) {\n acc[processName] = [];\n }\n acc[processName].push(item);\n return acc;\n }, {});\n\n // ★ 공정별 작업지시 생성 프리뷰 (공정관리 기준)\n const processWorkflows = {\n '스크린': { steps: ['원단절단', '미싱', '앤드락작업', '중간검사', '포장'], color: 'blue' },\n '슬랫': { steps: ['코일절단', '중간검사', '미미작업', '포장'], color: 'green' },\n '절곡': { steps: ['절단', '절곡', '중간검사', '포장'], color: 'orange' },\n '미분류': { steps: ['작업준비', '가공', '검사', '포장'], color: 'gray' },\n };\n\n const previewWorkOrders = [];\n const dateCode = new Date().toISOString().slice(2, 10).replace(/-/g, '');\n let seqNum = 1;\n\n // ★ 분류된 공정별로 작업지시 생성 (동적 생성)\n Object.entries(itemsByProcess).forEach(([processName, processItems]) => {\n if (processItems.length === 0) return;\n\n const workflow = processWorkflows[processName] || processWorkflows['미분류'];\n previewWorkOrders.push({\n processType: processName,\n processId: classifyItemToProcess(processItems[0]).processId,\n workOrderNo: `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`,\n steps: workflow.steps,\n color: workflow.color,\n itemCount: processItems.length,\n totalQty: processItems.reduce((sum, item) => sum + (item.qty || 1), 0),\n items: processItems, // ★ 해당 공정의 품목 목록 포함\n });\n });\n\n // ★ 절곡 공정은 모든 품목에 공통 적용 (가이드레일, 케이스 등)\n if (!itemsByProcess['절곡'] && items.length > 0) {\n previewWorkOrders.push({\n processType: '절곡',\n processId: 'P-002',\n workOrderNo: `KD-PL-${dateCode}-${String(seqNum++).padStart(2, '0')}`,\n steps: processWorkflows['절곡'].steps,\n color: 'orange',\n itemCount: items.length,\n totalQty: items.length,\n items: items, // 전체 품목 대상\n note: '공통 절곡 부품 제작',\n });\n }\n\n // 레거시 호환을 위한 카테고리 체크\n const hasScreen = !!itemsByProcess['스크린'];\n const hasSlat = !!itemsByProcess['슬랫'];\n\n // 레거시 inferCategory 함수 (자재 요구량 계산용)\n const inferCategory = (item) => {\n const processName = classifyItem(item);\n if (processName === '슬랫') return '슬랫';\n if (processName === '절곡') return '철재';\n return '스크린';\n };\n\n const materialRequirements = [\n { code: 'SCR-MAT-001', name: '스크린 원단', unit: '㎡', required: hasScreen ? items.filter(i => inferCategory(i) === '스크린').length * 15 : 0, stock: 500 },\n { code: 'SCR-MAT-002', name: '앤드락', unit: 'EA', required: hasScreen ? items.filter(i => inferCategory(i) === '스크린').length * 2 : 0, stock: 800 },\n { code: 'BND-MAT-001', name: '철판', unit: 'KG', required: items.length * 30, stock: 2000 },\n { code: 'BND-MAT-002', name: '가이드레일', unit: 'M', required: items.length * 6, stock: 300 },\n { code: 'BND-MAT-003', name: '케이스', unit: 'EA', required: items.length, stock: 100 },\n ].filter(m => m.required > 0);\n\n const materialsWithStock = materialRequirements.map(m => {\n const inventoryItem = inventory.find(i => i.materialCode === m.code);\n return { ...m, stock: inventoryItem?.stock ?? m.stock, isShort: (inventoryItem?.stock ?? m.stock) < m.required };\n });\n\n const screenItems = order?.items?.map((item, idx) => {\n const spec = item.productionSpec || {};\n const openW = spec.openWidth || parseInt(item.spec?.split('×')[0]) || item.openWidth || 0;\n const openH = spec.openHeight || parseInt(item.spec?.split('×')[1]) || item.openHeight || 0;\n return {\n seq: String(idx + 1).padStart(2, '0'), type: spec.type || '와이어',\n drawingNo: spec.drawingNo || `${item.floor || ''} ${item.location || ''}`.trim() || `품목 ${idx + 1}`,\n openWidth: openW, openHeight: openH, prodWidth: openW + 140, prodHeight: Math.max(openH + 400, 2950),\n guideRailType: spec.guideRailType || '백면형', guideRailSpec: spec.guideRailSpec || '120-70',\n shaft: openW > 6000 ? 5 : 4, capacity: openW > 6000 ? 300 : 160, finish: spec.finish || 'SUS마감',\n productName: item.productName || '', qty: item.qty || 1,\n };\n }) || [];\n\n const motorSpec = {\n motors380V: [\n { model: 'KD-150K', qty: screenItems.filter(i => i.capacity <= 160).length },\n { model: 'KD-300K', qty: screenItems.filter(i => i.capacity > 160 && i.capacity <= 300).length },\n ],\n brackets: [\n { spec: '380-180 [2-4\"]', qty: screenItems.filter(i => i.shaft === 4).length },\n { spec: '380-180 [2-5\"]', qty: screenItems.filter(i => i.shaft === 5).length },\n ],\n };\n\n const bomData = {\n guideRails: {\n items: [\n { type: '백면형', spec: '120-70', code: 'KSE01/KWE01', lengths: [{ length: 3000, qty: screenItems.length * 2 }] },\n ]\n },\n cases: { mainSpec: '500-330', items: [{ length: 4000, qty: Math.ceil(screenItems.length * 0.6) }], sideCover: { spec: '500-355', qty: screenItems.length * 2 } },\n bottomFinish: { items: [{ name: '하단마감재', spec: '50-40', lengths: [{ length: 4000, qty: screenItems.length }] }] },\n };\n\n const hasShortage = materialsWithStock.some(m => m.isShort);\n\n if (!order) return
;\n\n return (\n
\n {/* 헤더 영역 - 타이틀 위, 버튼 아래 */}\n
\n {/* 타이틀 영역 */}\n
\n
\n \n 생산지시 생성\n
\n
{previewWorkOrders.length}개 작업지시 생성 예정\n {hasShortage &&
자재 부족}\n
\n {/* 버튼 영역 */}\n
\n
\n
\n
\n
\n\n {/* 수주 정보 섹션 */}\n
\n \n
\n
\n
\n
\n
\n
총수량{items.reduce((sum, i) => sum + (i.qty || 1), 0)}EA
\n
\n
\n
\n \n\n {/* 생산지시 옵션 섹션 - 우선순위 매핑 테이블 포함 */}\n
\n \n {/* 우선순위 선택 + 매핑 테이블 */}\n
\n
\n
\n {['긴급', '높음', '일반', '낮음'].map(p => (\n \n ))}\n
\n\n {/* 우선순위 매핑 테이블 */}\n
\n
\n \n \n | 생산지시 (영업) | \n 작업지시 기본값 (공장) | \n 비고 (현장의 여유 공간) | \n
\n \n \n \n | 긴급 | \n 1순위 | \n 무조건 제일 먼저 | \n
\n \n | 높음 | \n 3순위 | \n (2순위는 현장에서 '새치기' 할 때 쓰도록 비워둠) | \n
\n \n | 일반 | \n 5순위 | \n (4순위 비워둠) | \n
\n \n | 낮음 | \n 9순위 | \n 뒤로 쪽 밀어둠 | \n
\n \n
\n\n {/* 선택된 우선순위 결과 표시 */}\n
\n 선택된 설정:\n \n 생산지시: {priority}\n \n →\n \n 작업지시: {priority === '긴급' ? '1순위' : priority === '높음' ? '3순위' : priority === '일반' ? '5순위' : '9순위'}\n \n
\n
\n
\n\n {/* 메모 */}\n
\n \n
\n
\n \n\n {/* 생성될 작업지시 섹션 */}\n
\n \n {previewWorkOrders.map((wo, idx) => (\n
\n
\n {wo.processType}\n {wo.workOrderNo}\n
\n
\n
품목 수{wo.itemCount}건
\n
총 수량{wo.totalQty}EA
\n
\n
\n
공정 순서
\n
{wo.steps.map((step, i) => {i + 1}. {step})}
\n
\n
\n ))}\n
\n \n\n {/* 자재 소요량 섹션 */}\n
\n {hasShortage && 일부 자재의 재고가 부족합니다. 구매 요청이 필요합니다. }\n \n
\n | 자재코드 | 자재명 | 단위 | 소요량 | 현재고 | 상태 |
\n \n {materialsWithStock.map((m, idx) => (\n \n | {m.code} | \n {m.name} | \n {m.unit} | \n {m.required.toLocaleString()} | \n {m.stock.toLocaleString()} | \n {m.isShort ? 부족 : 충분} | \n
\n ))}\n \n
\n
\n \n\n {/* 스크린 품목 상세 섹션 */}\n {screenItems.length > 0 && (\n
\n \n
\n | No | 품목명 | 도번/위치 | 개구폭 | 개구높이 | 제작폭 | 제작높이 | 가이드레일 | 샤프트 | 용량 | 마감 | 수량 |
\n \n {screenItems.map((item, idx) => (\n \n | {item.seq} | \n {item.productName || '국민방화스크린셔터'} | \n {item.drawingNo} | \n {item.openWidth.toLocaleString()} | \n {item.openHeight.toLocaleString()} | \n {item.prodWidth.toLocaleString()} | \n {item.prodHeight.toLocaleString()} | \n {item.guideRailType} {item.guideRailSpec} | \n {item.shaft}\" | \n {item.capacity}kg | \n {item.finish} | \n {item.qty} | \n
\n ))}\n \n
\n
\n \n )}\n\n {/* 모터/전장품 섹션 - 수량 0보다 큰 항목만 표시 */}\n {(motorSpec.motors380V.some(m => m.qty > 0) || motorSpec.brackets.some(b => b.qty > 0)) && (\n
\n \n {motorSpec.motors380V.filter(m => m.qty > 0).length > 0 && (\n
\n
모터 사양 (380V)
\n
{motorSpec.motors380V.filter(m => m.qty > 0).map((m, idx) =>
{m.model}{m.qty}대
)}
\n
\n )}\n {motorSpec.brackets.filter(b => b.qty > 0).length > 0 && (\n
\n
모터 브라켓
\n
{motorSpec.brackets.filter(b => b.qty > 0).map((b, idx) =>
{b.spec}{b.qty}개
)}
\n
\n )}\n
\n \n )}\n\n {/* 절곡물 BOM 섹션 */}\n
\n \n {/* 가이드레일 */}\n
\n
가이드레일
\n
\n | 형태 | 규격 | 코드 | 길이 | 수량 |
\n {bomData.guideRails.items.map((item, idx) => | {item.type} | {item.spec} | {item.code} | {item.lengths[0]?.length} | {item.lengths[0]?.qty} |
)}\n
\n
\n {/* 케이스 */}\n
\n
케이스(셔터박스) - 메인 규격: {bomData.cases.mainSpec}
\n
\n | 품목 | 길이 | 수량 |
\n \n {bomData.cases.items.map((item, idx) => | 케이스 본체 | L: {item.length} | {item.qty} |
)}\n | 측면 덮개 | {bomData.cases.sideCover.spec} | {bomData.cases.sideCover.qty} |
\n \n
\n
\n {/* 하단 마감재 */}\n
\n
하단 마감재
\n
\n | 품목 | 규격 | 길이 | 수량 |
\n {bomData.bottomFinish.items.map((item, idx) => | {item.name} | {item.spec} | L: {item.lengths[0]?.length} | {item.lengths[0]?.qty} |
)}\n
\n
\n
\n \n\n {/* 하단 확정 버튼 영역 */}\n
\n
\n {priority !== '일반' && 우선순위: {priority}}\n {note && 메모: {note.substring(0, 50)}{note.length > 50 ? '...' : ''}}\n
\n
\n
\n
\n
\n
\n\n {/* ★ 생산지시 완료 확인 다이얼로그 */}\n {showCompleteDialog && (\n
\n
\n {/* 헤더 */}\n
\n
\n
\n \n
\n
\n
생산지시 완료
\n
작업지시가 성공적으로 생성되었습니다.
\n
\n
\n
\n\n {/* 내용 */}\n
\n
\n
\n 생성된 작업지시 ({completeInfo.workOrders?.length || 0}건)\n
\n
\n {completeInfo.workOrders?.map((wo, idx) => (\n
\n \n {wo.processType}:\n {wo.workOrderNo}\n
\n ))}\n
\n
\n\n {completeInfo.itemCount && (\n
\n 품목 수: {completeInfo.itemCount}건\n
\n )}\n
\n\n {/* 버튼 */}\n
\n \n \n
\n
\n
\n )}\n
\n );\n};\n\n// ============ 생산지시서(발주서 양식) 출력 모달 ============\nconst ProductionOrderSheet = ({ workOrder, order, orderData, onClose, title = '생산지시서' }) => {\n const printRef = React.useRef(null);\n\n // 수주/작업지시 데이터에서 발주서 형식으로 변환\n const convertToSheetData = () => {\n // 외부에서 orderData가 제공되면 그대로 사용\n if (orderData) return orderData;\n\n // order(수주) 데이터가 있으면 변환\n const sourceOrder = order || workOrder;\n if (!sourceOrder) return sampleDetailedOrderData;\n\n // 스크린 아이템 변환\n const screenItems = (sourceOrder.items || []).map((item, idx) => {\n const spec = item.productionSpec || {};\n // spec 문자열에서 크기 파싱 (예: \"7660×2550\")\n const [openW, openH] = (item.spec || '0×0').split('×').map(s => parseInt(s) || 0);\n\n return {\n seq: idx + 1,\n type: spec.type || '와이어',\n drawingNo: spec.drawingNo || `${item.floor} ${item.location}`,\n openWidth: spec.openWidth || openW,\n openHeight: spec.openHeight || openH,\n prodWidth: spec.prodWidth || openW + 140,\n prodHeight: spec.prodHeight || 2950,\n guideRailType: spec.guideRailType || '백면형',\n guideRailSpec: spec.guideRailSpec || '120-70',\n shaft: spec.shaft || (openW > 6000 ? 5 : 4),\n caseSpec: spec.caseSpec || '500-330',\n motorBracket: spec.motorBracket || '380-180',\n capacity: spec.capacity || (openW > 6000 ? 300 : 160),\n finish: spec.finish || 'SUS마감',\n };\n });\n\n // 모터 스펙 (있으면 사용, 없으면 기본값)\n const motorSpec = sourceOrder.motorSpec || {\n motors220V: [\n { model: 'KD-150K', qty: 0 },\n { model: 'KD-300K', qty: 0 },\n { model: 'KD-400K', qty: 0 },\n ],\n motors380V: [\n { model: 'KD-150K', qty: screenItems.filter(i => i.capacity <= 160).length },\n { model: 'KD-300K', qty: screenItems.filter(i => i.capacity > 160).length },\n { model: 'KD-400K', qty: 0 },\n ],\n brackets: [\n { spec: '380-180 [2-4\"]', qty: 0 },\n { spec: '380-180 [2-5\"]', qty: screenItems.length },\n ],\n heatSinks: [\n { spec: '40-60', qty: screenItems.length * 2 },\n { spec: 'L-380', qty: 0 },\n ],\n controllers: [\n { type: '매입형', qty: 0 },\n { type: '노출형', qty: 0 },\n { type: '핫라스', qty: 0 },\n ],\n };\n\n // BOM 데이터 (있으면 사용, 없으면 자동 계산)\n const bomData = sourceOrder.bomData || {\n guideRails: {\n description: '가이드레일 - EGI 1.55T + 마감재 EGI 1.15T + 별도마감재 SUS 1.15T',\n items: [\n {\n type: '백면형', spec: '120-70', code: 'KSE01/KWE01', lengths: [\n { length: 3000, qty: screenItems.length * 2 },\n ]\n },\n {\n type: '측면형', spec: '120-120', code: 'KSS01', lengths: [\n { length: 4300, qty: 0 },\n { length: 4000, qty: 0 },\n { length: 3500, qty: 0 },\n { length: 3000, qty: 0 },\n ]\n },\n {\n type: '하부BASE', spec: '130-80', code: '', lengths: [\n { length: 0, qty: screenItems.length * 2 },\n ]\n },\n ],\n smokeBarrier: {\n spec: 'W80',\n lengths: [{ length: 2950, qty: screenItems.length * 2 }],\n },\n },\n cases: {\n description: '케이스(셔터박스) - EGI 1.55T',\n mainSpec: '500-330 (150,300,400/K용)',\n items: [\n { length: 4000, qty: Math.ceil(screenItems.length / 2) },\n { length: 3500, qty: Math.floor(screenItems.length / 2) },\n ],\n sideCover: { spec: '500-355', qty: screenItems.length * 2 },\n topCover: { qty: 0 },\n extension: { spec: '1219+무게', qty: screenItems.length * 3 },\n smokeBarrier: { spec: 'W80', length: 3000, qty: screenItems.length * 2 },\n },\n bottomFinish: {\n description: '하단마감재 - 마단마감재(EGI 1.55T) + 하단보강별바(EGI 1.55T) + 하단 보강횡철(EGI 1.15T) + 하단 부재횡철(50-12T)',\n items: [\n {\n name: '하단마감재', spec: '50-40', lengths: [\n { length: 4000, qty: screenItems.length },\n { length: 3000, qty: 0 },\n ]\n },\n {\n name: '하단보강빔바', spec: '80-17', lengths: [\n { length: 4000, qty: screenItems.length * 2 },\n { length: 3000, qty: 0 },\n ]\n },\n {\n name: '하단보강철', spec: '', lengths: [\n { length: 4000, qty: screenItems.length },\n { length: 3000, qty: 0 },\n ]\n },\n {\n name: '하단부재횡철', spec: '50-12T', lengths: [\n { length: 2000, qty: screenItems.length * 4 },\n ]\n },\n ],\n },\n };\n\n return {\n orderInfo: {\n lotNo: sourceOrder.orderNo || sourceOrder.workOrderNo || 'N/A',\n productName: '국민방화스크린셔터스테',\n productType: 'KWE01',\n certNo: 'FDS-OTS23-0117-4',\n orderDate: sourceOrder.orderDate || new Date().toISOString().split('T')[0],\n siteName: sourceOrder.siteName || '',\n customerName: sourceOrder.customerName || '',\n dueDate: sourceOrder.dueDate || '',\n receiverManager: sourceOrder.receiverName || sourceOrder.manager || '',\n receiverContact: sourceOrder.receiverPhone || sourceOrder.contact || '',\n orderManager: sourceOrder.manager || '',\n deliveryDate: sourceOrder.dueDate || '',\n totalQty: screenItems.length,\n deliveryMethod: sourceOrder.deliveryMethod || '상차',\n deliveryAddress: sourceOrder.deliveryAddress || '',\n // 결재라인 정보\n approval: sourceOrder.approval || {\n drafter: sourceOrder.createdBy ? {\n name: sourceOrder.createdBy.replace(/^(판매팀|생산팀|전진팀)\\s*/, ''),\n date: sourceOrder.createdAt || sourceOrder.orderDate\n } : null,\n approver: sourceOrder.approvedBy ? {\n name: sourceOrder.approvedBy,\n date: sourceOrder.approvedAt\n } : null,\n cc: ['회계팀'], // 회계는 항상 참조\n },\n },\n screenItems,\n motorSpec,\n bomData,\n };\n };\n\n const data = convertToSheetData();\n\n const handlePrint = () => {\n window.print();\n };\n\n // PDF 다운로드 (브라우저 인쇄 기능 활용)\n const handleDownloadPDF = () => {\n // CSS로 인쇄 스타일 적용 후 브라우저 인쇄 다이얼로그에서 PDF 저장\n const printContent = printRef.current;\n if (!printContent) return;\n\n const printWindow = window.open('', '_blank');\n printWindow.document.write(`\n \n \n \n
생산지시서_${data.orderInfo.lotNo}\n \n \n \n ${printContent.innerHTML}\n \n \n `);\n printWindow.document.close();\n printWindow.focus();\n\n setTimeout(() => {\n printWindow.print();\n printWindow.close();\n }, 500);\n };\n\n return (\n
\n
\n {/* 컨트롤 바 (인쇄 시 숨김) - 고정 */}\n
\n
\n \n
{title}
\n {data.orderInfo.lotNo}\n \n
\n
\n
\n
\n
\n
\n\n {/* 발주서 본문 - 스크롤 */}\n
\n
\n\n {/* === 헤더 영역 === */}\n
\n {/* 상단 헤더 */}\n
\n {/* 회사 로고/정보 */}\n
\n
\n
KD
\n
\n
경동기업
\n
\n 전화: 031-883-6130 | 팩스: 02-6911-6315 | 이메일: kd513@naver.com\n
\n
\n
\n
\n {/* 발주서 타이틀 */}\n
\n
발 주 서
\n \n {/* 로트번호/결재라인 */}\n
\n
\n
로트번호
\n
{data.orderInfo.lotNo}
\n
\n
\n
\n
결
재
\n
\n
\n 판매/전진\n {data.orderInfo.approval?.drafter && (\n <>\n {data.orderInfo.approval.drafter.name}\n {data.orderInfo.approval.drafter.date}\n >\n )}\n
\n
\n 생산관리\n {data.orderInfo.approval?.approver ? (\n <>\n {data.orderInfo.approval.approver.name}\n {data.orderInfo.approval.approver.date}\n >\n ) : (\n 대기\n )}\n
\n
\n 회계\n CC\n
\n
\n
\n
\n
\n\n {/* 제품 정보 행 */}\n
\n
\n
상품명
\n
{data.orderInfo.productName}
\n
\n
\n
제품형
\n
{data.orderInfo.productType}
\n
\n
\n
인정번호
\n
{data.orderInfo.certNo}
\n
\n
\n
\n\n {/* === 신청 내용 === */}\n
\n
신 청 내 용
\n
\n {/* 좌측 */}\n
\n
\n
발 주 일
\n
{data.orderInfo.orderDate}
\n
현 장 명
\n
{data.orderInfo.siteName}
\n
\n
\n
발 주 처
\n
{data.orderInfo.customerName}
\n
납기요청일
\n
{data.orderInfo.dueDate}
\n
\n
\n
발주담당자
\n
{data.orderInfo.orderManager}
\n
고 객
\n
{data.orderInfo.deliveryDate}
\n
\n
\n
담당자연락처
\n
셔터총수량 {data.orderInfo.totalQty} 개소
\n
배 송 방 법
\n
{data.orderInfo.deliveryMethod}
\n
\n
\n {/* 우측 */}\n
\n
\n
인수담당자
\n
{data.orderInfo.receiverManager}
\n
\n
\n
인수자연락처
\n
{data.orderInfo.receiverContact}
\n
\n
\n
배송지주소
\n
{data.orderInfo.deliveryAddress}
\n
\n
\n
\n
\n\n {/* 주의 문구 */}\n
\n 아래와 같이 주문하오니 품질 및 납기일을 준수하여 주시기 바랍니다.\n
\n\n {/* === 1. 스크린 테이블 === */}\n
\n
1. 스크린
\n
\n
\n \n \n 일련 번호 | \n 종류 | \n 도면부호 | \n 오픈사이즈(mm) | \n 제작사이즈(mm) | \n 가이드 레일유형 | \n 샤프트 (인치) | \n 케이스 (규격) | \n 모터 브라켓 | \n 용량 (KG) | \n 마감 | \n
\n \n | 가로 | \n 세로 | \n 가로 | \n 세로 | \n
\n \n \n {data.screenItems.map((item, idx) => (\n \n | {String(item.seq).padStart(2, '0')} | \n {item.type} | \n {item.drawingNo} | \n {item.openWidth.toLocaleString()} | \n {item.openHeight.toLocaleString()} | \n {item.prodWidth.toLocaleString()} | \n {item.prodHeight.toLocaleString()} | \n {item.guideRailType} ({item.guideRailSpec}) | \n {item.shaft} | \n {item.caseSpec} | \n {item.motorBracket} | \n {item.capacity} | \n {item.finish} | \n
\n ))}\n \n
\n
\n
\n\n {/* === 2. 모터 테이블 === */}\n
\n
2. 모터
\n
\n {/* 2-1. 모터(220V 단상) */}\n
\n
2-1. 모터(220V 단상)
\n
\n \n \n | 모터용량 | \n 수량 | \n
\n \n \n {data.motorSpec.motors220V.map((m, idx) => (\n \n | {m.model} | \n {m.qty || ''} | \n
\n ))}\n \n
\n
\n\n {/* 2-2. 모터(380V 삼상) */}\n
\n
2-2. 모터(380V 삼상)
\n
\n \n \n | 모터용량 | \n 수량 | \n
\n \n \n {data.motorSpec.motors380V.map((m, idx) => (\n \n | {m.model} | \n {m.qty || ''} | \n
\n ))}\n \n
\n
\n\n {/* 2-3. 브라켓 */}\n
\n
2-3. 브라켓
\n
\n \n \n | 브라켓 | \n 수량 | \n
\n \n \n {data.motorSpec.brackets.map((b, idx) => (\n \n | {b.spec} | \n {b.qty || ''} | \n
\n ))}\n {data.motorSpec.heatSinks.map((h, idx) => (\n \n | 방열등 {h.spec} | \n {h.qty || ''} | \n
\n ))}\n \n
\n
\n\n {/* 2-4. 연동제어기 */}\n
\n
2-4. 연동제어기
\n
\n \n \n | 용량 | \n 수량 | \n
\n \n \n {data.motorSpec.controllers.map((c, idx) => (\n \n | {c.type} | \n {c.qty || ''} | \n
\n ))}\n \n
\n
\n
\n
* 별도 추가사항
\n
\n\n {/* === 3. 절곡물 === */}\n
\n
3. 절곡물
\n\n {/* 3-1. 가이드레일 */}\n
\n
3-1. {data.bomData.guideRails.description}
\n
\n {/* 백면형 */}\n
\n
백면형 (120-70)
\n
\n \n \n | 코드 | \n 길이 | \n 수량 | \n
\n \n \n {data.bomData.guideRails.items\n .filter(g => g.type === '백면형')\n .flatMap(g => g.lengths.map((l, idx) => (\n \n | {idx === 0 ? g.code : ''} | \n L : {l.length.toLocaleString()} | \n {l.qty || ''} | \n
\n )))\n }\n \n
\n
\n\n {/* 측면형 */}\n
\n
측면형 (120-120)
\n
\n \n \n | 길이 | \n 수량 | \n
\n \n \n {data.bomData.guideRails.items\n .filter(g => g.type === '측면형')\n .flatMap(g => g.lengths.map((l, idx) => (\n \n | L : {l.length.toLocaleString()} | \n {l.qty || ''} | \n
\n )))\n }\n \n
\n
\n\n {/* 하부BASE + 연기차단재 */}\n
\n
하부BASE (130-80)
\n
\n \n \n | 수량 | \n \n {data.bomData.guideRails.items.find(g => g.type === '하부BASE')?.lengths[0]?.qty || ''}\n | \n
\n \n
\n
연기차단재 (W80)
\n
\n \n \n | 규격 L | \n {data.bomData.guideRails.smokeBarrier.lengths[0]?.length} | \n
\n \n | 수량 | \n {data.bomData.guideRails.smokeBarrier.lengths[0]?.qty} | \n
\n \n
\n
\n
\n
\n\n {/* 3-2. 케이스(셔터박스) */}\n
\n
3-2. {data.bomData.cases.description}
\n
\n
\n
\n
규격
\n
{data.bomData.cases.mainSpec}
\n
\n
\n \n \n | 길이 | \n 수량 | \n 측면덮개 | \n 수량 | \n
\n \n \n {data.bomData.cases.items.map((item, idx) => (\n \n | L : {item.length.toLocaleString()} | \n {item.qty || ''} | \n {idx === 0 && (\n <>\n {data.bomData.cases.sideCover.spec} | \n {data.bomData.cases.sideCover.qty} | \n >\n )}\n
\n ))}\n \n
\n
\n\n {/* 상부덮개/연기차단재 */}\n
\n
상부덮개
\n
{data.bomData.cases.topCover.qty}
\n
연기차단재 (W80)
\n
\n \n \n | 길이 | \n L : {data.bomData.cases.smokeBarrier.length.toLocaleString()} | \n
\n \n | 수량 | \n {data.bomData.cases.smokeBarrier.qty} | \n
\n \n
\n
\n
\n
\n\n {/* 3-3. 하단마감재 */}\n
\n
3-3. {data.bomData.bottomFinish.description}
\n
\n
\n \n \n | 구성품 | \n 길이(mm) | \n 수량 | \n 구성품 | \n 길이 | \n 수량 | \n 구성품 | \n 길이 | \n 수량 | \n
\n \n \n {data.bomData.bottomFinish.items.map((item, idx) => (\n \n {item.name} ({item.spec}) | \n {item.lengths.map((l, lidx) => (\n \n L : {l.length.toLocaleString()} | \n {l.qty} | \n \n ))}\n {/* 빈 셀 채우기 */}\n {item.lengths.length < 3 && Array(3 - item.lengths.length).fill(0).map((_, i) => (\n \n | \n | \n \n ))}\n
\n ))}\n \n
\n
\n
\n
\n\n {/* 푸터 */}\n
\n 생성일시: {new Date().toLocaleString('ko-KR')} | 작업지시번호: {workOrder?.workOrderNo || data.orderInfo.lotNo}\n
\n
\n
\n
\n
\n );\n};\n\n// ============ 작업일지 시스템 (공정별) ============\n\n// 공정별 작업일지 초기 데이터 생성 (로컬 정의 - mesCompleteIntegration의 것보다 상세)\nconst createWorkLogDataLocal = (workOrder, order, processType) => {\n const items = workOrder?.items || order?.items || [];\n const itemCount = items.length;\n const process = processType || workOrder?.processType || '절곡';\n const materialInputs = workOrder?.materialInputs || []; // ★ 자재 투입 이력 (LOT 정보 포함)\n\n // 자재코드로 투입된 LOT 찾기 헬퍼 함수\n const findInputLotNo = (materialCode) => {\n const input = materialInputs.find(m => m.materialCode === materialCode);\n return input?.lotNo || '';\n };\n\n // 공통 헤더 정보\n const baseData = {\n processType: process,\n orderDate: order?.orderDate || workOrder?.orderDate || '',\n company: order?.customerName || workOrder?.customerName || '',\n manager: order?.manager || '',\n contact: order?.contact || '',\n siteName: order?.siteName || workOrder?.siteName || '',\n workDate: new Date().toISOString().split('T')[0],\n productLotNo: workOrder?.workOrderNo || '',\n productionManager: workOrder?.assignee || '',\n totalQty: workOrder?.totalQty || itemCount,\n remarks: '',\n approval: {\n writer: '',\n checker: '전진',\n approver: '',\n },\n };\n\n // 스크린 공정 작업일지\n if (process === '스크린') {\n // 스크린 공정 자재코드 매핑\n const screenMaterialMap = {\n '스크린원단': 'SCR-MAT-001',\n '미싱실': 'SCR-MAT-004',\n '앤드락': 'SCR-MAT-002',\n '하단바': 'SCR-MAT-003',\n '포장재': 'SCR-MAT-005',\n };\n\n return {\n ...baseData,\n department: '스크린 생산부서',\n // 원단 정보\n fabric: {\n type: '망사', // 망사, 암막 등\n color: '',\n width: '',\n inputLotNo: findInputLotNo(screenMaterialMap['스크린원단']), // ★ LOT 자동 연동\n },\n // 작업 내역 - ★ 투입된 LOT 자동 연동\n workItems: [\n { id: 1, step: '원단절단', material: '스크린원단', inputLotNo: findInputLotNo(screenMaterialMap['스크린원단']), qty: itemCount, unit: 'EA', note: '' },\n { id: 2, step: '미싱', material: '미싱실', inputLotNo: findInputLotNo(screenMaterialMap['미싱실']), qty: null, unit: 'M', note: '' },\n { id: 3, step: '앤드락작업', material: '앤드락', inputLotNo: findInputLotNo(screenMaterialMap['앤드락']), qty: itemCount * 2, unit: 'EA', note: '' },\n { id: 4, step: '하단바조립', material: '하단바', inputLotNo: findInputLotNo(screenMaterialMap['하단바']), qty: itemCount, unit: 'EA', note: '' },\n { id: 5, step: '중간검사', material: '-', inputLotNo: '', qty: itemCount, unit: 'EA', note: '' },\n { id: 6, step: '포장', material: '포장재', inputLotNo: findInputLotNo(screenMaterialMap['포장재']), qty: itemCount, unit: 'EA', note: '' },\n ],\n // 품목별 상세 (규격)\n itemDetails: items.map((item, idx) => ({\n id: idx + 1,\n floor: item.floor || '',\n location: item.location || '',\n width: item.width || '',\n height: item.height || '',\n qty: item.qty || 1,\n fabricLotNo: '',\n productLotNo: '',\n inspectionResult: '', // 합격/불합격\n })),\n // 생산 실적\n productionResult: {\n totalProduced: null,\n goodQty: null,\n defectQty: null,\n defectType: '',\n },\n };\n }\n\n // 슬랫 공정 작업일지\n if (process === '슬랫') {\n // 슬랫 공정 자재코드 매핑\n const slatMaterialMap = {\n '슬랫코일': 'SLT-MAT-001',\n '미미캡': 'SLT-MAT-002',\n '포장재': 'SCR-MAT-005',\n };\n\n return {\n ...baseData,\n department: '슬랫 생산부서',\n // 코일 정보\n coil: {\n type: '알루미늄', // 알루미늄, 스틸 등\n color: '',\n thickness: '',\n inputLotNo: findInputLotNo(slatMaterialMap['슬랫코일']), // ★ LOT 자동 연동\n },\n // 작업 내역 - ★ 투입된 LOT 자동 연동\n workItems: [\n { id: 1, step: '코일절단', material: '슬랫코일', inputLotNo: findInputLotNo(slatMaterialMap['슬랫코일']), length: '', qty: null, unit: 'EA', note: '' },\n { id: 2, step: '슬랫성형', material: '-', inputLotNo: '', length: '', qty: null, unit: 'EA', note: '' },\n { id: 3, step: '중간검사', material: '-', inputLotNo: '', length: '', qty: itemCount, unit: 'EA', note: '' },\n { id: 4, step: '미미작업', material: '미미캡', inputLotNo: findInputLotNo(slatMaterialMap['미미캡']), length: '', qty: itemCount * 2, unit: 'EA', note: '' },\n { id: 5, step: '조립', material: '연결부품', inputLotNo: '', length: '', qty: null, unit: 'SET', note: '' },\n { id: 6, step: '포장', material: '포장재', inputLotNo: findInputLotNo(slatMaterialMap['포장재']), length: '', qty: itemCount, unit: 'EA', note: '' },\n ],\n // 절단 상세\n cuttingDetails: [\n { id: 1, length: 'L:4,000', qty: null },\n { id: 2, length: 'L:3,500', qty: null },\n { id: 3, length: 'L:3,000', qty: null },\n { id: 4, length: 'L:2,500', qty: null },\n { id: 5, length: 'L:2,000', qty: null },\n ],\n // 생산 실적\n productionResult: {\n totalProduced: null,\n goodQty: null,\n defectQty: null,\n defectType: '',\n totalWeight: null, // kg\n },\n };\n }\n\n // 절곡 공정 작업일지 (기존)\n // 절곡 공정 자재코드 매핑\n const bendingMaterialMap = {\n 'EGI1.55T': 'EGI-1.55T',\n 'EGI1.15T': 'EGI-1.55T', // EGI 계열은 같은 코드 사용\n 'SUS1.2T': 'SUS-1.2T',\n '연기차단재': 'BND-SMK-001',\n };\n\n return {\n ...baseData,\n department: '절곡 생산부서',\n productCode: items[0]?.productCode || 'KWE01',\n productType: '와이어',\n finishType: 'SUS마감',\n railType: '벽면형(120·70)',\n\n // ★ 전개도 및 전개 상세정보 (품목에서 등록한 절곡 부품 데이터 기준)\n developedParts: [\n { itemCode: 'SD30', itemName: '엘바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 4, weight: 0.9, dimensions: '75', note: '' },\n { itemCode: 'SD31', itemName: '하장바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 1.158, dimensions: '67→126→165→178→193', note: '' },\n { itemCode: 'SD32', itemName: '짜부가스켓', material: 'E.G.I 0.8T', totalWidth: 500, length: 3000, qty: 4, weight: 0.576, dimensions: '48', note: '80*4,50*8' },\n { itemCode: 'SD33', itemName: '50평철', material: 'E.G.I 1.2T', totalWidth: 500, length: 3000, qty: 2, weight: 0.3, dimensions: '50', note: '' },\n { itemCode: 'SD34', itemName: '500*380전면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.764, dimensions: '120→499→553→588', note: '' },\n { itemCode: 'SD35', itemName: 'S/BOX 린텔박스', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 0.504, dimensions: '84→138→168', note: '' },\n { itemCode: 'SD36', itemName: '500*380밑면 점검구', material: 'E.G.I 1.6T', totalWidth: 500, length: 2438, qty: 2, weight: 0.98, dimensions: '90→240→310', note: '점검구' },\n { itemCode: 'SD37', itemName: '후면코너부', material: 'E.G.I 1.2T', totalWidth: 500, length: 1219, qty: itemCount * 2, weight: 0.45, dimensions: '35→85→120', note: '' },\n ],\n\n // 1. 벽면형 (120·70) - ★ 투입된 LOT 자동 연동\n guideRail: [\n { id: 1, name: '마감재', material: 'EGI1.15T', inputLotNo: findInputLotNo(bendingMaterialMap['EGI1.15T']), spec: 'L:4,300', qty: null },\n { id: 2, name: '가이드레일', material: '', inputLotNo: '', spec: 'L:4,000', qty: null },\n { id: 3, name: 'C형', material: 'EGI1.55T', inputLotNo: findInputLotNo(bendingMaterialMap['EGI1.55T']), spec: 'L:3,500', qty: null },\n { id: 4, name: 'D형', material: '', inputLotNo: '', spec: 'L:3,000', qty: itemCount * 2 },\n { id: 5, name: '별도마감재', material: 'SUS1.2T', inputLotNo: findInputLotNo(bendingMaterialMap['SUS1.2T']), spec: 'L:2,438', qty: null },\n { id: 6, name: '하부BASE', material: 'EGI1.55T', inputLotNo: findInputLotNo(bendingMaterialMap['EGI1.55T']), spec: '하부BASE 130·80', qty: itemCount * 2 },\n ],\n\n // 2. 하단마감재 [60·40] - ★ 투입된 LOT 자동 연동\n bottomFinish: [\n {\n id: 1, name: '하단마감재', material: 'EGI1.55T', inputLotNo: findInputLotNo(bendingMaterialMap['EGI1.55T']), rows: [\n { spec: 'L:4,000', qty: Math.ceil(itemCount * 1.1) },\n { spec: 'L:3,000', qty: Math.ceil(itemCount * 0.8) },\n ]\n },\n {\n id: 2, name: '별도마감재', material: 'SUS1.2T', inputLotNo: findInputLotNo(bendingMaterialMap['SUS1.2T']), rows: [\n { spec: 'L:4,000', qty: Math.ceil(itemCount * 1.1) },\n { spec: 'L:3,000', qty: Math.ceil(itemCount * 0.8) },\n ]\n },\n {\n id: 3, name: '하단보강엘바', material: 'EGI1.55T', inputLotNo: findInputLotNo(bendingMaterialMap['EGI1.55T']), rows: [\n { spec: 'L:4,000', qty: Math.ceil(itemCount * 2.6) },\n { spec: 'L:3,000', qty: Math.ceil(itemCount * 1.2) },\n ]\n },\n {\n id: 4, name: '하단보강평철', material: 'EGI1.15T', inputLotNo: findInputLotNo(bendingMaterialMap['EGI1.15T']), rows: [\n { spec: 'L:4,000', qty: Math.ceil(itemCount * 1.3) },\n { spec: 'L:3,000', qty: Math.ceil(itemCount * 0.6) },\n ]\n },\n ],\n\n // 3. 케이스 [500*330] - ★ 투입된 LOT 자동 연동\n caseBox: {\n spec: '500*330',\n material: 'EGI1.55T',\n inputLotNo: findInputLotNo(bendingMaterialMap['EGI1.55T']),\n items: [\n {\n id: 1, name: '전면부', rows: [\n { spec: 'L:4,150', qty: 1 },\n { spec: 'L:4,000', qty: 7 },\n ]\n },\n {\n id: 2, name: '린텔부', rows: [\n { spec: 'L:3,500', qty: 5 },\n { spec: 'L:3,000', qty: 3 },\n ]\n },\n {\n id: 3, name: '하부점검구', rows: [\n { spec: 'L:2,438', qty: 3 },\n { spec: 'L:1,219', qty: null },\n ]\n },\n { id: 4, name: '후면코너부', spec: '상부덮개[1219·389]', qty: Math.ceil(itemCount * 5.9) },\n { id: 5, name: '상부덮개', spec: '마구리 505·335', qty: itemCount * 2 },\n { id: 6, name: '측면부(마구리)', spec: '', qty: null },\n ],\n },\n\n // 4. 연기차단재 - ★ 투입된 LOT 자동 연동\n smokeBarrier: {\n rail: {\n name: '레일용[W50]',\n material: 'EG0.8T + 화이바 글라스 코팅직물',\n inputLotNo: findInputLotNo(bendingMaterialMap['연기차단재']),\n rows: [\n { spec: 'L:4,300', qty: null },\n { spec: 'L:4,000', qty: null },\n { spec: 'L:3,500', qty: null },\n { spec: 'L:3,000', qty: Math.ceil(itemCount * 4.4) },\n { spec: 'L:2,438', qty: null },\n ]\n },\n caseItem: {\n name: '케이스용[W80]',\n inputLotNo: findInputLotNo(bendingMaterialMap['연기차단재']),\n spec: 'L:3,000',\n qty: Math.ceil(itemCount * 4.7)\n },\n },\n\n // 생산량 합계\n totalProduction: {\n sus: null,\n egi: null,\n },\n };\n};\n\n// ============ 스크린 작업일지 에디터 ============\nconst ScreenWorkLogEditor = ({ workOrder, order, workLog, onClose, onSave }) => {\n const [data, setData] = useState(workLog || createWorkLogDataLocal(workOrder, order, '스크린'));\n const [activeSection, setActiveSection] = useState('header');\n\n const sections = [\n { id: 'header', label: '기본정보' },\n { id: 'fabric', label: '원단정보' },\n { id: 'work', label: '투입자재' },\n { id: 'items', label: '규격매수 (자동계산)' },\n { id: 'result', label: '생산실적' },\n ];\n\n const handleSave = () => {\n onSave?.(data);\n alert('✅ 스크린 작업일지가 저장되었습니다.');\n };\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n
\n
\n
스크린 작업일지
\n
{workOrder?.workOrderNo}
\n
\n
\n
\n
\n\n {/* 섹션 탭 */}\n
\n {sections.map(section => (\n \n ))}\n
\n\n {/* 콘텐츠 */}\n
\n {/* 기본정보 */}\n {activeSection === 'header' && (\n
\n )}\n\n {/* 원단정보 */}\n {activeSection === 'fabric' && (\n
\n \n
\n \n \n
\n
\n \n setData({ ...data, fabric: { ...data.fabric, color: e.target.value } })} placeholder=\"예: 아이보리\" className=\"w-full px-3 py-2 border rounded-lg\" />\n
\n
\n \n setData({ ...data, fabric: { ...data.fabric, width: e.target.value } })} placeholder=\"예: 3000\" className=\"w-full px-3 py-2 border rounded-lg\" />\n
\n
\n \n setData({ ...data, fabric: { ...data.fabric, inputLotNo: e.target.value } })} placeholder=\"LOT 번호\" className=\"w-full px-3 py-2 border rounded-lg font-mono\" />\n
\n
\n \n )}\n\n {/* 작업내역 */}\n {activeSection === 'work' && (\n
\n \n \n )}\n\n {/* 품목상세 - 규격매수 계산 테이블 (문서양식관리 TBL-WORKLOG-SCREEN 기준) */}\n {activeSection === 'items' && (\n
\n {/* 규격매수 계산 공식 안내 */}\n \n
자동 계산 공식
\n
• 규격 1210: 가로 크기에 따라 매수 자동 계산 (1070~1970→1매, 1970~3140→2매, ...)
\n
• 규격 300~900: 나머지높이에 따라 분배 (0~300→300, 300~400→400, 400~600→600, 600~900→900)
\n
\n \n
\n \n \n 일련 번호 | \n 입고 LOT NO. | \n 제품명 | \n 부호 | \n 제작사이즈(mm) | \n 규격(매수) | \n
\n \n | 가로 | \n 세로 | \n 나머지 높이 | \n 1210 | \n 900 | \n 600 | \n 400 | \n 300 | \n
\n \n \n {data.itemDetails.map((item, idx) => {\n // ★ 규격매수 자동 계산 공식 (documentTemplateConfig.js TBL-WORKLOG-SCREEN 기준)\n const calculateSpec = (width, remainHeight) => {\n // 가로 기준 1210 규격 매수 계산\n let spec1210 = 0;\n const ranges = [\n { min: 0, max: 1070, count: 0 },\n { min: 1070, max: 1970, count: 1 },\n { min: 1970, max: 3140, count: 2 },\n { min: 3140, max: 4310, count: 3 },\n { min: 4310, max: 5480, count: 4 },\n { min: 5480, max: 6650, count: 5 },\n { min: 6650, max: 7820, count: 6 },\n { min: 7820, max: 8990, count: 7 },\n { min: 8990, max: 10160, count: 8 },\n { min: 10160, max: 99999, count: 9 },\n ];\n for (const range of ranges) {\n if (width > range.min && width <= range.max) {\n spec1210 = range.count;\n break;\n }\n }\n // 나머지높이 기준 규격 분배\n const specs = { spec1210, spec900: 0, spec600: 0, spec400: 0, spec300: 0 };\n if (remainHeight > 0 && remainHeight <= 300) specs.spec300 = 1;\n else if (remainHeight > 300 && remainHeight <= 400) specs.spec400 = 1;\n else if (remainHeight > 400 && remainHeight <= 600) specs.spec600 = 1;\n else if (remainHeight > 600 && remainHeight <= 900) specs.spec900 = 1;\n return specs;\n };\n const specs = calculateSpec(item.width || 0, item.remainHeight || 0);\n return (\n \n | {item.id} | \n \n {\n const newItems = [...data.itemDetails];\n newItems[idx].fabricLotNo = e.target.value;\n setData({ ...data, itemDetails: newItems });\n }} placeholder=\"LOT\" className=\"w-full px-1 py-1 border rounded text-xs font-mono\" />\n | \n {item.productName || '방화스크린'} | \n {item.location} | \n \n {\n const newItems = [...data.itemDetails];\n newItems[idx].width = e.target.value ? parseInt(e.target.value) : null;\n setData({ ...data, itemDetails: newItems });\n }} className=\"w-full px-1 py-1 border rounded text-xs text-center\" />\n | \n \n {\n const newItems = [...data.itemDetails];\n newItems[idx].height = e.target.value ? parseInt(e.target.value) : null;\n setData({ ...data, itemDetails: newItems });\n }} className=\"w-full px-1 py-1 border rounded text-xs text-center\" />\n | \n \n {\n const newItems = [...data.itemDetails];\n newItems[idx].remainHeight = e.target.value ? parseInt(e.target.value) : null;\n setData({ ...data, itemDetails: newItems });\n }} className=\"w-full px-1 py-1 border rounded text-xs text-center\" />\n | \n {specs.spec1210 || '-'} | \n {specs.spec900 || '-'} | \n {specs.spec600 || '-'} | \n {specs.spec400 || '-'} | \n {specs.spec300 || '-'} | \n
\n );\n })}\n {/* 합계 행 */}\n \n | 합계 | \n \n {data.itemDetails.reduce((sum, item) => {\n const spec = item.width > 1070 ? (item.width <= 1970 ? 1 : item.width <= 3140 ? 2 : item.width <= 4310 ? 3 : item.width <= 5480 ? 4 : item.width <= 6650 ? 5 : item.width <= 7820 ? 6 : item.width <= 8990 ? 7 : item.width <= 10160 ? 8 : 9) : 0;\n return sum + spec;\n }, 0)}\n | \n \n {data.itemDetails.filter(i => i.remainHeight > 600 && i.remainHeight <= 900).length}\n | \n \n {data.itemDetails.filter(i => i.remainHeight > 400 && i.remainHeight <= 600).length}\n | \n \n {data.itemDetails.filter(i => i.remainHeight > 300 && i.remainHeight <= 400).length}\n | \n \n {data.itemDetails.filter(i => i.remainHeight > 0 && i.remainHeight <= 300).length}\n | \n
\n \n
\n
\n {/* 사용량 요약 */}\n \n
\n
사용량(M) - 규격별
\n
\n
\n 1210\n {(data.itemDetails.reduce((sum, item) => {\n const spec = item.width > 1070 ? (item.width <= 1970 ? 1 : item.width <= 3140 ? 2 : item.width <= 4310 ? 3 : item.width <= 5480 ? 4 : item.width <= 6650 ? 5 : item.width <= 7820 ? 6 : item.width <= 8990 ? 7 : 8) : 0;\n return sum + (spec * 1.21 * (item.height || 0) / 1000);\n }, 0)).toFixed(2)}\n
\n
\n 900\n {(data.itemDetails.filter(i => i.remainHeight > 600 && i.remainHeight <= 900).reduce((sum, i) => sum + 0.9 * (i.height || 0) / 1000, 0)).toFixed(2)}\n
\n
\n 600\n {(data.itemDetails.filter(i => i.remainHeight > 400 && i.remainHeight <= 600).reduce((sum, i) => sum + 0.6 * (i.height || 0) / 1000, 0)).toFixed(2)}\n
\n
\n 400\n {(data.itemDetails.filter(i => i.remainHeight > 300 && i.remainHeight <= 400).reduce((sum, i) => sum + 0.4 * (i.height || 0) / 1000, 0)).toFixed(2)}\n
\n
\n 300\n {(data.itemDetails.filter(i => i.remainHeight > 0 && i.remainHeight <= 300).reduce((sum, i) => sum + 0.3 * (i.height || 0) / 1000, 0)).toFixed(2)}\n
\n
\n
\n
\n
\n
총 사용량 (m²)
\n
\n {(data.itemDetails.reduce((sum, item) => sum + ((item.width || 0) * (item.height || 0) / 1000000), 0)).toFixed(2)}\n
\n
\n
\n
\n \n )}\n\n {/* 생산실적 */}\n {activeSection === 'result' && (\n
\n
\n \n
\n \n setData({ ...data, productionResult: { ...data.productionResult, totalProduced: e.target.value ? parseInt(e.target.value) : null } })}\n className=\"w-24 px-3 py-2 border rounded-lg text-xl font-bold text-center mx-auto\"\n />\n
\n
\n \n setData({ ...data, productionResult: { ...data.productionResult, goodQty: e.target.value ? parseInt(e.target.value) : null } })}\n className=\"w-24 px-3 py-2 border rounded-lg text-xl font-bold text-center mx-auto\"\n />\n
\n
\n \n setData({ ...data, productionResult: { ...data.productionResult, defectQty: e.target.value ? parseInt(e.target.value) : null } })}\n className=\"w-24 px-3 py-2 border rounded-lg text-xl font-bold text-center mx-auto\"\n />\n
\n
\n \n \n
\n
\n \n
\n \n
\n )}\n
\n\n {/* 하단 버튼 */}\n
\n \n \n
\n
\n
\n );\n};\n\n// ============ 슬랫 작업일지 에디터 ============\nconst SlatWorkLogEditor = ({ workOrder, order, workLog, onClose, onSave }) => {\n const [data, setData] = useState(workLog || createWorkLogDataLocal(workOrder, order, '슬랫'));\n const [activeSection, setActiveSection] = useState('header');\n\n const sections = [\n { id: 'header', label: '기본정보' },\n { id: 'coil', label: '코일정보' },\n { id: 'cutting', label: '슬랫 생산 (자동계산)' },\n { id: 'work', label: '투입자재' },\n { id: 'result', label: '생산실적' },\n ];\n\n const handleSave = () => {\n onSave?.(data);\n alert('✅ 슬랫 작업일지가 저장되었습니다.');\n };\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n
\n
\n
슬랫 작업일지
\n
{workOrder?.workOrderNo}
\n
\n
\n
\n
\n\n {/* 섹션 탭 */}\n
\n {sections.map(section => (\n \n ))}\n
\n\n {/* 콘텐츠 */}\n
\n {/* 기본정보 */}\n {activeSection === 'header' && (\n
\n )}\n\n {/* 코일정보 */}\n {activeSection === 'coil' && (\n
\n \n
\n \n \n
\n
\n \n setData({ ...data, coil: { ...data.coil, color: e.target.value } })} placeholder=\"예: 실버\" className=\"w-full px-3 py-2 border rounded-lg\" />\n
\n
\n \n setData({ ...data, coil: { ...data.coil, thickness: e.target.value } })} placeholder=\"예: 0.5T\" className=\"w-full px-3 py-2 border rounded-lg\" />\n
\n
\n \n setData({ ...data, coil: { ...data.coil, inputLotNo: e.target.value } })} placeholder=\"LOT 번호\" className=\"w-full px-3 py-2 border rounded-lg font-mono\" />\n
\n
\n \n )}\n\n {/* 절단내역 - 슬랫 생산내역 계산 테이블 (문서양식관리 TBL-WORKLOG-SLAT 기준) */}\n {activeSection === 'cutting' && (\n
\n {/* 슬랫 계산 공식 안내 */}\n \n
자동 계산 공식
\n
• 매수(세로) = ROUNDDOWN(세로/72+1, 0) - 슬랫 피치 72mm 기준
\n
• 조인트바 수량 = 매수 ÷ 10 (올림)
\n
• 코일사용량 = (가로 × 세로 / 1,000,000) × 14.33
\n
\n \n
\n \n \n 일련 번호 | \n 입고 LOT NO. | \n 제품명 | \n 부호 | \n 제작사이즈(mm) | \n 매수 (세로) | \n 조인트바 | \n 방화유리 수량 | \n 코일 사용량 | \n 설치층/ 부호 | \n
\n \n | 가로 | \n 세로 | \n
\n \n \n {(data.slatItems || data.cuttingDetails || []).map((item, idx) => {\n // ★ 슬랫 자동 계산 공식 (documentTemplateConfig.js slateCalculation 기준)\n const height = item.height || item.length || 0;\n const width = item.width || 0;\n // 매수(세로) = ROUNDDOWN(세로/72+1, 0)\n const sheetCount = height > 0 ? Math.floor(height / 72 + 1) : 0;\n // 조인트바 수량 = 매수 ÷ 10 (올림)\n const jointBarQty = sheetCount > 0 ? Math.ceil(sheetCount / 10) : 0;\n // 코일사용량 = (가로 × 세로 / 1,000,000) × 14.33\n const coilUsage = width > 0 && height > 0 ? Math.round((width * height / 1000000) * 14.33 * 10) / 10 : 0;\n return (\n \n | {idx + 1} | \n \n {\n const key = data.slatItems ? 'slatItems' : 'cuttingDetails';\n const newItems = [...(data[key] || [])];\n newItems[idx] = { ...newItems[idx], inputLotNo: e.target.value };\n setData({ ...data, [key]: newItems });\n }} placeholder=\"LOT\" className=\"w-full px-1 py-1 border rounded text-xs font-mono\" />\n | \n {item.productName || '방화셔터'} | \n \n {\n const key = data.slatItems ? 'slatItems' : 'cuttingDetails';\n const newItems = [...(data[key] || [])];\n newItems[idx] = { ...newItems[idx], partCode: e.target.value };\n setData({ ...data, [key]: newItems });\n }} placeholder=\"부호\" className=\"w-full px-1 py-1 border rounded text-xs text-center\" />\n | \n \n {\n const key = data.slatItems ? 'slatItems' : 'cuttingDetails';\n const newItems = [...(data[key] || [])];\n newItems[idx] = { ...newItems[idx], width: e.target.value ? parseInt(e.target.value) : null };\n setData({ ...data, [key]: newItems });\n }} className=\"w-full px-1 py-1 border rounded text-xs text-center\" />\n | \n \n {\n const key = data.slatItems ? 'slatItems' : 'cuttingDetails';\n const newItems = [...(data[key] || [])];\n newItems[idx] = { ...newItems[idx], height: e.target.value ? parseInt(e.target.value) : null, length: e.target.value ? parseInt(e.target.value) : null };\n setData({ ...data, [key]: newItems });\n }} className=\"w-full px-1 py-1 border rounded text-xs text-center\" />\n | \n {sheetCount || '-'} | \n {jointBarQty || '-'} | \n \n {\n const key = data.slatItems ? 'slatItems' : 'cuttingDetails';\n const newItems = [...(data[key] || [])];\n newItems[idx] = { ...newItems[idx], fireGlassQty: e.target.value ? parseInt(e.target.value) : null };\n setData({ ...data, [key]: newItems });\n }} className=\"w-full px-1 py-1 border rounded text-xs text-center\" />\n | \n {coilUsage || '-'} | \n \n {\n const key = data.slatItems ? 'slatItems' : 'cuttingDetails';\n const newItems = [...(data[key] || [])];\n newItems[idx] = { ...newItems[idx], installFloor: e.target.value };\n setData({ ...data, [key]: newItems });\n }} placeholder=\"층/부호\" className=\"w-full px-1 py-1 border rounded text-xs text-center\" />\n | \n
\n );\n })}\n {/* 합계 행 */}\n \n | 합계 | \n \n {(data.slatItems || data.cuttingDetails || []).reduce((sum, item) => {\n const height = item.height || item.length || 0;\n return sum + (height > 0 ? Math.floor(height / 72 + 1) : 0);\n }, 0)}\n | \n \n {(data.slatItems || data.cuttingDetails || []).reduce((sum, item) => {\n const height = item.height || item.length || 0;\n const sheetCount = height > 0 ? Math.floor(height / 72 + 1) : 0;\n return sum + Math.ceil(sheetCount / 10);\n }, 0)}\n | \n \n {(data.slatItems || data.cuttingDetails || []).reduce((sum, item) => sum + (item.fireGlassQty || 0), 0)}\n | \n \n {((data.slatItems || data.cuttingDetails || []).reduce((sum, item) => {\n const width = item.width || 0;\n const height = item.height || item.length || 0;\n return sum + (width > 0 && height > 0 ? (width * height / 1000000) * 14.33 : 0);\n }, 0)).toFixed(1)}\n | \n | \n
\n \n
\n
\n {/* 생산량 요약 */}\n \n
\n
매수 합계
\n
\n {(data.slatItems || data.cuttingDetails || []).reduce((sum, item) => {\n const height = item.height || item.length || 0;\n return sum + (height > 0 ? Math.floor(height / 72 + 1) : 0);\n }, 0)} 매\n
\n
\n
\n
조인트바 합계
\n
\n {(data.slatItems || data.cuttingDetails || []).reduce((sum, item) => {\n const height = item.height || item.length || 0;\n const sheetCount = height > 0 ? Math.floor(height / 72 + 1) : 0;\n return sum + Math.ceil(sheetCount / 10);\n }, 0)} 개\n
\n
\n
\n
생산량 (m²)
\n
\n {((data.slatItems || data.cuttingDetails || []).reduce((sum, item) => {\n const width = item.width || 0;\n const height = item.height || item.length || 0;\n return sum + (width * height / 1000000);\n }, 0)).toFixed(2)}\n
\n
\n
\n \n )}\n\n {/* 작업내역 */}\n {activeSection === 'work' && (\n
\n \n \n )}\n\n {/* 생산실적 */}\n {activeSection === 'result' && (\n
\n
\n \n
\n \n setData({ ...data, productionResult: { ...data.productionResult, totalProduced: e.target.value ? parseInt(e.target.value) : null } })}\n className=\"w-20 px-2 py-2 border rounded-lg text-xl font-bold text-center mx-auto\"\n />\n
\n
\n \n setData({ ...data, productionResult: { ...data.productionResult, goodQty: e.target.value ? parseInt(e.target.value) : null } })}\n className=\"w-20 px-2 py-2 border rounded-lg text-xl font-bold text-center mx-auto\"\n />\n
\n
\n \n setData({ ...data, productionResult: { ...data.productionResult, defectQty: e.target.value ? parseInt(e.target.value) : null } })}\n className=\"w-20 px-2 py-2 border rounded-lg text-xl font-bold text-center mx-auto\"\n />\n
\n
\n \n \n
\n
\n \n setData({ ...data, productionResult: { ...data.productionResult, totalWeight: e.target.value ? parseFloat(e.target.value) : null } })}\n className=\"w-20 px-2 py-2 border rounded-lg text-xl font-bold text-center mx-auto\"\n />\n
\n
\n \n
\n \n
\n )}\n
\n\n {/* 하단 버튼 */}\n
\n \n \n
\n
\n
\n );\n};\n\n// ============ 절곡 작업일지 에디터 ============\nconst BendingWorkLogEditor = ({ workOrder, order, workLog, onClose, onSave }) => {\n const [data, setData] = useState(workLog || createWorkLogDataLocal(workOrder, order, '절곡'));\n const [activeSection, setActiveSection] = useState('header');\n\n // ★ 투입자재에서 LOT 정보 가져오기 함수\n const getMaterialLotByCode = (materialCode) => {\n const materialInputs = workOrder?.materialInputs || [];\n const input = materialInputs.find(m => m.materialCode === materialCode);\n return input?.lotNo || '';\n };\n\n // ★ 재질별 LOT 매핑 (EGI/SUS)\n const getEgiLot = () => getMaterialLotByCode('EGI-1.55T') || getMaterialLotByCode('EGI-1.6T') || getMaterialLotByCode('EGI-1.2T');\n const getSusLot = () => getMaterialLotByCode('SUS-1.2T') || getMaterialLotByCode('SUS-1.5T');\n\n // ★ 투입자재 LOT 자동 반영 (컴포넌트 마운트 시)\n useEffect(() => {\n const materialInputs = workOrder?.materialInputs || [];\n if (materialInputs.length > 0 && !workLog) {\n const egiLot = getEgiLot();\n const susLot = getSusLot();\n\n setData(prev => {\n const updated = { ...prev };\n\n // 가이드레일 LOT 자동 입력\n if (updated.guideRail) {\n updated.guideRail = updated.guideRail.map(item => ({\n ...item,\n inputLotNo: item.inputLotNo || (item.material?.includes('SUS') ? susLot : egiLot)\n }));\n }\n\n // 하단마감재 LOT 자동 입력\n if (updated.bottomFinish) {\n updated.bottomFinish = updated.bottomFinish.map(item => ({\n ...item,\n inputLotNo: item.inputLotNo || (item.material?.includes('SUS') ? susLot : egiLot)\n }));\n }\n\n // 케이스 LOT 자동 입력\n if (updated.caseBox) {\n updated.caseBox = {\n ...updated.caseBox,\n inputLotNo: updated.caseBox.inputLotNo || (updated.caseBox.material?.includes('SUS') ? susLot : egiLot)\n };\n }\n\n // 연기차단재 LOT 자동 입력\n if (updated.smokeBarrier) {\n updated.smokeBarrier = updated.smokeBarrier.map(item => ({\n ...item,\n inputLotNo: item.inputLotNo || getMaterialLotByCode('SM-SMOKE-01') || egiLot\n }));\n }\n\n return updated;\n });\n }\n }, [workOrder?.materialInputs]);\n\n const sections = [\n { id: 'header', label: '기본정보' },\n { id: 'developedParts', label: '전개도 상세' },\n { id: 'developedDrawing', label: '전개도 도면' },\n { id: 'guideRail', label: '1. 벽면형' },\n { id: 'bottomFinish', label: '2. 하단마감재' },\n { id: 'caseBox', label: '3. 케이스' },\n { id: 'smokeBarrier', label: '4. 연기차단재' },\n { id: 'summary', label: '생산량 합계' },\n ];\n\n const updateField = (path, value) => {\n setData(prev => {\n const newData = { ...prev };\n const keys = path.split('.');\n let current = newData;\n for (let i = 0; i < keys.length - 1; i++) {\n if (keys[i].includes('[')) {\n const [key, idx] = keys[i].split('[');\n current = current[key][parseInt(idx.replace(']', ''))];\n } else {\n current = current[keys[i]];\n }\n }\n const lastKey = keys[keys.length - 1];\n if (lastKey.includes('[')) {\n const [key, idx] = lastKey.split('[');\n current[key][parseInt(idx.replace(']', ''))] = value;\n } else {\n current[lastKey] = value;\n }\n return newData;\n });\n };\n\n const handleSave = () => {\n onSave?.(data);\n alert('✅ 작업일지가 저장되었습니다.');\n };\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n
\n
\n
작업일지 입력
\n
{workOrder?.workOrderNo} · 절곡 공정
\n
\n
\n
\n
\n\n {/* 섹션 탭 */}\n
\n {sections.map(section => (\n \n ))}\n
\n\n {/* 콘텐츠 */}\n
\n {/* 기본정보 섹션 */}\n {activeSection === 'header' && (\n
\n
\n\n
\n \n
\n \n updateField('productCode', e.target.value)}\n className=\"w-full px-3 py-2 border rounded-lg\"\n />\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n
\n )}\n\n {/* 전개도 상세 섹션 */}\n {activeSection === 'developedParts' && (\n
\n 품목 등록 기준 절곡 부품 전개 치수
\n \n
\n \n \n | 품번 | \n 품명 | \n 재질 | \n 전개폭 | \n 길이 | \n 수량 | \n 중량(kg) | \n 전개치수 | \n 비고 | \n
\n \n \n {(data.developedParts || [\n { itemCode: 'SD30', itemName: '엘바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 4, weight: 0.9, dimensions: '75', note: '' },\n { itemCode: 'SD31', itemName: '하장바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 1.158, dimensions: '67→126→165→178→193', note: '' },\n { itemCode: 'SD32', itemName: '짜부가스켓', material: 'E.G.I 0.8T', totalWidth: 500, length: 3000, qty: 4, weight: 0.576, dimensions: '48', note: '80*4,50*8' },\n { itemCode: 'SD33', itemName: '50평철', material: 'E.G.I 1.2T', totalWidth: 500, length: 3000, qty: 2, weight: 0.3, dimensions: '50', note: '' },\n { itemCode: 'SD34', itemName: '전면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.764, dimensions: '120→499→553→588', note: '500*380' },\n { itemCode: 'SD35', itemName: '린텔박스', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 0.504, dimensions: '84→138→168', note: 'S/BOX' },\n { itemCode: 'SD36', itemName: '밑면 점검구', material: 'E.G.I 1.6T', totalWidth: 500, length: 2438, qty: 2, weight: 0.98, dimensions: '90→240→310', note: '500*380' },\n { itemCode: 'SD37', itemName: '후면코너부', material: 'E.G.I 1.2T', totalWidth: 500, length: 1219, qty: 4, weight: 0.45, dimensions: '35→85→120', note: '' },\n ]).map((part, idx) => (\n \n | {part.itemCode} | \n {part.itemName} | \n {part.material} | \n {part.totalWidth} | \n {part.length} | \n {part.qty} | \n {part.weight} | \n {part.dimensions} | \n {part.note} | \n
\n ))}\n \n
\n
\n \n )}\n\n {/* 전개도 도면 섹션 */}\n {activeSection === 'developedDrawing' && (\n
\n 절곡 부품별 전개도 (바라시)
\n \n {/* 엘바 전개도 */}\n
\n
엘바 (SD30)
\n
\n \n
\n
E.G.I 1.6T / L:3000
\n
\n\n {/* 하장바 전개도 */}\n
\n
하장바 (SD31)
\n
\n
\n
\n
E.G.I 1.6T / L:3000
\n
\n\n {/* 전면판 전개도 */}\n
\n
전면판 (SD34)
\n
\n
\n
\n
E.G.I 1.6T / L:3000
\n
\n\n {/* 린텔박스 전개도 */}\n
\n
린텔박스 (SD35)
\n
\n
E.G.I 1.6T / L:3000
\n
\n\n {/* 짜부가스켓 전개도 */}\n
\n
짜부가스켓 (SD32)
\n
\n \n
\n
E.G.I 0.8T / L:3000
\n
\n\n {/* 50평철 전개도 */}\n
\n
50평철 (SD33)
\n
\n \n
\n
E.G.I 1.2T / L:3000
\n
\n\n {/* 밑면 점검구 전개도 */}\n
\n
밑면 점검구 (SD36)
\n
\n
E.G.I 1.6T / L:2438
\n
\n\n {/* 후면코너부 전개도 */}\n
\n
후면코너부 (SD37)
\n
\n
E.G.I 1.2T / L:1219
\n
\n
\n \n )}\n\n {/* 1. 벽면형 섹션 */}\n {activeSection === 'guideRail' && (\n
\n 절곡 상세도면 참조
\n \n \n )}\n\n {/* 2. 하단마감재 섹션 */}\n {activeSection === 'bottomFinish' && (\n
\n \n \n )}\n\n {/* 3. 케이스 섹션 */}\n {activeSection === 'caseBox' && (\n
\n \n \n \n )}\n\n {/* 4. 연기차단재 섹션 */}\n {activeSection === 'smokeBarrier' && (\n
\n \n {/* 레일용 */}\n
\n
{data.smokeBarrier.rail.name}
\n
\n
\n
\n\n {/* 케이스용 */}\n
\n
{data.smokeBarrier.caseItem.name}
\n
\n
\n
\n \n )}\n\n {/* 생산량 합계 섹션 */}\n {activeSection === 'summary' && (\n
\n )}\n
\n\n {/* 하단 버튼 */}\n
\n
\n
\n
\n
\n
\n
\n );\n};\n\n// ★ 작업일지 템플릿 선택/미리보기 모달 (공정별 ISO 인증용 문서 양식 연결)\nconst WorkLogTemplateModal = ({ workOrder, order, workLog, workResults, onClose }) => {\n const processType = workOrder?.processType || workOrder?.processName || '절곡';\n\n // 공정별 작업일지 템플릿 매핑 (문서양식관리 코드와 동일)\n const templateOptions = {\n '절곡': { code: 'WL-FLD', name: '절곡 작업일지', dept: '절곡 생산부서' },\n '슬랫': { code: 'WL-SLT', name: '슬랫 작업일지', dept: '슬랫 생산부서' },\n '스크린': { code: 'WL-SCR', name: '스크린 작업일지', dept: '스크린 생산부서' },\n '재고생산': { code: 'WL-STK', name: '절곡품 재고생산 작업일지', dept: '절곡 생산부서' },\n '포밍': { code: 'WL-STK', name: '포밍 작업일지', dept: '포밍 생산부서' },\n '포장': { code: 'WL-PKG', name: '포장 작업일지', dept: '포장 생산부서' },\n };\n\n const currentTemplate = templateOptions[processType] || templateOptions['절곡'];\n\n // 작업일지 데이터 생성\n const generateWorkLogDataFromWorkOrder = () => {\n const now = new Date();\n const materialInputs = workOrder?.materialInputs || []; // ★ 투입 자재 LOT 정보\n\n // LOT 번호 조회 헬퍼 함수\n const getInputLotNo = (materialCode) => {\n const input = materialInputs.find(m => m.materialCode === materialCode);\n return input?.lotNo || '';\n };\n\n return {\n workLogNo: `WL-${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}-${workOrder.workOrderNo?.split('-').pop() || '001'}`,\n workOrderNo: workOrder.workOrderNo,\n productLotNo: workOrder.productionLot || workOrder.lotNo || `LOT-${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}-001`,\n customerName: workOrder.customerName || order?.customerName || '경동건설',\n siteName: workOrder.siteName || order?.siteName || '강남현장',\n processType: processType,\n workDate: now.toISOString().split('T')[0],\n writer: workOrder.assignee || '작업자',\n totalQty: workOrder.totalQty || workOrder.items?.reduce((sum, i) => sum + (i.qty || 0), 0) || 50,\n goodQty: workOrder.goodQty || workOrder.totalQty || 50,\n // ★ 투입 자재 LOT 정보\n materialInputs: materialInputs,\n inputLotNos: {\n '스크린원단': getInputLotNo('SCR-MAT-001'),\n '미싱실': getInputLotNo('SCR-MAT-004'),\n '앤드락': getInputLotNo('SCR-MAT-002'),\n '슬랫코일': getInputLotNo('SLT-MAT-001'),\n 'EGI철판': getInputLotNo('EGI-1.55T'),\n 'SUS철판': getInputLotNo('SUS-1.2T'),\n '연기차단재': getInputLotNo('BND-SMK-001'),\n },\n defectQty: workOrder.defectQty || 0,\n yieldRate: 98.5,\n // 공정별 상세 데이터\n items: workOrder.items || [\n { itemNo: 1, itemName: '케이스-전면부', spec: 'EGI 1.55T', qty: 16, unit: 'EA' },\n { itemNo: 2, itemName: '케이스-후면부', spec: 'EGI 1.55T', qty: 16, unit: 'EA' },\n ],\n // 결재라인\n approvalLine: {\n writer: { name: workOrder.assignee || '작업자', date: now.toISOString().split('T')[0], signed: true },\n reviewer: { name: '검토자', date: '', signed: false },\n approver: { name: '승인자', date: '', signed: false },\n },\n // 검사 데이터\n inspectionData: {\n appearance: '양호',\n dimensions: '합격',\n judgment: '합격',\n },\n // ★ 스크린 공정: 망규격×폭규격 매트릭스 자동 계산 (수주 데이터 기반)\n screenProductionMatrix: (() => {\n if (processType !== '스크린') return null;\n // 망규격 (원단 폭): 1016, 1270, 1524, 1780\n // 폭규격 (제품 폭): 900, 1000, 1100, 1200, 1300, 1400, 1500\n const meshSizes = ['1016', '1270', '1524', '1780'];\n const widthSizes = ['900', '1000', '1100', '1200', '1300', '1400', '1500'];\n const matrix = {};\n\n // 수주 품목에서 규격 분석 (W, H 기준)\n const items = workOrder.items || order?.items || [];\n items.forEach(item => {\n // productionSpec 필드 우선 참조 (수주 품목 구조)\n const spec = item.productionSpec || {};\n const w = spec.prodWidth || spec.openWidth || item.width || item.productionSizeW ||\n (item.spec ? parseInt(item.spec.split('×')[0]) : 0) || 0;\n const h = spec.prodHeight || spec.openHeight || item.height || item.productionSizeH ||\n (item.spec ? parseInt(item.spec.split('×')[1]) : 0) || 0;\n const qty = item.qty || 1;\n\n // 원단 폭 선택 (높이 기준 - 원단을 세로로 사용)\n let meshSize = '1016';\n if (h > 1600) meshSize = '1780';\n else if (h > 1400) meshSize = '1524';\n else if (h > 1100) meshSize = '1270';\n\n // 제품 폭 선택 (가장 가까운 값)\n let widthSize = '900';\n for (const ws of widthSizes) {\n if (w <= parseInt(ws) + 50) {\n widthSize = ws;\n break;\n }\n }\n if (w > 1500) widthSize = '1500';\n\n const key = `${meshSize}_${widthSize}`;\n matrix[key] = (matrix[key] || 0) + qty;\n });\n\n // 샘플 데이터 (수주 품목이 없는 경우)\n if (Object.keys(matrix).length === 0) {\n matrix['1016_1200'] = 12;\n matrix['1270_1100'] = 8;\n }\n\n return matrix;\n })(),\n // ★ 슬랫 공정: LOT별 생산내역 자동 계산\n slatProductionLots: (() => {\n if (processType !== '슬랫') return null;\n const items = workOrder.items || order?.items || [];\n const lots = [];\n\n // 수주 품목에서 슬랫 수량 계산 (높이 기반 절단매수)\n items.forEach((item, idx) => {\n // productionSpec 필드 우선 참조 (수주 품목 구조)\n const spec = item.productionSpec || {};\n const h = spec.prodHeight || spec.openHeight || item.height || item.productionSizeH ||\n (item.spec ? parseInt(item.spec.split('×')[1]) : 0) || 3000;\n const qty = item.qty || 1;\n const slatHeight = 75; // 슬랫 높이 (mm)\n const slatCount = Math.ceil(h / slatHeight) * qty;\n\n lots.push({\n lotNo: item.lotNo || `LOT-SLT-${now.getFullYear()}-${String(idx + 1).padStart(3, '0')}`,\n spec: '100mm',\n color: item.color || '백색',\n productionQty: slatCount,\n defectQty: Math.floor(slatCount * 0.004), // 0.4% 불량\n yieldRate: 99.6,\n });\n });\n\n // 샘플 데이터 (수주 품목이 없는 경우)\n if (lots.length === 0) {\n lots.push({\n lotNo: `LOT-SLT-${now.getFullYear()}-001`,\n spec: '100mm',\n color: '백색',\n productionQty: 450,\n defectQty: 2,\n yieldRate: 99.6,\n });\n }\n\n return lots;\n })(),\n };\n };\n\n const handlePrint = () => {\n // 실제 문서양식관리의 템플릿을 사용하여 출력\n const printContent = document.getElementById('work-log-template-preview');\n if (printContent) {\n const printWindow = window.open('', '_blank');\n printWindow.document.write(`\n \n \n
${currentTemplate.name} - ${workOrder.workOrderNo}\n \n \n ${printContent.innerHTML}\n \n `);\n printWindow.document.close();\n printWindow.print();\n }\n };\n\n const data = workLog || generateWorkLogDataFromWorkOrder();\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n
\n
\n
{currentTemplate.name}
\n
{currentTemplate.code} · {currentTemplate.dept}
\n
\n
\n
\n
\n\n {/* 작업 정보 요약 */}\n
\n
\n
\n
작업지시번호\n
{workOrder.workOrderNo}
\n
\n
\n
\n
발주처\n
{data.customerName}
\n
\n
\n
현장명\n
{data.siteName}
\n
\n
\n
\n\n {/* 미리보기 영역 - 공정별 전용 템플릿 */}\n
\n {/* 절곡 작업일지 템플릿 (WL-FLD 스타일 - 문서양식관리 동일) */}\n {processType === '절곡' && (\n
\n {/* 문서 헤더 */}\n
\n
절 곡 작 업 일 지
\n
{data.workLogNo}
\n
\n\n {/* 결재라인 (우측 상단) - APR-2LINE 스타일 */}\n
\n
\n \n \n | 작성 | \n 검토 | \n 승인 | \n
\n \n \n \n \n {data.approvalLine.writer.signed ? (\n \n {data.approvalLine.writer.name}\n {data.approvalLine.writer.date} \n \n ) : (\n (인)\n )}\n | \n \n (인)\n | \n \n (인)\n | \n
\n \n
\n
\n\n {/* 기본 정보 테이블 (WL-HEADER-INFO 블록) */}\n
\n \n \n | 신청업체 | \n {data.customerName} | \n 작성일 | \n {data.workDate} | \n
\n \n | 현장명 | \n {data.siteName} | \n 작성자 | \n {data.writer} | \n
\n \n | 작업지시번호 | \n {data.workOrderNo} | \n
\n \n | LOT번호 | \n {data.productLotNo} | \n
\n \n
\n\n {/* ★ 부품 작업 내역 (FoldWorkLogTable 스타일) */}\n
\n
부 품 작 업 내 역
\n
\n \n \n | NO | \n 품목코드 | \n 품명 | \n 재질 | \n 전폭 | \n 길이 | \n 수량 | \n 중량(kg) | \n 전개치수 | \n
\n \n \n {(workOrder.developedParts || data.developedParts || [\n { itemCode: 'SD30', itemName: '엘바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 4, weight: 0.9, dimensions: '75' },\n { itemCode: 'SD31', itemName: '하장바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 1.158, dimensions: '67→126→165→178→193' },\n { itemCode: 'SD32', itemName: '짜부가스켓', material: 'E.G.I 0.8T', totalWidth: 500, length: 3000, qty: 4, weight: 0.576, dimensions: '48' },\n ]).map((part, idx) => (\n \n | {idx + 1} | \n {part.itemCode} | \n {part.itemName} | \n {part.material} | \n {part.totalWidth} | \n {part.length} | \n {part.qty} | \n {part.weight} | \n {part.dimensions} | \n
\n ))}\n \n | 총 생산량: | \n \n {(workOrder.developedParts || data.developedParts || []).reduce((sum, p) => sum + (p.qty || 0), 0) || 14}\n | \n \n {(workOrder.developedParts || data.developedParts || []).reduce((sum, p) => sum + (p.weight || 0), 0).toFixed(2) || '5.20'} kg\n | \n EA | \n
\n \n
\n
\n\n {/* 생산실적 요약 (WL-SUMMARY 블록) */}\n
\n
생 산 실 적 요 약
\n
\n
\n
총 생산량
\n
{data.totalQty}
\n
\n
\n
양품 수량
\n
{data.goodQty}
\n
\n
\n
불량 수량
\n
{data.defectQty}
\n
\n
\n
양품율
\n
{data.yieldRate}%
\n
\n
\n
\n\n {/* 철판 사용 내역 (WL-MATERIAL-USAGE 블록) - ★ 실제 투입 LOT 데이터 표시 */}\n
\n
철 판 사 용 내 역 (입고 LOT 정보)
\n
\n \n \n | NO | \n 자재명 | \n 규격 | \n 입고LOT | \n 사용량 | \n 단위 | \n
\n \n \n {/* ★ 실제 materialInputs 데이터 또는 샘플 데이터 표시 */}\n {(data.materialInputs?.length > 0 ? data.materialInputs : [\n { materialName: 'E.G.I 철판', spec: '1.6T × 4\\' × 8\\'', lotNo: 'LOT-241201-01', qty: 5, unit: '매' },\n { materialName: 'E.G.I 철판', spec: '1.2T × 4\\' × 8\\'', lotNo: 'LOT-241201-02', qty: 2, unit: '매' },\n { materialName: 'E.G.I 철판', spec: '0.8T × 4\\' × 8\\'', lotNo: 'LOT-241201-03', qty: 3, unit: '매' },\n ]).map((input, idx) => (\n \n | {idx + 1} | \n {input.materialName} | \n {input.spec || '-'} | \n {input.lotNo} | \n {input.qty} | \n {input.unit || '매'} | \n
\n ))}\n \n | 합 계: | \n \n {(data.materialInputs?.length > 0 ? data.materialInputs : [{ qty: 5 }, { qty: 2 }, { qty: 3 }]).reduce((sum, i) => sum + (i.qty || 0), 0)}\n | \n 매 | \n
\n \n
\n
\n\n {/* 특기사항 */}\n
\n
\n )}\n\n {/* 슬랫 작업일지 템플릿 (WL-SLT 스타일 - 문서양식관리 동일) */}\n {processType === '슬랫' && (\n
\n {/* 문서 헤더 */}\n
\n
슬 랫 작 업 일 지
\n
{data.workLogNo}
\n
\n\n {/* 결재라인 (우측 상단) - APR-2LINE 스타일 */}\n
\n
\n \n \n | 작성 | \n 검토 | \n 승인 | \n
\n \n \n \n \n {data.approvalLine.writer.signed ? (\n \n {data.approvalLine.writer.name}\n {data.approvalLine.writer.date} \n \n ) : (\n (인)\n )}\n | \n \n (인)\n | \n \n (인)\n | \n
\n \n
\n
\n\n {/* 기본 정보 테이블 (WL-HEADER-INFO 블록) */}\n
\n \n \n | 신청업체 | \n {data.customerName} | \n 작성일 | \n {data.workDate} | \n
\n \n | 현장명 | \n {data.siteName} | \n 작성자 | \n {data.writer} | \n
\n \n | 작업지시번호 | \n {data.workOrderNo} | \n
\n \n | LOT번호 | \n {data.productLotNo} | \n
\n \n
\n\n {/* ★ LOT별 생산내역 (SlatWorkLogTable 스타일) */}\n
\n
L O T 별 생 산 내 역
\n
\n \n \n | NO | \n LOT번호 | \n 규격 | \n 색상 | \n 생산량 | \n 불량 | \n 양품 | \n 양품율 | \n
\n \n \n {(data.slatProductionLots || [\n { lotNo: 'SLT-241210-01', spec: '100mm', color: '화이트', productionQty: 500, defectQty: 5 },\n { lotNo: 'SLT-241210-02', spec: '100mm', color: '아이보리', productionQty: 300, defectQty: 3 },\n ]).map((lot, idx) => {\n const good = lot.productionQty - lot.defectQty;\n const rate = lot.productionQty > 0 ? Math.round((good / lot.productionQty) * 100) : 0;\n return (\n \n | {idx + 1} | \n {lot.lotNo} | \n {lot.spec} | \n {lot.color} | \n {lot.productionQty} | \n {lot.defectQty} | \n {good} | \n {rate}% | \n
\n );\n })}\n \n | 합 계: | \n \n {(data.slatProductionLots || [{ productionQty: 500 }, { productionQty: 300 }]).reduce((sum, l) => sum + (l.productionQty || 0), 0)}\n | \n \n {(data.slatProductionLots || [{ defectQty: 5 }, { defectQty: 3 }]).reduce((sum, l) => sum + (l.defectQty || 0), 0)}\n | \n \n {(() => {\n const lots = data.slatProductionLots || [{ productionQty: 500, defectQty: 5 }, { productionQty: 300, defectQty: 3 }];\n const totalProd = lots.reduce((sum, l) => sum + (l.productionQty || 0), 0);\n const totalDefect = lots.reduce((sum, l) => sum + (l.defectQty || 0), 0);\n return totalProd - totalDefect;\n })()}\n | \n \n {(() => {\n const lots = data.slatProductionLots || [{ productionQty: 500, defectQty: 5 }, { productionQty: 300, defectQty: 3 }];\n const totalProd = lots.reduce((sum, l) => sum + (l.productionQty || 0), 0);\n const totalDefect = lots.reduce((sum, l) => sum + (l.defectQty || 0), 0);\n return totalProd > 0 ? Math.round(((totalProd - totalDefect) / totalProd) * 100) : 0;\n })()}%\n | \n
\n \n
\n
\n\n {/* 생산실적 요약 (WL-SUMMARY 블록) */}\n
\n
생 산 실 적 요 약
\n
\n
\n
총 생산량
\n
{data.totalQty}
\n
\n
\n
양품 수량
\n
{data.goodQty}
\n
\n
\n
불량 수량
\n
{data.defectQty}
\n
\n
\n
양품율
\n
{data.yieldRate}%
\n
\n
\n
\n\n {/* 슬랫 자재 사용 내역 (WL-MATERIAL-USAGE 블록) - ★ 실제 투입 LOT 데이터 표시 */}\n
\n
슬 랫 자 재 사 용 내 역 (입고 LOT 정보)
\n
\n \n \n | NO | \n 자재명 | \n 규격 | \n 입고LOT | \n 사용량 | \n 단위 | \n
\n \n \n {/* ★ 실제 materialInputs 데이터 또는 샘플 데이터 표시 */}\n {(data.materialInputs?.length > 0 ? data.materialInputs : [\n { materialName: '슬랫 원자재', spec: '100mm × 6M', lotNo: 'LOT-SLT-RAW-01', qty: 50, unit: 'EA' },\n { materialName: '슬랫 캡', spec: '100mm', lotNo: 'LOT-CAP-01', qty: 100, unit: 'EA' },\n ]).map((input, idx) => (\n \n | {idx + 1} | \n {input.materialName} | \n {input.spec || '-'} | \n {input.lotNo} | \n {input.qty} | \n {input.unit || 'EA'} | \n
\n ))}\n \n | 합 계: | \n \n {(data.materialInputs?.length > 0 ? data.materialInputs : [{ qty: 50 }, { qty: 100 }]).reduce((sum, i) => sum + (i.qty || 0), 0)}\n | \n EA | \n
\n \n
\n
\n\n {/* 특기사항 */}\n
\n
\n )}\n\n {/* 스크린 작업일지 템플릿 (WL-SCR 스타일 - 엑셀 양식 기반 업데이트) */}\n {processType === '스크린' && (\n
\n {/* ===== 문서 헤더: KD 로고 + 타이틀 + 결재란 ===== */}\n
\n \n \n {/* KD 로고 */}\n | \n KD \n 경동기업 \n KYUNGDONG COMPANY \n | \n {/* 타이틀 + 부서 */}\n \n 작 업 일 지 \n WL-SCR \n 스크린 생산부서 \n | \n {/* 결재란 헤더 */}\n \n 결 \n 재 \n | \n {/* 결재란 - 작성/검토/승인 */}\n 작성 | \n 검토 | \n 승인 | \n
\n \n | \n {data.approvalLine?.writer?.name || '손금주'}\n | \n | \n | \n
\n \n | \n 판매/손금주\n | \n \n 생산\n | \n \n 품질\n | \n
\n \n
\n\n {/* ===== 신청업체 + 신청내용 ===== */}\n
\n \n \n | 신 청 업 체 | \n 신 청 내 용 | \n
\n \n | 발 주 일 | \n \n {(() => {\n const d = new Date(data.workDate || new Date());\n const days = ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'];\n return `${d.getFullYear()}년 ${d.getMonth() + 1}월 ${d.getDate()}일 ${days[d.getDay()]}`;\n })()}\n | \n 현 장 명 | \n \n {data.siteName || '은행동 현장 A동'}\n | \n
\n \n | 업 체 명 | \n {data.customerName || '대운기업'} | \n 작 업 일 자 | \n \n {data.workDate || '2025. . '}\n | \n
\n \n | 담 당 자 | \n {data.manager || '이양게 소장'} | \n 제품 LOT NO | \n \n {data.productLotNo || 'KD-SA-251203-07'}\n | \n
\n \n | 연 락 처 | \n {data.contact || ''} | \n 생산담당자 | \n {data.writer || ''} | \n
\n \n
\n\n {/* ===== ■ 작업 내역 테이블 (엑셀 양식) ===== */}\n
\n
■ 작업 내역
\n
\n \n \n 일련 번호 | \n 입고 LOT NO. | \n 제품명 | \n 부호 | \n 제작사이즈(mm) | \n 규격(매수) | \n
\n \n | 가로 | \n 세로 | \n 나머지 높이 | \n 1220 | \n 900 | \n 600 | \n 400 | \n 300 | \n
\n \n \n {/* 작업 데이터 행 - 샘플 또는 실제 데이터 */}\n {(data.workItems?.length > 0 ? data.workItems : [\n { seq: '01', lotNo: '', productName: '실리카', code: 'A동 FSS-01', width: 6240, height: 3250, remainHeight: 1030, qty1220: 2, qty900: 0, qty600: 0, qty400: 0, qty300: 0 },\n { seq: '02', lotNo: '', productName: '실리카', code: 'A동 FSS-02', width: 7960, height: 3250, remainHeight: 1030, qty1220: 2, qty900: 0, qty600: 0, qty400: 0, qty300: 0 },\n { seq: '03', lotNo: '', productName: '실리카', code: 'A동 FSS-03', width: 3650, height: 3250, remainHeight: 1030, qty1220: 2, qty900: 0, qty600: 0, qty400: 0, qty300: 0 },\n { seq: '04', lotNo: '', productName: '실리카', code: 'A동 FSS-04', width: 4130, height: 3250, remainHeight: 1030, qty1220: 2, qty900: 0, qty600: 0, qty400: 0, qty300: 0 },\n { seq: '05', lotNo: '', productName: '실리카', code: 'A동 FSS-05', width: 4130, height: 3250, remainHeight: 1030, qty1220: 2, qty900: 0, qty600: 0, qty400: 0, qty300: 0 },\n ]).map((item, idx) => (\n \n | {item.seq || String(idx + 1).padStart(2, '0')} | \n {item.lotNo || ''} | \n {item.productName} | \n {item.code} | \n {item.width} | \n {item.height} | \n {item.remainHeight} | \n {item.qty1220 || ''} | \n {item.qty900 || ''} | \n {item.qty600 || ''} | \n {item.qty400 || ''} | \n {item.qty300 || ''} | \n
\n ))}\n {/* 합계 행 */}\n \n | 합 계 | \n \n {(data.workItems?.length > 0 ? data.workItems : [{ qty1220: 2 }, { qty1220: 2 }, { qty1220: 2 }, { qty1220: 2 }, { qty1220: 2 }]).reduce((sum, i) => sum + (i.qty1220 || 0), 0) || 10}\n | \n \n {(data.workItems || []).reduce((sum, i) => sum + (i.qty900 || 0), 0) || ''}\n | \n \n {(data.workItems || []).reduce((sum, i) => sum + (i.qty600 || 0), 0) || ''}\n | \n \n {(data.workItems || []).reduce((sum, i) => sum + (i.qty400 || 0), 0) || ''}\n | \n \n {(data.workItems || []).reduce((sum, i) => sum + (i.qty300 || 0), 0) || ''}\n | \n
\n \n
\n
\n\n {/* ===== 내화실 입고 LOT.NO + 사용량 ===== */}\n
\n {/* 내화실 입고 LOT.NO */}\n
\n
\n \n \n | 내화실 입고 LOT.NO | \n {data.fireLotNo || ''} | \n
\n \n
\n
\n\n {/* 사용량(M) + 사용량(㎡) */}\n
\n
\n \n \n 사용량 (M) | \n 1220 | \n \n {data.usage1220m || '52.22'}\n | \n 400 | \n {data.usage400m || ''} | \n \n 사용량 (㎡) {data.totalUsageM2 || '63.71'}\n | \n
\n \n | 900 | \n {data.usage900m || ''} | \n 300 | \n {data.usage300m || ''} | \n
\n \n | 600 | \n {data.usage600m || ''} | \n | \n | \n
\n \n
\n
\n
\n\n {/* ===== 비고 (노란색 배너) ===== */}\n
\n [비 고] {data.note || '사이즈 착오없이 부탁드립니다'}\n
\n
\n )}\n\n {/* 재고생산 작업일지 템플릿 (WL-STK 스타일) */}\n {processType === '재고생산' && (\n
\n {/* 헤더 */}\n
\n \n \n | \n KD \n 경동기업 \n KD FIRE DOOR COMPANY \n | \n \n 절곡품 재고생산 작업일지 \n 중간검사성적서 \n | \n \n 결 \n 재 \n | \n 작성 | \n 검토 | \n 승인 | \n
\n \n {data.writer} {data.workDate?.slice(5, 10).replace('-', '/')} | \n | \n | \n
\n \n | \n 판매/개발자\n | \n \n 생산\n | \n \n 품질\n | \n
\n \n
\n\n {/* 제품 기본정보 */}\n
\n \n \n | 품명 | \n 케이스 - 전면부 | \n 생산 LOT NO | \n {data.productLotNo} | \n
\n \n | 규격 | \n EGI 1.55T (W576) | \n 로트크기 | \n {data.totalQty} EA | \n
\n \n | 검사일자 | \n {data.workDate} | \n 검사자 | \n {data.writer} | \n
\n \n
\n\n {/* 종합판정 */}\n
\n \n \n | \n [부적합 내용]\n | \n \n \n \n \n | 종합판정 | \n \n \n | \n 합격\n | \n \n \n \n | \n
\n \n
\n
\n )}\n
\n\n {/* 푸터 */}\n
\n
\n
\n 📋 문서코드: {currentTemplate.code} | ISO 인증용 작업일지\n
\n
\n
\n
\n
\n
\n );\n};\n\n// 작업일지 출력용 시트\nconst WorkLogSheet = ({ workOrder, order, workLog, onClose }) => {\n const printRef = React.useRef(null);\n const processType = workOrder?.processType || workLog?.processType || '절곡';\n const data = workLog || createWorkLogDataLocal(workOrder, order, processType);\n\n const handlePrint = () => {\n const content = printRef.current;\n const printWindow = window.open('', '_blank');\n printWindow.document.write(`\n \n \n
작업일지 - ${data.productLotNo}\n \n \n ${content.innerHTML}\n \n `);\n printWindow.document.close();\n printWindow.print();\n };\n\n const getDepartmentName = () => {\n switch (processType) {\n case '스크린': return '스크린 생산부서';\n case '슬랫': return '슬랫 생산부서';\n case '절곡': return '절곡 생산부서';\n case '재고생산': return '절곡 생산부서';\n default: return '생산부서';\n }\n };\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n {processType === '재고생산' ? '재고생산 작업일지 (중간검사성적서)' : '작업일지 출력'}\n {processType}\n
\n
\n
\n\n {/* 출력 미리보기 */}\n
\n
\n {/* 문서 헤더 (공통) */}\n
\n \n \n | \n [KD] \n 경동기업 \n | \n \n \n {processType === '재고생산' ? '절곡품 재고생산 작업일지' : '작 업 일 지'}\n \n {processType === '재고생산' && (\n (중간검사성적서) \n )}\n | \n 작성 | \n 검토 | \n 승인 | \n
\n \n | {data.approval?.writer} | \n {data.approval?.checker} | \n {data.approval?.approver} | \n
\n \n | \n {processType === '재고생산' ? '판매/개발자' : '판매/전진'} | 생산 | 품질\n | \n
\n \n
\n\n {/* 부서 */}\n
\n {getDepartmentName()}\n
\n\n {/* 신청업체 / 신청내용 (공통) */}\n
\n \n \n | 신 청 업 체 | \n 신 청 내 용 | \n
\n \n | 발 주 일 | \n {data.orderDate} | \n 현 장 명 | \n {data.siteName} | \n
\n \n | 업 체 명 | \n {data.company} | \n 작업일자 | \n {data.workDate} | \n
\n \n | 담 당 자 | \n {data.manager} | \n 제품 LOT NO. | \n {data.productLotNo} | \n
\n \n | 연 락 처 | \n {data.contact} | \n 생산담당자 | \n {data.productionManager} | \n
\n \n
\n\n {/* 스크린 공정 내용 (첨부 이미지 기준 - 규격별 매수 테이블) */}\n {processType === '스크린' && (\n <>\n {/* ■ 작업 내역 테이블 (첨부 이미지 기준) */}\n
■ 작업 내역
\n
\n \n \n 일련 번호 | \n 입고 LOT NO. | \n 제품명 | \n 부호 | \n 제작사이즈(mm) | \n 규격(매수) | \n
\n \n | 가로 | \n 세로 | \n 나머지 높이 | \n 1180 | \n 900 | \n 600 | \n 400 | \n 300 | \n
\n \n \n {(data.screenWorkItems || [\n { no: 1, lotNo: '', productName: '와이어', partCode: '4층 FSS17', width: 7400, height: 2950 },\n { no: 2, lotNo: '', productName: '와이어', partCode: '4층 FSS5', width: 4700, height: 2950 },\n { no: 3, lotNo: '', productName: '와이어', partCode: '4층 FSS17A', width: 6790, height: 2950 },\n ]).map((item, idx) => {\n // 스크린 규격별 장수 자동 계산: 높이를 규격(1180, 900, 600, 400, 300)으로 분할\n const specCalc = item.spec1180 !== undefined ? item : calculateScreenSpecSheetCount(item.height);\n return (\n \n | {item.no || idx + 1} | \n {item.lotNo} | \n {item.productName} | \n {item.partCode} | \n {item.width} | \n {item.height} | \n {item.remainHeight || specCalc.remainHeight} | \n {specCalc.spec1180 || ''} | \n {specCalc.spec900 || ''} | \n {specCalc.spec600 || ''} | \n {specCalc.spec400 || ''} | \n {specCalc.spec300 || ''} | \n
\n );\n })}\n {/* 합계 행 */}\n \n | 합 계 | \n {data.screenTotals?.spec1180 || '22'} | \n {data.screenTotals?.spec900 || '11'} | \n {data.screenTotals?.spec600 || ''} | \n {data.screenTotals?.spec400 || ''} | \n {data.screenTotals?.spec300 || ''} | \n
\n \n
\n\n {/* 내화실 입고 LOT.NO / 사용량 */}\n
\n \n \n | 내화실 입고 LOT.NO | \n {data.fireRoomLotNo || ''} | \n
\n \n 사용량 (M) | \n 1220 | \n {data.usage?.['1220'] || '127.60'} | \n 400 | \n {data.usage?.['400'] || ''} | \n \n 사용량 (m²) \n {data.totalUsageArea || '213.09'} \n | \n
\n \n | 900 | \n {data.usage?.['900'] || '63.8'} | \n 300 | \n {data.usage?.['300'] || ''} | \n
\n \n
\n >\n )}\n\n {/* 슬랫 공정 내용 (첨부 이미지 기준 - 상세 작업내역 테이블) */}\n {processType === '슬랫' && (\n <>\n {/* ■ 작업내역 테이블 (첨부 이미지 기준) */}\n
■ 작업내역
\n
\n \n \n 일련 번호 | \n 입고 LOT NO. | \n 방화유리 수량 | \n 품명 | \n 제작사이즈(mm) - 미미제외 | \n 조인트바 수량 | \n 코일 사용량 | \n 설치층/부호 | \n
\n \n | 가로 | \n 세로 | \n 매수(세로) | \n
\n \n \n {(data.slatWorkItems || [\n { no: 1, lotNo: '250508-01i', itemName: '슬랫\\n[EGI1.55T]', width: 3610, height: 4350, jointBarQty: 5, coilUsage: 225, installFloor: '/' },\n { no: 2, lotNo: '250425-01i', itemName: '슬랫\\n[EGI1.55T]', width: 3610, height: 4350, jointBarQty: 5, coilUsage: 225, installFloor: '/' },\n ]).map((item, idx) => {\n // 슬랫 매수 자동 계산: 코일길이(305m) ÷ 제품높이 = 절단매수\n const calculatedSheetCount = item.sheetCount || calculateWorkLogSlatSheetCount(item);\n return (\n \n | {item.no || idx + 1} | \n {item.lotNo} | \n | \n {item.itemName} | \n {item.width} | \n {item.height} | \n {calculatedSheetCount} | \n {item.jointBarQty} | \n {item.coilUsage} | \n {item.installFloor} | \n
\n );\n })}\n \n
\n\n {/* 합계 테이블 */}\n
\n \n \n | 생산량 합계 (m²) | \n {data.totalProduction?.area || '450'} | \n 조인트바 합계 | \n {data.totalProduction?.jointBar || '10'} | \n
\n \n
\n >\n )}\n\n {/* 절곡 공정 내용 */}\n {processType === '절곡' && (\n <>\n {/* 제품정보 */}\n
\n \n \n | 제품명 | \n {data.productCode} | \n {data.productType} | \n 마감유형 | \n {data.finishType} | \n {data.railType} | \n
\n \n
\n\n {/* 1. 벽면형 */}\n
1. 작업 내역 - 절곡 상세도면 참조
\n
\n \n \n | 세부품명 | \n 재질 | \n 입고 & 생산 LOT NO | \n 길이/규격 | \n 수량 | \n
\n \n \n {(data.guideRail || generateBendingWorkLogParts({\n productHeight: data.productHeight || 3000,\n productWidth: data.productWidth || 3000,\n qty: data.qty || 1\n })).map((item, idx) => {\n // 절곡 부품 수량 자동 계산 (수량이 없을 경우)\n const calculatedQty = item.qty || calculateBendingPartQty(\n item.name,\n data.productHeight || 3000,\n data.productWidth || 3000,\n data.qty || 1\n );\n return (\n \n | \n {idx < 9 ? '①②③④⑤⑥⑦⑧⑨'[idx] : '⑩'}{item.name}\n | \n {item.material} | \n {item.inputLotNo || ''} | \n {item.spec} | \n {calculatedQty} | \n
\n );\n })}\n \n
\n\n {/* 생산량 합계 */}\n
\n \n \n | 생산량 합계 (kg) | \n SUS | \n EGI | \n
\n \n | {data.totalProduction?.sus || ''} | \n {data.totalProduction?.egi || ''} | \n
\n \n
\n >\n )}\n\n {/* 재고생산 (중간검사성적서) */}\n {processType === '재고생산' && (\n <>\n {/* 제품 기본 정보 */}\n
\n \n \n | 품명 | \n {data.productName || '가이드 레일(GR-120-14)'} | \n 규격 | \n {data.spec || '120 x 14 x 2500'} | \n 길이 | \n {data.length || '2500'} | \n
\n \n 입고 LOT NO | \n {data.inboundLotNo || 'LOT-2024-001'} | \n 생산 LOT NO | \n {data.productLotNo || 'PLT-2024-001'} | \n 로트 크기 | \n {data.lotSize || '100'} | \n
\n \n
\n\n {/* 중간검사 기준서 */}\n
■ 중간검사 기준서
\n
\n
\n
\n
[절곡 상세 도면]
\n
제품 치수 및 형상 기준
\n {data.drawingImage ? (\n

\n ) : (\n
\n
• 길이: {data.length || '2500'}mm
\n
• 너비: {data.width || '120'}mm
\n
• 간격: {data.interval || '14'}mm
\n
\n )}\n
\n
\n
\n\n {/* 중간검사 DATA 테이블 */}\n
■ 중간검사 DATA
\n
\n \n \n | 검사항목 | \n 겉모양/절곡상태 | \n 치수 | \n 판정 | \n
\n \n | 육안검사 | \n 길이 | \n 너비 | \n 간격 | \n
\n \n | 기준값 | \n 이상무 | \n {data.standardLength || '2500±3'} | \n {data.standardWidth || '120±1'} | \n {data.standardInterval || '14±0.5'} | \n - | \n
\n \n \n {(data.inspectionData || [\n { no: 1, appearance: '양호', length: 2501, width: 120, interval: 14.0, result: '합격' },\n { no: 2, appearance: '양호', length: 2499, width: 119.8, interval: 14.1, result: '합격' },\n { no: 3, appearance: '양호', length: 2500, width: 120.2, interval: 13.9, result: '합격' },\n { no: 4, appearance: '양호', length: 2502, width: 120.0, interval: 14.0, result: '합격' },\n { no: 5, appearance: '양호', length: 2498, width: 119.9, interval: 14.2, result: '합격' },\n ]).map((item, idx) => (\n \n | {item.no || idx + 1} | \n {item.appearance} | \n {item.length} | \n {item.width} | \n {item.interval} | \n \n {item.result}\n | \n
\n ))}\n \n
\n\n {/* 종합판정 */}\n
\n\n {/* 검사자 정보 */}\n
\n \n \n | 검사일자 | \n {data.inspectionDate || new Date().toLocaleDateString()} | \n 검사자 | \n {data.inspector || ''} | \n
\n \n
\n >\n )}\n\n {/* 비고 (공통) */}\n
\n
[비 고]
\n
{data.remarks}
\n
\n
\n
\n
\n
\n );\n};\n\n// ============ 재고 확인 모달 ============\nconst MaterialCheckModal = ({ workOrder, onClose }) => {\n // 샘플 BOM 데이터 (실제로는 품목별 BOM에서 가져옴)\n const bomData = [\n { id: 1, materialCode: 'SCR-MAT-001', materialName: '스크린 원단', unit: '㎡', required: 26, stock: 150, status: 'ok' },\n { id: 2, materialCode: 'SCR-MAT-002', materialName: '앤드락', unit: 'EA', required: 40, stock: 200, status: 'ok' },\n { id: 3, materialCode: 'SCR-MAT-003', materialName: '하단바', unit: 'EA', required: 10, stock: 8, status: 'shortage' },\n { id: 4, materialCode: 'SCR-MAT-004', materialName: '미싱실', unit: 'M', required: 500, stock: 2000, status: 'ok' },\n { id: 5, materialCode: 'SCR-MAT-005', materialName: '포장박스', unit: 'EA', required: 10, stock: 50, status: 'ok' },\n ];\n\n const hasShortage = bomData.some(b => b.status === 'shortage');\n\n return (\n
\n
\n
\n
\n
\n
\n
자재 재고 확인
\n
{workOrder?.workOrderNo}
\n
\n
\n
\n
\n\n {hasShortage && (\n
\n
\n
재고 부족 자재가 있습니다. 자재 발주가 필요합니다.\n
\n )}\n\n
\n
\n \n \n | 자재코드 | \n 자재명 | \n 필요량 | \n 재고 | \n 상태 | \n
\n \n \n {bomData.map(item => (\n \n | {item.materialCode} | \n {item.materialName} | \n {item.required} {item.unit} | \n \n \n {item.stock} {item.unit}\n \n | \n \n {item.status === 'ok' ? (\n \n 충분\n \n ) : (\n \n 부족\n \n )}\n | \n
\n ))}\n \n
\n
\n\n
\n {hasShortage && (\n \n )}\n \n
\n
\n
\n );\n};\n\n// ============ 자재 투입 모달 ============\nconst MaterialInputModal = ({\n workOrder,\n inventory = [],\n materialLots: allMaterialLots = [],\n onClose,\n onSubmit,\n // 기능정의서 관련 props\n showFeatureDescription,\n featureBadges = {},\n selectedBadge,\n setSelectedBadge,\n updateFeatureBadge,\n setEditingBadge\n}) => {\n const [selectedMaterial, setSelectedMaterial] = useState(null);\n const [selectedLot, setSelectedLot] = useState(null);\n const [qty, setQty] = useState(0);\n const [inputBy, setInputBy] = useState(workOrder?.assignee || '');\n const [note, setNote] = useState('');\n\n // 공정별 필요 자재 매핑 (LOT 정보 포함)\n const getProcessMaterials = () => {\n const mapping = {\n // 스크린 공정: 원단, 앤드락, 하단바, 미싱실, 포장박스\n '스크린': ['SCR-MAT-001', 'SCR-MAT-002', 'SCR-MAT-003', 'SCR-MAT-004', 'SCR-MAT-005'],\n // 슬랫 공정: 슬랫코일, 미미자재, 포장박스\n '슬랫': ['SLT-MAT-001', 'SLT-MAT-002', 'SCR-MAT-005'],\n // 절곡 공정: EGI철판, SUS철판, 리벳, 볼트, 실리콘\n '절곡': ['EGI-1.55T', 'SUS-1.2T', 'SM-RIVET-01', 'SM-BOLT-01', 'SM-SILICON-01'],\n };\n const codes = mapping[workOrder?.processType] || [];\n return inventory.filter(m => codes.includes(m.materialCode));\n };\n\n const processMaterials = getProcessMaterials();\n\n // 선택된 자재의 LOT 목록 (실제 materialLots 데이터 사용 + FIFO 정렬)\n const getMaterialLots = (material) => {\n if (!material) return [];\n\n // 해당 자재의 LOT 필터링 (materialCode로 매칭)\n const matchingLots = allMaterialLots.filter(lot =>\n lot.materialCode === material.materialCode &&\n lot.remainingQty > 0 &&\n lot.status === 'AVAILABLE'\n );\n\n // FIFO 정렬: 입고일 기준 오름차순 (가장 오래된 것 먼저)\n const sortedLots = matchingLots.sort((a, b) =>\n new Date(a.inboundDate) - new Date(b.inboundDate)\n );\n\n // FIFO 순위 추가\n return sortedLots.map((lot, index) => ({\n lotNo: lot.lotNo,\n inboundDate: lot.inboundDate,\n remainQty: lot.remainingQty,\n expiryDate: lot.expiryDate,\n supplier: lot.supplier,\n location: lot.location,\n fifoRank: index + 1, // FIFO 순위 (1이 가장 먼저 사용 권장)\n isRecommended: index === 0, // 첫 번째가 FIFO 추천\n }));\n };\n\n const materialLots = selectedMaterial ? getMaterialLots(selectedMaterial) : [];\n\n const handleSubmit = () => {\n if (!selectedMaterial) {\n alert('자재를 선택해주세요.');\n return;\n }\n if (!selectedLot) {\n alert('LOT를 선택해주세요.');\n return;\n }\n if (qty <= 0) {\n alert('투입 수량을 입력해주세요.');\n return;\n }\n if (qty > selectedLot.remainQty) {\n alert(`LOT 잔량이 부족합니다.\\n\\nLOT 잔량: ${selectedLot.remainQty} ${selectedMaterial.unit}`);\n return;\n }\n onSubmit?.(selectedMaterial.materialCode, qty, selectedLot.lotNo, inputBy, note);\n };\n\n // 모달용 뱃지 가져오기\n const modalBadges = featureBadges['modal-material-input-modal'] || [];\n\n return (\n
\n
\n {/* 기능정의서 모드일 때 뱃지 오버레이 */}\n {showFeatureDescription && (\n
updateFeatureBadge?.(badgeId, pos)}\n onEditBadge={(badge) => setEditingBadge?.(badge)}\n modalId=\"material-input-modal\"\n screenId=\"DLG-P02-02\"\n />\n )}\n\n {/* 헤더 */}\n \n
투입자재 등록
\n \n \n\n \n {/* FIFO 안내 범례 */}\n
\n
FIFO 순위:\n
\n \n 1\n 최우선\n \n \n 2\n 차선\n \n \n 3+\n 대기\n \n
\n
\n\n {/* 자재 선택 */}\n
\n
\n
\n
\n {processMaterials.length === 0 && (\n
\n 이 공정에 배정된 자재가 없습니다.\n
\n )}\n
\n
\n\n {/* LOT 선택 */}\n {selectedMaterial && (\n
\n
\n
\n
\n {materialLots.length === 0 && (\n
\n 선택한 자재의 LOT가 없습니다.\n
\n )}\n
\n
\n )}\n\n {/* 투입 정보 입력 */}\n {selectedLot && (\n
\n
\n
\n
\n
\n
{selectedLot.lotNo}
\n
\n
\n
\n
{selectedLot.remainQty} {selectedMaterial.unit}
\n
\n
\n
\n
\n setQty(Number(e.target.value))}\n min={1}\n max={selectedLot.remainQty}\n className=\"flex-1 px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-gray-400 focus:border-gray-400 text-base font-bold text-black\"\n />\n {selectedMaterial.unit}\n
\n
\n
\n \n setInputBy(e.target.value)}\n placeholder=\"작업자명\"\n className=\"w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-gray-400 focus:border-gray-400 text-base text-black\"\n />\n
\n
\n \n setNote(e.target.value)}\n placeholder=\"특이사항 입력 (선택)\"\n className=\"w-full px-3 py-2 border border-gray-300 rounded focus:ring-2 focus:ring-gray-400 focus:border-gray-400 text-base text-black\"\n />\n
\n
\n
\n )}\n
\n\n {/* 하단 버튼 */}\n \n \n \n
\n \n
\n );\n};\n\n// ============ 부적합품 처리 모달 ============\nconst DefectProcessModal = ({ inspection, onClose, onProcess }) => {\n const [processType, setProcessType] = useState('');\n const [reason, setReason] = useState('');\n const [note, setNote] = useState('');\n\n const processTypes = [\n { id: 'rework', label: '재작업', description: '수정 후 재검사', icon: '🔄' },\n { id: 'scrap', label: '폐기', description: '원자재 재투입 필요', icon: '🗑️' },\n { id: 'downgrade', label: '등급 하향', description: '품질 등급 조정', icon: '⬇️' },\n { id: 'special', label: '특채', description: '특별 허용 승인', icon: '✅' },\n ];\n\n const handleSubmit = () => {\n if (!processType) {\n alert('처리 방법을 선택해주세요.');\n return;\n }\n onProcess?.({\n type: processType,\n reason,\n note,\n processedAt: new Date().toISOString(),\n });\n };\n\n return (\n
\n
\n
\n
\n
\n
\n
부적합품 처리
\n
검사 불합격 - 처리 방법 선택
\n
\n
\n
\n
\n\n
\n {/* 검사 정보 */}\n
\n
\n
\n 검사번호:\n {inspection?.inspectionNo || 'INS-001'}\n
\n
\n 품목:\n {inspection?.productName || '스크린 셔터'}\n
\n
\n
\n\n {/* 처리 방법 선택 */}\n
\n
\n
\n {processTypes.map(type => (\n
\n ))}\n
\n
\n\n {/* 사유 */}\n
\n \n \n
\n\n {/* 비고 */}\n
\n \n
\n\n {/* 안내 */}\n {processType === 'scrap' && (\n
\n
\n ⚠️ 폐기 처리 시 해당 LOT의 원자재를 재투입하여 다시 생산해야 합니다.\n
\n
\n )}\n
\n\n
\n \n \n
\n
\n
\n );\n};\n\n// ============ 알림 컴포넌트 ============\nconst NotificationBell = ({ notifications = [], onViewAll }) => {\n const [isOpen, setIsOpen] = useState(false);\n\n const sampleNotifications = [\n { id: 1, type: 'urgent', message: '긴급 작업지시가 추가되었습니다.', time: '5분 전', read: false },\n { id: 2, type: 'change', message: '작업지시 WO-001 일정이 변경되었습니다.', time: '30분 전', read: false },\n { id: 3, type: 'issue', message: '김생산님이 이슈를 보고했습니다.', time: '1시간 전', read: true },\n { id: 4, type: 'complete', message: '작업지시 WO-002가 완료되었습니다.', time: '2시간 전', read: true },\n ];\n\n const allNotifications = notifications.length > 0 ? notifications : sampleNotifications;\n const unreadCount = allNotifications.filter(n => !n.read).length;\n\n const getIcon = (type) => {\n switch (type) {\n case 'urgent': return
;\n case 'change': return
;\n case 'issue': return
;\n case 'complete': return
;\n default: return
;\n }\n };\n\n return (\n
\n
\n\n {isOpen && (\n <>\n
setIsOpen(false)} />\n
\n
\n
알림
\n {unreadCount > 0 && (\n {unreadCount}개 새 알림\n )}\n \n
\n {allNotifications.map(notif => (\n
\n
\n {getIcon(notif.type)}\n
\n
\n {notif.message}\n
\n
{notif.time}
\n
\n
\n
\n ))}\n
\n
\n
\n >\n )}\n
\n );\n};\n\n// ==================== 생산지시 관리 컴포넌트 ====================\nconst ProductionOrderList = ({ productionOrders, workOrders, onNavigate, onCreateWorkOrders, onDeleteProductionOrders }) => {\n const [search, setSearch] = useState('');\n const [activeTab, setActiveTab] = useState('all');\n const [showCreateModal, setShowCreateModal] = useState(false);\n const [selectedPO, setSelectedPO] = useState(null);\n const [selectedIds, setSelectedIds] = useState([]);\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n const tabs = [\n { id: 'all', label: '전체', count: productionOrders.length },\n { id: '생산대기', label: '생산대기', count: productionOrders.filter(po => po.status === '생산대기').length },\n { id: '생산중', label: '생산중', count: productionOrders.filter(po => po.status === '생산중').length },\n { id: '생산완료', label: '생산완료', count: productionOrders.filter(po => po.status === '생산완료').length },\n ];\n\n const filteredOrders = productionOrders.filter(po => {\n const matchSearch = !search ||\n po.productionOrderNo.toLowerCase().includes(search.toLowerCase()) ||\n po.orderNo.toLowerCase().includes(search.toLowerCase()) ||\n po.siteName.toLowerCase().includes(search.toLowerCase()) ||\n po.customerName.toLowerCase().includes(search.toLowerCase());\n const matchTab = activeTab === 'all' || po.status === activeTab;\n return matchSearch && matchTab;\n }).sort((a, b) => b.id - a.id); // ID 기준 내림차순 (최신 등록 최상단)\n\n // 전체 선택/해제\n const handleSelectAll = (e) => {\n if (e.target.checked) {\n setSelectedIds(filteredOrders.map(po => po.id));\n } else {\n setSelectedIds([]);\n }\n };\n\n // 개별 선택\n const handleSelectOne = (id, e) => {\n e.stopPropagation();\n setSelectedIds(prev =>\n prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]\n );\n };\n\n // 삭제 확인\n const handleDeleteConfirm = () => {\n if (onDeleteProductionOrders && selectedIds.length > 0) {\n onDeleteProductionOrders(selectedIds);\n }\n setSelectedIds([]);\n setShowDeleteModal(false);\n };\n\n // 해당 생산지시의 작업지시서 개수 조회\n const getWorkOrderCount = (po) => {\n return workOrders.filter(wo => wo.orderNo === po.orderNo).length;\n };\n\n const handleCreateWorkOrders = (po) => {\n setSelectedPO(po);\n setShowCreateModal(true);\n };\n\n const confirmCreateWorkOrders = () => {\n if (selectedPO && onCreateWorkOrders) {\n onCreateWorkOrders(selectedPO);\n }\n setShowCreateModal(false);\n setSelectedPO(null);\n };\n\n return (\n
\n {/* 헤더 */}\n
\n
\n \n 생산지시 목록\n
\n \n\n {/* 프로세스 안내 */}\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n\n {/* 검색 및 탭 */}\n
\n
\n
\n \n setSearch(e.target.value)}\n className=\"w-full pl-10 pr-4 py-2 border rounded-lg text-sm\"\n />\n
\n
\n {tabs.map(tab => (\n \n ))}\n
\n
\n
\n\n {/* 목록 */}\n
\n {/* 선택 삭제 버튼 (2개 이상 선택 시) */}\n {selectedIds.length >= 2 && (\n
\n \n {selectedIds.length}개 항목 선택됨\n \n \n
\n )}\n
\n {filteredOrders.length === 0 && (\n
\n )}\n
\n\n {/* 작업지시 생성 확인 모달 */}\n {showCreateModal && selectedPO && (\n
\n
\n
\n
\n \n 작업지시서 자동 생성\n
\n
\n
\n
\n
\n
생산지시: {selectedPO.productionOrderNo}
\n
현장명: {selectedPO.siteName}
\n
수량: {selectedPO.qty}개
\n
\n
\n\n
\n
BOM 기반 공정별 작업지시서 생성
\n
\n {selectedPO.processGroups?.map((pg, idx) => (\n
\n
\n {pg.processSeq}\n
\n
{pg.processName} 공정\n
({pg.items?.length || 0}개 품목)\n
\n ))}\n
\n
\n\n
\n 위 공정에 대한 작업지시서가 자동으로 생성되어 생산팀으로 전달됩니다.\n
\n
\n
\n
\n
\n
\n
\n
\n )}\n\n {/* 삭제 확인 모달 */}\n {showDeleteModal && (\n
\n
\n
\n
\n
\n
\n
삭제 확인
\n
선택한 항목을 삭제하시겠습니까?
\n
\n
\n
\n
\n {selectedIds.length}개의 생산지시가 삭제됩니다.\n 삭제된 데이터는 복구할 수 없습니다.\n
\n
\n
\n \n \n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 생산지시 상세 화면\nconst ProductionOrderDetail = ({ productionOrder, workOrders, orders, onNavigate, onBack, onCreateWorkOrders, onUpdateStatus }) => {\n const [showCreateModal, setShowCreateModal] = useState(false);\n\n if (!productionOrder) {\n return (\n
\n
생산지시 정보를 찾을 수 없습니다.
\n
\n
\n );\n }\n\n const relatedWorkOrders = workOrders.filter(wo => wo.orderNo === productionOrder.orderNo);\n const relatedOrder = orders.find(o => o.orderNo === productionOrder.orderNo);\n\n const handleCreateWorkOrders = () => {\n if (onCreateWorkOrders) {\n onCreateWorkOrders(productionOrder);\n setShowCreateModal(false);\n }\n };\n\n const getProcessStatusColor = (processName) => {\n const wo = relatedWorkOrders.find(w => w.processName === processName);\n if (!wo) return 'bg-gray-200 text-gray-600';\n if (wo.status === '완료') return 'bg-green-100 text-green-700';\n if (wo.status === '진행중' || wo.status === '작업중') return 'bg-blue-100 text-blue-700';\n return 'bg-yellow-100 text-yellow-700';\n };\n\n return (\n
\n {/* 상단 헤더 - 타이틀 위, 버튼 아래 */}\n
\n {/* 타이틀 영역 */}\n
\n
\n \n 생산지시 상세\n
\n
\n {productionOrder.productionOrderNo}\n \n
\n
\n {/* 버튼 영역 */}\n
\n \n {!productionOrder.workOrdersGenerated && (\n \n )}\n
\n
\n\n {/* 공정 진행 현황 시각화 */}\n
\n
\n \n 공정 진행 현황\n
\n\n
\n {productionOrder.processGroups?.map((pg, idx) => {\n const wo = relatedWorkOrders.find(w => w.processName === pg.processName);\n const isComplete = wo?.status === '완료';\n const isActive = wo?.status === '진행중' || wo?.status === '작업중';\n\n return (\n
\n \n
\n {isComplete ?
:\n isActive ?
:\n
{pg.processSeq}}\n
\n
{pg.processName}\n
\n {wo ? wo.status : '대기'}\n \n {wo && (\n
\n )}\n
\n {idx < productionOrder.processGroups.length - 1 && (\n \n )}\n \n );\n })}\n
\n
\n\n {/* 정보 카드들 */}\n
\n {/* 기본 정보 */}\n
\n
기본 정보
\n
\n
\n 생산지시번호\n {productionOrder.productionOrderNo}\n
\n
\n 수주번호\n \n
\n
\n 생산지시일\n {productionOrder.productionOrderDate}\n
\n
\n 납기일\n {productionOrder.dueDate}\n
\n
\n 수량\n {productionOrder.qty}개\n
\n
\n
\n\n {/* 거래처/현장 정보 */}\n
\n
거래처/현장 정보
\n
\n
\n 거래처\n {productionOrder.customerName}\n
\n
\n 현장명\n {productionOrder.siteName}\n
\n
\n 제품유형\n {productionOrder.productType}\n
\n
\n
\n
\n\n {/* BOM 품목별 공정 매핑 */}\n
\n
\n \n BOM 품목별 공정 분류\n
\n
\n {productionOrder.processGroups?.map((pg, idx) => (\n
\n
\n \n {pg.processSeq}\n \n {pg.processName} 공정\n ({pg.items?.length || 0}개 품목)\n
\n
\n
\n {pg.items?.map((item, itemIdx) => (\n
\n {item.itemCode}\n {item.itemName}\n {item.qty} {item.unit}\n
\n ))}\n
\n
\n
\n ))}\n
\n
\n\n {/* 작업지시서 목록 */}\n
\n
\n
\n \n 작업지시서 목록\n
\n {relatedWorkOrders.length === 0 && (\n
\n )}\n
\n\n {relatedWorkOrders.length > 0 ? (\n
\n
\n \n \n | 작업지시번호 | \n 공정 | \n 수량 | \n 상태 | \n 담당자 | \n
\n \n \n {relatedWorkOrders.map(wo => (\n \n | \n \n | \n {wo.processName} | \n {wo.qty}개 | \n \n \n | \n {wo.worker || '-'} | \n
\n ))}\n \n
\n
\n ) : (\n
\n
\n
아직 작업지시서가 생성되지 않았습니다
\n
위 버튼을 클릭하여 BOM 기반 작업지시서를 자동 생성하세요
\n
\n )}\n
\n\n {/* 절곡 부품 전개도 정보 (FOLD/절곡 공정이 있는 경우에만 표시) */}\n {(productionOrder.processGroups?.some(pg => pg.processName === 'FOLD' || pg.processName === '절곡') || productionOrder.productType === '절곡') && (\n
\n
\n \n 절곡 부품 전개도 정보\n
\n\n {/* 전개도 섹션들 */}\n
\n {/* 1. 벽면형 섹션 */}\n
\n
\n 1.1 벽면형 [120·70]\n
\n
\n
\n \n \n | 부품코드 | \n 세부품명 | \n 재질 | \n LOT NO | \n 길이/규격 | \n 수량 | \n
\n \n \n \n | ① | \n 마감재 | \n EGI1.15T | \n LOT-M-2024-001 | \n 4300 | \n 8 | \n
\n \n | ② | \n 가이드레일 | \n EGI1.6T | \n LOT-M-2024-002 | \n 3000 | \n 4 | \n
\n \n | ③ | \n C형 | \n EGI1.55T | \n LOT-M-2024-003 | \n 4300 | \n 4 | \n
\n \n | ④ | \n D형 | \n EGI1.6T | \n LOT-M-2024-004 | \n 4300 | \n 4 | \n
\n \n | ⑤ | \n 별도마감재 | \n SUS1.2T | \n LOT-M-2024-005 | \n 4300 | \n 2 | \n
\n \n | - | \n 하부BASE | \n EGI1.55T | \n LOT-M-2024-006 | \n 하부BASE 130·80 | \n 4 | \n
\n \n
\n
\n
\n\n {/* 2. 하단마감재 섹션 */}\n
\n
\n 2. 하단마감재 [60·40]\n
\n
\n
\n \n \n | 부품코드 | \n 세부품명 | \n 재질 | \n LOT NO | \n 길이/규격 | \n 수량 | \n
\n \n \n \n | ① | \n 하단마감재 | \n EGI1.55T | \n LOT-M-2024-007 | \n 3000 | \n 4 | \n
\n \n | ② | \n 하단보강엘바 | \n EGI1.55T | \n LOT-M-2024-009 | \n 3000 | \n 4 | \n
\n \n | ③ | \n 하단보강평철 | \n EGI1.15T | \n LOT-M-2024-010 | \n 3000 | \n 2 | \n
\n \n | ④ | \n 별도마감재 | \n SUS1.2T | \n LOT-M-2024-008 | \n 3000 | \n 2 | \n
\n \n
\n
\n
\n\n {/* 3. 케이스 섹션 */}\n
\n
\n 3.1 케이스 [500*330]\n
\n
\n
\n \n \n | 부품코드 | \n 세부품명 | \n 재질 | \n LOT NO | \n 길이/규격 | \n 수량 | \n
\n \n \n \n | ① | \n 전면부 | \n EGI1.55T | \n LOT-M-2024-011 | \n 3000/2438 | \n 2 | \n
\n \n | ② | \n 린텔부 | \n EGI1.55T | \n LOT-M-2024-012 | \n 3000/2438 | \n 2 | \n
\n \n | ③⑤ | \n 하부점검구 | \n EGI1.55T | \n LOT-M-2024-013 | \n 3000/2438 | \n 4 | \n
\n \n | ④ | \n 후면코너부 | \n EGI1.55T | \n LOT-M-2024-014 | \n 3000/2438 | \n 4 | \n
\n \n | ⑥ | \n 상부덮개 | \n EGI1.55T | \n LOT-M-2024-015 | \n 1220 | \n 5 | \n
\n \n | ⑦ | \n 측면부(마구리) | \n EGI1.55T | \n LOT-M-2024-016 | \n 505*385 | \n 4 | \n
\n \n
\n
\n
\n\n {/* 4. 연기차단재 섹션 */}\n
\n
\n 4. 연기차단재\n
\n
\n
\n \n \n | 부품코드 | \n 세부품명 | \n 재질 | \n LOT NO | \n 길이/규격 | \n 수량 | \n
\n \n \n \n | - | \n 레일용 [W50] | \n EG0.8T + 화이바 글라스 | \n LOT-M-2024-017 | \n W50 | \n 8 | \n
\n \n | - | \n 케이스용 [W80] | \n EG0.8T + 화이바 글라스 | \n LOT-M-2024-018 | \n W80 | \n 4 | \n
\n \n
\n
\n
\n
\n\n {/* 전개도 요약 정보 */}\n
\n
\n
\n 총 부품 종류:\n 18개\n
\n
\n 총 중량:\n 25.8 kg\n
\n
\n 비고:\n V컷팅 작업 완료 후 절곡 진행\n
\n
\n
\n
\n )}\n\n {/* 작업지시 생성 모달 */}\n {showCreateModal && (\n
\n
\n
\n
\n \n 작업지시서 자동 생성\n
\n
\n
\n
다음 공정에 대한 작업지시서가 생성됩니다:
\n
\n {productionOrder.processGroups?.map((pg, idx) => (\n
\n
\n {pg.processSeq}\n
\n
{pg.processName} 공정\n
({pg.items?.length || 0}개 품목)\n
\n ))}\n
\n
\n 생성된 작업지시서는 생산팀에서 확인하고 작업을 진행할 수 있습니다.\n
\n
\n
\n
\n
\n
\n
\n
\n )}\n
\n );\n};\n\nconst WorkOrderList = ({ workOrders, orders = [], onNavigate, onApprove, onAssign, onStartWork, onCompleteWork, onProgressWork, onDeleteWorkOrders }) => {\n // 수주 lotNo 조회 헬퍼 함수\n const getOrderLotNo = (orderId) => {\n const order = orders.find(o => o.id === orderId);\n return order?.lotNo || '-';\n };\n\n const [search, setSearch] = useState('');\n const [activeTab, setActiveTab] = useState('all');\n const [processFilter, setProcessFilter] = useState('all');\n const [viewMode, setViewMode] = useState('list'); // 'list' | 'manager'\n\n // 체크박스 선택 상태\n const [selectedIds, setSelectedIds] = useState([]);\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n // ★ 공정 진행 모달 상태\n const [showProgressModal, setShowProgressModal] = useState(false);\n const [selectedWorkOrder, setSelectedWorkOrder] = useState(null);\n const [progressData, setProgressData] = useState({\n completedQty: 0,\n defectQty: 0,\n currentStep: '',\n note: ''\n });\n\n // ★ 공정 진행 모달 열기\n const handleOpenProgressModal = (wo) => {\n setSelectedWorkOrder(wo);\n setProgressData({\n completedQty: wo.completedQty || 0,\n defectQty: wo.defectQty || 0,\n currentStep: wo.currentStep || '',\n note: ''\n });\n setShowProgressModal(true);\n };\n\n // ★ 공정 진행 저장\n const handleSaveProgress = () => {\n if (selectedWorkOrder && onProgressWork) {\n onProgressWork(selectedWorkOrder.id, progressData);\n }\n setShowProgressModal(false);\n setSelectedWorkOrder(null);\n };\n\n // 공정별 단계 정의\n const getProcessSteps = (processType) => {\n const steps = {\n '스크린': ['원단절단', '미싱', '앤드락작업', '중간검사', '포장'],\n '슬랫': ['코일절단', '중간검사', '미미작업', '포장'],\n '절곡': ['절단', '절곡', '중간검사', '포장'],\n '재고생산': ['절단', '절곡', '중간검사', '입고'],\n };\n return steps[processType] || ['작업준비', '작업진행', '검사', '완료'];\n };\n\n // ★ 담당자 배정 모달 상태\n const [showAssignModal, setShowAssignModal] = useState(false);\n const [selectedForAssign, setSelectedForAssign] = useState(null);\n const [selectedAssignees, setSelectedAssignees] = useState([]);\n\n // 공정별 관리자 정보\n const processManagers = {\n '절곡': { name: '김절곡', unit: 'kg→장', desc: '자재LOT 선택 후 수량 지정' },\n '스크린': { name: '이스크린', unit: 'W×H', desc: '수주W × 자재LOT H 조합' },\n '슬랫': { name: '박슬랫', unit: '자동산출', desc: '높이 기반 절단매수 계산' },\n };\n\n // ★ 팀별 작업자 정의\n const teams = [\n { id: 'screen', name: '스크린팀', members: ['김스크린', '이스크린', '박스크린'] },\n { id: 'slat', name: '슬랫팀', members: ['김슬랫', '이슬랫', '박슬랫'] },\n { id: 'bending', name: '절곡팀', members: ['김절곡', '이절곡'] },\n { id: 'production', name: '생산팀', members: ['김생산', '이생산', '박생산'] },\n ];\n\n // ★ 담당자 표시 헬퍼 함수 (팀명/다중/개인 구분)\n const formatAssigneeDisplay = (wo) => {\n const assignees = wo.assignees || (wo.assignee ? wo.assignee.split(', ') : []);\n if (assignees.length === 0) return { text: '미배정', type: 'none', color: 'red' };\n if (assignees.length === 1) return { text: assignees[0], type: 'single', color: 'gray' };\n\n // 팀 전체 선택 여부 확인\n for (const team of teams) {\n const allInTeam = team.members.every(m => assignees.includes(m));\n const anyInTeam = team.members.some(m => assignees.includes(m));\n if (allInTeam && assignees.length === team.members.length) {\n return { text: team.name, type: 'team', color: 'blue', members: team.members };\n }\n if (allInTeam && assignees.length > team.members.length) {\n const othersCount = assignees.length - team.members.length;\n return { text: `${team.name} 외 ${othersCount}명`, type: 'team-plus', color: 'blue', members: assignees };\n }\n }\n\n // 여러 명이지만 팀 전체는 아닌 경우\n if (assignees.length > 2) {\n return { text: `${assignees[0]} 외 ${assignees.length - 1}명`, type: 'multi', color: 'green', members: assignees };\n }\n return { text: assignees.join(', '), type: 'multi', color: 'green', members: assignees };\n };\n\n const pendingApprovalCount = workOrders.filter(w => w.approvalStatus === '승인대기').length;\n const unassignedCount = workOrders.filter(w => !w.assignee && w.status === '작업대기').length;\n\n const tabs = [\n { id: 'all', label: '전체', count: workOrders.length },\n { id: 'unassigned', label: '미배정', count: unassignedCount, highlight: unassignedCount > 0 },\n { id: 'pending-approval', label: '승인대기', count: pendingApprovalCount, highlight: pendingApprovalCount > 0 },\n { id: 'waiting', label: '작업대기', count: workOrders.filter(w => w.status === '작업대기' && w.approvalStatus !== '승인대기').length },\n { id: 'working', label: '작업중', count: workOrders.filter(w => w.status === '작업중').length },\n { id: 'complete', label: '작업완료', count: workOrders.filter(w => w.status === '작업완료').length },\n ];\n\n const processTypes = ['전체', '스크린', '슬랫', '절곡', '재고생산'];\n\n const statusFilter = {\n all: () => true,\n unassigned: (w) => !w.assignee && w.status === '작업대기',\n 'pending-approval': (w) => w.approvalStatus === '승인대기',\n waiting: (w) => w.status === '작업대기' && w.approvalStatus !== '승인대기',\n working: (w) => w.status === '작업중',\n complete: (w) => w.status === '작업완료',\n };\n\n // 승인 처리 함수\n const handleApprove = (wo, e) => {\n e.stopPropagation();\n if (onApprove) {\n onApprove(wo.id);\n }\n };\n\n // ★ 담당자 배정 모달 열기\n const handleOpenAssignModal = (wo, e) => {\n e?.stopPropagation();\n setSelectedForAssign(wo);\n setSelectedAssignees(wo.assignees || (wo.assignee ? wo.assignee.split(', ') : []));\n setShowAssignModal(true);\n };\n\n // ★ 담당자 토글\n const handleWorkerToggle = (worker) => {\n setSelectedAssignees(prev => {\n const exists = prev.includes(worker);\n return exists ? prev.filter(a => a !== worker) : [...prev, worker];\n });\n };\n\n // ★ 팀 전체 선택\n const handleTeamSelect = (team) => {\n const allSelected = team.members.every(m => selectedAssignees.includes(m));\n if (allSelected) {\n setSelectedAssignees(prev => prev.filter(a => !team.members.includes(a)));\n } else {\n setSelectedAssignees(prev => [...new Set([...prev, ...team.members])]);\n }\n };\n\n // ★ 담당자 배정 저장\n const handleSaveAssignment = () => {\n if (selectedForAssign && onAssign) {\n onAssign(selectedForAssign.id, selectedAssignees);\n }\n setShowAssignModal(false);\n setSelectedForAssign(null);\n setSelectedAssignees([]);\n };\n\n // ID 내림차순 정렬 - 최신 등록 최상단\n const filtered = workOrders\n .filter(statusFilter[activeTab])\n .filter(w => processFilter === 'all' || processFilter === '전체' || w.processType === processFilter)\n .filter(w =>\n w.workOrderNo.toLowerCase().includes(search.toLowerCase()) ||\n (w.customerName || '').includes(search) ||\n (w.siteName || '').includes(search)\n )\n .sort((a, b) => b.id - a.id);\n\n // 전체 선택/해제\n const handleSelectAll = (e) => {\n if (e.target.checked) {\n setSelectedIds(filtered.map(w => w.id));\n } else {\n setSelectedIds([]);\n }\n };\n\n // 개별 선택\n const handleSelectOne = (id, e) => {\n e.stopPropagation();\n setSelectedIds(prev =>\n prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]\n );\n };\n\n // 삭제 확인\n const handleDeleteConfirm = () => {\n if (onDeleteWorkOrders && selectedIds.length > 0) {\n onDeleteWorkOrders(selectedIds);\n }\n setSelectedIds([]);\n setShowDeleteModal(false);\n };\n\n return (\n
\n
onNavigate('work-order-register')}>\n 등록\n \n }\n />\n\n {/* 대시보드 - 4개 카드 */}\n \n
\n
\n
\n \n
\n
\n
전체
\n
{workOrders.length}건
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
작업대기
\n
{workOrders.filter(w => w.status === '작업대기').length}건
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
작업중
\n
{workOrders.filter(w => w.status === '작업중').length}건
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
작업완료
\n
{workOrders.filter(w => w.status === '작업완료').length}건
\n
\n
\n
\n
\n\n {/* 검색 */}\n \n\n {viewMode === 'list' && (\n \n )}\n\n {/* 공정관리뷰 */}\n {viewMode === 'manager' ? (\n \n {/* 공정별 관리자 카드 */}\n
\n {Object.entries(processManagers).map(([process, manager]) => {\n const processOrders = workOrders.filter(w => w.processType === process);\n const pending = processOrders.filter(w => w.status === '작업대기').length;\n const working = processOrders.filter(w => w.status === '작업중').length;\n const noMaterial = processOrders.filter(w => !w.materialInputted).length;\n const noAssignee = processOrders.filter(w => !w.assignee).length;\n\n return (\n
\n
\n
\n {process}\n 관리자: {manager.name}\n
\n
{manager.unit}\n
\n
{manager.desc}
\n\n
\n\n
\n
\n );\n })}\n
\n\n {/* 선택된 공정의 작업지시 목록 */}\n {processFilter !== 'all' && processFilter !== '전체' && (\n
\n
\n
{processFilter} 공정 작업지시 현황
\n \n \n
\n \n \n | 작업지시번호 | \n 현장/수주LOT | \n 자재투입 | \n 작업시작 | \n 작업자배정 | \n 작업일 | \n 우선순위 | \n 납기일 | \n 액션 | \n
\n \n \n {filtered.filter(w => w.processType === processFilter).map((wo) => (\n \n | {wo.workOrderNo} | \n \n {wo.siteName} \n {wo.orderNo} \n | \n \n {wo.materialInputted ? (\n \n \n \n ) : (\n \n \n \n )}\n | \n \n {wo.status === '작업중' || wo.status === '작업완료' ? (\n \n \n \n ) : (\n \n \n \n )}\n | \n \n {(() => {\n const assigneeInfo = formatAssigneeDisplay(wo);\n return (\n { e.stopPropagation(); handleOpenAssignModal(wo, e); }}\n >\n \n {assigneeInfo.type === 'team' && }\n {assigneeInfo.text}\n \n {/* 툴팁: 여러 명일 때 전체 목록 표시 */}\n {assigneeInfo.members && (\n \n {assigneeInfo.members.join(', ')}\n \n )}\n \n );\n })()}\n | \n {wo.orderDate} | \n | \n {wo.dueDate} | \n \n \n | \n
\n ))}\n \n
\n
\n )}\n
\n ) : (\n /* 기존 목록뷰 */\n \n {/* 선택 삭제 버튼 (2개 이상 선택 시) */}\n {selectedIds.length >= 2 && (\n
\n \n {selectedIds.length}개 항목 선택됨\n \n \n
\n )}\n
\n
\n )}\n\n {/* ★ 담당자 배정 모달 */}\n {showAssignModal && selectedForAssign && (\n \n
\n {/* 모달 헤더 */}\n
\n
\n
담당자 배정
\n
\n {selectedForAssign.workOrderNo} - {selectedForAssign.processType}\n
\n
\n
\n
\n\n {/* 작업지시 정보 */}\n
\n
\n
\n 현장명:\n {selectedForAssign.siteName}\n
\n
\n 납기일:\n {selectedForAssign.dueDate}\n
\n
\n 우선순위:\n \n {selectedForAssign.priority}\n \n
\n
\n 상태:\n \n
\n
\n
\n\n {/* 팀별 작업자 선택 */}\n
\n {teams.map(team => {\n const isFullySelected = team.members.every(m => selectedAssignees.includes(m));\n const isPartiallySelected = team.members.some(m => selectedAssignees.includes(m)) && !isFullySelected;\n\n return (\n
\n {/* 팀 헤더 */}\n
\n\n {/* 개별 작업자 */}\n
\n {team.members.map(worker => (\n
\n ))}\n
\n
\n );\n })}\n
\n\n {/* 선택된 담당자 표시 */}\n {selectedAssignees.length > 0 && (\n
\n
\n 선택된 담당자: {selectedAssignees.join(', ')}\n
\n
\n )}\n\n {/* 모달 푸터 */}\n
\n
\n
\n
\n
\n
\n )}\n\n {/* ★ 공정 진행 모달 */}\n {showProgressModal && selectedWorkOrder && (\n \n
\n {/* 모달 헤더 */}\n
\n
\n
공정 진행 입력
\n
\n {selectedWorkOrder.workOrderNo} - {selectedWorkOrder.processType}\n
\n
\n
\n
\n\n {/* 작업지시 정보 */}\n
\n
\n
\n 현장명:\n {selectedWorkOrder.siteName}\n
\n
\n 담당자:\n {selectedWorkOrder.assignee || '미배정'}\n
\n
\n 총수량:\n {selectedWorkOrder.totalQty}개\n
\n
\n 납기일:\n {selectedWorkOrder.dueDate}\n
\n
\n
\n\n {/* 공정 단계 선택 */}\n
\n
\n
\n
\n {getProcessSteps(selectedWorkOrder.processType).map((step, idx) => (\n \n ))}\n
\n
\n\n {/* 수량 입력 */}\n
\n
\n
\n
setProgressData(prev => ({ ...prev, completedQty: parseInt(e.target.value) || 0 }))}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500\"\n />\n
최대: {selectedWorkOrder.totalQty}개
\n
\n
\n \n setProgressData(prev => ({ ...prev, defectQty: parseInt(e.target.value) || 0 }))}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500\"\n />\n
\n
\n\n {/* 진행률 표시 */}\n
\n
\n 진행률\n \n {Math.round((progressData.completedQty / selectedWorkOrder.totalQty) * 100)}%\n \n
\n
\n
\n\n {/* 메모 */}\n
\n \n
\n
\n\n {/* 모달 푸터 */}\n
\n
\n {progressData.defectQty > 0 && (\n 불량 {progressData.defectQty}개 발생\n )}\n
\n
\n \n \n
\n
\n
\n
\n )}\n\n {/* 삭제 확인 모달 */}\n {showDeleteModal && (\n \n
\n
\n
\n
\n
\n
삭제 확인
\n
선택한 항목을 삭제하시겠습니까?
\n
\n
\n
\n
\n {selectedIds.length}개의 작업지시가 삭제됩니다.\n 삭제된 데이터는 복구할 수 없습니다.\n
\n
\n
\n \n \n
\n
\n
\n
\n )}\n \n );\n};\n\n// 작업지시 등록\nconst WorkOrderCreate = ({ orders, customers, sites, onNavigate, onBack, onSave }) => {\n // 매출 거래처만 필터링 (발주처 선택용)\n const salesCustomers = customers?.filter(c => c.customerType === '매출') || [];\n // 공정관리에서 사용중인 공정 목록 동적으로 가져오기\n const activeProcesses = sampleProcesses.filter(p => p.isActive);\n const processTypes = activeProcesses.map(p => p.processName);\n\n // 등록 모드: 'order' (수주 연동) / 'manual' (수동 등록)\n const [registrationMode, setRegistrationMode] = useState('order');\n\n const [selectedOrder, setSelectedOrder] = useState(null);\n const [selectedSplit, setSelectedSplit] = useState(null);\n const [showOrderPanel, setShowOrderPanel] = useState(false);\n const [orderSearch, setOrderSearch] = useState('');\n const [formData, setFormData] = useState({\n processType: processTypes[0] || '스크린',\n processId: activeProcesses[0]?.id || null, // 공정 ID 연결\n processCode: activeProcesses[0]?.processCode || null, // 공정 코드 연결\n workSheetType: activeProcesses[0]?.workSheetType || null, // 작업일지 양식 연결\n dueDate: '',\n priority: 5, // 기본값: 5 (1=긴급, 5=일반, 9=낮음)\n assignees: [], // 다중 담당자 배열\n note: '',\n // 수동 등록용 필드\n customerId: '', // 거래처 ID\n customerName: '',\n siteId: '', // 현장 ID\n siteName: '',\n manualItems: [{ id: 1, itemName: '', specification: '', qty: 1, unit: 'EA' }],\n });\n const [showAssigneePanel, setShowAssigneePanel] = useState(false);\n\n // 선택한 거래처에 해당하는 현장 목록 필터링\n const filteredSites = formData.customerId\n ? (sites || []).filter(s => s.customerId === parseInt(formData.customerId))\n : [];\n\n // 유효성 검사 규칙 (수동 등록 모드에서만 적용)\n const validationRules = {\n customerId: { required: registrationMode === 'manual', label: '발주처', message: '발주처를 선택해주세요.' },\n siteId: { required: registrationMode === 'manual', label: '현장명', message: '현장명을 선택해주세요.' },\n dueDate: { required: registrationMode === 'manual', label: '출고예정일', message: '출고예정일을 선택해주세요.' },\n };\n const { errors, touched, hasError, getFieldError, validateForm, handleBlur, clearFieldError } = useFormValidation(formData, validationRules);\n\n // 수동 등록 시 품목 추가\n const addManualItem = () => {\n setFormData(prev => ({\n ...prev,\n manualItems: [...prev.manualItems, {\n id: Date.now(),\n itemName: '',\n specification: '',\n qty: 1,\n unit: 'EA'\n }]\n }));\n };\n\n // 수동 등록 시 품목 삭제\n const removeManualItem = (itemId) => {\n if (formData.manualItems.length <= 1) return;\n setFormData(prev => ({\n ...prev,\n manualItems: prev.manualItems.filter(item => item.id !== itemId)\n }));\n };\n\n // 수동 등록 시 품목 수정\n const updateManualItem = (itemId, field, value) => {\n setFormData(prev => ({\n ...prev,\n manualItems: prev.manualItems.map(item =>\n item.id === itemId ? { ...item, [field]: value } : item\n )\n }));\n };\n\n // 등록 모드 변경 시 데이터 초기화\n const handleModeChange = (mode) => {\n setRegistrationMode(mode);\n if (mode === 'manual') {\n setSelectedOrder(null);\n setSelectedSplit(null);\n }\n };\n\n // 생산지시 가능한 수주 (회계확인완료 상태)\n const availableOrders = orders.filter(o =>\n o.accountingStatus === '회계확인완료' || o.accountingStatus === '입금확인'\n );\n\n // 검색 필터링\n const filteredOrders = availableOrders.filter(order => {\n if (!orderSearch) return true;\n const search = orderSearch.toLowerCase();\n return (\n order.orderNo?.toLowerCase().includes(search) ||\n order.customerName?.toLowerCase().includes(search) ||\n order.siteName?.toLowerCase().includes(search)\n );\n });\n\n // 공정 선택 시 관련 정보도 함께 업데이트\n const handleProcessTypeChange = (processName) => {\n const selectedProcess = activeProcesses.find(p => p.processName === processName);\n setFormData(prev => ({\n ...prev,\n processType: processName,\n processId: selectedProcess?.id || null,\n processCode: selectedProcess?.processCode || null,\n workSheetType: selectedProcess?.workSheetType || null,\n }));\n };\n\n // 우선순위 1~9 (1=가장 긴급, 9=가장 낮음)\n const priorities = [1, 2, 3, 4, 5, 6, 7, 8, 9];\n\n // 팀별 작업자 정의 - 공정관리에서 동적으로 생성\n const teams = activeProcesses.map(p => ({\n id: p.processCode.toLowerCase(),\n name: `${p.processName}팀`,\n members: p.assignedWorkers || [`${p.processName}작업자1`, `${p.processName}작업자2`] // 공정에 배정된 작업자 또는 기본값\n }));\n const allWorkers = teams.flatMap(t => t.members);\n\n // 팀 전체 선택\n const handleTeamSelect = (team) => {\n const currentAssignees = formData.assignees;\n const allSelected = team.members.every(m => currentAssignees.includes(m));\n\n if (allSelected) {\n // 팀 전체 해제\n setFormData(prev => ({\n ...prev,\n assignees: prev.assignees.filter(a => !team.members.includes(a))\n }));\n } else {\n // 팀 전체 선택\n const newAssignees = [...new Set([...currentAssignees, ...team.members])];\n setFormData(prev => ({ ...prev, assignees: newAssignees }));\n }\n };\n\n // 개별 작업자 토글\n const handleWorkerToggle = (worker) => {\n setFormData(prev => {\n const exists = prev.assignees.includes(worker);\n return {\n ...prev,\n assignees: exists\n ? prev.assignees.filter(a => a !== worker)\n : [...prev.assignees, worker]\n };\n });\n };\n\n // 팀 선택 여부 확인\n const isTeamFullySelected = (team) => team.members.every(m => formData.assignees.includes(m));\n const isTeamPartiallySelected = (team) =>\n team.members.some(m => formData.assignees.includes(m)) && !isTeamFullySelected(team);\n\n const handleOrderSelect = (order) => {\n setSelectedOrder(order);\n setSelectedSplit(null);\n setShowOrderPanel(false);\n setFormData(prev => ({\n ...prev,\n dueDate: order.dueDate,\n }));\n };\n\n const handleClearOrder = () => {\n setSelectedOrder(null);\n setSelectedSplit(null);\n setFormData(prev => ({\n ...prev,\n dueDate: '',\n }));\n };\n\n const handleSplitSelect = (split) => {\n setSelectedSplit(split);\n if (split) {\n setFormData(prev => ({\n ...prev,\n dueDate: split.dueDate,\n }));\n }\n };\n\n const handleChange = (field, value) => {\n setFormData(prev => ({ ...prev, [field]: value }));\n };\n\n // 날짜를 YYMMDD 형식으로 변환\n const formatDateCode = (date) => {\n const d = new Date(date);\n const yy = String(d.getFullYear()).slice(-2);\n const mm = String(d.getMonth() + 1).padStart(2, '0');\n const dd = String(d.getDate()).padStart(2, '0');\n return `${yy}${mm}${dd}`;\n };\n\n const handleSubmit = () => {\n // 모드별 유효성 검사\n if (registrationMode === 'order') {\n if (!selectedOrder) {\n alert('수주를 선택해주세요.');\n return;\n }\n } else {\n // 수동 등록 모드 - 유효성 검사 훅 사용\n if (!validateForm()) {\n return;\n }\n const validItems = formData.manualItems.filter(item => item.itemName.trim());\n if (validItems.length === 0) {\n alert('최소 1개 이상의 품목을 입력해주세요.');\n return;\n }\n }\n\n // 생산LOT 생성: KD-PL-YYMMDD-순번\n const dateCode = formatDateCode(new Date());\n const seq = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0');\n const workOrderNo = `KD-PL-${dateCode}-${seq}`;\n\n // 품목 데이터 구성\n let items = [];\n if (registrationMode === 'order') {\n if (selectedSplit && selectedOrder) {\n items = selectedOrder.items\n .filter(item => selectedSplit.itemIds.includes(item.id))\n .map(item => ({\n ...item,\n lotNo: null,\n materialLotNo: null,\n }));\n } else if (selectedOrder) {\n items = selectedOrder.items.map(item => ({\n ...item,\n lotNo: null,\n materialLotNo: null,\n }));\n }\n } else {\n // 수동 등록 모드: 입력된 품목 데이터 변환\n items = formData.manualItems\n .filter(item => item.itemName.trim())\n .map((item, idx) => ({\n id: Date.now() + idx,\n itemName: item.itemName,\n specification: item.specification,\n qty: parseInt(item.qty) || 1,\n unit: item.unit,\n lotNo: null,\n materialLotNo: null,\n }));\n }\n\n // 공정별 작업단계 설정\n const getStepStatus = (processType) => {\n const steps = {\n '스크린': ['원단절단', '미싱', '앤드락작업', '중간검사', '포장'],\n '슬랫': ['코일절단', '중간검사', '미미작업', '포장'],\n '절곡': ['절단', '절곡', '중간검사', '포장'],\n };\n\n const stepStatus = {};\n (steps[processType] || steps['스크린']).forEach(step => {\n stepStatus[step] = { status: '대기' };\n });\n return stepStatus;\n };\n\n const newWorkOrder = {\n id: Date.now(),\n workOrderNo,\n orderNo: registrationMode === 'order' ? selectedOrder.orderNo : null,\n orderId: registrationMode === 'order' ? selectedOrder.id : null, // ★ 수주 ID 연결\n splitNo: selectedSplit?.splitNo || null,\n orderDate: new Date().toISOString().split('T')[0],\n processType: formData.processType,\n customerName: registrationMode === 'order' ? selectedOrder.customerName : formData.customerName,\n siteName: registrationMode === 'order' ? selectedOrder.siteName : formData.siteName,\n dueDate: formData.dueDate,\n shipRequestDate: registrationMode === 'order' ? (selectedOrder.shipRequestDate || selectedOrder.dueDate || formData.dueDate) : formData.dueDate, // ★ 출고예정일 추가\n status: '작업대기',\n workPriority: formData.priority, // ★ 숫자 우선순위 직접 저장 (1~9)\n priority: formData.priority <= 2 ? '긴급' : formData.priority <= 5 ? '일반' : '낮음', // ★ 호환성 위한 문자열 버전\n assignees: formData.assignees, // 다중 담당자 배열\n assignee: formData.assignees.join(', '), // 호환성을 위한 문자열 버전\n totalQty: items.reduce((sum, item) => sum + (item.qty || 0), 0),\n completedQty: 0,\n currentStep: null,\n stepStatus: getStepStatus(formData.processType),\n movedToShippingArea: false,\n movedToShippingAreaAt: null,\n items,\n issues: [],\n createdAt: new Date().toISOString().split('T')[0],\n createdBy: '현재 사용자',\n note: formData.note,\n registrationType: registrationMode, // 등록 유형 저장 (order/manual)\n };\n\n onSave?.(newWorkOrder);\n onBack();\n };\n\n // 등록 버튼 활성화 조건\n const isSubmitDisabled = registrationMode === 'order' ? !selectedOrder : false;\n\n return (\n
\n {/* 헤더 - 타이틀/버튼 분리 */}\n
\n
\n
\n \n \n
\n
\n\n {/* 등록 방식 선택 섹션 */}\n
\n \n \n \n
\n \n\n {/* 수주 연동 모드: 수주 불러오기 영역 */}\n {registrationMode === 'order' && (\n
\n \n
\n
\n
\n {selectedOrder ? (\n
\n
\n {selectedOrder.orderNo}\n \n 에서 불러옴\n
\n
\n {selectedOrder.customerName} / {selectedOrder.siteName} / {selectedOrder.items?.length || 0}개 품목\n {selectedSplit && 분할: {selectedSplit.splitNo}}\n
\n
\n ) : (\n
\n
수주에서 불러오기
\n
회계확인 완료된 수주를 선택하면 정보가 자동으로 채워집니다
\n
\n )}\n
\n
\n {selectedOrder && (\n \n )}\n \n
\n
\n
\n {/* 분할 선택 (수주에 분할이 있을 경우) */}\n {selectedOrder && selectedOrder.splits?.length > 0 && (\n \n )}\n \n )}\n\n {/* 수동 등록 모드: 안내 메시지 */}\n {registrationMode === 'manual' && (\n
\n \n
\n
\n
수동 등록 모드
\n
수주 없이 직접 작업지시를 생성합니다. 긴급 작업, 재작업, 샘플/시제품, 내부 작업 등에 사용됩니다.
\n
\n
\n \n )}\n\n {/* 기본 정보 */}\n
\n \n \n {registrationMode === 'order' ? (\n selectedOrder ? (\n \n ) : (\n \n )\n ) : (\n \n )}\n \n \n {registrationMode === 'order' ? (\n selectedOrder ? (\n \n ) : (\n \n )\n ) : (\n \n )}\n \n {registrationMode === 'order' && (\n <>\n \n {selectedOrder ? (\n \n ) : (\n \n )}\n \n \n \n \n >\n )}\n
\n \n\n {/* 수동 등록 모드: 품목 입력 */}\n {registrationMode === 'manual' && (\n
\n \n {formData.manualItems.map((item, index) => (\n
\n
\n 품목 {index + 1}\n {formData.manualItems.length > 1 && (\n \n )}\n
\n
\n
\n ))}\n
\n
\n \n )}\n\n {/* 작업지시 정보 */}\n
\n \n
\n \n {formData.processCode && (\n \n 공정코드: {formData.processCode} | 작업일지: {formData.workSheetType || '-'}\n
\n )}\n \n\n
\n {\n handleChange('dueDate', e.target.value);\n clearFieldError('dueDate');\n }}\n onBlur={() => handleBlur('dueDate')}\n className={registrationMode === 'manual' ? getInputClassName(hasError('dueDate')) : \"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500\"}\n />\n \n\n
\n \n \n\n
\n setShowAssigneePanel(true)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg cursor-pointer hover:border-purple-300 transition-colors min-h-[42px] flex items-center gap-2 flex-wrap\"\n >\n {formData.assignees.length === 0 ? (\n 담당자를 선택하세요 (팀/개인)\n ) : (\n <>\n {formData.assignees.map(a => (\n \n {a}\n \n \n ))}\n >\n )}\n
\n \n
\n \n\n {/* 비고 - 수주 선택 여부와 관계없이 항상 표시 */}\n
\n \n\n {/* 수주 선택 모달 */}\n {showOrderPanel && (\n
\n
\n
\n
수주 선택
\n \n \n
\n
\n \n setOrderSearch(e.target.value)} placeholder=\"수주번호, 거래처, 현장명 검색...\" className=\"w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500\" />\n
\n
생산지시 가능한 수주 {availableOrders.length}건 (회계확인 완료 상태)
\n
\n {filteredOrders.length === 0 ? (\n
생산지시 가능한 수주가 없습니다
회계확인이 완료된 수주만 표시됩니다
\n ) : (\n filteredOrders.map((order) => (\n
handleOrderSelect(order)} className={`p-4 hover:bg-purple-50 cursor-pointer transition-colors ${selectedOrder?.id === order.id ? 'bg-purple-50 border-l-4 border-l-purple-500' : ''}`}>\n
\n
\n
{order.orderNo}
\n
{order.customerName}
\n
{order.siteName}
\n
\n
\n
납기: {order.dueDate}
\n
{order.items?.length || 0}개 품목
\n {order.splits?.length > 0 &&
분할 {order.splits.length}건
}\n
\n
\n
\n ))\n )}\n
\n
\n
\n
\n )}\n\n {/* 담당자 선택 모달 */}\n {showAssigneePanel && (\n
\n
\n
\n
담당자 선택
\n \n \n
\n
팀 전체 또는 개별 작업자를 선택할 수 있습니다
\n {formData.assignees.length > 0 && (\n
\n
\n 선택된 담당자 ({formData.assignees.length}명)\n \n
\n
{formData.assignees.map(a => ({a}))}
\n
\n )}\n
\n {teams.map(team => (\n
\n
handleTeamSelect(team)} className={`p-3 cursor-pointer transition-colors flex items-center justify-between ${isTeamFullySelected(team) ? 'bg-purple-100 border-purple-300' : isTeamPartiallySelected(team) ? 'bg-purple-50' : 'bg-gray-50 hover:bg-gray-100'}`}>\n
\n
\n {isTeamFullySelected(team) &&
}\n {isTeamPartiallySelected(team) &&
}\n
\n
\n
{team.name}\n
({team.members.length}명)\n
\n
{isTeamFullySelected(team) ? '팀 전체 선택됨' : '팀 전체 선택'}\n
\n
\n {team.members.map(member => (\n
handleWorkerToggle(member)} className={`p-2.5 pl-10 cursor-pointer transition-colors flex items-center gap-2 ${formData.assignees.includes(member) ? 'bg-purple-50' : 'hover:bg-gray-50'}`}>\n
\n {formData.assignees.includes(member) && }\n
\n
\n
{member}\n
\n ))}\n
\n
\n ))}\n
\n
\n \n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 작업지시 수정\nconst WorkOrderEdit = ({ workOrder, onNavigate, onBack, onSave }) => {\n // 기존 데이터가 배열이면 그대로, 문자열이면 파싱\n const parseAssignees = () => {\n if (Array.isArray(workOrder?.assignees)) return workOrder.assignees;\n if (workOrder?.assignee) return workOrder.assignee.split(', ').filter(Boolean);\n return [];\n };\n\n const [formData, setFormData] = useState({\n dueDate: workOrder?.dueDate || '',\n priority: workOrder?.priority || '일반',\n assignees: parseAssignees(),\n note: workOrder?.note || '',\n });\n const [showAssigneePanel, setShowAssigneePanel] = useState(false);\n\n // 유효성 검사 규칙\n const validationRules = {\n dueDate: { required: true, label: '납기일', message: '납기일을 선택해주세요.' },\n };\n const { errors, touched, hasError, getFieldError, validateForm, handleBlur, clearFieldError } = useFormValidation(formData, validationRules);\n\n const priorities = ['긴급', '일반', '낮음'];\n\n // 팀별 작업자 정의\n const teams = [\n { id: 'screen', name: '스크린팀', members: ['김스크린', '이스크린', '박스크린'] },\n { id: 'slat', name: '슬랫팀', members: ['김슬랫', '이슬랫', '박슬랫'] },\n { id: 'bending', name: '절곡팀', members: ['김절곡', '이절곡'] },\n ];\n\n // 팀 전체 선택\n const handleTeamSelect = (team) => {\n const currentAssignees = formData.assignees;\n const allSelected = team.members.every(m => currentAssignees.includes(m));\n\n if (allSelected) {\n setFormData(prev => ({\n ...prev,\n assignees: prev.assignees.filter(a => !team.members.includes(a))\n }));\n } else {\n const newAssignees = [...new Set([...currentAssignees, ...team.members])];\n setFormData(prev => ({ ...prev, assignees: newAssignees }));\n }\n };\n\n // 개별 작업자 토글\n const handleWorkerToggle = (worker) => {\n setFormData(prev => {\n const exists = prev.assignees.includes(worker);\n return {\n ...prev,\n assignees: exists\n ? prev.assignees.filter(a => a !== worker)\n : [...prev.assignees, worker]\n };\n });\n };\n\n const isTeamFullySelected = (team) => team.members.every(m => formData.assignees.includes(m));\n const isTeamPartiallySelected = (team) =>\n team.members.some(m => formData.assignees.includes(m)) && !isTeamFullySelected(team);\n\n const handleChange = (field, value) => {\n setFormData(prev => ({ ...prev, [field]: value }));\n };\n\n const handleSubmit = () => {\n // 유효성 검사\n if (!validateForm()) {\n return;\n }\n onSave?.({\n ...workOrder,\n ...formData,\n assignee: formData.assignees.join(', '), // 호환성을 위한 문자열 버전\n });\n onBack();\n };\n\n if (!workOrder) return null;\n\n return (\n
\n
\n
\n
\n
\n
\n
작업지시 수정
\n
{workOrder.workOrderNo}
\n
\n
\n
\n
\n \n \n
\n
\n\n
\n {/* 기본 정보 (읽기 전용) */}\n
\n \n \n \n \n \n \n \n \n \n \n \n
\n \n\n {/* 수정 가능 정보 */}\n
\n \n
\n {\n handleChange('dueDate', e.target.value);\n clearFieldError('dueDate');\n }}\n onBlur={() => handleBlur('dueDate')}\n className={getInputClassName(hasError('dueDate'))}\n />\n \n\n
\n \n \n\n
\n setShowAssigneePanel(true)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg cursor-pointer hover:border-purple-300 transition-colors min-h-[42px] flex items-center gap-2 flex-wrap\"\n >\n {formData.assignees.length === 0 ? (\n 담당자를 선택하세요 (팀/개인)\n ) : (\n <>\n {formData.assignees.map(a => (\n \n {a}\n \n \n ))}\n >\n )}\n
\n \n\n
\n \n
\n \n\n {/* 작업 품목 */}\n
\n \n
\n \n \n | 품목코드 | \n 품목명 | \n 층 | \n 부호 | \n 규격 | \n 수량 | \n 완제품LOT | \n
\n \n \n {workOrder.items?.map((item, idx) => (\n \n | {item.productCode} | \n {item.productName} | \n {item.floor} | \n {item.location} | \n {item.spec} | \n {item.qty} | \n {item.lotNo || '-'} | \n
\n ))}\n \n
\n
\n \n
\n\n {/* 담당자 선택 모달 */}\n {showAssigneePanel && (\n
\n
\n
\n
담당자 선택
\n \n \n
\n
팀 전체 또는 개별 작업자를 선택할 수 있습니다
\n {formData.assignees.length > 0 && (\n
\n
\n 선택된 담당자 ({formData.assignees.length}명)\n \n
\n
{formData.assignees.map(a => ({a}))}
\n
\n )}\n
\n {teams.map(team => (\n
\n
handleTeamSelect(team)} className={`p-3 cursor-pointer transition-colors flex items-center justify-between ${isTeamFullySelected(team) ? 'bg-purple-100 border-purple-300' : isTeamPartiallySelected(team) ? 'bg-purple-50' : 'bg-gray-50 hover:bg-gray-100'}`}>\n
\n
\n {isTeamFullySelected(team) &&
}\n {isTeamPartiallySelected(team) &&
}\n
\n
\n
{team.name}\n
({team.members.length}명)\n
\n
{isTeamFullySelected(team) ? '팀 전체 선택됨' : '팀 전체 선택'}\n
\n
\n {team.members.map(member => (\n
handleWorkerToggle(member)} className={`p-2.5 pl-10 cursor-pointer transition-colors flex items-center gap-2 ${formData.assignees.includes(member) ? 'bg-purple-50' : 'hover:bg-gray-50'}`}>\n
\n {formData.assignees.includes(member) && }\n
\n
\n
{member}\n
\n ))}\n
\n
\n ))}\n
\n
\n \n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 작업지시 상세\nconst WorkOrderDetail = ({\n workOrder,\n workResults,\n orders,\n inventory = [],\n materialLots = [],\n onNavigate,\n onBack,\n onUpdate,\n onUpdateOrder,\n onUseMaterial,\n // 기능정의서 관련 props\n setActiveModal,\n showFeatureDescription,\n featureBadges = {},\n selectedBadge,\n setSelectedBadge,\n updateFeatureBadge,\n setEditingBadge\n}) => {\n // ★ workOrder가 없으면 에러 화면 표시\n if (!workOrder) {\n return (\n
\n
목록으로}\n />\n \n
\n
작업지시를 찾을 수 없습니다
\n
선택된 작업지시 정보가 없습니다. 목록에서 다시 선택해 주세요.
\n
\n
\n \n );\n }\n\n const [showMaterialCheck, setShowMaterialCheck] = useState(false);\n const [showProductionSheet, setShowProductionSheet] = useState(false);\n const [showAssignModal, setShowAssignModal] = useState(false);\n const [showStepModal, setShowStepModal] = useState(false);\n const [showMaterialInputModal, setShowMaterialInputModal] = useState(false);\n const [showWorkLogEditor, setShowWorkLogEditor] = useState(false);\n const [showWorkLogSheet, setShowWorkLogSheet] = useState(false);\n const [showCombinedSheet, setShowCombinedSheet] = useState(false); // 작업지시서+작업일지 함께 출력\n const [showWorkLogDataPreview, setShowWorkLogDataPreview] = useState(false); // 작업일지 양식보기 (데이터 연동)\n const [showApprovalModal, setShowApprovalModal] = useState(false); // 결재 상신/승인 모달\n const [workLogData, setWorkLogData] = useState(null);\n const [workLogApproval, setWorkLogApproval] = useState({\n status: 'draft', // draft, step1_pending, step1_approved, step2_pending, step2_approved, approved, rejected\n // 1단계: 생산지시자 (작성)\n step1: {\n status: 'draft',\n submittedAt: null,\n submittedBy: null,\n },\n // 2단계: 생산담당자 (확인)\n step2: {\n status: 'pending',\n approvedAt: null,\n approvedBy: null,\n },\n // 3단계: 품질팀장 (최종결재)\n step3: {\n status: 'pending',\n approvedAt: null,\n approvedBy: null,\n },\n rejectedAt: null,\n rejectedBy: null,\n rejectReason: null,\n rejectedStep: null,\n });\n\n // 투입자재 선택 모달\n const [showMaterialSelectModal, setShowMaterialSelectModal] = useState(false);\n const [selectedMaterials, setSelectedMaterials] = useState([]);\n\n // 중간검사성적서 모달\n const [showInspectionCertModal, setShowInspectionCertModal] = useState(false);\n const [inspectionCertData, setInspectionCertData] = useState({\n lotNo: '',\n inspectionDate: new Date().toISOString().slice(0, 10),\n inspector: '',\n items: [],\n result: 'pending',\n inputMaterials: [],\n });\n const [currentStep, setCurrentStep] = useState(null);\n // ★ 다중 작업자 배정을 위한 상태\n const parseInitialAssignees = () => {\n if (Array.isArray(workOrder?.assignees)) return workOrder.assignees;\n if (workOrder?.assignee) return workOrder.assignee.split(', ').filter(Boolean);\n return [];\n };\n const [selectedAssignees, setSelectedAssignees] = useState(parseInitialAssignees);\n\n // 해당 작업지시의 수주 정보 찾기\n const relatedOrder = orders?.find(o => o.orderNo === workOrder.orderNo);\n\n // ★ 작업팀 정의 - 공정관리(sampleProcesses)의 assignedWorkers와 연동\n const workerTeams = sampleProcesses\n .filter(p => p.isActive && p.assignedWorkers?.length > 0)\n .map(p => ({\n name: `${p.processName}팀`,\n processCode: p.processCode,\n department: p.department,\n members: p.assignedWorkers,\n }));\n\n // 현재 공정에 배정된 작업자 목록 (해당 공정 우선 표시)\n const currentProcessWorkers = (() => {\n const matchedProcess = sampleProcesses.find(p =>\n p.processName === workOrder.processType ||\n p.processCode === workOrder.processCode\n );\n return matchedProcess?.assignedWorkers || [];\n })();\n\n // 전체 작업자 목록 (모든 공정에서 수집)\n const allWorkers = [...new Set(sampleProcesses.flatMap(p => p.assignedWorkers || []))];\n const workers = allWorkers.length > 0 ? allWorkers : ['김생산', '이생산', '박생산', '최생산', '정생산'];\n const inspectors = ['이검사', '최품질', '김검사'];\n\n // ★ 팀 선택/해제 토글\n const handleTeamToggle = (team) => {\n const allSelected = team.members.every(m => selectedAssignees.includes(m));\n if (allSelected) {\n setSelectedAssignees(prev => prev.filter(a => !team.members.includes(a)));\n } else {\n const newAssignees = [...new Set([...selectedAssignees, ...team.members])];\n setSelectedAssignees(newAssignees);\n }\n };\n\n // ★ 개별 작업자 선택/해제 토글\n const handleWorkerToggle = (worker) => {\n setSelectedAssignees(prev => {\n const exists = prev.includes(worker);\n return exists ? prev.filter(a => a !== worker) : [...prev, worker];\n });\n };\n\n // ★ 팀 전체 선택 여부 확인\n const isTeamFullySelected = (team) => team.members.every(m => selectedAssignees.includes(m));\n const isTeamPartiallySelected = (team) =>\n team.members.some(m => selectedAssignees.includes(m)) && !isTeamFullySelected(team);\n\n // 해당 작업지시의 실적 필터링\n const relatedResults = workResults.filter(r => r.workOrderNo === workOrder.workOrderNo);\n\n // 공정 단계 정보 - sampleProcesses(공정관리)에서 동적으로 가져옴\n const getProcessSteps = () => {\n // 공정관리에서 해당 공정 찾기 (processType 또는 processName으로 매칭)\n const matchedProcess = sampleProcesses.find(p =>\n p.processName === workOrder.processType ||\n p.processCode === workOrder.processCode\n );\n\n // 공정관리에 workSteps가 있으면 사용, 없으면 기본값\n if (matchedProcess?.workSteps?.length > 0) {\n return matchedProcess.workSteps;\n }\n\n // 기본값 (공정관리에 데이터 없을 경우)\n const defaultSteps = {\n '스크린': ['원단절단', '미싱', '앤드락작업', '중간검사', '포장'],\n '슬랫': ['코일절단', '중간검사', '미미작업', '포장'],\n '절곡': ['절단', '절곡', '중간검사', '포장'],\n '재고(포밍)': ['포밍', '검사', '포장'],\n };\n return defaultSteps[workOrder.processType] || ['준비', '작업', '검사', '완료'];\n };\n\n // 공정관리에서 현재 공정 정보 가져오기\n const getCurrentProcessInfo = () => {\n return sampleProcesses.find(p =>\n p.processName === workOrder.processType ||\n p.processCode === workOrder.processCode\n );\n };\n\n const processInfo = getCurrentProcessInfo();\n\n // 단계 상태 아이콘\n const getStepIcon = (status) => {\n if (status === '완료') return
;\n if (status === '진행중') return
;\n return
;\n };\n\n // ★ 다중 작업자 배정 핸들러\n const handleAssign = () => {\n if (selectedAssignees.length === 0) {\n alert('작업자를 1명 이상 선택해주세요.');\n return;\n }\n onUpdate?.({\n ...workOrder,\n assignees: selectedAssignees, // 배열로 저장\n assignee: selectedAssignees.join(', '), // 호환성을 위한 문자열 버전\n });\n setShowAssignModal(false);\n alert(`✅ 작업자 ${selectedAssignees.length}명이 배정되었습니다.\\n\\n${selectedAssignees.join(', ')}`);\n };\n\n // 작업 시작 핸들러\n const handleStartWork = () => {\n if (!workOrder.assignee && (!workOrder.assignees || workOrder.assignees.length === 0)) {\n alert('먼저 작업자를 배정해주세요.');\n setShowAssignModal(true);\n return;\n }\n\n const now = new Date().toISOString().replace('T', ' ').slice(0, 16);\n const firstStep = getProcessSteps()[0];\n\n onUpdate?.({\n ...workOrder,\n status: '작업중',\n startedAt: now,\n currentStep: firstStep,\n stepStatus: {\n ...workOrder.stepStatus,\n [firstStep]: {\n status: '진행중',\n worker: workOrder.assignee,\n startedAt: now,\n },\n },\n });\n\n // 수주 상태는 생산지시완료 유지 (작업지시 실행 상태와 별도 관리)\n // onUpdateOrder?.(workOrder.orderNo, { status: '생산중' });\n\n alert(`✅ 작업이 시작되었습니다!\\n\\n작업자: ${workOrder.assignee}\\n현재 공정: ${firstStep}`);\n };\n\n // 공정 시작 핸들러\n const handleStepStart = (step) => {\n const now = new Date().toISOString().replace('T', ' ').slice(0, 16);\n\n onUpdate?.({\n ...workOrder,\n currentStep: step,\n stepStatus: {\n ...workOrder.stepStatus,\n [step]: {\n status: '진행중',\n worker: workOrder.assignee,\n startedAt: now,\n },\n },\n });\n };\n\n // 공정 완료 핸들러\n const handleStepComplete = (step, additionalData = {}) => {\n const now = new Date().toISOString().replace('T', ' ').slice(0, 16);\n const steps = getProcessSteps();\n const currentIndex = steps.indexOf(step);\n const isLastStep = currentIndex === steps.length - 1;\n const nextStep = isLastStep ? null : steps[currentIndex + 1];\n\n const updatedStepStatus = {\n ...workOrder.stepStatus,\n [step]: {\n ...workOrder.stepStatus[step],\n status: '완료',\n completedAt: now,\n ...additionalData,\n },\n };\n\n // 다음 공정이 있으면 진행중으로 설정\n if (nextStep) {\n updatedStepStatus[nextStep] = {\n status: '진행중',\n worker: workOrder.assignee,\n startedAt: now,\n };\n }\n\n // 완료 수량 계산 (마지막 공정 완료 시)\n const newCompletedQty = isLastStep ? workOrder.totalQty : workOrder.completedQty;\n const newStatus = isLastStep ? '작업완료' : workOrder.status;\n\n onUpdate?.({\n ...workOrder,\n status: newStatus,\n currentStep: nextStep,\n stepStatus: updatedStepStatus,\n completedQty: newCompletedQty,\n completedAt: isLastStep ? now : workOrder.completedAt,\n });\n\n // 마지막 공정 완료 시 (수주 상태는 생산지시완료 유지, 출하완료는 출하 처리 시 변경)\n if (isLastStep) {\n // onUpdateOrder?.(workOrder.orderNo, { status: '생산완료' });\n alert(`🎉 모든 공정이 완료되었습니다!\\n\\n작업지시: ${workOrder.workOrderNo}\\n완료수량: ${workOrder.totalQty}건`);\n } else {\n alert(`✅ ${step} 공정이 완료되었습니다.\\n\\n다음 공정: ${nextStep}`);\n }\n\n setShowStepModal(false);\n };\n\n // 중간검사 완료 핸들러\n const handleInspectionComplete = (result, inspector, rejectReason = '') => {\n const now = new Date().toISOString().replace('T', ' ').slice(0, 16);\n const inspectionLot = `KD-SC-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`;\n\n if (result === '합격') {\n // 합격 시 다음 공정으로 진행\n handleStepComplete('중간검사', {\n result: '합격',\n inspector: inspector,\n approvedBy: '품질팀장 최품질',\n inspectionLot: inspectionLot,\n });\n } else {\n // 불합격 시 재작업 처리\n const steps = getProcessSteps();\n const inspectionIndex = steps.indexOf('중간검사');\n const previousStep = inspectionIndex > 0 ? steps[inspectionIndex - 1] : steps[0];\n\n // 중간검사를 불합격으로 기록하고 이전 공정으로 되돌림\n const updatedStepStatus = {\n ...workOrder.stepStatus,\n '중간검사': {\n status: '불합격',\n result: '불합격',\n inspector: inspector,\n inspectionLot: inspectionLot,\n rejectReason: rejectReason,\n rejectedAt: now,\n },\n [previousStep]: {\n status: '진행중', // 이전 공정을 다시 진행중으로\n worker: workOrder.assignee,\n startedAt: now,\n reworkCount: (workOrder.stepStatus?.[previousStep]?.reworkCount || 0) + 1,\n reworkReason: `중간검사 불합격: ${rejectReason}`,\n },\n };\n\n // 불량 이슈 추가\n const newIssue = {\n id: Date.now(),\n type: '품질불량',\n step: '중간검사',\n description: `중간검사 불합격 - ${rejectReason}`,\n reportedBy: inspector,\n reportedAt: now,\n status: '처리중',\n };\n\n onUpdate?.({\n ...workOrder,\n currentStep: previousStep,\n stepStatus: updatedStepStatus,\n issues: [...(workOrder.issues || []), newIssue],\n hasQualityIssue: true,\n qualityIssueNote: `중간검사 불합격 - ${rejectReason}`,\n });\n\n setShowStepModal(false);\n alert(`⚠️ 중간검사 불합격\\n\\n사유: ${rejectReason}\\n\\n${previousStep} 공정으로 돌아가서 재작업을 진행합니다.`);\n }\n };\n\n // 포장 완료 핸들러\n const handlePackingComplete = (labelInfo) => {\n handleStepComplete('포장', {\n labelInfo: labelInfo,\n });\n };\n\n // 출고구역 이동 핸들러\n const handleMoveToShipping = () => {\n const now = new Date().toISOString().replace('T', ' ').slice(0, 16);\n\n onUpdate?.({\n ...workOrder,\n movedToShippingArea: true,\n movedToShippingAreaAt: now,\n });\n\n // 수주 상태는 생산지시완료 유지 (출하완료는 출하 처리 시 변경)\n // onUpdateOrder?.(workOrder.orderNo, { status: '생산완료' });\n\n alert(`✅ 출고구역으로 이동 완료!\\n\\n이동시각: ${now}\\n\\n[출하관리]에서 출하 진행이 가능합니다.`);\n };\n\n // ============ 개별 품목 작업 처리 핸들러 ============\n\n // 히스토리 추가 헬퍼 함수\n const addItemHistory = (item, action, note = '') => {\n const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);\n return {\n timestamp,\n action,\n itemId: item.id,\n itemName: item.productName,\n floor: item.floor,\n location: item.location,\n worker: workOrder.assignee || '작업자',\n note,\n };\n };\n\n // 품목 작업 시작\n const handleItemStart = (itemId) => {\n const now = new Date().toISOString().replace('T', ' ').slice(0, 19);\n const item = workOrder.items.find(i => i.id === itemId);\n if (!item) return;\n\n const updatedItems = workOrder.items.map(i =>\n i.id === itemId\n ? { ...i, itemStatus: '진행중', worker: workOrder.assignee, startedAt: now }\n : i\n );\n\n const historyEntry = addItemHistory(item, '시작');\n\n onUpdate?.({\n ...workOrder,\n items: updatedItems,\n itemHistory: [...(workOrder.itemHistory || []), historyEntry],\n });\n };\n\n // 품목 작업 완료\n const handleItemComplete = (itemId) => {\n const now = new Date().toISOString().replace('T', ' ').slice(0, 19);\n const item = workOrder.items.find(i => i.id === itemId);\n if (!item) return;\n\n // 제품 LOT 번호 생성\n const dateCode = new Date().toISOString().slice(2, 10).replace(/-/g, '');\n const seq = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0');\n const lotNo = `KD-FG-${dateCode}-${seq}`;\n\n const updatedItems = workOrder.items.map(i =>\n i.id === itemId\n ? { ...i, itemStatus: '완료', completedAt: now, lotNo }\n : i\n );\n\n const historyEntry = addItemHistory(item, '완료');\n\n // 모든 품목이 완료되었는지 확인\n const allCompleted = updatedItems.every(i => i.itemStatus === '완료');\n const completedCount = updatedItems.filter(i => i.itemStatus === '완료').length;\n\n onUpdate?.({\n ...workOrder,\n items: updatedItems,\n itemHistory: [...(workOrder.itemHistory || []), historyEntry],\n completedQty: completedCount,\n });\n\n if (allCompleted) {\n alert(`🎉 모든 품목 작업이 완료되었습니다!\\n\\n완료: ${completedCount}건`);\n } else {\n alert(`✅ \"${item.productName}\" 작업 완료!\\n\\n완료: ${completedCount}/${workOrder.items.length}건`);\n }\n };\n\n // 품목 작업 중단 (다음날 이어서)\n const handleItemPause = (itemId) => {\n const now = new Date().toISOString().replace('T', ' ').slice(0, 19);\n const item = workOrder.items.find(i => i.id === itemId);\n if (!item) return;\n\n const reason = prompt('작업 중단 사유를 입력하세요 (퇴근, 자재부족 등):', '퇴근');\n if (reason === null) return; // 취소\n\n const updatedItems = workOrder.items.map(i =>\n i.id === itemId\n ? { ...i, itemStatus: '중단', pausedAt: now, pauseReason: reason }\n : i\n );\n\n const historyEntry = addItemHistory(item, '중단', reason);\n\n onUpdate?.({\n ...workOrder,\n items: updatedItems,\n itemHistory: [...(workOrder.itemHistory || []), historyEntry],\n });\n\n alert(`⏸️ \"${item.productName}\" 작업이 중단되었습니다.\\n\\n사유: ${reason}\\n\\n다음 작업일에 이어서 진행할 수 있습니다.`);\n };\n\n // 품목 작업 재개\n const handleItemResume = (itemId) => {\n const now = new Date().toISOString().replace('T', ' ').slice(0, 19);\n const item = workOrder.items.find(i => i.id === itemId);\n if (!item) return;\n\n const updatedItems = workOrder.items.map(i =>\n i.id === itemId\n ? { ...i, itemStatus: '진행중', resumedAt: now, worker: workOrder.assignee }\n : i\n );\n\n const historyEntry = addItemHistory(item, '재개', `이전 중단 사유: ${item.pauseReason || '-'}`);\n\n onUpdate?.({\n ...workOrder,\n items: updatedItems,\n itemHistory: [...(workOrder.itemHistory || []), historyEntry],\n });\n\n alert(`▶️ \"${item.productName}\" 작업을 재개합니다!`);\n };\n\n // 샘플 BOM 데이터\n const bomData = [\n { id: 1, materialCode: 'SCR-MAT-001', materialName: '스크린 원단', unit: '㎡', required: 26, stock: 150, status: 'ok' },\n { id: 2, materialCode: 'SCR-MAT-002', materialName: '앤드락', unit: 'EA', required: 40, stock: 200, status: 'ok' },\n { id: 3, materialCode: 'SCR-MAT-003', materialName: '하단바', unit: 'EA', required: 10, stock: 15, status: 'ok' },\n { id: 4, materialCode: 'SCR-MAT-004', materialName: '미싱실', unit: 'M', required: 500, stock: 2000, status: 'ok' },\n { id: 5, materialCode: 'SCR-MAT-005', materialName: '포장박스', unit: 'EA', required: 10, stock: 50, status: 'ok' },\n ];\n\n // 다음 처리할 공정 찾기\n const getNextActionStep = () => {\n const steps = getProcessSteps();\n for (const step of steps) {\n const status = workOrder.stepStatus?.[step]?.status;\n if (status === '진행중') return { step, action: 'complete' };\n if (status === '대기' || !status) return { step, action: 'start' };\n }\n return null;\n };\n\n return (\n
\n {/* 상단 헤더 - 타이틀 위, 버튼 아래 */}\n
\n {/* 타이틀 영역 */}\n
\n 작업지시 상세\n
\n {/* 버튼 영역 - 우측 정렬 */}\n
\n
\n \n \n
\n
\n
\n\n {/* 기본정보 섹션 - 컴팩트 레이아웃 */}\n
\n \n {workOrder.workOrderNo}} />\n {workOrder.orderNo}} />\n \n } />\n \n \n \n {workOrder.assignee}\n ) : (\n \n )\n } />\n
\n \n\n {/* 공정 진행 - 컴팩트 인라인 */}\n
\n \n {getProcessSteps().map((step, idx) => {\n const stepStatus = workOrder.stepStatus?.[step] || { status: '대기' };\n const canStart = idx === 0\n ? workOrder.status === '작업중' && stepStatus.status === '대기'\n : workOrder.stepStatus?.[getProcessSteps()[idx - 1]]?.status === '완료' && stepStatus.status === '대기';\n const canComplete = stepStatus.status === '진행중';\n\n return (\n
\n {idx + 1}\n {step}\n {canStart && (\n \n )}\n {canComplete && (\n \n )}\n
\n );\n })}\n
\n \n\n {/* 작업품목 섹션 - 컴팩트 테이블 */}\n
\n \n
\n \n \n | No | \n 상태 | \n 품목명 | \n 층/부호 | \n 규격 | \n 수량 | \n 작업 | \n
\n \n \n {workOrder.items.map((item, idx) => {\n const status = item.itemStatus || '대기';\n const statusColors = {\n '대기': 'bg-gray-100 text-gray-600',\n '진행중': 'bg-blue-100 text-blue-700',\n '완료': 'bg-green-100 text-green-700',\n '중단': 'bg-red-100 text-red-700',\n };\n return (\n \n | {idx + 1} | \n \n {status}\n | \n {item.productName} | \n {item.floor}/{item.location} | \n {item.spec} | \n {item.qty} | \n \n {workOrder.status === '작업중' && status === '대기' && (\n \n )}\n {workOrder.status === '작업중' && status === '진행중' && (\n \n )}\n | \n
\n );\n })}\n \n
\n
\n \n\n {/* 절곡 공정일 때 전개도 섹션 표시 - 품목별 통합 */}\n {workOrder.processType === '절곡' && (\n
\n \n {[\n { itemCode: 'SD30', itemName: '엘바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 4, weight: 0.9, dimensions: '75', note: '', svgType: 'rect' },\n { itemCode: 'SD31', itemName: '하장바', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 2, weight: 1.158, dimensions: '67→126→165→178→193', note: '', svgType: 'hajang' },\n { itemCode: 'SD32', itemName: '짜부가스켓', material: 'E.G.I 0.8T', totalWidth: 500, length: 3000, qty: 4, weight: 0.576, dimensions: '48', note: '80*4,50*8', svgType: 'thin' },\n { itemCode: 'SD33', itemName: '50평철', material: 'E.G.I 1.2T', totalWidth: 500, length: 3000, qty: 2, weight: 0.3, dimensions: '50', note: '', svgType: 'flat' },\n { itemCode: 'SD34', itemName: '전면판', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 1.764, dimensions: '120→499→553→588', note: '500*380', svgType: 'front' },\n { itemCode: 'SD35', itemName: '린텔박스', material: 'E.G.I 1.6T', totalWidth: 500, length: 3000, qty: 1, weight: 0.504, dimensions: '84→138→168', note: 'S/BOX', svgType: 'lintel' },\n { itemCode: 'SD36', itemName: '밑면 점검구', material: 'E.G.I 1.6T', totalWidth: 500, length: 2438, qty: 2, weight: 0.98, dimensions: '90→240→310', note: '500*380', svgType: 'bottom' },\n { itemCode: 'SD37', itemName: '후면코너부', material: 'E.G.I 1.2T', totalWidth: 500, length: 1219, qty: 4, weight: 0.45, dimensions: '35→85→120', note: '', svgType: 'corner' },\n ].map((part, idx) => (\n
\n {/* 품목 헤더 */}\n
\n
\n {part.itemCode}\n {part.itemName}\n {part.material}\n
\n
\n 수량: {part.qty}\n \n
\n {/* 품목 내용 - 정보 + 도면 */}\n
\n {/* 좌측: 상세 정보 */}\n
\n
\n
전개폭
\n
{part.totalWidth}mm
\n
\n
\n
길이
\n
{part.length}mm
\n
\n
\n
중량
\n
{part.weight}kg
\n
\n
\n
비고
\n
{part.note || '-'}
\n
\n
\n
전개치수
\n
{part.dimensions}
\n
\n
\n {/* 우측: 전개도 도면 */}\n
\n
\n
\n
\n
\n
\n
\n ))}\n
\n \n )}\n\n {/* 이슈 - 컴팩트 (이슈 있을때만 표시) */}\n {workOrder.issues?.length > 0 && (\n
\n \n {workOrder.issues.map(issue => (\n
\n {issue.status}\n {issue.issueType}\n {issue.description}\n
\n ))}\n
\n \n )}\n\n {/* 이하 작업일지/BOM 섹션은 컴팩트 버튼으로 대체됨 - 약 500줄 제거됨 */}\n {false && (
\n {relatedResults.length > 0 ? (\n
\n \n \n | 작업일 | \n 작업자 | \n 자재LOT | \n 수주LOT | \n 생산수량 | \n 양품 | \n 불량 | \n 이슈 | \n
\n \n \n {relatedResults.map(result => (\n \n | {result.workDate} | \n {result.worker} | \n {result.materialLotNo || '-'} | \n {result.lotNo} | \n {result.productionQty || result.completedQty} | \n {result.goodQty || result.completedQty} | \n {result.defectQty || 0} | \n \n {result.hasIssue ? ⚠️ : ✓}\n | \n
\n ))}\n \n
\n ) : (\n
\n
\n
작업 시작 시 자동으로 기록됩니다.
\n
\n )}\n\n {/* 작업일지 상태 - 공정별 요약 */}\n {workLogData ? (\n
\n {/* 기본정보 요약 (공통) */}\n
\n \n
\n
작업일자
\n
{workLogData.workDate || '-'}
\n
\n
\n
생산담당자
\n
{workLogData.productionManager || '-'}
\n
\n
\n
제품 LOT NO
\n
{workLogData.productLotNo || '-'}
\n
\n
\n
\n {workOrder.processType === '스크린' ? '원단유형' :\n workOrder.processType === '슬랫' ? '코일유형' : '마감유형'}\n
\n
\n {workOrder.processType === '스크린' ? workLogData.fabric?.type :\n workOrder.processType === '슬랫' ? workLogData.coil?.type :\n workLogData.finishType || '-'}\n
\n
\n
\n \n\n {/* 스크린 공정 요약 */}\n {workOrder.processType === '스크린' && (\n
\n
\n \n {workLogData.workItems?.filter(item => item.qty).map(item => (\n
\n {item.step}\n {item.qty} {item.unit}\n
\n ))}\n
\n \n
\n \n
\n
생산
\n
{workLogData.productionResult?.totalProduced || 0}
\n
\n
\n
양품
\n
{workLogData.productionResult?.goodQty || 0}
\n
\n
\n
불량
\n
{workLogData.productionResult?.defectQty || 0}
\n
\n
\n \n
\n )}\n\n {/* 슬랫 공정 요약 */}\n {workOrder.processType === '슬랫' && (\n
\n
\n \n {workLogData.cuttingDetails?.filter(item => item.qty).map(item => (\n
\n {item.length}\n {item.qty} EA\n
\n ))}\n
\n \n
\n \n
\n
양품
\n
{workLogData.productionResult?.goodQty || 0}
\n
\n
\n
총 중량
\n
{workLogData.productionResult?.totalWeight || 0} kg
\n
\n
\n \n
\n )}\n\n {/* 절곡 공정 요약 */}\n {workOrder.processType === '절곡' && (\n <>\n
\n
\n \n {workLogData.guideRail?.filter(item => item.qty).map(item => (\n
\n {item.name} ({item.spec})\n {item.qty}개\n
\n ))}\n {!workLogData.guideRail?.some(item => item.qty) && (\n
입력된 수량 없음
\n )}\n
\n \n
\n \n {workLogData.bottomFinish?.map(item => (\n
\n
{item.name}\n
\n {item.rows?.map((row, idx) => (\n row.qty && {row.spec}: {row.qty}\n ))}\n
\n
\n ))}\n
\n \n
\n
\n \n
\n
SUS
\n
{workLogData.totalProduction?.sus || 0} kg
\n
\n
\n
EGI
\n
{workLogData.totalProduction?.egi || 0} kg
\n
\n
\n \n >\n )}\n\n {/* 비고 (공통) */}\n {workLogData.remarks && (\n
\n {workLogData.remarks}
\n \n )}\n
\n ) : (\n
\n \n
\n
작업일지가 아직 작성되지 않았습니다
\n
\n {workOrder.processType} 공정에 필요한 작업 내역을 기록하세요.\n
\n
\n
\n \n )}\n\n {/* 안내 (공정별) */}\n
\n
📋 작업일지 작성 안내
\n
\n {workOrder.processType === '스크린' && (\n <>\n - • 원단 유형, 색상, 폭 정보를 입력합니다.
\n - • 원단절단, 미싱, 앤드락작업 등 각 공정별 작업 수량을 입력합니다.
\n - • 품목별 원단 LOT와 제품 LOT를 기록하여 추적성을 확보합니다.
\n >\n )}\n {workOrder.processType === '슬랫' && (\n <>\n - • 코일 유형, 색상, 두께 정보를 입력합니다.
\n - • 길이별 절단 수량과 미미작업 수량을 입력합니다.
\n - • 생산 완료 후 총 중량(kg)을 입력합니다.
\n >\n )}\n {workOrder.processType === '절곡' && (\n <>\n - • 벽면형, 하단마감재, 케이스, 연기차단재 각 섹션의 작업량을 입력합니다.
\n - • 각 부품별 입고 및 생산 LOT NO를 기록하여 추적성을 확보합니다.
\n - • 작업 완료 후 생산량 합계(SUS, EGI)를 입력합니다.
\n >\n )}\n - • 작성된 작업일지는 인쇄하여 현장 기록으로 활용할 수 있습니다.
\n
\n
\n\n {/* BOM/자재 섹션 */}\n
\n {/* 공정별 자재 투입 안내 */}\n
\n
\n
\n \n
\n
\n
\n {workOrder.processType} 공정 자재 투입 기준\n
\n {workOrder.processType === '절곡' && (\n
\n
단위: kg → 장 환산
\n
방식: 자재 LOT 선택 후 투입 수량(장) 지정
\n
계산: 총 중량(kg) ÷ 장당 중량 = 투입 장수
\n
\n 예) 100kg 코일, 장당 0.5kg → 200장 투입 가능\n
\n
\n )}\n {workOrder.processType === '스크린' && (\n
\n
단위: W(폭) × H(높이)
\n
방식: 수주 W값 × 자재LOT H값 조합
\n
계산: 수주 폭(W) 그대로, 높이(H)는 자재LOT에서 선택
\n
\n 예) 수주 W=2400 × 자재LOT H=1500 → 2400×1500 원단 투입\n
\n
\n )}\n {workOrder.processType === '슬랫' && (\n
\n
단위: 자동산출 (높이 기반)
\n
방식: 수주 높이에 따른 절단 매수 자동 계산
\n
계산: 코일 길이 ÷ 제품 높이 = 절단 매수
\n
\n 예) 코일 10M, 제품높이 500mm → 20매 절단\n
\n
\n )}\n
\n
\n
\n\n {/* 자재 재고 현황 요약 */}\n
\n
\n
\n
\n
\n
필요 자재
\n
{bomData.length}종
\n
\n
\n
\n
\n
\n
\n
\n
재고 충분
\n
{bomData.filter(b => b.stock >= b.required).length}종
\n
\n
\n
\n
\n
\n
\n
\n
재고 부족
\n
{bomData.filter(b => b.stock < b.required).length}종
\n
\n
\n
\n
\n\n
\n \n
작업에 필요한 자재 목록입니다. 입고 LOT와 생산 LOT를 연결할 수 있습니다.
\n
\n {workOrder.status === '작업중' && (\n <>\n
\n
\n >\n )}\n
\n
\n \n {/* LOT 연결 안내 */}\n \n
\n LOT 추적 안내: 입고LOT는 입고 시 부여된 LOT번호이며, 생산LOT는 이 작업지시에서 생산되는 제품의 LOT번호입니다.\n 투입 시 두 LOT가 연결되어 제품 추적이 가능합니다.\n
\n
\n \n\n
\n {workOrder.materialInputs?.length > 0 ? (\n \n \n \n | 투입일시 | \n 자재명 | \n 입고LOT | \n 생산LOT | \n 투입량 | \n 작업자 | \n
\n \n \n \n | 2024-12-04 09:00 | \n 스크린 원단 (W2400) | \n IN-241204-01 | \n PROD-241204-01 | \n 50 M | \n 김생산 | \n
\n \n
\n ) : (\n \n
\n
투입된 자재가 없습니다.
\n
작업 시작 후 자재 투입 시 입고LOT-생산LOT가 연결되어 기록됩니다.
\n
\n )}\n \n
\n
\n )}\n {/* 작업일지/BOM 섹션 끝 - false && 로 비활성화됨 */}\n\n {/* 재고 확인 모달 */}\n {showMaterialCheck && (\n
setShowMaterialCheck(false)}\n />\n )}\n\n {/* 투입자재 추가 모달 */}\n {showMaterialInputModal && (\n {\n setShowMaterialInputModal(false);\n setActiveModal?.(null);\n }}\n onSubmit={(materialCode, qty, lotNo, inputBy, note) => {\n // 재고 감소 처리\n onUseMaterial?.(materialCode, qty, {\n workOrderNo: workOrder.workOrderNo,\n lotNo,\n inputBy,\n note,\n });\n\n // 작업지시에 투입 이력 추가\n const material = inventory.find(m => m.materialCode === materialCode);\n const newInput = {\n id: Date.now(),\n materialCode,\n materialName: material?.materialName || '',\n lotNo,\n qty,\n unit: material?.unit || '',\n inputBy,\n inputAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n note,\n };\n\n onUpdate?.({\n ...workOrder,\n materialInputs: [...(workOrder.materialInputs || []), newInput],\n });\n\n setShowMaterialInputModal(false);\n setActiveModal?.(null);\n alert(`✅ 자재 투입이 등록되었습니다.\\n\\n자재: ${material?.materialName}\\n투입량: ${qty} ${material?.unit}\\nLOT: ${lotNo}`);\n }}\n />\n )}\n\n {/* ★ 다중 작업자 배정 모달 */}\n {showAssignModal && (\n \n
\n
\n
\n \n
작업자 배정 (다중 선택)
\n \n
\n
\n\n
\n {/* 작업지시 정보 */}\n
\n
\n
\n
작업지시번호
\n
{workOrder.workOrderNo}
\n
\n
\n
공정유형
\n
{workOrder.processType}
\n
\n
\n
\n\n {/* 선택된 작업자 표시 */}\n {selectedAssignees.length > 0 && (\n
\n
\n 선택된 작업자 ({selectedAssignees.length}명)\n \n
\n
\n {selectedAssignees.map(a => (\n \n {a}\n \n \n ))}\n
\n
\n )}\n\n {/* 팀별 작업자 선택 */}\n
\n
팀별 작업자 선택
\n {workerTeams.map(team => (\n
\n {/* 팀 헤더 */}\n
handleTeamToggle(team)}\n className={`p-3 flex items-center justify-between cursor-pointer transition-colors\n ${isTeamFullySelected(team) ? 'bg-purple-100' : isTeamPartiallySelected(team) ? 'bg-purple-50' : 'bg-gray-50 hover:bg-gray-100'}`}\n >\n
\n
\n {isTeamFullySelected(team) &&
}\n {isTeamPartiallySelected(team) &&
}\n
\n
{team.name}\n
({team.members.length}명)\n
\n
전체 선택\n
\n {/* 팀원 목록 */}\n
\n {team.members.map(member => (\n
handleWorkerToggle(member)}\n className={`p-2.5 pl-10 cursor-pointer transition-colors flex items-center gap-2\n ${selectedAssignees.includes(member) ? 'bg-purple-50' : 'hover:bg-gray-50'}`}\n >\n
\n {selectedAssignees.includes(member) && }\n
\n
{member}\n
\n ))}\n
\n
\n ))}\n
\n\n
\n * 다중 작업자를 배정할 수 있습니다. 팀 전체 선택도 가능합니다.\n
\n
\n\n
\n \n \n
\n
\n
\n )}\n\n {/* 공정 완료 모달 */}\n {showStepModal && currentStep && (\n \n
\n
\n
\n \n
{currentStep} 공정 완료
\n \n
\n
\n\n
\n
\n
\n
작업지시번호
\n
{workOrder.workOrderNo}
\n
\n
\n
현재 공정
\n
{currentStep}
\n
\n
\n
작업자
\n
{workOrder.assignee}
\n
\n
\n
시작시간
\n
{workOrder.stepStatus?.[currentStep]?.startedAt || '-'}
\n
\n
\n\n {/* 중간검사 공정일 때 */}\n {currentStep === '중간검사' && (\n
\n
\n \n
\n \n \n
\n
\n )}\n\n {/* 포장 공정일 때 */}\n {currentStep === '포장' && (\n
\n
\n { }}\n />\n \n
\n
\n )}\n\n {/* 일반 공정일 때 */}\n {currentStep !== '중간검사' && currentStep !== '포장' && (\n
\n )}\n
\n
\n
\n )}\n\n {/* 작업지시서 출력 모달 */}\n {showProductionSheet && (\n setShowProductionSheet(false)}\n />\n )}\n\n {/* 작업일지 에디터 모달 (공정별) */}\n {showWorkLogEditor && workOrder.processType === '스크린' && (\n setShowWorkLogEditor(false)}\n onSave={(data) => {\n setWorkLogData(data);\n setShowWorkLogEditor(false);\n }}\n />\n )}\n {showWorkLogEditor && workOrder.processType === '슬랫' && (\n setShowWorkLogEditor(false)}\n onSave={(data) => {\n setWorkLogData(data);\n setShowWorkLogEditor(false);\n }}\n />\n )}\n {showWorkLogEditor && workOrder.processType === '절곡' && (\n setShowWorkLogEditor(false)}\n onSave={(data) => {\n setWorkLogData(data);\n setShowWorkLogEditor(false);\n }}\n />\n )}\n\n {/* 작업일지 출력 모달 - 공정별 전용 템플릿 포함 */}\n {showWorkLogSheet && (\n setShowWorkLogSheet(false)}\n />\n )}\n\n {/* 작업지시서 + 작업일지 함께 출력 모달 */}\n {showCombinedSheet && workLogData && (\n setShowCombinedSheet(false)}\n />\n )}\n\n {/* 3단계 결재 프로세스 모달 */}\n {showApprovalModal && (\n \n
\n
\n
작업일지 결재 (3단계)
\n
작업지시: {workOrder?.workOrderNo}
\n
\n
\n {/* 결재 상태 요약 */}\n
\n
\n 결재 상태\n \n {workLogApproval.status === 'draft' ? '작성중' :\n workLogApproval.status === 'step2_pending' ? '2단계 결재중' :\n workLogApproval.status === 'step3_pending' ? '최종 결재중' :\n workLogApproval.status === 'approved' ? '최종승인완료' : '반려'}\n \n
\n
\n\n {/* 3단계 결재라인 */}\n
\n
결재라인 (3단계)
\n
\n
\n \n \n | \n 1단계 \n 생산지시자 \n | \n \n 2단계 \n 생산담당자 \n | \n \n 3단계 \n 품질팀장 \n | \n
\n \n \n \n | \n {workLogApproval.step1?.status === 'approved' ? (\n \n \n {workLogApproval.step1?.submittedBy}\n {workLogApproval.step1?.submittedAt?.slice(0, 10)}\n \n ) : (\n \n )}\n | \n \n {workLogApproval.step2?.status === 'approved' ? (\n \n \n {workLogApproval.step2?.approvedBy}\n {workLogApproval.step2?.approvedAt?.slice(0, 10)}\n \n ) : workLogApproval.status === 'step2_pending' ? (\n \n \n 결재 대기중\n \n ) : workLogApproval.rejectedStep === 2 ? (\n \n \n {workLogApproval.rejectedBy}\n \n ) : (\n \n )}\n | \n \n {workLogApproval.step3?.status === 'approved' ? (\n \n \n {workLogApproval.step3?.approvedBy}\n {workLogApproval.step3?.approvedAt?.slice(0, 10)}\n \n ) : workLogApproval.status === 'step3_pending' ? (\n \n \n 결재 대기중\n \n ) : workLogApproval.rejectedStep === 3 ? (\n \n \n {workLogApproval.rejectedBy}\n \n ) : (\n \n )}\n | \n
\n \n
\n
\n
\n\n {/* 결재 진행 기록 */}\n
\n
결재 진행 기록
\n
\n {workLogApproval.step1?.submittedAt && (\n
\n \n {workLogApproval.step1.submittedAt}\n {workLogApproval.step1.submittedBy}\n 작성 완료 (1단계)\n
\n )}\n {workLogApproval.step2?.approvedAt && (\n
\n \n {workLogApproval.step2.approvedAt}\n {workLogApproval.step2.approvedBy}\n 확인 완료 (2단계)\n
\n )}\n {workLogApproval.step3?.approvedAt && (\n
\n \n {workLogApproval.step3.approvedAt}\n {workLogApproval.step3.approvedBy}\n 최종 승인 (3단계)\n
\n )}\n {workLogApproval.status === 'rejected' && (\n
\n \n {workLogApproval.rejectedAt}\n {workLogApproval.rejectedBy}\n 반려 ({workLogApproval.rejectedStep}단계)\n
\n )}\n {workLogApproval.status === 'draft' && (\n
아직 결재 기록이 없습니다.
\n )}\n
\n
\n\n {/* 반려 사유 */}\n {workLogApproval.status === 'rejected' && workLogApproval.rejectReason && (\n
\n
반려 사유 ({workLogApproval.rejectedStep}단계)
\n
{workLogApproval.rejectReason}
\n
\n )}\n\n {/* 투입 자재 정보 */}\n {selectedMaterials.length > 0 && (\n
\n
투입 원자재 (IQC 연계)
\n
\n \n \n | 자재명 | \n 입고로트 | \n 수량 | \n IQC | \n
\n \n \n {selectedMaterials.map((mat, idx) => (\n \n | {mat.name} | \n {mat.lotNo} | \n {mat.qty} {mat.unit} | \n \n 합격\n | \n
\n ))}\n \n
\n
\n )}\n
\n\n
\n
\n
\n {/* 1단계: 작성 완료 */}\n {workLogApproval.status === 'draft' && (\n \n )}\n {/* 2단계: 생산담당자 확인 */}\n {workLogApproval.status === 'step2_pending' && (\n <>\n \n \n >\n )}\n {/* 3단계: 품질팀장 최종 결재 */}\n {workLogApproval.status === 'step3_pending' && (\n <>\n \n \n >\n )}\n {/* 재상신 (반려 시) */}\n {workLogApproval.status === 'rejected' && (\n \n )}\n {/* 최종 승인 후 중간검사성적서 생성 */}\n {workLogApproval.status === 'approved' && (\n \n )}\n
\n
\n
\n
\n )}\n\n {/* 투입 자재 선택 모달 (IQC 연계) */}\n {showMaterialSelectModal && (\n \n
\n
\n
투입 자재 선택 (IQC 합격 자재)
\n
IQC 검사 합격된 자재만 선택 가능합니다.
\n
\n
\n
\n \n \n | 선택 | \n 자재코드 | \n 자재명 | \n 입고로트 | \n 가용수량 | \n IQC | \n
\n \n \n {[\n { code: 'MAT-001', name: '원단 A (폴리에스터)', lotNo: '241210-001', qty: 500, unit: 'm', iqcStatus: '합격' },\n { code: 'MAT-002', name: '원단 B (면)', lotNo: '241210-002', qty: 300, unit: 'm', iqcStatus: '합격' },\n { code: 'MAT-003', name: '코일 (알루미늄)', lotNo: '241209-001', qty: 1000, unit: 'kg', iqcStatus: '합격' },\n { code: 'MAT-004', name: '철판 (아연도금)', lotNo: '241208-003', qty: 200, unit: '장', iqcStatus: '합격' },\n { code: 'MAT-005', name: '실 (흰색)', lotNo: '241207-001', qty: 50, unit: '롤', iqcStatus: '합격' },\n ].map((mat, idx) => (\n \n | \n m.lotNo === mat.lotNo)}\n onChange={(e) => {\n if (e.target.checked) { setSelectedMaterials([...selectedMaterials, { ...mat, inputQty: mat.qty }]); }\n else { setSelectedMaterials(selectedMaterials.filter(m => m.lotNo !== mat.lotNo)); }\n }} className=\"w-4 h-4\" />\n | \n {mat.code} | \n {mat.name} | \n {mat.lotNo} | \n {mat.qty} {mat.unit} | \n \n {mat.iqcStatus}\n | \n
\n ))}\n \n
\n
\n
\n
선택: {selectedMaterials.length}건
\n
\n
\n
\n
\n
\n
\n
\n )}\n\n {/* 중간검사성적서 자동 생성 모달 */}\n {showInspectionCertModal && (\n \n
\n
\n
중간검사성적서 자동 생성
\n
작업일지 기반으로 중간검사성적서가 자동 생성됩니다.
\n
\n
\n {/* LOT 번호 정보 */}\n
\n
\n
\n
\n
형식: KD-WE-YYMMDD-##-공정
\n
\n
\n \n setInspectionCertData({ ...inspectionCertData, inspectionDate: e.target.value })} />\n
\n
\n {/* 작업지시 정보 */}\n
\n
작업지시 정보
\n
\n
작업지시번호: {workOrder?.workOrderNo}
\n
공정: {workOrder?.processType}
\n
품목: {workOrder?.itemName}
\n
생산수량: {workOrder?.plannedQty}개
\n
\n
\n {/* 투입 원자재 */}\n
\n
투입 원자재 (입고로트 추적)
\n {selectedMaterials.length > 0 ? (\n
\n \n \n | 자재명 | \n 입고로트 | \n 투입수량 | \n IQC | \n
\n \n \n {selectedMaterials.map((mat, idx) => (\n \n | {mat.name} | \n {mat.lotNo} | \n {mat.inputQty || mat.qty} {mat.unit} | \n | \n
\n ))}\n \n
\n ) : (\n
\n
투입 자재가 등록되지 않았습니다.
\n
\n
\n )}\n
\n {/* 검사 항목 */}\n
\n
검사 항목 (자동 생성)
\n
\n \n \n | 검사항목 | \n 규격 | \n 측정값 | \n 판정 | \n
\n \n \n {[\n { item: '외관검사', spec: '스크래치/이물 없음', value: '양호', result: '합격' },\n { item: '치수검사', spec: '±0.5mm', value: '0.3mm', result: '합격' },\n { item: '동작검사', spec: '정상작동', value: '정상', result: '합격' },\n { item: '중량검사', spec: '규격 ±5%', value: '2.8kg (-3%)', result: '합격' },\n ].map((item, idx) => (\n \n | {item.item} | \n {item.spec} | \n {item.value} | \n \n {item.result}\n | \n
\n ))}\n \n
\n
\n {/* 검사자 정보 */}\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n )}\n\n {/* 작업일지 양식보기 모달 - 실제 데이터 연동 */}\n {showWorkLogDataPreview && (\n setShowWorkLogDataPreview(false)}\n />\n )}\n \n );\n};\n\n// ============ 작업지시서 + 작업일지 함께 출력 컴포넌트 ============\nconst CombinedWorkOrderSheet = ({ workOrder, order, workLog, approvalStatus, onClose }) => {\n const printRef = React.useRef(null);\n const processType = workOrder?.processType || '스크린';\n\n const handlePrint = () => {\n const printContent = printRef.current;\n const printWindow = window.open('', '_blank');\n printWindow.document.write(`\n \n \n
작업지시서 및 작업일지 - ${workOrder?.workOrderNo}\n \n \n ${printContent.innerHTML}\n \n `);\n printWindow.document.close();\n printWindow.print();\n };\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n \n 작업지시서 + 작업일지 출력\n
\n
\n
\n\n {/* 출력 미리보기 */}\n
\n {/* ========== 1. 작업지시서 ========== */}\n
\n {/* 결재란 */}\n
\n
\n \n \n | 담당 | \n 부서장 | \n
\n \n \n \n | \n {approvalStatus?.status !== 'draft' && '✓'}\n | \n \n {approvalStatus?.status === 'approved' && '✓'}\n | \n
\n \n
\n
\n\n
\n
작 업 지 시 서
\n
문서번호: {workOrder?.workOrderNo}
\n
\n\n {/* 기본정보 */}\n
작 업 지 시 정 보
\n
\n \n \n | 지시번호 | \n {workOrder?.workOrderNo} | \n 지시일자 | \n {workOrder?.orderDate} | \n
\n \n | 수주LOT | \n {order?.lotNo || workOrder?.orderNo} | \n 납기일 | \n {workOrder?.dueDate} | \n
\n \n | 거래처 | \n {workOrder?.customerName} | \n 현장명 | \n {workOrder?.siteName} | \n
\n \n | 공정 | \n {workOrder?.processType} | \n 작업자 | \n {workOrder?.assignee || '-'} | \n
\n \n
\n\n {/* 작업품목 */}\n
작 업 품 목
\n
\n \n \n | No | \n 품목코드 | \n 품목명 | \n 규격 | \n 지시수량 | \n 단위 | \n 비고 | \n
\n \n \n {workOrder?.items?.map((item, idx) => (\n \n | {idx + 1} | \n {item.productCode} | \n {item.productName} | \n {item.spec} | \n {item.qty} | \n {item.unit} | \n {item.floor ? `${item.floor} / ${item.location}` : ''} | \n
\n ))}\n \n
\n\n {/* 지시사항 */}\n
지 시 사 항
\n
\n {workOrder?.note || '특이사항 없음'}\n
\n
\n\n {/* 페이지 구분 */}\n
\n\n {/* ========== 2. 작업일지 ========== */}\n
\n {/* 결재란 */}\n
\n
\n \n \n | 담당 | \n 부서장 | \n
\n \n \n \n \n {approvalStatus?.status !== 'draft' && (\n \n ✓ \n {approvalStatus?.submittedBy} \n \n )}\n | \n \n {approvalStatus?.status === 'approved' && (\n \n ✓ \n {approvalStatus?.approvedBy} \n \n )}\n | \n
\n \n
\n
\n\n
\n
{processType} 작 업 일 지
\n
문서번호: WL-{processType === '스크린' ? 'SCR' : processType === '슬랫' ? 'SLT' : 'FLD'}-{workLog?.workDate?.replace(/-/g, '') || ''}-01
\n
\n\n {/* 기본정보 */}\n
기 본 정 보
\n
\n \n \n | 작업일자 | \n {workLog?.workDate} | \n 담당자 | \n {workLog?.productionManager || workOrder?.assignee} | \n
\n \n | 거래처 | \n {workOrder?.customerName} | \n 현장명 | \n {workOrder?.siteName} | \n
\n \n | 제품LOT | \n {workLog?.productLotNo || '-'} | \n 지시번호 | \n {workOrder?.workOrderNo} | \n
\n \n
\n\n {/* 생산실적 요약 */}\n
생 산 실 적
\n
\n
\n
생산량
\n
\n {workLog?.productionResult?.totalProduced || workLog?.totalProduction?.sus + workLog?.totalProduction?.egi || 0}\n
\n
\n
\n
양품
\n
\n {workLog?.productionResult?.goodQty || 0}\n
\n
\n
\n
불량
\n
\n {workLog?.productionResult?.defectQty || 0}\n
\n
\n
\n\n {/* 작업내역 (공정별) */}\n
작 업 내 역
\n {processType === '스크린' && workLog?.workItems && (\n
\n \n \n | 공정 | \n 수량 | \n 단위 | \n
\n \n \n {workLog.workItems.filter(item => item.qty).map(item => (\n \n | {item.step} | \n {item.qty} | \n {item.unit} | \n
\n ))}\n \n
\n )}\n {processType === '슬랫' && workLog?.cuttingDetails && (\n
\n \n \n | 길이 | \n 수량(EA) | \n
\n \n \n {workLog.cuttingDetails.filter(item => item.qty).map(item => (\n \n | {item.length} | \n {item.qty} | \n
\n ))}\n \n
\n )}\n {processType === '절곡' && (\n <>\n {workLog?.guideRail?.some(item => item.qty) && (\n
\n \n \n | 벽면형 (120·70) | \n
\n \n | 부품 | \n 규격 | \n 수량 | \n
\n \n \n {workLog.guideRail.filter(item => item.qty).map(item => (\n \n | {item.name} | \n {item.spec} | \n {item.qty}개 | \n
\n ))}\n \n
\n )}\n >\n )}\n\n {/* 비고 */}\n
비 고
\n
\n {workLog?.remarks || '특이사항 없음'}\n
\n
\n
\n
\n
\n );\n};\n\n// ============ 작업실적 ============\n\n// 작업실적 입력\nconst WorkResultInput = ({ workOrder, onNavigate, onBack, onSave }) => {\n const [formData, setFormData] = useState({\n workOrderNo: workOrder?.workOrderNo || '',\n workDate: new Date().toISOString().split('T')[0],\n productCode: workOrder?.items?.[0]?.productCode || '',\n productionQty: 1,\n goodQty: 1,\n defectQty: 0,\n defectType: '',\n inspectionCompleted: false,\n packingCompleted: false,\n worker: '',\n note: '',\n });\n\n const defectTypes = ['치수불량', '외관불량', '작동불량', '자재불량', '기타'];\n\n const handleChange = (field, value) => {\n setFormData(prev => {\n const updated = { ...prev, [field]: value };\n // 불량수량 자동 계산\n if (field === 'productionQty' || field === 'goodQty') {\n const prodQty = field === 'productionQty' ? value : prev.productionQty;\n const goodQty = field === 'goodQty' ? value : prev.goodQty;\n updated.defectQty = Math.max(0, prodQty - goodQty);\n }\n return updated;\n });\n };\n\n const handleSubmit = () => {\n // LOT번호 자동 생성\n const lotNo = `LOT-${formData.workDate.replace(/-/g, '')}-${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`;\n const newResult = {\n ...formData,\n lotNo,\n processType: workOrder?.processType,\n productName: workOrder?.items?.find(i => i.productCode === formData.productCode)?.productName || '',\n spec: workOrder?.items?.find(i => i.productCode === formData.productCode)?.spec || '',\n };\n onSave?.(newResult);\n onBack();\n };\n\n return (\n
\n
\n
\n
\n \n \n
\n
\n\n
\n
\n \n \n \n \n \n \n \n
\n \n\n
\n \n \n
\n\n {/* LOT 자동생성 안내 */}\n
\n
\n
\n
LOT번호 자동생성
\n
\n 저장 시 LOT번호가 자동으로 생성됩니다. (형식: LOT-YYYYMMDD-XXX)\n
\n
\n
\n
\n );\n};\n\n// 작업실적 조회\nconst WorkResultList = ({ workResults, onNavigate }) => {\n const [search, setSearch] = useState('');\n\n // ID 내림차순 정렬 - 최신 등록 최상단\n const filtered = workResults\n .filter(r =>\n r.lotNo.toLowerCase().includes(search.toLowerCase()) ||\n r.workOrderNo.toLowerCase().includes(search.toLowerCase()) ||\n r.productName.includes(search)\n )\n .sort((a, b) => b.id - a.id);\n\n // 통계\n const totalProduction = workResults.reduce((sum, r) => sum + r.productionQty, 0);\n const totalGood = workResults.reduce((sum, r) => sum + r.goodQty, 0);\n const totalDefect = workResults.reduce((sum, r) => sum + r.defectQty, 0);\n const defectRate = totalProduction > 0 ? ((totalDefect / totalProduction) * 100).toFixed(1) : 0;\n\n return (\n
\n
\n 엑셀 다운로드\n \n }\n />\n\n {/* 대시보드 */}\n \n \n \n \n \n
\n\n {/* 검색바 */}\n \n\n \n {/* 일괄 내보내기 기능 추가 예정 - 체크박스 비활성화 상태 */}\n
\n
\n \n );\n};\n\n// ============ 출고 관리 (수주 연동) ============\n\n/**\n * 출고관리 구조 설계\n * ===================\n *\n * 📋 출고 관리자 기능\n * - 출고 준비 확인: 출고단위별 공장별 작업 완료 확인\n * - 반제품/부품 출고구역 도착 확인\n * - 출고 우선순위 지정\n *\n * 🚚 배송 방식별 관리\n * 1. 직접배차 (발주처 차량): 입차시간 확인/조율, 휴대폰/카톡 소통\n * 2. 상차 (물류업체): 물량(차량톤수) 확정, 배송지 확인, 상차시간 조율, 입차시간 확정\n * 3. 택배/화물/직접배송: 운송담당자 지정, 운송시간 관리\n *\n * 📊 출고 변경 관리\n * - 출고일 변경\n * - 출고 취소\n * - 미출고 사유 관리\n *\n * 📦 상차 작업\n * - 상차담당자 지정\n * - 품목 체크리스트: 부품/부자재 누락 방지, 품목 리스트 확인, 수량 체크, 부자재 로트번호 입력\n *\n * 수주 연동 구조:\n * - 수주 → 출고 (lotNo 기준 연동)\n * - 수주분할번호(splitNo)로 출고 식별\n * - 수주의 생산완료 상태에서만 출고 가능\n */\n\n// 배차유형 옵션\nconst dispatchTypes = ['직접배차', '상차', '택배', '화물', '직접배송'];\nconst vehicleTypes = ['1톤', '2.5톤', '5톤', '11톤', '25톤'];\nconst logisticsCompanies = ['한진물류', 'CJ대한통운', '롯데택배', '로젠택배', '우체국택배', '경동택배', '기타'];\n\n// 출고 우선순위 옵션\nconst priorityOptions = [\n { value: 1, label: '긴급', color: 'bg-red-100 text-red-700' },\n { value: 2, label: '높음', color: 'bg-orange-100 text-orange-700' },\n { value: 3, label: '보통', color: 'bg-blue-100 text-blue-700' },\n { value: 4, label: '낮음', color: 'bg-gray-100 text-gray-700' },\n];\n\n// 미출고 사유 옵션\nconst nonShipmentReasons = [\n '생산 지연',\n '품질 이슈',\n '자재 미입고',\n '고객 요청',\n '배송 일정 조율',\n '입금 미확인',\n '서류 미비',\n '기타',\n];\n\n// 출고일정 캘린더 뷰 컴포넌트\nconst ShipmentCalendarView = ({ shipments, calendarDate, setCalendarDate, selectedCalendarDate, setSelectedCalendarDate, onNavigate }) => {\n const year = calendarDate.getFullYear();\n const month = calendarDate.getMonth();\n const startDayOfWeek = new Date(year, month, 1).getDay();\n const daysInMonth = new Date(year, month + 1, 0).getDate();\n const goToPrevMonth = () => setCalendarDate(new Date(year, month - 1, 1));\n const goToNextMonth = () => setCalendarDate(new Date(year, month + 1, 1));\n const goToToday = () => setCalendarDate(new Date());\n const getShipmentsForDate = (dateStr) => shipments.filter(s => s.shipmentDate === dateStr);\n const getStatusColor = (status) => {\n switch (status) {\n case '출고대기': case '출고지시': return 'bg-yellow-500';\n case '출고준비': case '출하준비': return 'bg-blue-500';\n case '상차대기': case '출하대기': return 'bg-purple-500';\n case '배송중': return 'bg-orange-500';\n case '배송완료': return 'bg-green-500';\n default: return 'bg-gray-400';\n }\n };\n const monthStr = `${year}-${String(month + 1).padStart(2, '0')}`;\n const monthlyStats = {\n total: shipments.filter(s => s.shipmentDate?.startsWith(monthStr)).length,\n waiting: shipments.filter(s => s.shipmentDate?.startsWith(monthStr) && (s.status === '출고대기' || s.status === '출고지시')).length,\n shipping: shipments.filter(s => s.shipmentDate?.startsWith(monthStr) && s.status === '배송중').length,\n complete: shipments.filter(s => s.shipmentDate?.startsWith(monthStr) && s.status === '배송완료').length,\n };\n const renderCalendarCells = () => {\n const cells = [];\n const today = new Date().toISOString().split('T')[0];\n for (let i = 0; i < startDayOfWeek; i++) cells.push(
);\n for (let day = 1; day <= daysInMonth; day++) {\n const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;\n const dayShipments = getShipmentsForDate(dateStr);\n const isToday = dateStr === today;\n const isSelected = dateStr === selectedCalendarDate;\n const dayOfWeek = new Date(year, month, day).getDay();\n cells.push(\n
setSelectedCalendarDate(dateStr === selectedCalendarDate ? null : dateStr)} className={`h-28 border border-gray-200 p-1 cursor-pointer transition-all hover:bg-blue-50 ${isToday ? 'bg-blue-50 border-blue-300' : 'bg-white'} ${isSelected ? 'ring-2 ring-blue-500' : ''}`}>\n
\n {day}\n {dayShipments.length > 0 && {dayShipments.length}건}\n
\n
\n {dayShipments.slice(0, 3).map((shipment, idx) => (
{ e.stopPropagation(); onNavigate('shipment-detail', shipment); }} className={`text-xs px-1.5 py-0.5 rounded truncate text-white ${getStatusColor(shipment.status)} hover:opacity-80`} title={`${shipment.splitNo} - ${shipment.customerName}`}>{shipment.customerName?.substring(0, 6)}
))}\n {dayShipments.length > 3 &&
+{dayShipments.length - 3}건
}\n
\n
\n );\n }\n return cells;\n };\n const selectedDateShipments = selectedCalendarDate ? getShipmentsForDate(selectedCalendarDate) : [];\n return (\n
\n
\n
이달 전체 출고
{monthlyStats.total}건
\n
출고대기
{monthlyStats.waiting}건
\n
배송중
{monthlyStats.shipping}건
\n
배송완료
{monthlyStats.complete}건
\n
\n
\n
\n
{year}년 {month + 1}월
\n
\n
\n
{['일', '월', '화', '수', '목', '금', '토'].map((d, idx) => (
0 && idx < 6 ? 'text-gray-600' : ''}`}>{d}
))}
\n
{renderCalendarCells()}
\n
\n
상태:\n
출고대기
\n
출고준비
\n
상차대기
\n
배송중
\n
배송완료
\n
\n
\n {selectedCalendarDate && (\n
\n
{selectedCalendarDate} 출고 일정 ({selectedDateShipments.length}건)
\n {selectedDateShipments.length > 0 ? (\n
{selectedDateShipments.map((shipment, idx) => {\n const priority = priorityOptions.find(p => p.value === shipment.shipmentPriority) || priorityOptions[2];\n return (
onNavigate('shipment-detail', shipment)} className=\"px-6 py-4 hover:bg-gray-50 cursor-pointer flex items-center justify-between\">\n
{priority.label}{shipment.splitNo}
{shipment.customerName} - {shipment.siteName}
\n
{shipment.dispatchType}
\n
);\n })}
\n ) : (
)}\n
\n )}\n
\n );\n};\n\n// 출하 목록 (수주 연동)\nconst ShipmentList = ({ shipments, orders = [], onNavigate, onDeleteShipments }) => {\n const [search, setSearch] = useState('');\n const [activeTab, setActiveTab] = useState('all');\n const [dispatchFilter, setDispatchFilter] = useState('all');\n const [priorityFilter, setPriorityFilter] = useState('all');\n const [selectedIds, setSelectedIds] = useState([]);\n const [showDeleteModal, setShowDeleteModal] = useState(false);\n const [showCancelModal, setShowCancelModal] = useState(false);\n const [cancelReason, setCancelReason] = useState('');\n const [selectedForCancel, setSelectedForCancel] = useState(null);\n const [showPriorityModal, setShowPriorityModal] = useState(false);\n const [selectedForPriority, setSelectedForPriority] = useState(null);\n const [newPriority, setNewPriority] = useState(3);\n\n // 캘린더 관련 상태\n const [calendarDate, setCalendarDate] = useState(new Date());\n const [selectedCalendarDate, setSelectedCalendarDate] = useState(null);\n\n // 수주 정보 가져오기 헬퍼\n const getOrderInfo = (lotNo) => orders.find(o => o.lotNo === lotNo || o.orderNo === lotNo);\n\n const tabs = [\n { id: 'all', label: '전체', count: shipments.length },\n { id: 'waiting', label: '출고예정', count: shipments.filter(s => s.status === '출고예정' || s.status === '출고대기' || s.status === '출고지시').length },\n { id: 'ready', label: '출하대기', count: shipments.filter(s => s.status === '출하대기' || s.status === '출고준비' || s.status === '출하준비').length },\n { id: 'shipping', label: '배송중', count: shipments.filter(s => s.status === '배송중').length },\n { id: 'complete', label: '배송완료', count: shipments.filter(s => s.status === '배송완료').length },\n { id: 'calendar', label: '출하일정', icon: Calendar },\n ];\n\n const statusFilter = {\n all: () => true,\n waiting: (s) => s.status === '출고대기' || s.status === '출고지시',\n ready: (s) => s.status === '출고준비' || s.status === '출하준비',\n loading: (s) => s.status === '상차대기' || s.status === '출하대기',\n shipping: (s) => s.status === '배송중',\n complete: (s) => s.status === '배송완료',\n };\n\n // 우선순위별 필터\n const applyPriorityFilter = (list) => {\n if (priorityFilter === 'all') return list;\n return list.filter(s => s.shipmentPriority === parseInt(priorityFilter));\n };\n\n const filtered = applyPriorityFilter(\n shipments\n .filter(statusFilter[activeTab] || (() => true))\n .filter(s => dispatchFilter === 'all' || s.dispatchType === dispatchFilter)\n .filter(s =>\n s.splitNo?.toLowerCase().includes(search.toLowerCase()) ||\n s.customerName?.includes(search) ||\n s.siteName?.includes(search) ||\n s.lotNo?.toLowerCase().includes(search.toLowerCase())\n )\n .sort((a, b) => b.id - a.id) // ID 기준 내림차순 (최신 등록 최상단)\n );\n\n // 전체 선택/해제\n const handleSelectAll = (e) => {\n if (e.target.checked) {\n setSelectedIds(filtered.map(s => s.id));\n } else {\n setSelectedIds([]);\n }\n };\n\n // 개별 선택\n const handleSelectOne = (id, e) => {\n e.stopPropagation();\n setSelectedIds(prev =>\n prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]\n );\n };\n\n // 삭제 확인\n const handleDeleteConfirm = () => {\n if (onDeleteShipments && selectedIds.length > 0) {\n onDeleteShipments(selectedIds);\n }\n setSelectedIds([]);\n setShowDeleteModal(false);\n };\n\n // 통계\n const todayShipments = shipments.filter(s => s.shipmentDate === new Date().toISOString().split('T')[0]).length;\n const unpaidShipments = shipments.filter(s =>\n (!s.paymentConfirmed || !s.taxInvoiceIssued) && s.status !== '배송완료'\n ).length;\n const urgentShipments = shipments.filter(s => s.shipmentPriority === 1 && s.status !== '배송완료').length;\n\n // 출고 취소 핸들러\n const handleCancelShipment = () => {\n if (selectedForCancel && cancelReason) {\n // 실제로는 onUpdate를 통해 출고 취소 처리\n alert(`출고가 취소되었습니다.\\n\\n출고번호: ${selectedForCancel.releaseNo || '-'}\\n취소사유: ${cancelReason}`);\n setShowCancelModal(false);\n setSelectedForCancel(null);\n setCancelReason('');\n }\n };\n\n // 우선순위 변경 핸들러\n const handlePriorityChange = () => {\n if (selectedForPriority) {\n // 실제로는 onUpdate를 통해 우선순위 변경 처리\n alert(`우선순위가 변경되었습니다.\\n\\n출고번호: ${selectedForPriority.releaseNo || '-'}\\n변경 우선순위: ${priorityOptions.find(p => p.value === newPriority)?.label}`);\n setShowPriorityModal(false);\n setSelectedForPriority(null);\n }\n };\n\n return (\n
\n
onNavigate('shipment-create')}>\n 출하 등록\n \n }\n />\n\n {/* 리포트 카드 - 핵심 지표 4개 */}\n \n
\n
\n
\n
오늘 출하
\n
{todayShipments}건
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
출고 대기
\n
{shipments.filter(s => s.status === '출고대기' || s.status === '출고지시' || s.status === '출고예정').length}건
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
배송중
\n
{shipments.filter(s => s.status === '배송중').length}건
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
긴급 출하
\n
{urgentShipments}건
\n
\n
\n
\n
\n
\n\n {/* 출고불가 경고 */}\n {unpaidShipments > 0 && (\n \n
\n
출고불가 {unpaidShipments}건 - 입금확인 및 세금계산서 발행 완료 후 출고 진행이 가능합니다.\n
\n )}\n\n {/* 검색 */}\n \n\n \n\n {/* 출고일정 캘린더 뷰 */}\n {activeTab === 'calendar' ? (\n \n ) : (\n \n {/* 선택 삭제 버튼 (2개 이상 선택 시) */}\n {selectedIds.length >= 2 && (\n
\n \n {selectedIds.length}개 항목 선택됨\n \n \n
\n )}\n
\n {filtered.length === 0 && (\n
\n )}\n
\n )}\n\n {/* 삭제 확인 모달 */}\n {showDeleteModal && (\n \n
\n
\n
\n
\n
\n
삭제 확인
\n
선택한 항목을 삭제하시겠습니까?
\n
\n
\n
\n
\n {selectedIds.length}개의 출고가 삭제됩니다.\n 삭제된 데이터는 복구할 수 없습니다.\n
\n
\n
\n \n \n
\n
\n
\n
\n )}\n\n {/* 우선순위 변경 모달 */}\n {showPriorityModal && selectedForPriority && (\n \n
\n
\n
\n
\n
\n
출고 우선순위 변경
\n
{selectedForPriority.splitNo}
\n
\n
\n
\n
출고 우선순위를 선택하세요:
\n
\n {priorityOptions.map(p => (\n \n ))}\n
\n
\n
\n \n \n
\n
\n
\n
\n )}\n\n {/* 출고 취소 모달 */}\n {showCancelModal && selectedForCancel && (\n \n
\n
\n
\n
\n \n
\n
\n
출고 취소
\n
{selectedForCancel.splitNo}
\n
\n
\n
\n
\n
\n {selectedForCancel.customerName} - {selectedForCancel.siteName}\n
\n
\n
\n \n \n
\n {cancelReason === '기타' && (\n
\n \n \n
\n )}\n
\n
\n \n \n
\n
\n
\n
\n )}\n \n );\n};\n\n// 출고 등록 (수주 연동)\nconst ShipmentCreate = ({ orders, onNavigate, onBack, onSave }) => {\n const [formData, setFormData] = useState({\n orderNo: '',\n splitNo: '',\n shipmentDate: new Date().toISOString().split('T')[0],\n shipmentPriority: 3, // 기본 우선순위: 보통\n dispatchType: '상차',\n logisticsCompany: '',\n vehicleType: '',\n vehicleNo: '',\n driverName: '',\n driverPhone: '',\n scheduledArrival: '',\n shippingCost: 0,\n loadingWorker: '', // 상차담당자\n note: '',\n });\n\n const [selectedOrder, setSelectedOrder] = useState(null);\n const [selectedSplit, setSelectedSplit] = useState(null);\n\n // 유효성 검사 규칙\n const validationRules = {\n orderNo: { required: true, label: '로트번호', message: '로트(수주)를 선택해주세요.' },\n shipmentDate: { required: true, label: '출고예정일', message: '출고예정일을 선택해주세요.' },\n };\n const { errors, touched, hasError, getFieldError, validateForm, handleBlur, clearFieldError } = useFormValidation(formData, validationRules);\n\n // 수주 선택 시\n const handleOrderSelect = (orderNo) => {\n const order = orders.find(o => o.orderNo === orderNo);\n setSelectedOrder(order);\n setSelectedSplit(null);\n setFormData(prev => ({\n ...prev,\n orderNo,\n splitNo: '',\n }));\n };\n\n // 분할 선택 시\n const handleSplitSelect = (splitNo) => {\n const split = selectedOrder?.splits?.find(s => s.splitNo === splitNo);\n setSelectedSplit(split);\n setFormData(prev => ({\n ...prev,\n splitNo,\n }));\n };\n\n const handleChange = (field, value) => {\n setFormData(prev => ({ ...prev, [field]: value }));\n };\n\n const handleSubmit = () => {\n // 유효성 검사\n if (!validateForm()) {\n return;\n }\n\n // 출하 조건 체크\n if (!selectedOrder) {\n alert('수주를 선택해주세요.');\n return;\n }\n\n // 생산완료 체크\n if (selectedOrder.status !== '생산완료' && selectedOrder.status !== '출하대기') {\n alert(`⚠️ 출하 불가\\n\\n현재 상태: ${selectedOrder.status}\\n\\n생산이 완료된 수주만 출하 등록이 가능합니다.`);\n return;\n }\n\n // B/C등급 입금확인 체크\n if ((selectedOrder.creditGrade === 'B' || selectedOrder.creditGrade === 'C') &&\n selectedOrder.paymentStatus !== '전액입금') {\n alert(`⚠️ 출하 불가 (입금 미확인)\\n\\n거래처 등급: ${selectedOrder.creditGrade}등급\\n입금 상태: ${selectedOrder.paymentStatus || '미입금'}\\n\\n${selectedOrder.creditGrade}등급 거래처는 입금 확인 후 출하가 가능합니다.\\n\\n[수주관리 > 해당 수주 > 회계현황]에서\\n입금 확인을 먼저 진행해주세요.`);\n return;\n }\n\n // C등급 경리승인 체크\n if (selectedOrder.creditGrade === 'C' && selectedOrder.accountingStatus !== '회계확인완료') {\n alert(`⚠️ 출하 불가 (경리승인 미완료)\\n\\n거래처 등급: C등급\\n회계 상태: ${selectedOrder.accountingStatus || '미확인'}\\n\\nC등급 거래처는 경리 승인 후 출하가 가능합니다.\\n\\n[수주관리 > 해당 수주 > 회계현황]에서\\n경리 승인을 먼저 진행해주세요.`);\n return;\n }\n\n // 품목 데이터 구성\n let items = [];\n if (selectedSplit && selectedOrder) {\n items = selectedOrder.items\n .filter(item => selectedSplit.itemIds.includes(item.id))\n .map(item => ({\n ...item,\n lotNo: null,\n arrivedAtLoadingArea: false,\n loadingChecked: false,\n accessoryLotNo: '',\n }));\n } else if (selectedOrder) {\n items = selectedOrder.items.map(item => ({\n ...item,\n lotNo: null,\n arrivedAtLoadingArea: false,\n loadingChecked: false,\n accessoryLotNo: '',\n }));\n }\n\n // splitNo (수주분할번호)\n const splitNo = selectedSplit\n ? selectedSplit.splitNo\n : `${formData.orderNo}-01`; // 분할 없으면 -01 자동 부여\n\n // releaseNo (출고번호) - 채번규칙: 날짜-P-순번 (예: 251217-P-01)\n const today = new Date();\n const yy = String(today.getFullYear()).slice(-2);\n const mm = String(today.getMonth() + 1).padStart(2, '0');\n const dd = String(today.getDate()).padStart(2, '0');\n const dateCode = `${yy}${mm}${dd}`;\n const seq = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0'); // 실제로는 DB에서 순번 조회\n const releaseNo = `${dateCode}-P-${seq}`;\n\n const newShipment = {\n ...formData,\n id: Date.now(),\n releaseNo, // 출고번호 (채번규칙 적용)\n splitNo, // 수주분할번호\n lotNo: selectedOrder?.lotNo || selectedOrder?.orderNo,\n customerName: selectedOrder?.customerName,\n siteName: selectedOrder?.siteName,\n creditGrade: selectedOrder?.creditGrade,\n deliveryAddress: selectedOrder?.deliveryAddress,\n receiverName: selectedOrder?.receiverName,\n receiverPhone: selectedOrder?.receiverPhone,\n personInCharge: formData.loadingWorker, // 상차담당자 → 담당자 컬럼 매핑\n arrivalTime: formData.scheduledArrival, // 입차시간 → 입차/수령시간 컬럼 매핑\n status: '출고대기',\n paymentConfirmed: selectedOrder?.paymentStatus === '전액입금',\n taxInvoiceIssued: selectedOrder?.taxInvoiceIssued || false,\n items,\n changeHistory: [{\n id: 1,\n changedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n changeType: '출고등록',\n description: '출고 등록 완료',\n changedBy: '현재 사용자',\n }],\n loadingCompleted: false,\n cancelReason: null,\n canceledAt: null,\n canceledBy: null,\n createdAt: new Date().toISOString().split('T')[0],\n createdBy: '현재 사용자',\n };\n\n onSave?.(newShipment);\n alert(`✅ 출고가 등록되었습니다.\\n\\n출고번호: ${releaseNo}\\n발주처: ${selectedOrder.customerName}\\n현장: ${selectedOrder.siteName}`);\n onBack();\n };\n\n return (\n
\n
\n
\n
\n \n \n
\n
\n\n
\n {/* 수주 선택 */}\n
\n \n
\n \n \n\n {selectedOrder?.splits?.length > 0 && (\n
\n \n \n )}\n\n {selectedOrder && (\n
\n
\n 발주처\n {selectedOrder.customerName}\n
\n
\n 현장명\n {selectedOrder.siteName}\n
\n
\n 배송주소\n {selectedOrder.deliveryAddress}\n
\n
\n 결제상태\n \n
\n
\n )}\n
\n \n\n {/* 출고 정보 */}\n
\n \n
\n \n {\n handleChange('shipmentDate', e.target.value);\n clearFieldError('shipmentDate');\n }}\n onBlur={() => handleBlur('shipmentDate')}\n className={getInputClassName(hasError('shipmentDate'))}\n />\n \n\n \n \n \n
\n\n
\n \n \n\n {/* 직접배차 (발주처 차량) 옵션 */}\n {formData.dispatchType === '직접배차' && (\n
\n
직접배차 (발주처 차량)
\n
\n handleChange('scheduledArrival', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n \n
\n 발주처와 휴대폰/카톡으로 입차시간 조율 후 입력하세요.\n
\n
\n )}\n\n {/* 상차 (물류업체) 옵션 */}\n {formData.dispatchType === '상차' && (\n
\n
상차 (물류업체)
\n
\n \n \n \n \n \n \n
\n
\n handleChange('scheduledArrival', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n \n
\n 물류업체와 입차시간 확정 후 입력하세요.\n
\n
\n )}\n\n {/* 택배/화물/직접배송 옵션 */}\n {(formData.dispatchType === '택배' || formData.dispatchType === '화물' || formData.dispatchType === '직접배송') && (\n
\n )}\n\n
\n handleChange('loadingWorker', e.target.value)}\n placeholder=\"상차 작업 담당자명\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n \n\n
\n \n
\n \n\n {/* 출하 품목 */}\n {selectedOrder && (\n
\n \n \n \n | No | \n 품목코드 | \n 품목명 | \n 층 | \n 부호 | \n 규격 | \n 수량 | \n
\n \n \n {(selectedSplit\n ? selectedOrder.items.filter(item => selectedSplit.itemIds.includes(item.id))\n : selectedOrder.items\n ).map((item, idx) => (\n \n | {idx + 1} | \n {item.productCode} | \n {item.productName} | \n {item.floor} | \n {item.location} | \n {item.spec} | \n {item.qty} | \n
\n ))}\n \n
\n \n )}\n
\n
\n );\n};\n\n// 출하 상세\nconst ShipmentDetail = ({ shipment, shipments, orders = [], onNavigate, onBack, onUpdate }) => {\n const [showPriorityModal, setShowPriorityModal] = useState(false);\n const [showCancelModal, setShowCancelModal] = useState(false);\n const [cancelReason, setCancelReason] = useState('');\n const [cancelReasonDetail, setCancelReasonDetail] = useState('');\n const [showShipmentDocModal, setShowShipmentDocModal] = useState(false);\n const [showDeliveryConfirmationModal, setShowDeliveryConfirmationModal] = useState(false);\n const [showTransactionStatementModal, setShowTransactionStatementModal] = useState(false);\n\n // 연결된 수주 정보 조회\n const linkedOrder = orders.find(o => o.lotNo === shipment.lotNo);\n\n // 동일 로트의 다른 출하 조회\n const relatedShipments = shipments?.filter(s => s.lotNo === shipment.lotNo) || [];\n\n // 상차 체크 핸들러\n const handleLoadingCheck = (itemId, field, value) => {\n const updatedItems = shipment.items.map(item =>\n item.id === itemId ? { ...item, [field]: value } : item\n );\n onUpdate?.({ ...shipment, items: updatedItems });\n };\n\n // 상차 완료 처리\n const handleLoadingComplete = () => {\n const allChecked = shipment.items.every(item => item.loadingChecked);\n if (!allChecked) {\n alert('모든 품목의 상차를 확인해주세요.');\n return;\n }\n onUpdate?.({\n ...shipment,\n loadingCompleted: true,\n loadingCompletedAt: new Date().toISOString(),\n loadingWorker: '현재 사용자',\n status: '상차완료',\n });\n };\n\n // 상태 변경\n const handleStatusChange = (newStatus) => {\n const history = {\n id: (shipment.changeHistory?.length || 0) + 1,\n changedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n changeType: '상태 변경',\n beforeValue: shipment.status,\n afterValue: newStatus,\n reason: '',\n changedBy: '현재 사용자',\n };\n onUpdate?.({\n ...shipment,\n status: newStatus,\n changeHistory: [...(shipment.changeHistory || []), history],\n ...(newStatus === '배송완료' ? { completedAt: new Date().toISOString() } : {}),\n });\n };\n\n // 거래처 등급에 따른 출하가능 여부 판단\n // A등급: 무조건 출하가능, B등급: 입금확인 필요, C등급: 입금+세금계산서 필요\n const creditGrade = shipment.creditGrade || 'B';\n const canShip = creditGrade === 'A'\n ? true\n : creditGrade === 'B'\n ? shipment.paymentConfirmed\n : (shipment.paymentConfirmed && shipment.taxInvoiceIssued);\n\n // 우선순위 정보\n const currentPriority = priorityOptions.find(p => p.value === shipment.shipmentPriority) || priorityOptions[2];\n\n // 우선순위 변경 핸들러\n const handlePriorityChange = (newPriority) => {\n const history = {\n id: (shipment.changeHistory?.length || 0) + 1,\n changedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n changeType: '우선순위 변경',\n beforeValue: currentPriority.label,\n afterValue: priorityOptions.find(p => p.value === newPriority)?.label,\n reason: '',\n changedBy: '현재 사용자',\n };\n onUpdate?.({\n ...shipment,\n shipmentPriority: newPriority,\n changeHistory: [...(shipment.changeHistory || []), history],\n });\n setShowPriorityModal(false);\n };\n\n // 출고 취소 핸들러\n const handleCancelShipment = () => {\n if (!cancelReason) {\n alert('취소 사유를 선택해주세요.');\n return;\n }\n const history = {\n id: (shipment.changeHistory?.length || 0) + 1,\n changedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n changeType: '출고 취소',\n beforeValue: shipment.status,\n afterValue: '출고취소',\n reason: cancelReason + (cancelReasonDetail ? ` - ${cancelReasonDetail}` : ''),\n changedBy: '현재 사용자',\n };\n onUpdate?.({\n ...shipment,\n status: '출고취소',\n cancelReason: cancelReason,\n cancelReasonDetail: cancelReasonDetail,\n cancelledAt: new Date().toISOString(),\n changeHistory: [...(shipment.changeHistory || []), history],\n });\n setShowCancelModal(false);\n };\n\n return (\n
\n {/* 상단 헤더 - 타이틀 위, 버튼 아래 */}\n
\n {/* 타이틀 영역 */}\n
\n \n 출하 상세\n
\n {/* 버튼 영역 - 좌측: 문서버튼 / 우측: 액션버튼 */}\n
\n {/* 좌측: 문서 버튼 */}\n
\n
\n
\n
\n
\n {/* 우측: 액션 버튼 */}\n
\n
\n {shipment.status !== '출고취소' && shipment.status !== '배송완료' && (\n
\n )}\n
\n {shipment.status === '출고대기' && (\n
\n )}\n {shipment.status === '출고준비' && (\n
\n )}\n {shipment.status === '상차대기' && shipment.loadingCompleted && canShip && (\n
\n )}\n {shipment.status === '배송중' && (\n
\n )}\n
\n
\n
\n\n {/* 출고불가 경고 */}\n {!canShip && shipment.status !== '배송완료' && shipment.status !== '출고취소' && (\n
\n
\n
\n 출고 불가 -\n {!shipment.paymentConfirmed && ' 입금확인'}\n {!shipment.paymentConfirmed && !shipment.taxInvoiceIssued && ' 및'}\n {!shipment.taxInvoiceIssued && ' 세금계산서 발행'}\n 이 완료되어야 출고 진행이 가능합니다. 회계팀에 확인 요청하세요.\n \n
\n )}\n\n {/* 출고 취소 상태 표시 */}\n {shipment.status === '출고취소' && (\n
\n \n \n 출고 취소됨 -\n 사유: {shipment.cancelReason || '미입력'}\n {shipment.cancelReasonDetail && ` (${shipment.cancelReasonDetail})`}\n \n
\n )}\n\n {/* 기본정보 섹션 */}\n
\n
\n \n \n \n {shipment.lotNo}\n } />\n } />\n \n {currentPriority.label}\n \n } />\n \n {shipment.dispatchType}\n \n } />\n ✓ 확인됨\n : - 미확인\n } />\n ✓ 발행됨\n : - 미발행\n } />\n \n {creditGrade}등급\n \n } />\n ✓ 가능\n : ✗ 불가 (조건 미충족)\n } />\n \n ✓ 완료 {shipment.loadingCompletedAt && `(${new Date(shipment.loadingCompletedAt).toLocaleDateString()})`}\n : - 미완료\n } />\n \n
\n \n\n
\n \n \n\n
\n \n \n \n | No | \n 품목코드 | \n 품목명 | \n 층/부호 | \n 규격 | \n 수량 | \n LOT번호 | \n
\n \n \n {shipment.items.map((item, idx) => (\n \n | {idx + 1} | \n {item.productCode} | \n {item.productName} | \n {item.floor}/{item.location} | \n {item.spec} | \n {item.qty} | \n {item.lotNo || '-'} | \n
\n ))}\n \n
\n \n
\n\n {/* 배차정보 섹션 */}\n
\n
\n \n \n {shipment.dispatchType}\n \n } />\n \n \n \n
\n \n\n
\n \n \n \n \n {shipment.trackingNo && (\n \n )}\n
\n \n\n {shipment.note && (\n
\n {shipment.note}
\n \n )}\n
\n\n {/* 우선순위 변경 모달 */}\n {showPriorityModal && (\n
\n
\n
\n
우선순위 변경
\n \n \n
\n
출고 우선순위를 선택하세요.
\n {priorityOptions.map(p => (\n
\n ))}\n
\n
\n
\n )}\n\n {/* 출고 취소 모달 */}\n {showCancelModal && (\n
\n
\n
\n
출고 취소
\n \n \n
\n
\n
\n 출고를 취소하면 해당 건은 미출고 처리됩니다. 취소 사유를 반드시 입력해주세요.\n
\n\n
\n \n \n
\n\n
\n \n
\n\n
\n \n \n
\n
\n
\n
\n )}\n\n {/* 출고증 출력 모달 - 문서양식관리 양식 연동 */}\n {showShipmentDocModal && (\n
\n
\n
\n
출고증 미리보기
\n
\n
\n
\n
\n
\n
\n {/* 출고증 양식 - 문서양식관리 TS 양식 */}\n
\n {/* 헤더 - KD 로고 + 출고증 + 결재란 */}\n
\n \n \n | \n KD \n 경동기업 \n KYUNGDONG COMPANY \n | \n \n 출 고 증 \n | \n \n 결 \n 재 \n | \n 작성 | \n 검토 | \n 승인 | \n
\n \n | \n {shipment.createdBy || ''} \n {shipment.shipmentDate?.slice(5, 10) || ''} \n | \n | \n | \n
\n \n | \n 출하 관리\n | \n 판매/전진 | \n 출하 | \n 생산관리 | \n
\n \n
\n\n {/* LOT번호 */}\n
\n \n \n | 제품 LOT NO. | \n {shipment.lotNo} | \n
\n \n
\n\n {/* 전화 / 팩스 / 이메일 */}\n
\n 전화 : 031-938-5130 | 팩스 : 02-6911-6315 | 이메일 : kd5130@naver.com\n
\n\n {/* 상품명 / 제품명 / 인정번호 */}\n
\n \n \n | 상 품 명 | \n {linkedOrder?.productName || '국민방화스크린플러스셔터'} | \n 제품명 | \n {shipment.productCode || 'KWE01'} | \n 인정번호 | \n {shipment.certNo || 'FDS-OTS23-0117-4'} | \n
\n \n
\n\n {/* 신청업체 / 신청내용 / 납품정보 - 3단 구조 */}\n
\n \n \n | 신 청 업 체 | \n 신 청 내 용 | \n 납 품 정 보 | \n
\n \n | 발 주 일 | \n {linkedOrder?.orderDate || shipment.orderDate || '-'} | \n 현 장 명 | \n {shipment.siteName} | \n 인수담당자 | \n {shipment.receiverName || '-'} | \n
\n \n | 발 주 처 | \n {shipment.customerName} | \n 납기요청일 | \n {linkedOrder?.dueDate || shipment.dueDate || '-'} | \n 인수자연락처 | \n {shipment.receiverPhone || '-'} | \n
\n \n | 발주 담당자 | \n {linkedOrder?.contactPerson || '-'} | \n 출 고 일 | \n {shipment.shipmentDate} | \n 배 송 방 법 | \n \n {shipment.dispatchType}\n {shipment.paymentMethod || '선불'}\n | \n
\n \n | 담당자 연락처 | \n {linkedOrder?.contactPhone || '-'} | \n 셔터총수량 | \n {shipment.items?.reduce((sum, item) => sum + item.qty, 0) || 0} | \n 개소 | \n | \n
\n \n | 배송지 주소 | \n {shipment.deliveryAddress} | \n
\n \n
\n\n {/* 1. 부자재 - 감기샤프트, 각파이프, 앵글 */}\n
1. 부자재 - 감기샤프트, 각파이프, 앵글
\n
\n \n \n | \n 감기샤프트 \n 4인치 \n \n \n | L : 3,000 | {shipment.items?.length > 0 ? Math.ceil(shipment.items.length * 0.5) : 0} | \n | L : 4,500 | {shipment.items?.length > 0 ? Math.ceil(shipment.items.length * 0.3) : 0} | \n \n \n | \n \n 수량 \n {shipment.items?.reduce((sum, item) => sum + item.qty, 0) || 0} \n | \n \n 감기샤프트 \n 5인치 \n \n \n | L : 6,000 | {shipment.items?.length > 2 ? Math.ceil(shipment.items.length * 0.2) : 0} | \n | L : 7,000 | {shipment.items?.length > 3 ? 2 : 0} | \n \n \n | \n \n 수량 \n | \n \n 각파이프 \n (50*30*1.4T) \n L : 6,000 \n | \n \n 수량 \n {(shipment.items?.reduce((sum, item) => sum + item.qty, 0) || 0) * 5} \n | \n
\n \n | \n ※ 별도 추가사항 - 부자재\n | \n \n 앵글 \n (40*40*3T) \n | \n \n 수량 \n {(shipment.items?.reduce((sum, item) => sum + item.qty, 0) || 0) * 4} \n | \n
\n \n
\n\n {/* 2. 모터 */}\n
2. 모터
\n
\n \n \n | \n 2-1. 모터(220V 단상) \n \n \n \n | 모터 용량 | \n 수량 | \n 입고 LOT NO. | \n \n \n \n | KD-150K | {shipment.items?.length >= 3 ? Math.ceil(shipment.items.length * 0.3) : 0} | | \n | KD-300K | {shipment.items?.length >= 3 ? Math.ceil(shipment.items.length * 0.5) : 0} | | \n \n \n | \n \n 2-2. 모터(380V 삼상) \n \n \n \n | 모터 용량 | \n 수량 | \n 입고 LOT NO. | \n \n \n \n | KD-300K | {shipment.items?.length >= 5 ? Math.ceil(shipment.items.length * 0.2) : 0} | | \n | KD-400K | | | \n \n \n | \n
\n \n | \n 2-3. 브라켓트 \n \n \n | 380*180 (2-4\") | {shipment.items?.length >= 3 ? shipment.items.length : 0} | \n | 380*180 (2-5\") | {shipment.items?.length >= 5 ? Math.ceil(shipment.items.length * 0.4) : 0} | \n \n \n | \n \n 2-4. 연동제어기 \n \n \n | 매립형 | {shipment.items?.reduce((sum, item) => sum + item.qty, 0) || 0} | \n | 노출형 | | \n \n \n | \n
\n \n
\n\n {/* 하단 서명 영역 */}\n
\n
\n \n \n | 출고 담당 | \n | \n
\n \n | 인수 확인 | \n | \n
\n \n
\n
\n
\n
\n
\n
\n )}\n\n {/* 거래명세서 출력 모달 */}\n {showTransactionStatementModal && (\n
\n
\n
\n
거래명세서 미리보기
\n
\n
\n
\n
\n
\n
\n {/* 거래명세서 양식 */}\n
\n {/* 헤더 */}\n
\n
거 래 명 세 서
\n
TRANSACTION STATEMENT
\n
\n\n {/* 공급자/공급받는자 정보 */}\n
\n {/* 공급받는자 */}\n
\n
공급받는자
\n
\n \n \n | 상 호 | \n {shipment.customerName} | \n
\n \n | 현장명 | \n {shipment.siteName} | \n
\n \n | 주 소 | \n {shipment.deliveryAddress} | \n
\n \n
\n
\n\n {/* 공급자 */}\n
\n
공급자
\n
\n \n \n | 상 호 | \n 경동기업 | \n
\n \n | 대표자 | \n 홍길동 | \n
\n \n | 주 소 | \n 경기도 화성시 팔탄면 산업로 123 | \n
\n \n | 연락처 | \n 031-123-4567 | \n
\n \n
\n
\n
\n\n {/* 거래정보 */}\n
\n
\n \n \n | 거래일자 | \n {shipment.shipmentDate} | \n 출고번호 | \n {shipment.releaseNo || '-'} | \n
\n \n
\n
\n\n {/* 품목 테이블 */}\n
\n \n \n | No | \n 품 목 명 | \n 규 격 | \n 수량 | \n 단가 | \n 금액 | \n 비고 | \n
\n \n \n {shipment.items.map((item, idx) => (\n \n | {idx + 1} | \n {item.productName} | \n {item.spec} | \n {item.qty} | \n {(item.unitPrice || 0).toLocaleString()} | \n {((item.unitPrice || 0) * (item.qty || 0)).toLocaleString()} | \n {item.floor}/{item.location} | \n
\n ))}\n {/* 빈 행 추가 */}\n {Array.from({ length: Math.max(0, 8 - shipment.items.length) }).map((_, idx) => (\n \n | | \n | \n | \n | \n | \n | \n | \n
\n ))}\n \n \n \n | 합 계 | \n \n {shipment.items.reduce((sum, item) => sum + ((item.unitPrice || 0) * (item.qty || 0)), 0).toLocaleString()}\n | \n | \n
\n \n
\n\n {/* 하단 안내 */}\n
\n
위와 같이 거래하였음을 확인합니다.
\n
경 동 기 업
\n
\n
\n
\n
\n
\n )}\n\n {/* 납품확인서 출력 모달 - 문서양식관리 DC 형식 */}\n {showDeliveryConfirmationModal && (\n
\n
\n {/* 헤더 */}\n
\n
\n \n
납품확인서
\n \n
\n
\n
\n
\n
\n
\n\n {/* 납품확인서 내용 - A4 세로 */}\n
\n
\n {/* 헤더 - KD 로고 + 타이틀 + 결재란 */}\n
\n {/* 좌측: 로고 */}\n
\n\n {/* 중앙: 타이틀 */}\n
\n\n {/* 우측: 결재란 */}\n
\n \n \n | 결 | \n 작성 | \n 검토 | \n 승인 | \n
\n \n | 재 | \n | \n | \n | \n
\n \n | \n 판매/전진 | \n 출하 | \n 품질 | \n
\n \n
\n
\n\n {/* 전화 / 팩스 / 이메일 */}\n
\n 전화 : 031-938-5130 | 팩스 : 02-6911-6315 | 이메일 : kd5130@naver.com\n
\n\n {/* 납품 정보 테이블 */}\n
\n \n \n | 발 주 정 보 | \n 납 품 정 보 | \n
\n \n | 발 주 일 | \n {linkedOrder?.orderDate || shipment.orderDate || '-'} | \n 납 품 일 | \n {shipment.shipmentDate || '-'} | \n
\n \n | 발 주 처 | \n {linkedOrder?.customerName || shipment.customerName || '-'} | \n 현 장 명 | \n {linkedOrder?.siteName || shipment.siteName || '-'} | \n
\n \n | 담 당 자 | \n {linkedOrder?.contactPerson || '-'} | \n 인수담당자 | \n {shipment.receiverName || '-'} | \n
\n \n | 연 락 처 | \n {linkedOrder?.contactPhone || '-'} | \n 인수자연락처 | \n {shipment.receiverPhone || '-'} | \n
\n \n | 제품 LOT NO. | \n {shipment.lotNo || '-'} | \n 납품지 주소 | \n {shipment.deliveryAddress || '-'} | \n
\n \n
\n\n {/* 납품 품목 테이블 */}\n
납품 품목
\n
\n \n \n | No | \n 품 명 | \n 규 격 | \n 단위 | \n 수량 | \n 비 고 | \n
\n \n \n {shipment.items.map((item, idx) => (\n \n | {idx + 1} | \n {item.productName} | \n {item.spec} | \n SET | \n {item.qty} | \n {item.floor}/{item.location} | \n
\n ))}\n {/* 빈 행 추가 (총 10행) */}\n {Array.from({ length: Math.max(0, 10 - shipment.items.length) }).map((_, idx) => (\n \n | {shipment.items.length + idx + 1} | \n | \n | \n | \n | \n | \n
\n ))}\n \n
\n\n {/* 특기사항 */}\n
특기사항
\n
\n \n \n | \n 위 물품을 상기와 같이 납품합니다.\n | \n
\n \n
\n\n {/* 하단 - 서명 영역 */}\n
\n {/* 납품자 서명 */}\n
\n \n \n | 납 품 자 | \n
\n \n | 회 사 명 | \n 경동기업 | \n
\n \n | 담 당 자 | \n 전진 | \n
\n \n | 서명/날인 | \n | \n
\n \n
\n\n {/* 인수자 서명 */}\n
\n \n \n | 인 수 자 | \n
\n \n | 회 사 명 | \n | \n
\n \n | 담 당 자 | \n | \n
\n \n | 서명/날인 | \n | \n
\n \n
\n
\n\n {/* 하단 안내문 */}\n
\n 상기 물품을 정히 인수하였음을 확인합니다.\n
\n
\n
\n
\n
\n )}\n
\n );\n};\n\n// 출고 수정\nconst ShipmentEdit = ({ shipment, onNavigate, onBack, onSave }) => {\n const [formData, setFormData] = useState({\n shipmentDate: shipment.shipmentDate,\n dispatchType: shipment.dispatchType,\n shipmentPriority: shipment.shipmentPriority || 3,\n loadingWorker: shipment.loadingWorker || '',\n logisticsCompany: shipment.logisticsCompany || '',\n vehicleType: shipment.vehicleType || '',\n vehicleNo: shipment.vehicleNo || '',\n driverName: shipment.driverName || '',\n driverPhone: shipment.driverPhone || '',\n scheduledArrival: shipment.scheduledArrival || '',\n confirmedArrival: shipment.confirmedArrival || '',\n shippingCost: shipment.shippingCost || 0,\n trackingNo: shipment.trackingNo || '',\n note: shipment.note || '',\n changeReason: '',\n });\n\n const handleChange = (field, value) => {\n setFormData(prev => ({ ...prev, [field]: value }));\n };\n\n const handleSubmit = () => {\n // 변경 이력 추가\n const changes = [];\n if (formData.shipmentDate !== shipment.shipmentDate) {\n changes.push({\n changeType: '출고일 변경',\n beforeValue: shipment.shipmentDate,\n afterValue: formData.shipmentDate,\n });\n }\n if (formData.dispatchType !== shipment.dispatchType) {\n changes.push({\n changeType: '배송방식 변경',\n beforeValue: shipment.dispatchType,\n afterValue: formData.dispatchType,\n });\n }\n if (formData.shipmentPriority !== shipment.shipmentPriority) {\n const oldPriority = priorityOptions.find(p => p.value === shipment.shipmentPriority)?.label || '보통';\n const newPriority = priorityOptions.find(p => p.value === formData.shipmentPriority)?.label || '보통';\n changes.push({\n changeType: '우선순위 변경',\n beforeValue: oldPriority,\n afterValue: newPriority,\n });\n }\n if (formData.loadingWorker !== (shipment.loadingWorker || '')) {\n changes.push({\n changeType: '상차담당자 변경',\n beforeValue: shipment.loadingWorker || '미배정',\n afterValue: formData.loadingWorker || '미배정',\n });\n }\n\n const newHistory = changes.map((change, idx) => ({\n id: (shipment.changeHistory?.length || 0) + idx + 1,\n changedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n ...change,\n reason: formData.changeReason,\n changedBy: '현재 사용자',\n }));\n\n const updatedShipment = {\n ...shipment,\n ...formData,\n changeHistory: [...(shipment.changeHistory || []), ...newHistory],\n };\n delete updatedShipment.changeReason;\n\n onSave?.(updatedShipment);\n onBack();\n };\n\n return (\n
\n
\n
\n
\n
\n
\n
출고 수정
\n
{shipment.splitNo}
\n
\n
\n
\n
\n \n \n
\n
\n\n
\n {/* 기본 정보 (읽기 전용) */}\n
\n \n \n\n {/* 출고 정보 수정 */}\n
\n \n
\n \n handleChange('shipmentDate', e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500\"\n />\n \n\n \n \n \n
\n\n
\n \n \n \n\n \n handleChange('loadingWorker', v)}\n placeholder=\"상차담당자명\"\n />\n \n
\n\n {formData.dispatchType === '상차' && (\n <>\n
\n \n \n
\n \n \n >\n )}\n\n {(formData.dispatchType === '택배' || formData.dispatchType === '화물') && (\n
\n handleChange('trackingNo', v)}\n placeholder=\"운송장번호 입력\"\n />\n \n )}\n
\n \n\n {/* 배차 정보 */}\n
\n \n \n\n {/* 변경 사유 */}\n
\n \n \n \n \n \n
\n
\n );\n};\n\n// ============ 품질관리 - 제품검사 ============\n\n// 제품검사 목록\nconst ProductInspectionList = ({\n inspections,\n onNavigate,\n shipments = [], // 출하 정보\n onApprovalRequest, // 결재요청 콜백\n onShipmentCertify, // 출하증명 발행 콜백\n}) => {\n const [search, setSearch] = useState('');\n const [activeTab, setActiveTab] = useState('all');\n const [showApprovalModal, setShowApprovalModal] = useState(false);\n const [selectedForApproval, setSelectedForApproval] = useState(null);\n\n // ★ 결재 처리 핸들러\n const handleApproval = (inspection, approverName) => {\n const today = new Date();\n const updatedApprovalLine = {\n ...inspection.approvalLine,\n roles: (inspection.approvalLine?.roles || []).map(role => {\n if (role.status === 'pending') {\n const pendingRoles = inspection.approvalLine.roles.filter(r => r.status === 'pending');\n if (pendingRoles[0]?.id === role.id) {\n return { ...role, name: approverName, date: today.toISOString().split('T')[0], status: 'approved' };\n }\n }\n return role;\n })\n };\n\n const allApproved = updatedApprovalLine.roles?.every(r => r.status === 'approved');\n\n // 결재 완료 시 후속 처리\n if (allApproved && inspection.result === '합격') {\n // 출하증명서 발행 가능\n onShipmentCertify?.({\n inspectionNo: inspection.inspectionNo,\n customerName: inspection.customerName,\n siteName: inspection.siteName,\n setLotNo: inspection.setLotNo,\n certifiedAt: today.toISOString(),\n });\n }\n\n // 결재 요청 콜백\n onApprovalRequest?.({\n ...inspection,\n approvalStatus: allApproved ? '결재완료' : '결재중',\n approvalLine: updatedApprovalLine,\n processFlow: {\n ...inspection.processFlow,\n currentStep: allApproved ? (inspection.result === '합격' ? '출하증명발행가능' : '재검사필요') : '결재진행중',\n }\n });\n\n setShowApprovalModal(false);\n setSelectedForApproval(null);\n\n if (allApproved) {\n alert(`✅ 결재가 완료되었습니다.\\n\\n검사번호: ${inspection.inspectionNo}\\n${inspection.result === '합격' ? '출하증명서 발행이 가능합니다.' : '재검사가 필요합니다.'}`);\n }\n };\n\n // 현재 결재 단계 확인\n const getCurrentApprovalStep = (approvalLine) => {\n if (!approvalLine?.roles) return null;\n return approvalLine.roles.find(r => r.status === 'pending');\n };\n\n const tabs = [\n { id: 'all', label: '전체', count: inspections.length },\n { id: 'request', label: '검사신청', count: inspections.filter(i => i.status === '검사신청').length },\n { id: 'scheduled', label: '검사예정', count: inspections.filter(i => i.status === '검사예정').length },\n { id: 'progress', label: '검사중', count: inspections.filter(i => i.status === '검사중').length },\n { id: 'approval', label: '결재대기', count: inspections.filter(i => i.approvalStatus === '결재대기').length },\n { id: 'complete', label: '완료', count: inspections.filter(i => i.approvalStatus === '결재완료').length },\n ];\n\n const statusFilter = {\n all: () => true,\n request: (i) => i.status === '검사신청',\n scheduled: (i) => i.status === '검사예정',\n progress: (i) => i.status === '검사중',\n approval: (i) => i.approvalStatus === '결재대기',\n complete: (i) => i.approvalStatus === '결재완료',\n };\n\n const filtered = inspections\n .filter(statusFilter[activeTab])\n .filter(i =>\n i.inspectionNo.toLowerCase().includes(search.toLowerCase()) ||\n i.customerName.includes(search) ||\n i.siteName.includes(search)\n );\n\n return (\n
\n
onNavigate('product-inspection-create')}>\n 검사 등록\n \n }\n />\n\n {/* 대시보드 카드 */}\n \n i.status === '검사신청').length}건`} color=\"yellow\" />\n i.status === '검사예정').length}건`} color=\"blue\" />\n i.approvalStatus === '결재대기').length}건`} color=\"purple\" />\n i.approvalStatus === '결재완료').length}건`} color=\"green\" />\n
\n\n {/* 검색 */}\n \n\n {/* 탭 필터 */}\n \n\n {/* 테이블 */}\n \n
\n \n \n | 번호 | \n 검사번호 | \n 신청일 | \n 상태 | \n 발주처 | \n 현장명 | \n 세트LOT | \n 검사항목 | \n 검사예정일 | \n 결재 | \n 작업 | \n
\n \n \n {filtered.map((inspection, idx) => (\n onNavigate('product-inspection-detail', inspection)}\n >\n | {filtered.length - idx} | \n {inspection.inspectionNo} | \n {inspection.requestDate} | \n | \n {inspection.customerName} | \n {inspection.siteName} | \n {inspection.setLotNo || '-'} | \n \n {inspection.completedItems}\n /{inspection.totalItems}\n | \n {inspection.scheduledDate || '-'} | \n \n \n {inspection.approvalStatus}\n \n | \n e.stopPropagation()}>\n \n {inspection.approvalStatus === '결재대기' && (\n \n )}\n {inspection.approvalStatus === '결재완료' && (\n \n )}\n \n \n | \n
\n ))}\n \n
\n
\n\n {/* ★ 결재 모달 (APR-3LINE) */}\n {showApprovalModal && selectedForApproval && (\n \n
\n
\n
제품검사 결재
\n \n \n
\n {/* 검사 정보 */}\n
\n
\n
검사번호: {selectedForApproval.inspectionNo}
\n
발주처: {selectedForApproval.customerName}
\n
현장명: {selectedForApproval.siteName}
\n
세트LOT: {selectedForApproval.setLotNo || '-'}
\n
\n 검사진행:\n {selectedForApproval.completedItems}/{selectedForApproval.totalItems}\n
\n
\n 검사상태:\n \n {selectedForApproval.status}\n \n
\n
\n
\n\n {/* 결재라인 표시 (APR-3LINE: 작성 → 검토 → 승인) */}\n
\n
결재라인 (3단계)
\n
\n {(selectedForApproval.approvalLine?.roles || [\n { id: 'writer', label: '작성', name: '', status: 'pending' },\n { id: 'reviewer', label: '검토', name: '', status: 'pending' },\n { id: 'approver', label: '승인', name: '', status: 'pending' },\n ]).map((role, idx, arr) => (\n
\n r.status === 'pending')\n ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-gray-50'\n }`}>\n
{role.label}
\n
{role.name || '-'}
\n
\n {role.status === 'approved' ? role.date : role.status === 'pending' ? '대기' : ''}\n
\n {role.status === 'approved' && (\n
\n )}\n
\n {idx < arr.length - 1 && (\n \n )}\n \n ))}\n
\n
\n\n {/* 현재 결재자 입력 */}\n
\n \n \n
\n\n {/* 프로세스 플로우 정보 */}\n
\n
\n 검사유형:\n \n 출하 후 고객 요청 검사\n \n
\n
\n 결재완료 시: 합격→출하증명서 발행 가능 / 불합격→재검사 또는 불량 처리\n
\n
\n
\n
\n \n \n
\n
\n
\n )}\n \n );\n};\n\n// 제품검사 상세\nconst ProductInspectionDetail = ({ inspection, onNavigate, onBack, onUpdate }) => {\n const [inspectionData, setInspectionData] = useState(inspection);\n\n // 제품검사성적서 검사항목 상태\n const [inspectionSheet, setInspectionSheet] = useState({\n productLotNo: inspection.setLotNo || '',\n productName: inspection.items?.[0]?.productName || '스크린 셔터',\n specification: inspection.items?.[0]?.orderSpec || '',\n inspectionDate: new Date().toISOString().split('T')[0],\n inspector: inspection.inspector || '',\n // 검사항목 (제품검사성적서 양식 기반)\n categories: [\n {\n id: 1,\n name: '겉모양',\n items: [\n { id: 1, name: '가공상태', standard: '사용상 해로운 결함이 없을 것', method: '육안검사', cycle: '전수검사', judgement: null },\n { id: 2, name: '재봉상태', standard: '내화실에 의해 견고하게 접합되어야 함', method: '육안검사', cycle: '전수검사', judgement: null },\n { id: 3, name: '조립상태', standard: '앤드락이 견고하게 조립되어야 함', method: '육안검사', cycle: '전수검사', judgement: null },\n { id: 4, name: '연기차단재', standard: '연기차단재 설치여부(케이스 W80, 가이드레일 W50)', method: '육안검사', cycle: '전수검사', judgement: null },\n { id: 5, name: '하단마감재', standard: '내부 무게평철 설치 유무', method: '육안검사', cycle: '전수검사', judgement: null },\n ]\n },\n {\n id: 2,\n name: '모터',\n items: [\n { id: 6, name: '모터', standard: '인정제품과 동일사양', method: '육안검사', cycle: '전수검사', judgement: null },\n ]\n },\n {\n id: 3,\n name: '재질',\n items: [\n { id: 7, name: '재질', standard: 'WY-SC780 인쇄상태 확인', method: '육안검사', cycle: '전수검사', judgement: null },\n ]\n },\n {\n id: 4,\n name: '치수(오픈사이즈)',\n items: [\n { id: 8, name: '길이', standard: '발주치수 ± 30mm', method: '체크검사', cycle: '전수검사', orderValue: '', measuredValue: '', judgement: null },\n { id: 9, name: '높이', standard: '발주치수 ± 30mm', method: '체크검사', cycle: '전수검사', orderValue: '', measuredValue: '', judgement: null },\n { id: 10, name: '가이드레일 홈간격', standard: '10 ± 5mm (ⓐ 높이 100 이내)', method: '체크검사', cycle: '전수검사', orderValue: '10', measuredValue: '', judgement: null },\n { id: 11, name: '하단마감재간격 (③+④)', standard: '가이드레일과 하단마감재 틈새 25mm 이내', method: '체크검사', cycle: '전수검사', orderValue: '25', measuredValue: '', judgement: null },\n ]\n },\n {\n id: 5,\n name: '작동테스트',\n items: [\n { id: 12, name: '개폐성능', standard: '작동 유무 확인(일부 및 완전폐쇄)', method: '실동작시험', cycle: '전수검사', judgement: null },\n ]\n },\n {\n id: 6,\n name: '내화시험',\n isExternalTest: true,\n items: [\n { id: 13, name: '비차열', standard: '6mm 균열게이지 관통 후 150mm 이동 유무', method: '공인시험기관', cycle: '1회/5년', judgement: null },\n { id: 14, name: '차열', standard: '25mm 균열게이지 관통 유무', method: '공인시험기관', cycle: '1회/5년', judgement: null },\n { id: 15, name: '열성', standard: '10초 이상 지속되는 화염발생 유무', method: '공인시험기관', cycle: '1회/5년', judgement: null },\n ]\n },\n {\n id: 7,\n name: '차연시험',\n isExternalTest: true,\n items: [\n { id: 16, name: '공기누설량', standard: '25Pa 일때 공기누설량 0.9㎥/min·㎡ 이하', method: '공인시험기관', cycle: '1회/5년', judgement: null },\n ]\n },\n {\n id: 8,\n name: '개폐시험',\n isExternalTest: true,\n items: [\n { id: 17, name: '개폐의 원활한 작동', standard: '개폐의 원활한 작동', method: '공인시험기관', cycle: '1회/5년', judgement: null },\n { id: 18, name: '평균속도(전도개폐)', standard: '2.5 ~ 6.5m/min', method: '공인시험기관', cycle: '1회/5년', judgement: null },\n { id: 19, name: '평균속도(자중강하)', standard: '3 ~ 7m/min', method: '공인시험기관', cycle: '1회/5년', judgement: null },\n { id: 20, name: '상하부 끝부분 자동정지', standard: '개폐 시 상부 및 하부 끝부분에서 자동정지', method: '공인시험기관', cycle: '1회/5년', judgement: null },\n { id: 21, name: '임의위치 정지', standard: '강하 중 임의의 위치에서 정지', method: '공인시험기관', cycle: '1회/5년', judgement: null },\n ]\n },\n {\n id: 9,\n name: '내충격시험',\n isExternalTest: true,\n items: [\n { id: 22, name: '내충격시험', standard: '방화상 유해한 파괴, 박리 탈락 유무', method: '공인시험기관', cycle: '1회/5년', judgement: null },\n ]\n },\n ],\n overallJudgement: null,\n specialNotes: '',\n });\n\n // 검사항목 판정 업데이트\n const handleInspectionSheetUpdate = (categoryId, itemId, field, value) => {\n setInspectionSheet(prev => ({\n ...prev,\n categories: prev.categories.map(cat =>\n cat.id === categoryId\n ? {\n ...cat,\n items: cat.items.map(item =>\n item.id === itemId ? { ...item, [field]: value } : item\n )\n }\n : cat\n )\n }));\n };\n\n // 종합판정 계산\n const calculateOverallJudgement = () => {\n const allItems = inspectionSheet.categories.flatMap(cat => cat.items);\n const judgedItems = allItems.filter(item => item.judgement !== null);\n const hasFailure = allItems.some(item => item.judgement === '부적합');\n\n if (judgedItems.length === allItems.length) {\n return hasFailure ? '부적합' : '적합';\n }\n return null;\n };\n\n // 검사 결과 입력\n const handleInspectionResult = (itemId, field, value) => {\n const updatedItems = inspectionData.items.map(item => {\n if (item.id === itemId) {\n const updated = { ...item, [field]: value };\n // 규격 일치 여부 자동 판정\n if (field === 'installedSpec' && updated.orderSpec) {\n updated.specMatch = value === updated.orderSpec;\n }\n // 적합/부적합에 따라 합격/불합격 자동 판정\n if (field === 'result') {\n updated.inspectedAt = new Date().toISOString().split('T')[0];\n }\n return updated;\n }\n return item;\n });\n\n const completedItems = updatedItems.filter(i => i.result !== null).length;\n const passedItems = updatedItems.filter(i => i.result === '합격').length;\n const failedItems = updatedItems.filter(i => i.result === '불합격').length;\n\n setInspectionData({\n ...inspectionData,\n items: updatedItems,\n completedItems,\n passedItems,\n failedItems,\n });\n };\n\n return (\n
\n {/* 상단 헤더 - 타이틀 위, 버튼 아래 */}\n
\n {/* 타이틀 영역 */}\n
\n
\n \n 제품검사 상세\n
\n
\n {inspection.inspectionNo}\n \n \n {inspection.approvalStatus}\n \n
\n
\n {/* 버튼 영역 - 좌측: 문서버튼 / 우측: 액션버튼 */}\n
\n {/* 좌측: 문서 버튼 */}\n
\n {/* 우측: 액션 버튼 */}\n
\n \n {inspection.status === '검사신청' && (\n \n )}\n {inspectionData.completedItems === inspectionData.totalItems && inspection.approvalStatus === '대기' && (\n \n )}\n
\n
\n
\n\n {/* 기본정보 섹션 */}\n
\n
\n \n \n \n \n } />\n \n \n
\n \n\n
\n \n \n \n {inspection.lotNo}\n } />\n \n
\n \n\n
\n \n
{inspection.setLotNo}\n ) : (\n \n )\n } />\n {inspection.lotNos.length > 0 && (\n \n
포함된 LOT
\n
\n {inspection.lotNos.map((lot, idx) => (\n {lot}\n ))}\n
\n
\n )}\n \n \n\n
\n \n \n \n
\n {!inspection.scheduledDate && (\n \n
\n 검사 일정이 확정되지 않았습니다. 발주처와 일정 조율 후 확정해주세요.\n
\n )}\n \n\n
\n \n
\n
총 검사항목
\n
{inspectionData.totalItems}
\n
\n
\n
검사완료
\n
{inspectionData.completedItems}
\n
\n
\n
합격
\n
{inspectionData.passedItems}
\n
\n
\n
불합격
\n
{inspectionData.failedItems}
\n
\n
\n \n
\n 검사 진행률\n \n {inspectionData.totalItems > 0 ? Math.round((inspectionData.completedItems / inspectionData.totalItems) * 100) : 0}%\n \n
\n
\n
0 ? (inspectionData.completedItems / inspectionData.totalItems) * 100 : 0}%` }}\n />\n
\n
\n \n
\n\n {/* 제품검사성적서 섹션 */}\n \n {/* 문서 헤더 */}\n
\n
\n
\n
KD 경동기업
\n
KYUNGDONG COMPANY
\n
\n
\n
제품검사성적서
\n \n
\n
\n \n \n | 작성 | \n 승인 | \n
\n \n \n \n | \n | \n
\n \n
\n
\n
\n\n {/* 기본 정보 */}\n
\n
\n 상품명\n {inspectionSheet.productName}\n
\n
\n 제품 LOT NO\n {inspectionSheet.productLotNo || '-'}\n
\n
\n 로트크기\n {inspectionData.totalItems} EA\n
\n
\n 검사일자\n {inspectionSheet.inspectionDate}\n
\n
\n 제품명\n {inspectionSheet.productName}\n
\n
\n 발주처\n {inspection.customerName}\n
\n
\n 현장명\n {inspection.siteName}\n
\n
\n 검사자\n setInspectionSheet(prev => ({ ...prev, inspector: e.target.value }))}\n className=\"w-full border-b focus:outline-none\"\n placeholder=\"검사자 입력\"\n />\n
\n
\n\n {/* 검사항목 테이블 */}\n
\n
\n \n \n | No | \n 검사항목 | \n 검사기준 | \n 검사방법 | \n 검사주기 | \n 판정 | \n
\n \n \n {inspectionSheet.categories.map((category, catIdx) => (\n <>\n {category.items.map((item, itemIdx) => (\n \n {itemIdx === 0 && (\n | \n {catIdx + 1}\n | \n )}\n \n {itemIdx === 0 && {category.name}}\n {category.items.length > 1 && {item.name} }\n {category.items.length === 1 && {item.name} }\n | \n \n {item.standard}\n {item.orderValue !== undefined && (\n \n 도면치수:\n handleInspectionSheetUpdate(category.id, item.id, 'orderValue', e.target.value)}\n className=\"w-16 px-1 border rounded text-center\"\n />\n 측정값:\n handleInspectionSheetUpdate(category.id, item.id, 'measuredValue', e.target.value)}\n className=\"w-16 px-1 border rounded text-center\"\n />\n \n )}\n | \n \n {category.isExternalTest ? (\n 공인시험기관 시험성적서\n ) : (\n item.method\n )}\n | \n {item.cycle} | \n \n \n \n \n \n | \n
\n ))}\n >\n ))}\n \n
\n
\n\n {/* 특이사항 및 종합판정 */}\n
\n
\n
\n
종합판정
\n
\n {calculateOverallJudgement() || '검사 진행중'}\n
\n
\n {inspectionSheet.categories.flatMap(c => c.items).filter(i => i.judgement !== null).length} /\n {inspectionSheet.categories.flatMap(c => c.items).length} 항목 검사 완료\n
\n
\n
\n\n {/* 문서 정보 */}\n
\n KDQP-01-005\n ㈜경동기업\n
\n
\n
\n\n {/* 개소별 검사 섹션 */}\n \n
\n
개소별 제품검사 안내
\n
\n * 각 개소별로 납품사이즈와 실제 시공사이즈 일치 여부를 확인합니다.
\n * 적합/부적합 결과에 따라 합격/불합격이 자동 판정됩니다.
\n * 모든 항목 검사 완료 후 품질팀장에게 결재를 요청합니다.\n
\n
\n\n
\n \n \n
\n\n {/* 일정변경이력 섹션 */}\n \n {inspection.scheduleHistory?.length > 0 ? (\n \n {inspection.scheduleHistory.map((history, idx) => (\n
\n
\n
\n
\n {history.changeType}\n {history.changedAt}\n
\n
\n {history.beforeDate} → {history.afterDate}\n
\n
사유: {history.reason}
\n
{history.changedBy}
\n
\n
\n ))}\n
\n ) : (\n \n )}\n \n * 검사가 불가능하여 미실시하거나 일정 내에 검사를 완료하지 못한 경우
\n * 일정 변경을 등록하고 사유를 기록합니다.\n
\n \n \n );\n};\n\n// ============ 사이드바 ============\n\nconst Sidebar = ({ activeMenu, onMenuChange, menuConfig, onOpenSettings, currentUser }) => {\n const [expandedMenus, setExpandedMenus] = useState(['master', 'sales', 'production']);\n\n // 전체 메뉴 정의\n const allMenuGroups = [\n {\n id: 'dashboard',\n label: '대시보드',\n icon: BarChart3,\n isTopLevel: true, // 서브메뉴 없이 바로 클릭\n },\n {\n id: 'master',\n label: '기준정보',\n icon: Settings,\n subMenus: [\n { id: 'item-master', label: '품목기준관리', icon: Layers },\n { id: 'process-master', label: '공정기준관리', icon: GitBranch },\n { id: 'quality-master', label: '품질기준관리', icon: ShieldCheck },\n { id: 'site-master', label: '현장기준관리', icon: MapPin },\n { id: 'order-master', label: '수주기준관리', icon: Package },\n { id: 'production-master', label: '생산기준관리', icon: Factory },\n { id: 'outbound-master', label: '출고기준관리', icon: Truck },\n { id: 'process', label: '공정관리', icon: GitBranch },\n { id: 'number-rule', label: '채번관리', icon: ClipboardList },\n { id: 'code-rule', label: '공통코드관리', icon: Layers },\n { id: 'quote-formula', label: '견적수식관리', icon: Calculator },\n { id: 'document-template', label: '문서양식관리', icon: FileText },\n ],\n },\n {\n id: 'sales',\n label: '판매관리',\n icon: DollarSign,\n subMenus: [\n { id: 'customer', label: '거래처관리', icon: Users },\n { id: 'quote', label: '견적관리', icon: FileText },\n { id: 'order', label: '수주관리', icon: Package },\n { id: 'site', label: '현장관리', icon: Building },\n { id: 'price', label: '단가관리', icon: DollarSign },\n ],\n },\n {\n id: 'production',\n label: '생산관리',\n icon: Factory,\n subMenus: [\n { id: 'item', label: '품목관리', icon: Package },\n { id: 'production-dashboard', label: '생산 현황판', icon: BarChart3 },\n { id: 'work-order', label: '작업지시 관리', icon: ClipboardList },\n { id: 'work-result', label: '작업실적', icon: Clipboard },\n { id: 'worker-task', label: '작업자 화면', icon: User },\n ],\n },\n {\n id: 'quality',\n label: '품질관리',\n icon: ShieldCheck,\n subMenus: [\n { id: 'inspection', label: '검사관리', icon: ShieldCheck },\n { id: 'defect', label: '부적합품관리', icon: XCircle },\n ],\n },\n {\n id: 'inventory',\n label: '자재관리',\n icon: Warehouse,\n subMenus: [\n { id: 'stock', label: '재고현황', icon: Box },\n { id: 'inbound', label: '입고관리', icon: Download },\n ],\n },\n {\n id: 'outbound',\n label: '출고관리',\n icon: Upload,\n subMenus: [\n { id: 'shipment', label: '출하관리', icon: Truck },\n ],\n },\n {\n id: 'accounting',\n label: '회계관리',\n icon: CreditCard,\n subMenus: [\n {\n id: 'acc-customer',\n label: '거래처관리',\n icon: Building,\n depth3: [\n { id: 'acc-customer-list', label: '목록', screenId: 'A0-1' },\n { id: 'acc-customer-register', label: '등록', screenId: 'A0-2' },\n { id: 'acc-customer-edit', label: '수정', screenId: 'A0-3' },\n ]\n },\n {\n id: 'sales-account',\n label: '매출관리',\n icon: BarChart3,\n depth3: [\n { id: 'sales-list', label: '목록', screenId: 'A1' },\n { id: 'sales-statement', label: '거래명세서', screenId: 'A1-1' },\n { id: 'sales-tax-invoice', label: '세금계산서', screenId: 'A1-2' },\n ]\n },\n {\n id: 'purchase',\n label: '매입관리',\n icon: ShoppingCart,\n depth3: [\n { id: 'purchase-list', label: '목록', screenId: 'A2' },\n { id: 'purchase-register', label: '등록', screenId: 'A2-1' },\n { id: 'expense-list', label: '지출결의서목록', screenId: 'A2-2' },\n { id: 'expense-register', label: '지출결의서등록', screenId: 'A2-3' },\n { id: 'expense-estimate', label: '지출예상', screenId: 'A2-4' },\n ]\n },\n {\n id: 'cashbook',\n label: '금전출납부',\n icon: Clipboard,\n depth3: [\n { id: 'cashbook-list', label: '목록', screenId: 'A3' },\n { id: 'cashbook-register', label: '등록', screenId: 'A3-1' },\n { id: 'cashbook-edit', label: '수정', screenId: 'A3-2' },\n ]\n },\n {\n id: 'collection',\n label: '수금관리',\n icon: DollarSign,\n depth3: [\n { id: 'collection-list', label: '목록', screenId: 'A4' },\n { id: 'collection-register', label: '등록', screenId: 'A4-1' },\n { id: 'receivable-list', label: '미수금관리', screenId: 'A4-2' },\n { id: 'bill-list', label: '어음관리', screenId: 'A4-3' },\n ]\n },\n {\n id: 'cost-analysis',\n label: '원가관리',\n icon: Calculator,\n depth3: [\n { id: 'cost-list', label: '목록', screenId: 'A5' },\n { id: 'cost-detail', label: '상세', screenId: 'A5-1' },\n ]\n },\n ],\n },\n ];\n\n // menuConfig에 따라 메뉴 필터링\n const menuGroups = allMenuGroups\n .filter(group => menuConfig[group.id]?.enabled)\n .map(group => ({\n ...group,\n subMenus: group.subMenus.filter(sub =>\n menuConfig[group.id]?.subMenus?.[sub.id] !== false\n )\n }))\n .filter(group => group.subMenus.length > 0);\n\n const toggleMenu = (menuId) => {\n setExpandedMenus(prev =>\n prev.includes(menuId)\n ? prev.filter(id => id !== menuId)\n : [...prev, menuId]\n );\n };\n\n return (\n
\n );\n};\n\n// ============ 메뉴 설정 화면 ============\n\nconst MenuSettings = ({ menuConfig, onSave, onClose }) => {\n const [config, setConfig] = useState(menuConfig);\n const [selectedRole, setSelectedRole] = useState('custom');\n\n // 역할별 프리셋\n const rolePresets = {\n sales: {\n label: '판매팀',\n config: {\n sales: { enabled: true, subMenus: { customer: true, quote: true, order: true, site: true, price: true } },\n outbound: { enabled: true, subMenus: { shipment: true } },\n production: { enabled: false },\n quality: { enabled: false },\n inventory: { enabled: false },\n accounting: { enabled: false },\n }\n },\n shipping: {\n label: '출고팀',\n config: {\n sales: { enabled: false },\n outbound: { enabled: true, subMenus: { shipment: true } },\n production: { enabled: false },\n quality: { enabled: false },\n inventory: { enabled: true, subMenus: { stock: true, inbound: true, outbound: true } },\n accounting: { enabled: false },\n }\n },\n production: {\n label: '생산팀',\n config: {\n sales: { enabled: true, subMenus: { order: true } },\n outbound: { enabled: false },\n production: { enabled: true, subMenus: { 'production-dashboard': true, 'work-order': true, 'work-result': true, 'worker-task': true } },\n quality: { enabled: true, subMenus: { inspection: true, defect: true } },\n inventory: { enabled: true, subMenus: { stock: true, inbound: true, outbound: true } },\n accounting: { enabled: false },\n }\n },\n accounting: {\n label: '회계팀',\n config: {\n sales: { enabled: true, subMenus: { order: true } },\n outbound: { enabled: true, subMenus: { shipment: true } },\n production: { enabled: false },\n quality: { enabled: false },\n inventory: { enabled: false },\n accounting: { enabled: true, subMenus: { 'acc-customer': true, 'sales-account': true, purchase: true, cashbook: true, collection: true, 'cost-analysis': true } },\n }\n },\n admin: {\n label: '관리자 (전체)',\n config: {\n sales: { enabled: true, subMenus: { customer: true, quote: true, order: true, site: true, price: true } },\n outbound: { enabled: true, subMenus: { shipment: true } },\n production: { enabled: true, subMenus: { 'production-dashboard': true, 'work-order': true, 'work-result': true, 'worker-task': true } },\n quality: { enabled: true, subMenus: { inspection: true, defect: true } },\n inventory: { enabled: true, subMenus: { stock: true, inbound: true, outbound: true } },\n accounting: { enabled: true, subMenus: { 'acc-customer': true, 'sales-account': true, purchase: true, cashbook: true, collection: true, 'cost-analysis': true } },\n }\n },\n };\n\n const menuDefinitions = [\n {\n id: 'master',\n label: '기준정보',\n icon: Settings,\n subMenus: [\n { id: 'item-master', label: '품목기준관리' },\n { id: 'process-master', label: '공정기준관리' },\n { id: 'quality-master', label: '품질기준관리' },\n { id: 'customer-master', label: '거래처기준관리' },\n { id: 'site-master', label: '현장기준관리' },\n { id: 'order-master', label: '수주기준관리' },\n { id: 'process', label: '공정관리' },\n { id: 'number-rule', label: '채번관리' },\n { id: 'code-rule', label: '공통코드관리' },\n ],\n },\n {\n id: 'sales',\n label: '판매관리',\n icon: DollarSign,\n subMenus: [\n { id: 'customer', label: '거래처관리' },\n { id: 'quote', label: '견적관리' },\n { id: 'order', label: '수주관리' },\n { id: 'site', label: '현장관리' },\n { id: 'price', label: '단가관리' },\n { id: 'shipment', label: '출하관리' },\n ],\n },\n {\n id: 'production',\n label: '생산관리',\n icon: Factory,\n subMenus: [\n { id: 'item', label: '품목관리' },\n { id: 'production-dashboard', label: '생산 현황판' },\n { id: 'work-order', label: '작업지시 관리' },\n { id: 'work-result', label: '작업실적' },\n { id: 'worker-task', label: '작업자 화면' },\n ],\n },\n {\n id: 'quality',\n label: '품질관리',\n icon: ShieldCheck,\n subMenus: [\n { id: 'inspection', label: '검사관리' },\n { id: 'defect', label: '부적합품관리' },\n ],\n },\n {\n id: 'inventory',\n label: '자재관리',\n icon: Warehouse,\n subMenus: [\n { id: 'stock', label: '재고현황' },\n { id: 'inbound', label: '입고관리' },\n ],\n },\n {\n id: 'accounting',\n label: '회계관리',\n icon: CreditCard,\n subMenus: [\n { id: 'acc-customer', label: '거래처관리' },\n { id: 'sales-account', label: '매출관리' },\n { id: 'purchase', label: '매입관리' },\n { id: 'cashbook', label: '금전출납부' },\n { id: 'collection', label: '수금관리' },\n { id: 'cost-analysis', label: '원가관리' },\n ],\n },\n ];\n\n const handleRoleChange = (role) => {\n setSelectedRole(role);\n if (role !== 'custom') {\n setConfig(rolePresets[role].config);\n }\n };\n\n const toggleGroup = (groupId) => {\n setSelectedRole('custom');\n setConfig(prev => ({\n ...prev,\n [groupId]: {\n ...prev[groupId],\n enabled: !prev[groupId]?.enabled\n }\n }));\n };\n\n const toggleSubMenu = (groupId, subId) => {\n setSelectedRole('custom');\n setConfig(prev => ({\n ...prev,\n [groupId]: {\n ...prev[groupId],\n subMenus: {\n ...prev[groupId]?.subMenus,\n [subId]: prev[groupId]?.subMenus?.[subId] === false ? true : false\n }\n }\n }));\n };\n\n const handleSave = () => {\n onSave(config);\n onClose();\n };\n\n return (\n
\n
\n {/* 헤더 */}\n
\n
\n \n
메뉴 설정
\n \n
\n
\n\n {/* 역할 프리셋 */}\n
\n
역할별 프리셋을 선택하거나 직접 설정하세요
\n
\n {Object.entries(rolePresets).map(([key, preset]) => (\n \n ))}\n \n
\n
\n\n {/* 메뉴 설정 */}\n
\n
\n {menuDefinitions.map(group => {\n const GroupIcon = group.icon;\n const isEnabled = config[group.id]?.enabled;\n\n return (\n
\n {/* 대메뉴 */}\n
\n
\n \n \n {group.label}\n \n
\n
\n
\n\n {/* 서브메뉴 */}\n {isEnabled && (\n
\n
\n {group.subMenus.map(sub => {\n const isSubEnabled = config[group.id]?.subMenus?.[sub.id] !== false;\n return (\n \n );\n })}\n
\n
\n )}\n
\n );\n })}\n
\n
\n\n {/* 푸터 */}\n
\n \n \n
\n
\n
\n );\n};\n\n// ============ 메인 앱 ============\n\nexport default function App() {\n const [activeMenu, setActiveMenu] = useState('dashboard');\n const [view, setView] = useState('dashboard');\n const [selectedItem, setSelectedItem] = useState(null);\n const [selectedData, setSelectedData] = useState(null);\n const [showSettings, setShowSettings] = useState(false);\n const [showMobileMenu, setShowMobileMenu] = useState(false);\n\n // ═══════════════════════════════════════════════════════════════════\n // 와이어프레임 모드 (기본 활성화)\n // ═══════════════════════════════════════════════════════════════════\n const wireframeMode = true; // 와이어프레임 모드 항상 활성화\n\n // ═══════════════════════════════════════════════════════════════════\n // 닉네임 시스템 (사용자 식별)\n // ═══════════════════════════════════════════════════════════════════\n const [userNickname, setUserNickname] = useState(() => {\n return localStorage.getItem('userNickname') || null;\n });\n const [showNicknameModal, setShowNicknameModal] = useState(false);\n\n // 닉네임 저장 (기존 뱃지들의 author도 함께 변경)\n const saveNickname = (nickname) => {\n const oldNickname = userNickname;\n localStorage.setItem('userNickname', nickname);\n setUserNickname(nickname);\n setShowNicknameModal(false);\n\n // 기존 닉네임이 있고, 새 닉네임과 다르면 모든 뱃지의 author 업데이트\n if (oldNickname && oldNickname !== nickname) {\n setFeatureBadges(prevBadges => {\n const updatedBadges = {};\n Object.keys(prevBadges).forEach(screenKey => {\n updatedBadges[screenKey] = prevBadges[screenKey].map(badge => {\n // 이전 닉네임으로 작성된 뱃지만 업데이트\n if (badge.author === oldNickname) {\n return { ...badge, author: nickname };\n }\n return badge;\n });\n });\n return updatedBadges;\n });\n }\n };\n\n // 처음 접속 시 닉네임 입력 모달 표시\n useEffect(() => {\n if (!userNickname) {\n setShowNicknameModal(true);\n }\n }, [userNickname]);\n\n // ═══════════════════════════════════════════════════════════════════\n // 기능정의서 시스템 (Feature Description System)\n // ═══════════════════════════════════════════════════════════════════\n const [showFeatureDescription, setShowFeatureDescription] = useState(false);\n // 현재 열린 모달/다이얼로그 추적 (기능정의서 뱃지 용)\n const [activeModal, setActiveModal] = useState(null);\n const [isFeatureAdmin, setIsFeatureAdmin] = useState(() => {\n return localStorage.getItem('featureAdmin') === 'true';\n });\n const [showAdminPasswordModal, setShowAdminPasswordModal] = useState(false);\n const [adminPassword, setAdminPassword] = useState('');\n const [adminPasswordError, setAdminPasswordError] = useState('');\n\n // 기능정의서 어드민 상태 저장\n useEffect(() => {\n localStorage.setItem('featureAdmin', isFeatureAdmin.toString());\n }, [isFeatureAdmin]);\n\n // 기능정의서 토글 핸들러 (최초 접근 시 비밀번호 요청)\n const handleToggleFeatureDescription = () => {\n if (showFeatureDescription) {\n // 이미 열려있으면 그냥 닫기\n setShowFeatureDescription(false);\n } else {\n // 최초 열 때 비밀번호 모달 표시\n setShowAdminPasswordModal(true);\n setAdminPassword('');\n setAdminPasswordError('');\n }\n };\n\n // 어드민 비밀번호 확인\n const handleAdminPasswordSubmit = () => {\n if (adminPassword === '4321') {\n setIsFeatureAdmin(true);\n setShowAdminPasswordModal(false);\n setShowFeatureDescription(true);\n setAdminPassword('');\n setAdminPasswordError('');\n } else if (adminPassword === '') {\n // 빈 입력 또는 일반 사용자로 진입\n setIsFeatureAdmin(false);\n setShowAdminPasswordModal(false);\n setShowFeatureDescription(true);\n setAdminPassword('');\n } else {\n setAdminPasswordError('비밀번호가 일치하지 않습니다.');\n }\n };\n\n // 현재 뱃지 데이터 (작업 중인 상태)\n const [featureBadges, setFeatureBadges] = useState(() => {\n const saved = localStorage.getItem('featureBadges_current');\n return saved ? JSON.parse(saved) : {};\n });\n\n // 현재 작업 상태 저장\n useEffect(() => {\n localStorage.setItem('featureBadges_current', JSON.stringify(featureBadges));\n }, [featureBadges]);\n\n // ═══════════════════════════════════════════════════════════════════\n // 기능정의문서 패널 시스템 (UI별 기능 정의 조회)\n // ═══════════════════════════════════════════════════════════════════\n const [showFeatureDocPanel, setShowFeatureDocPanel] = useState(false);\n const [hoveredBadgeNumber, setHoveredBadgeNumber] = useState(null);\n\n // ═══════════════════════════════════════════════════════════════════\n // 버전 히스토리 시스템 (Git 스타일 버전 관리)\n // ═══════════════════════════════════════════════════════════════════\n const [showVersionHistory, setShowVersionHistory] = useState(false);\n\n // ═══════════════════════════════════════════════════════════════════\n // 유저플로우 패널 시스템 (모달 대신 우측 패널 사용)\n // ═══════════════════════════════════════════════════════════════════\n const [showUserFlow, setShowUserFlow] = useState(false);\n const [showFlowPanel, setShowFlowPanel] = useState(false);\n\n // ═══════════════════════════════════════════════════════════════════\n // 종합 플로우차트 및 전체 메뉴 기능정의서 패널\n // ═══════════════════════════════════════════════════════════════════\n const [showComprehensiveFlowPanel, setShowComprehensiveFlowPanel] = useState(false);\n const [comprehensiveFlowPanelWidth, setComprehensiveFlowPanelWidth] = useState(600); // 드래그 가능한 패널 너비\n const [showAllMenuFeatureDocPanel, setShowAllMenuFeatureDocPanel] = useState(false);\n\n // ═══════════════════════════════════════════════════════════════════\n // 상세 플로우 다이어그램 (ReactFlow 기반)\n // ═══════════════════════════════════════════════════════════════════\n const [showDetailedFlowDiagram, setShowDetailedFlowDiagram] = useState(false);\n const [detailedFlowType, setDetailedFlowType] = useState('quote-to-shipment');\n\n // ═══════════════════════════════════════════════════════════════════\n // 작업자 화면 자재투입 모달 상태\n // ═══════════════════════════════════════════════════════════════════\n const [showWorkerMaterialModal, setShowWorkerMaterialModal] = useState(false);\n const [workerMaterialTask, setWorkerMaterialTask] = useState(null);\n\n // ★ 자재 투입 시 재고 감소 핸들러\n const handleUseMaterial = (materialCode, qty, usage) => {\n setInventory(prev => prev.map(item =>\n item.materialCode === materialCode\n ? { ...item, stock: Math.max(0, item.stock - qty), lastUpdated: new Date().toISOString().split('T')[0] }\n : item\n ));\n console.log('자재 사용:', { materialCode, qty, usage });\n };\n\n // ═══════════════════════════════════════════════════════════════════\n // 공유 품목 마스터 상태 (품목관리 ↔ 단가관리 연동)\n // ═══════════════════════════════════════════════════════════════════\n const [sharedItems, setSharedItems] = useState(() => {\n // initialItems와 동일한 구조로 초기화\n return itemPriceMaster.map((item, idx) => ({\n id: idx + 1,\n itemCode: item.itemCode,\n itemName: item.itemName,\n category: item.category,\n itemType: item.category.includes('절곡') || item.category.includes('가이드') || item.category.includes('케이스') || item.category.includes('하단') || item.category.includes('연기') ? '반제품' :\n item.category.includes('전동') || item.category.includes('연동') || item.category.includes('샤프트') || item.category.includes('브라켓') || item.category.includes('앵글') ? '부품' :\n item.category.includes('검사') ? '서비스' : '원자재',\n unit: item.unit,\n spec: item.itemName.match(/\\d+/) ? item.itemName.match(/[\\d×\\-]+/)?.[0] || '-' : '-',\n safetyStock: Math.floor(Math.random() * 20) + 5,\n currentStock: Math.floor(Math.random() * 50) + 10,\n leadTime: Math.floor(Math.random() * 7) + 1,\n isActive: true,\n createdAt: '2024-01-01',\n // 단가 정보\n purchasePrice: item.unitPrice || 0,\n sellingPrice: item.sellingPrice || 0,\n processingCost: 0,\n lossRate: 0,\n marginRate: item.sellingPrice && item.unitPrice ? Math.round((item.sellingPrice - item.unitPrice) / item.unitPrice * 100) : 20,\n }));\n });\n\n // 품목 추가 핸들러 (품목관리에서 호출)\n const handleAddItem = (newItem) => {\n const maxId = sharedItems.reduce((max, item) => Math.max(max, item.id), 0);\n const itemWithId = { ...newItem, id: maxId + 1, createdAt: new Date().toISOString().slice(0, 10) };\n setSharedItems(prev => [...prev, itemWithId]);\n return itemWithId;\n };\n\n // 품목 수정 핸들러 (품목관리에서 호출)\n const handleUpdateItem = (updatedItem) => {\n setSharedItems(prev => prev.map(item => item.id === updatedItem.id ? { ...item, ...updatedItem } : item));\n };\n\n // 품목 삭제 핸들러 (품목관리에서 호출)\n const handleDeleteItem = (itemId) => {\n setSharedItems(prev => prev.filter(item => item.id !== itemId));\n };\n\n // 단가 정보 업데이트 핸들러 (단가관리에서 호출)\n const handleUpdatePrice = (priceDataArray) => {\n // 단가관리에서 업데이트된 정보를 품목 마스터에 반영\n setSharedItems(prev => prev.map(item => {\n const priceInfo = priceDataArray.find(p => p.itemCode === item.itemCode);\n if (priceInfo) {\n return {\n ...item,\n purchasePrice: priceInfo.purchasePrice,\n sellingPrice: priceInfo.sellingPrice,\n processingCost: priceInfo.processingCost,\n lossRate: priceInfo.lossRate,\n marginRate: priceInfo.marginRate,\n };\n }\n return item;\n }));\n };\n\n // 버전 히스토리 목록\n const [versionHistory, setVersionHistory] = useState(() => {\n const saved = localStorage.getItem('versionHistory');\n return saved ? JSON.parse(saved) : [];\n });\n\n // 마지막 커밋된 스냅샷 (변경 감지용)\n const [lastCommittedSnapshot, setLastCommittedSnapshot] = useState(() => {\n const saved = localStorage.getItem('lastCommittedSnapshot');\n return saved ? JSON.parse(saved) : null;\n });\n\n // 버전 히스토리 저장\n useEffect(() => {\n localStorage.setItem('versionHistory', JSON.stringify(versionHistory));\n }, [versionHistory]);\n\n // 마지막 커밋 스냅샷 저장\n useEffect(() => {\n if (lastCommittedSnapshot) {\n localStorage.setItem('lastCommittedSnapshot', JSON.stringify(lastCommittedSnapshot));\n }\n }, [lastCommittedSnapshot]);\n\n // 현재 버전 번호\n const currentVersion = versionHistory.length > 0 ? versionHistory[0].version : 0;\n\n // 변경사항 있는지 확인\n const hasChanges = React.useMemo(() => {\n if (!lastCommittedSnapshot) return Object.keys(featureBadges).length > 0;\n return JSON.stringify(featureBadges) !== JSON.stringify(lastCommittedSnapshot);\n }, [featureBadges, lastCommittedSnapshot]);\n\n // 변경사항 요약 생성 (상세)\n const generateChangeSummary = React.useCallback((oldBadges, newBadges) => {\n const allScreens = new Set([...Object.keys(oldBadges || {}), ...Object.keys(newBadges || {})]);\n\n let addedCount = 0;\n let removedCount = 0;\n let modifiedCount = 0;\n const changeDetails = []; // 상세 변경 내역\n\n // 화면 이름 변환 맵\n const screenNameMap = {\n 'dashboard': '대시보드',\n 'quote': '견적관리',\n 'order': '수주관리',\n 'customer': '거래처관리',\n 'shipment': '출하관리',\n 'production': '생산계획',\n 'workOrder': '작업지시',\n 'workResult': '작업실적',\n 'inventory': '재고관리',\n 'inspection': '품질검사',\n 'defect': '불량관리',\n 'accounting': '회계관리',\n 'collection': '수금관리',\n 'cost': '원가관리',\n 'system': '시스템설정',\n 'masterConfig': '기준정보설정',\n 'itemMaster': '품목기준관리',\n 'processMaster': '공정기준관리',\n 'qualityMaster': '품질기준관리',\n 'customerMaster': '거래처기준관리',\n 'orderMaster': '수주기준관리',\n 'documentTemplate': '문서양식관리',\n };\n\n const getScreenDisplayName = (screen) => {\n const [menu, viewType] = screen.split('-');\n const menuName = screenNameMap[menu] || menu;\n const viewName = viewType === 'list' ? '목록' : viewType === 'detail' ? '상세' : viewType === 'create' ? '등록' : viewType === 'edit' ? '수정' : viewType || '';\n return viewName ? `${menuName} ${viewName}` : menuName;\n };\n\n allScreens.forEach(screen => {\n const oldList = oldBadges?.[screen] || [];\n const newList = newBadges?.[screen] || [];\n\n const oldIds = new Set(oldList.map(b => b.id));\n const newIds = new Set(newList.map(b => b.id));\n\n const screenDisplayName = getScreenDisplayName(screen);\n const screenChanges = { added: [], removed: [], modified: [] };\n\n // 추가된 뱃지\n newList.forEach(b => {\n if (!oldIds.has(b.id)) {\n addedCount++;\n screenChanges.added.push(b.label || `#${b.number || '?'}`);\n }\n });\n\n // 삭제된 뱃지\n oldList.forEach(b => {\n if (!newIds.has(b.id)) {\n removedCount++;\n screenChanges.removed.push(b.label || `#${b.number || '?'}`);\n }\n });\n\n // 수정된 뱃지\n newList.forEach(newB => {\n const oldB = oldList.find(b => b.id === newB.id);\n if (oldB && JSON.stringify(oldB) !== JSON.stringify(newB)) {\n modifiedCount++;\n screenChanges.modified.push(newB.label || `#${newB.number || '?'}`);\n }\n });\n\n // 화면별 상세 변경 내역 추가\n if (screenChanges.added.length || screenChanges.removed.length || screenChanges.modified.length) {\n const details = [];\n if (screenChanges.added.length) {\n details.push(`추가: ${screenChanges.added.join(', ')}`);\n }\n if (screenChanges.removed.length) {\n details.push(`삭제: ${screenChanges.removed.join(', ')}`);\n }\n if (screenChanges.modified.length) {\n details.push(`수정: ${screenChanges.modified.join(', ')}`);\n }\n changeDetails.push(`[${screenDisplayName}] ${details.join(' / ')}`);\n }\n });\n\n // 요약 생성\n const summaryParts = [];\n if (addedCount > 0) summaryParts.push(`${addedCount}개 추가`);\n if (removedCount > 0) summaryParts.push(`${removedCount}개 삭제`);\n if (modifiedCount > 0) summaryParts.push(`${modifiedCount}개 수정`);\n\n const totalSummary = summaryParts.length > 0 ? summaryParts.join(', ') : '변경없음';\n const detailText = changeDetails.length > 0 ? '\\n' + changeDetails.join('\\n') : '';\n\n return `${totalSummary}${detailText}`;\n }, []);\n\n // 자동 커밋 메시지 생성\n const autoCommitMessage = React.useMemo(() => {\n if (!hasChanges) return '';\n return generateChangeSummary(lastCommittedSnapshot, featureBadges);\n }, [hasChanges, lastCommittedSnapshot, featureBadges, generateChangeSummary]);\n\n // 새 버전 커밋\n const commitVersion = (message) => {\n const newVersion = currentVersion + 1;\n const summary = generateChangeSummary(lastCommittedSnapshot, featureBadges);\n\n const newCommit = {\n id: Date.now(),\n version: newVersion,\n message,\n summary,\n author: userNickname || '익명',\n timestamp: new Date().toISOString(),\n snapshot: JSON.parse(JSON.stringify(featureBadges)) // 깊은 복사\n };\n\n setVersionHistory(prev => [newCommit, ...prev]);\n setLastCommittedSnapshot(JSON.parse(JSON.stringify(featureBadges)));\n\n // 공유용 커밋된 데이터 저장 (다른 사용자가 볼 수 있음)\n localStorage.setItem('featureBadges_committed', JSON.stringify(featureBadges));\n\n alert(`v${newVersion} 커밋 완료!\\n${summary}`);\n };\n\n // 이전 버전으로 롤백\n const rollbackToVersion = (commitId) => {\n const targetCommit = versionHistory.find(v => v.id === commitId);\n if (!targetCommit) return;\n\n setFeatureBadges(JSON.parse(JSON.stringify(targetCommit.snapshot)));\n setLastCommittedSnapshot(JSON.parse(JSON.stringify(targetCommit.snapshot)));\n alert(`v${targetCommit.version}로 롤백되었습니다.`);\n setShowVersionHistory(false);\n };\n\n // 다른 사용자의 최신 커밋 로드 (새로고침 시)\n useEffect(() => {\n const loadLatestCommit = () => {\n const committed = localStorage.getItem('featureBadges_committed');\n if (committed && !lastCommittedSnapshot) {\n const data = JSON.parse(committed);\n setFeatureBadges(data);\n setLastCommittedSnapshot(data);\n }\n };\n loadLatestCommit();\n }, []);\n\n const [isAddingBadge, setIsAddingBadge] = useState(false);\n const [selectedBadge, setSelectedBadge] = useState(null);\n const [badgeEditMode, setBadgeEditMode] = useState(false);\n\n // 새로운 UX: C키 → 클릭 → 인라인 입력\n const [isPlacingBadge, setIsPlacingBadge] = useState(false); // C키 누른 후 클릭 대기 상태\n const [inlineInputPosition, setInlineInputPosition] = useState(null); // 클릭한 위치에 입력창 표시\n const [nextBadgeNumber, setNextBadgeNumber] = useState(1); // 다음 뱃지 번호\n const contentAreaRef = React.useRef(null); // 콘텐츠 영역 ref\n const [scrollState, setScrollState] = useState({ top: 0, height: 0 }); // 스크롤 상태 추적\n const [contentAreaBounds, setContentAreaBounds] = useState({ top: 0, left: 0, width: 0, height: 0 }); // 콘텐츠 영역 위치\n\n // 스크롤 및 레이아웃 이벤트 리스너 - 뱃지 오버레이 위치 동기화\n useEffect(() => {\n const contentArea = contentAreaRef.current;\n if (!contentArea || !showFeatureDescription) return;\n\n const updateState = () => {\n const rect = contentArea.getBoundingClientRect();\n setScrollState({\n top: contentArea.scrollTop,\n height: contentArea.scrollHeight\n });\n setContentAreaBounds({\n top: rect.top,\n left: rect.left,\n width: contentArea.clientWidth,\n height: contentArea.clientHeight\n });\n };\n\n // 초기 상태 설정\n updateState();\n\n contentArea.addEventListener('scroll', updateState);\n window.addEventListener('resize', updateState);\n\n // ResizeObserver로 콘텐츠 영역 크기 변화 감지\n const resizeObserver = new ResizeObserver(updateState);\n resizeObserver.observe(contentArea);\n\n return () => {\n contentArea.removeEventListener('scroll', updateState);\n window.removeEventListener('resize', updateState);\n resizeObserver.disconnect();\n };\n }, [showFeatureDescription]);\n\n // 현재 화면의 다음 번호 계산\n const getNextBadgeNumber = React.useCallback(() => {\n const screenKey = `${activeMenu}-${view}`;\n const currentBadges = featureBadges[screenKey] || [];\n if (currentBadges.length === 0) return 1;\n const maxNumber = Math.max(...currentBadges.map(b => b.number || 0));\n return maxNumber + 1;\n }, [activeMenu, view, featureBadges]);\n\n // 키보드 C 단축키 (기능정의서 활성화 시) - 클릭 대기 모드로 전환\n useEffect(() => {\n if (!showFeatureDescription) return;\n\n const handleKeyDown = (e) => {\n // 입력 필드에서는 무시\n if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {\n return;\n }\n\n if (e.key === 'c' || e.key === 'C') {\n e.preventDefault();\n // 편집 모드 자동 활성화\n if (!badgeEditMode) {\n setBadgeEditMode(true);\n }\n // 클릭 대기 모드 활성화\n setIsPlacingBadge(true);\n setNextBadgeNumber(getNextBadgeNumber());\n }\n\n // ESC로 취소\n if (e.key === 'Escape') {\n setIsPlacingBadge(false);\n setInlineInputPosition(null);\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => window.removeEventListener('keydown', handleKeyDown);\n }, [showFeatureDescription, badgeEditMode, getNextBadgeNumber]);\n\n // 클릭 대기 모드에서 클릭 시 위치 지정\n const handleContentAreaClick = (e) => {\n if (!isPlacingBadge || !contentAreaRef.current) return;\n\n const rect = contentAreaRef.current.getBoundingClientRect();\n const scrollTop = contentAreaRef.current.scrollTop;\n const scrollLeft = contentAreaRef.current.scrollLeft;\n\n // 스크롤 위치를 포함한 절대 픽셀 좌표로 저장\n const absoluteX = e.clientX - rect.left + scrollLeft;\n const absoluteY = e.clientY - rect.top + scrollTop;\n\n // 클릭한 위치에 인라인 입력창 표시\n setInlineInputPosition({\n x: ((e.clientX - rect.left) / rect.width) * 100, // 팝업 위치용 (화면 비율)\n y: ((e.clientY - rect.top) / rect.height) * 100,\n absoluteX, // 뱃지 저장용 (절대 픽셀)\n absoluteY,\n scrollTop,\n clientX: e.clientX,\n clientY: e.clientY,\n });\n setIsPlacingBadge(false);\n };\n\n // 현재 화면의 뱃지들 가져오기\n const getCurrentScreenBadges = () => {\n const screenKey = `${activeMenu}-${view}`;\n return featureBadges[screenKey] || [];\n };\n\n // 기능 추출 함수 - 현재 화면에 대한 자동 기능 분석\n const extractFeatures = React.useCallback(() => {\n const screenKey = `${activeMenu}-${view}`;\n const currentBadges = featureBadges[screenKey] || [];\n const template = getScreenFeatureTemplate(activeMenu);\n\n if (!template) {\n alert(`'${activeMenu}' 화면에 대한 기능 정의 템플릿이 없습니다.\\n수동으로 뱃지를 추가해주세요.`);\n return;\n }\n\n if (currentBadges.length > 0) {\n const confirmReplace = window.confirm(\n `현재 화면에 ${currentBadges.length}개의 뱃지가 있습니다.\\n` +\n `기존 뱃지를 모두 삭제하고 자동 추출된 ${template.features.length}개의 기능으로 교체하시겠습니까?`\n );\n if (!confirmReplace) return;\n }\n\n const contentArea = contentAreaRef.current;\n if (!contentArea) {\n alert('콘텐츠 영역을 찾을 수 없습니다.');\n return;\n }\n const contentWidth = contentArea.scrollWidth;\n const contentHeight = contentArea.scrollHeight;\n\n const newBadges = template.features.map((feature, index) => {\n const absoluteX = (feature.position.x / 100) * contentWidth;\n const absoluteY = (feature.position.y / 100) * contentHeight;\n let description = '';\n\n // 액션 정보 (버튼/요소 클릭 시 어떤 동작이 일어나는지)\n if (feature.action) {\n description += `【액션】\\n`;\n if (feature.action.trigger) description += `• 트리거: ${feature.action.trigger}\\n`;\n if (feature.action.behavior) description += `• 동작: ${feature.action.behavior}\\n`;\n if (feature.action.result) description += `• 결과: ${feature.action.result}\\n`;\n if (feature.action.api) description += `• API: ${feature.action.api}\\n`;\n if (feature.action.relatedTable) description += `• 테이블: ${feature.action.relatedTable}\\n`;\n }\n\n // 상태 정보 (필드가 가진 상태들과 조건)\n if (feature.states && feature.states.length > 0) {\n if (description) description += '\\n';\n description += `【상태】\\n`;\n feature.states.forEach(stateInfo => {\n description += `• ${stateInfo.state}: ${stateInfo.condition} → ${stateInfo.display}\\n`;\n });\n }\n\n // 컬럼 정보 (테이블인 경우)\n if (feature.columns) {\n if (description) description += '\\n';\n description += `【컬럼】\\n`;\n description += `• ${feature.columns}\\n`;\n }\n\n return {\n id: `badge-${Date.now()}-${index}`,\n number: index + 1,\n label: feature.label,\n color: 'red', // 기본 색상 빨간색으로 통일\n x: feature.position.x,\n y: feature.position.y,\n absoluteX,\n absoluteY,\n description: description.trim(),\n action: feature.action || null,\n states: feature.states || [],\n columns: feature.columns || null,\n createdAt: new Date().toISOString(),\n createdBy: userNickname || '시스템',\n autoGenerated: true,\n };\n });\n\n setFeatureBadges(prev => ({ ...prev, [screenKey]: newBadges }));\n alert(`✅ ${template.screenName}에서 ${newBadges.length}개의 기능이 자동 추출되었습니다.`);\n }, [activeMenu, view, featureBadges, userNickname]);\n\n // 메뉴 매핑 정보 (공용)\n const menuMapping = {\n // 대시보드/홈\n 'dashboard': { group: '홈', label: '대시보드' },\n 'home': { group: '홈', label: '홈' },\n // 기준정보\n 'item-master': { group: '기준정보', label: '품목기준관리' },\n 'process-master': { group: '기준정보', label: '공정기준관리' },\n 'quality-master': { group: '기준정보', label: '품질기준관리' },\n 'customer-master': { group: '기준정보', label: '거래처기준관리' },\n 'site-master': { group: '기준정보', label: '현장기준관리' },\n 'order-master': { group: '기준정보', label: '수주기준관리' },\n 'process': { group: '기준정보', label: '공정관리' },\n 'number-rule': { group: '기준정보', label: '채번관리' },\n 'code-rule': { group: '기준정보', label: '공통코드관리' },\n 'quote-formula': { group: '기준정보', label: '견적수식관리' },\n 'document-template': { group: '기준정보', label: '문서양식관리' },\n // 판매관리\n 'customer': { group: '판매관리', label: '거래처관리' },\n 'quote': { group: '판매관리', label: '견적관리' },\n 'order': { group: '판매관리', label: '수주관리' },\n 'shipment': { group: '판매관리', label: '출하관리' },\n 'site': { group: '판매관리', label: '현장관리' },\n 'price': { group: '판매관리', label: '단가관리' },\n // 물류관리\n 'logistics-shipment': { group: '물류관리', label: '출하관리' },\n // 생산관리\n 'item': { group: '생산관리', label: '품목관리' },\n 'production-dashboard': { group: '생산관리', label: '생산 현황판' },\n 'work-order': { group: '생산관리', label: '작업지시 관리' },\n 'work-result': { group: '생산관리', label: '작업실적' },\n 'worker-task': { group: '생산관리', label: '작업자 화면' },\n 'traceability': { group: '생산관리', label: '제품이력추적' },\n // 품질관리\n 'inspection': { group: '품질관리', label: '검사관리' },\n 'iqc': { group: '품질관리', label: '수입검사(IQC)' },\n 'pqc': { group: '품질관리', label: '중간검사(PQC)' },\n 'fqc': { group: '품질관리', label: '제품검사(FQC)' },\n 'defect': { group: '품질관리', label: '부적합품관리' },\n // 자재관리\n 'stock': { group: '자재관리', label: '재고현황' },\n 'inbound': { group: '자재관리', label: '입고관리' },\n // 회계관리\n 'acc-customer': { group: '회계관리', label: '거래처관리' },\n 'sales-account': { group: '회계관리', label: '매출관리' },\n 'purchase': { group: '회계관리', label: '매입관리' },\n 'cashbook': { group: '회계관리', label: '금전출납부' },\n 'collection': { group: '회계관리', label: '수금관리' },\n 'cost-analysis': { group: '회계관리', label: '원가관리' },\n // 시스템\n 'process-flowchart': { group: '시스템', label: '업무 플로우차트' },\n 'detailed-process-flow': { group: '시스템', label: '상세 프로세스 플로우' },\n 'users': { group: '시스템', label: '사용자관리' },\n 'roles': { group: '시스템', label: '권한관리' },\n 'settings': { group: '시스템', label: '시스템설정' },\n 'common-ux': { group: '시스템', label: '공통 UX' },\n 'integrated-test': { group: '시스템', label: '통합테스트' },\n };\n\n // 뷰 타입 한글 변환\n const viewLabels = {\n 'list': '목록',\n 'register': '등록',\n 'detail': '상세',\n 'edit': '수정',\n };\n\n // 화면명 생성 함수 (실제 페이지 타이틀)\n const getScreenName = () => {\n // 화면 타이틀 매핑 (view 값 기준 - PageHeader의 title과 동일하게)\n const screenTitleMap = {\n // 기준정보\n 'item-master-list': '품목기준 목록',\n 'item-master-register': '품목기준 등록',\n 'item-master-detail': '품목기준 상세',\n 'process-master-list': '공정기준 목록',\n 'process-master-register': '공정기준 등록',\n 'quality-master-list': '품질기준 목록',\n 'customer-master-list': '거래처기준 목록',\n 'site-master-list': '현장기준 목록',\n 'order-master-list': '수주기준 목록',\n 'number-rule-list': '채번규칙 목록',\n 'code-rule-list': '공통코드 목록',\n 'quote-formula-list': '견적수식 목록',\n 'document-template-list': '문서양식 목록',\n 'document-template-register': '문서양식 등록',\n 'document-template-detail': '문서양식 상세',\n // 판매관리\n 'customer-list': '거래처 목록',\n 'customer-register': '거래처 등록',\n 'customer-detail': '거래처 상세',\n 'quote-list': '견적 목록',\n 'quote-register': '견적 등록',\n 'quote-detail': '견적 상세',\n 'order-list': '수주 목록',\n 'order-register': '수주 등록',\n 'order-detail': '수주 상세',\n 'order-edit': '수주 수정',\n 'shipment-list': '출하 목록',\n 'shipment-register': '출하 등록',\n 'shipment-detail': '출하 상세',\n 'site-list': '현장 목록',\n 'price-list': '단가 목록',\n // 생산관리\n 'production-dashboard-list': '생산현황',\n 'work-order-list': '작업지시 목록',\n 'work-order-register': '작업지시 등록',\n 'work-order-detail': '작업지시 상세',\n 'work-result-list': '작업실적 목록',\n 'worker-task-list': '작업자 태스크',\n // 품질관리\n 'inspection-list': '검사 목록',\n 'iqc-list': '수입검사 목록',\n 'pqc-list': '공정검사 목록',\n 'fqc-list': '최종검사 목록',\n 'defect-list': '부적합 목록',\n // 자재관리\n 'stock-list': '재고 목록',\n 'inbound-list': '입고 목록',\n 'stock-adjustment-list': '재고 조정',\n // 회계관리\n 'acc-customer-list': '거래처 목록',\n 'sales-account-list': '매출 목록',\n 'purchase-list': '매입 목록',\n 'cashbook-list': '금전출납부 목록',\n 'collection-list': '수금 목록',\n 'cost-analysis-list': '원가분석',\n // 시스템\n 'users-list': '사용자 목록',\n 'roles-list': '권한 설정',\n 'settings-list': '시스템 설정',\n // 대시보드\n 'dashboard': '대시보드',\n };\n\n // view 값이 이미 화면 키 역할을 함 (예: 'order-list', 'customer-register')\n if (screenTitleMap[view]) {\n return screenTitleMap[view];\n }\n // 매핑에 없으면 기본 형식 사용\n const menuInfo = menuMapping[activeMenu] || { group: '', label: activeMenu };\n return `${menuInfo.label} 화면`;\n };\n\n // 화면ID 코드 체계 설명\n const [showIdCodeHelp, setShowIdCodeHelp] = useState(false);\n\n const idCodeHelpContent = {\n title: '화면ID 코드 체계',\n sections: [\n {\n category: '기준정보 (M)',\n prefix: 'M',\n description: 'Master Data 관리 화면',\n examples: ['M01: 품목기준', 'M02: 공정기준', 'M03: 검사기준', 'M04: 거래처기준', 'M10: 문서양식']\n },\n {\n category: '판매관리 (S)',\n prefix: 'S',\n description: 'Sales 관련 화면',\n examples: ['S01: 거래처', 'S02: 견적', 'S03: 수주', 'S04: 출하']\n },\n {\n category: '생산관리 (P)',\n prefix: 'P',\n description: 'Production 관련 화면',\n examples: ['P01: 생산현황', 'P02: 작업지시', 'P03: 작업실적', 'P04: 작업자태스크']\n },\n {\n category: '품질관리 (Q)',\n prefix: 'Q',\n description: 'Quality 관련 화면',\n examples: ['Q01: 검사관리', 'Q02: 수입검사', 'Q03: 공정검사', 'Q04: 최종검사', 'Q05: 부적합']\n },\n {\n category: '자재관리 (I)',\n prefix: 'I',\n description: 'Inventory 관련 화면',\n examples: ['I01: 재고', 'I02: 입고']\n },\n {\n category: '회계관리 (A)',\n prefix: 'A',\n description: 'Accounting 관련 화면',\n examples: ['A01: 거래처', 'A02: 매출', 'A03: 매입', 'A04: 금전출납부', 'A05: 수금', 'A06: 원가분석']\n },\n {\n category: '시스템 (SYS)',\n prefix: 'SYS',\n description: 'System 관련 화면',\n examples: ['SYS01: 사용자', 'SYS02: 권한', 'SYS03: 설정']\n },\n {\n category: '대시보드 (D)',\n prefix: 'D',\n description: 'Dashboard 화면',\n examples: ['D01: 대시보드']\n },\n ],\n suffix: '뷰 구분: -01(목록), -02(등록), -03(상세), -04(수정)'\n };\n\n // 화면 경로 생성 함수 (역할 > 그룹 > 메뉴)\n const getScreenPath = () => {\n const menuInfo = menuMapping[activeMenu] || { group: '', label: activeMenu };\n const roleName = currentUser?.roleName || '대표이사';\n return `${roleName} > ${menuInfo.group} > ${menuInfo.label}`;\n };\n\n // 화면 ID 생성 함수\n const getScreenId = () => {\n // 모달 ID 매핑 (DLG: Dialog)\n const modalIdMap = {\n // 판매관리 모달\n 'quote-convert-modal': 'DLG-S02-01', // 견적 → 수주 전환\n 'order-split-modal': 'DLG-S03-01', // 수주 분할\n 'order-production-modal': 'DLG-S03-02', // 생산지시 생성\n 'order-po-create-modal': 'DLG-S03-03', // 발주서 생성\n 'order-document-modal': 'DLG-S03-04', // 문서 출력\n 'shipment-detail-modal': 'DLG-S04-01', // 출하 상세\n // 생산관리 모달\n 'work-order-create-modal': 'DLG-P02-01', // 작업지시 생성\n 'material-input-modal': 'DLG-P02-02', // 투입자재 등록\n 'work-log-modal': 'DLG-P03-01', // 작업일지\n 'issue-report-modal': 'DLG-P03-02', // 이슈보고\n // 품질관리 모달\n 'defect-action-modal': 'DLG-Q05-01', // 부적합 처리\n 'defect-approval-modal': 'DLG-Q05-02', // 부적합 승인\n // 자재관리 모달\n 'receive-modal': 'DLG-I02-01', // 입고 등록\n 'stock-create-modal': 'DLG-I01-01', // 재고 조정\n // 기준정보 모달\n 'rule-modal': 'DLG-M07-01', // 채번규칙 편집\n 'formula-modal': 'DLG-M09-01', // 견적공식 편집\n 'item-add-modal': 'DLG-M01-01', // 품목 추가\n // 공통 모달\n 'delete-confirm-modal': 'DLG-CMN-01', // 삭제 확인\n 'approval-modal': 'DLG-CMN-02', // 결재 모달\n 'preview-modal': 'DLG-CMN-03', // 미리보기\n };\n\n // 모달이 열려있으면 모달 ID 반환\n if (activeModal && modalIdMap[activeModal]) {\n return modalIdMap[activeModal];\n }\n\n // 화면 ID 매핑 (view 값 기준 - view가 이미 'menu-viewType' 형태)\n const screenIdMap = {\n // 기준정보 (M: Master)\n 'item-master-list': 'M01-01',\n 'item-master-register': 'M01-02',\n 'item-master-detail': 'M01-03',\n 'process-master-list': 'M02-01',\n 'process-master-register': 'M02-02',\n 'quality-master-list': 'M03-01',\n 'customer-master-list': 'M04-01',\n 'site-master-list': 'M05-01',\n 'order-master-list': 'M06-01',\n 'number-rule-list': 'M07-01',\n 'code-rule-list': 'M08-01',\n 'quote-formula-list': 'M09-01',\n 'document-template-list': 'M10-01',\n 'document-template-register': 'M10-02',\n 'document-template-detail': 'M10-03',\n // 판매관리 (S: Sales)\n 'customer-list': 'S01-01',\n 'customer-register': 'S01-02',\n 'customer-detail': 'S01-03',\n 'quote-list': 'S02-01',\n 'quote-register': 'S02-02',\n 'quote-detail': 'S02-03',\n 'order-list': 'S03-01',\n 'order-register': 'S03-02',\n 'order-detail': 'S03-03',\n 'order-edit': 'S03-04',\n 'shipment-list': 'S04-01',\n 'shipment-register': 'S04-02',\n 'shipment-detail': 'S04-03',\n 'site-list': 'S05-01',\n 'price-list': 'S06-01',\n // 생산관리 (P: Production)\n 'production-dashboard-list': 'P01-01',\n 'work-order-list': 'P02-01',\n 'work-order-register': 'P02-02',\n 'work-order-detail': 'P02-03',\n 'work-result-list': 'P03-01',\n 'worker-task-list': 'P04-01',\n // 품질관리 (Q: Quality)\n 'inspection-list': 'Q01-01',\n 'iqc-list': 'Q02-01',\n 'pqc-list': 'Q03-01',\n 'fqc-list': 'Q04-01',\n 'defect-list': 'Q05-01',\n // 자재관리 (I: Inventory)\n 'stock-list': 'I01-01',\n 'inbound-list': 'I02-01',\n 'stock-adjustment-list': 'I03-01',\n // 회계관리 (A: Accounting)\n 'acc-customer-list': 'A01-01',\n 'sales-account-list': 'A02-01',\n 'purchase-list': 'A03-01',\n 'cashbook-list': 'A04-01',\n 'collection-list': 'A05-01',\n 'cost-analysis-list': 'A06-01',\n // 시스템 (SYS: System)\n 'users-list': 'SYS01-01',\n 'roles-list': 'SYS02-01',\n 'settings-list': 'SYS03-01',\n // 대시보드\n 'dashboard': 'D01-01',\n };\n\n // view 값이 이미 화면 키 역할을 함 (예: 'order-list', 'customer-register')\n return screenIdMap[view] || `SCR-${view.toUpperCase().replace(/-/g, '-').substring(0, 15)}`;\n };\n\n // 뱃지 추가 (일반 방식)\n const addFeatureBadge = (badge) => {\n const screenKey = `${activeMenu}-${view}`;\n const number = getNextBadgeNumber();\n const newBadge = {\n ...badge,\n number,\n id: Date.now(),\n author: userNickname,\n createdAt: new Date().toISOString(),\n };\n setFeatureBadges(prev => ({\n ...prev,\n [screenKey]: [...(prev[screenKey] || []), newBadge]\n }));\n setIsAddingBadge(false);\n };\n\n // 현재 화면의 screenKey 가져오기 (모달 포함)\n const getCurrentScreenKey = () => {\n // 모달이 열려있으면 모달 키 반환\n if (activeModal) {\n return `modal-${activeModal}`;\n }\n return `${activeMenu}-${view}`;\n };\n\n // 현재 화면에 템플릿이 있는지 확인\n const checkHasTemplate = () => {\n const screenKey = getCurrentScreenKey();\n // screenKeyMapping에서 매핑된 키 찾기\n const mappedKey = screenKeyMapping[screenKey] || screenKey;\n return !!getFeatureDefinition(mappedKey);\n };\n\n // 템플릿에서 뱃지 로드\n const loadTemplateToCurrentScreen = () => {\n const screenKey = getCurrentScreenKey();\n // screenKeyMapping에서 매핑된 키 찾기\n const mappedKey = screenKeyMapping[screenKey] || screenKey;\n const template = getFeatureDefinition(mappedKey);\n\n if (!template) {\n alert('이 화면에 대한 기능정의 템플릿이 없습니다.');\n return;\n }\n\n // 템플릿에서 뱃지 생성\n const templateBadges = generateDefaultBadges(mappedKey);\n\n if (templateBadges.length === 0) {\n alert('템플릿에 정의된 뱃지가 없습니다.');\n return;\n }\n\n // 기존 뱃지의 마지막 번호 확인\n const existingBadges = featureBadges[screenKey] || [];\n const maxNumber = existingBadges.length > 0\n ? Math.max(...existingBadges.map(b => b.number || 0))\n : 0;\n\n // 새 뱃지에 번호 재할당 (기존 번호 이후부터)\n const newBadges = templateBadges.map((badge, index) => ({\n ...badge,\n id: `${screenKey}-template-${Date.now()}-${index}`,\n number: maxNumber + index + 1,\n author: userNickname || 'Template',\n createdAt: new Date().toISOString(),\n isFixed: false,\n // 위치를 비율 좌표로 설정 (템플릿에서는 position.x/y를 사용)\n x: badge.position?.x || badge.x || 50,\n y: badge.position?.y || badge.y || 50,\n absoluteX: null, // 렌더링 시 계산됨\n absoluteY: null,\n }));\n\n // 기존 뱃지에 추가\n setFeatureBadges(prev => ({\n ...prev,\n [screenKey]: [...existingBadges, ...newBadges]\n }));\n\n alert(`✅ ${newBadges.length}개의 기능정의 뱃지가 로드되었습니다.\\n\\n화면: ${template.screenName}\\n화면ID: ${template.screenId}`);\n };\n\n // 인라인 뱃지 편집용 상태\n const [editingBadge, setEditingBadge] = useState(null);\n\n // 인라인 뱃지 추가 (C키 → 클릭 → 입력 방식)\n const addInlineBadge = (badgeData) => {\n if (!inlineInputPosition) return;\n const screenKey = `${activeMenu}-${view}`;\n const newBadge = {\n id: Date.now(),\n number: nextBadgeNumber,\n label: badgeData.label || '', // 기능 제목 추가\n description: badgeData.description,\n uiInfo: badgeData.uiInfo,\n color: badgeData.color || 'red',\n // 절대 픽셀 좌표로 저장 (스크롤과 함께 움직이도록)\n absoluteX: inlineInputPosition.absoluteX,\n absoluteY: inlineInputPosition.absoluteY,\n // 레거시 호환성을 위해 비율도 저장\n x: inlineInputPosition.x,\n y: inlineInputPosition.y,\n author: userNickname,\n createdAt: new Date().toISOString(),\n };\n setFeatureBadges(prev => ({\n ...prev,\n [screenKey]: [...(prev[screenKey] || []), newBadge]\n }));\n setInlineInputPosition(null);\n };\n\n // 인라인 뱃지 수정 (기존 뱃지 클릭 시)\n const updateInlineBadge = (badgeData) => {\n if (!editingBadge) return;\n const screenKey = `${activeMenu}-${view}`;\n\n setFeatureBadges(prev => ({\n ...prev,\n [screenKey]: (prev[screenKey] || []).map(b =>\n b.id === editingBadge.id ? {\n ...b,\n label: badgeData.label || b.label || '', // 기능 제목 수정\n description: badgeData.description,\n uiInfo: badgeData.uiInfo,\n color: badgeData.color || b.color\n } : b\n )\n }));\n setEditingBadge(null);\n };\n\n // 뱃지 업데이트\n const updateFeatureBadge = (badgeId, updates) => {\n const screenKey = `${activeMenu}-${view}`;\n\n setFeatureBadges(prev => ({\n ...prev,\n [screenKey]: (prev[screenKey] || []).map(b =>\n b.id === badgeId ? { ...b, ...updates } : b\n )\n }));\n };\n\n // 뱃지 삭제\n const deleteFeatureBadge = (badgeId) => {\n const screenKey = `${activeMenu}-${view}`;\n\n setFeatureBadges(prev => ({\n ...prev,\n [screenKey]: (prev[screenKey] || []).filter(b => b.id !== badgeId)\n }));\n setSelectedBadge(null);\n };\n\n // 뱃지에 댓글 추가\n const addBadgeComment = (badgeId, commentText) => {\n const screenKey = `${activeMenu}-${view}`;\n const newComment = {\n id: Date.now(),\n text: commentText,\n author: userNickname || '익명',\n createdAt: new Date().toISOString()\n };\n\n setFeatureBadges(prev => ({\n ...prev,\n [screenKey]: (prev[screenKey] || []).map(b =>\n b.id === badgeId ? {\n ...b,\n comments: [...(b.comments || []), newComment]\n } : b\n )\n }));\n\n // selectedBadge도 업데이트\n setSelectedBadge(prev => {\n if (prev && prev.id === badgeId) {\n return {\n ...prev,\n comments: [...(prev.comments || []), newComment]\n };\n }\n return prev;\n });\n };\n\n // 뱃지 댓글 삭제\n const deleteBadgeComment = (badgeId, commentId) => {\n const screenKey = `${activeMenu}-${view}`;\n\n setFeatureBadges(prev => ({\n ...prev,\n [screenKey]: (prev[screenKey] || []).map(b =>\n b.id === badgeId ? {\n ...b,\n comments: (b.comments || []).filter(c => c.id !== commentId)\n } : b\n )\n }));\n\n // selectedBadge도 업데이트\n setSelectedBadge(prev => {\n if (prev && prev.id === badgeId) {\n return {\n ...prev,\n comments: (prev.comments || []).filter(c => c.id !== commentId)\n };\n }\n return prev;\n });\n };\n\n // 비밀번호 확인 후 삭제 (인라인 팝업용)\n const DELETE_PASSWORD = '1234';\n const deleteWithPassword = (badgeId, onSuccess) => {\n const password = window.prompt('삭제하려면 비밀번호를 입력하세요:');\n if (password === null) return; // 취소\n if (password === DELETE_PASSWORD) {\n deleteFeatureBadge(badgeId);\n if (onSuccess) onSuccess();\n } else {\n alert('비밀번호가 올바르지 않습니다.');\n }\n };\n\n // 현재 사용자 상태 (역할별 로그인)\n const [currentUser, setCurrentUser] = useState({\n id: 1,\n name: '김대표',\n role: 'ceo',\n roleName: '대표이사',\n avatar: null,\n });\n\n // 사용자 목록 (테스트용)\n const users = [\n { id: 1, name: '김대표', role: 'ceo', roleName: '대표이사' },\n { id: 2, name: '이판매', role: 'sales', roleName: '판매팀' },\n { id: 3, name: '박생산', role: 'production', roleName: '생산관리' },\n { id: 4, name: '최품질', role: 'quality', roleName: '품질관리' },\n { id: 5, name: '정회계', role: 'accounting', roleName: '회계팀' },\n { id: 6, name: '강구매', role: 'purchase', roleName: '구매팀' },\n { id: 7, name: '김생산', role: 'worker', roleName: '생산담당자' },\n ];\n\n // 사용자 변경 핸들러\n const handleUserChange = (userId) => {\n const user = users.find(u => u.id === parseInt(userId));\n if (user) {\n setCurrentUser(user);\n // 역할에 따른 기본 대시보드로 이동\n const dashboardMap = {\n ceo: 'dashboard',\n sales: 'sales-dashboard',\n production: 'production-dashboard',\n quality: 'quality-dashboard',\n accounting: 'accounting-dashboard',\n purchase: 'purchase-dashboard',\n worker: 'worker-dashboard',\n };\n setView(dashboardMap[user.role] || 'dashboard');\n setActiveMenu('dashboard');\n }\n };\n\n // 반응형 훅\n const { isMobile, isTablet, isDesktop } = useResponsive();\n\n const [orders, setOrders] = useState(initialOrders);\n const [quotes, setQuotes] = useState(sampleQuotesData);\n const [productionOrders, setProductionOrders] = useState(integratedProductionOrderMaster);\n const [workOrders, setWorkOrders] = useState(initialWorkOrders);\n const [workResults, setWorkResults] = useState(initialWorkResults);\n const [shipments, setShipments] = useState(initialShipments);\n const [productInspections, setProductInspections] = useState(initialProductInspections);\n const [purchaseOrders, setPurchaseOrders] = useState([\n { id: 1, poNo: 'KD-SO-250301-001', materialCode: 'RM-COIL-05', materialName: '갈바코일 0.5T', requestQty: 10, unit: '톤', vendor: '철강공업', supplierName: '철강공업', status: '배송중', requestDate: '2025-03-01', requester: '강구매', qty: 10 },\n { id: 2, poNo: 'KD-SO-250302-001', materialCode: 'PP-MOTOR-01', materialName: '튜블러모터', requestQty: 50, unit: 'EA', vendor: '모터공급사', supplierName: '모터공급사', status: '발주완료', requestDate: '2025-03-02', requester: '강구매', qty: 50 },\n // 수입검사(IQC) 대기 항목 - 입고대기/검사대기 상태\n { id: 3, poNo: 'KD-SO-250310-001', materialCode: 'RM-SCREEN-01', materialName: '방충망 원단 1016', requestQty: 100, unit: 'M', vendor: '한국망사', supplierName: '한국망사', status: '검사대기', requestDate: '2025-03-10', requester: '김자재', qty: 100, arrivalDate: '2025-03-12' },\n { id: 4, poNo: 'KD-SO-250311-001', materialCode: 'PP-ENDLOCK-01', materialName: '앤드락 2100', requestQty: 200, unit: 'EA', vendor: '부품산업', supplierName: '부품산업', status: '입고대기', requestDate: '2025-03-11', requester: '김자재', qty: 200, arrivalDate: '2025-03-13' },\n { id: 5, poNo: 'KD-SO-250312-001', materialCode: 'RM-COIL-08', materialName: '갈바코일 0.8T', requestQty: 5, unit: '톤', vendor: '포항철강', supplierName: '포항철강', status: '검사대기', requestDate: '2025-03-12', requester: '강구매', qty: 5, arrivalDate: '2025-03-14' },\n ]);\n\n // 검사 이력 (수입검사, 중간검사 통합)\n const [allInspections, setAllInspections] = useState([\n // 수입검사(IQC) 이력 - LOT 채번 규칙: YYMMDD-## (예: 250308-01)\n { id: 1, type: 'incoming', inspectionDate: '2025-03-08', targetNo: 'PO-250305-001', lotNo: '250308-01', itemName: '방충망 원단 1270', result: '합격', inspector: '품질팀 박검사', qty: 80, passQty: 80, failQty: 0 },\n { id: 2, type: 'incoming', inspectionDate: '2025-03-07', targetNo: 'PO-250303-001', lotNo: '250307-01', itemName: '튜블러모터 50W', result: '불합격', inspector: '품질팀 박검사', qty: 30, passQty: 25, failQty: 5, ncrNo: 'NCR-250307-001' },\n { id: 3, type: 'incoming', inspectionDate: '2025-03-06', targetNo: 'PO-250302-002', lotNo: '250306-01', itemName: '앤드락 1800', result: '합격', inspector: '품질팀 이검사', qty: 150, passQty: 150, failQty: 0 },\n // 중간검사(PQC) 이력\n { id: 4, type: 'process', inspectionDate: '2025-03-09', targetNo: 'KD-WO-240115-01', lotNo: 'PQC-250309-01', itemName: '스크린 원단절단', result: '합격', inspector: '품질팀 이검사', qty: 10, passQty: 10, failQty: 0 },\n { id: 5, type: 'process', inspectionDate: '2025-03-08', targetNo: 'KD-WO-240115-02', lotNo: 'PQC-250308-01', itemName: '슬랫 코일절단', result: '불합격', inspector: '품질팀 박검사', qty: 5, passQty: 3, failQty: 2, ncrNo: 'NCR-250308-001' },\n // 제품검사(FQC) 이력\n { id: 6, type: 'product', inspectionDate: '2025-03-07', targetNo: 'KD-SA-250307-01', lotNo: 'FQC-250307-01', itemName: '스크린 셔터 (표준형)', result: '합격', inspector: '품질팀 최검사', qty: 3, passQty: 3, failQty: 0 },\n { id: 7, type: 'product', inspectionDate: '2025-03-05', targetNo: 'KD-SA-250305-01', lotNo: 'FQC-250305-01', itemName: '슬랫 셔터', result: '불합격', inspector: '품질팀 최검사', qty: 2, passQty: 1, failQty: 1, ncrNo: 'NCR-250305-002' },\n ]);\n\n // 부적합품 목록\n const [defects, setDefects] = useState([\n {\n id: 1,\n ncrNo: 'NCR-250307-001',\n registDate: '2025-03-07',\n source: 'IQC',\n sourceNo: 'PO-250303-001',\n itemName: '튜블러모터 50W',\n defectType: '외관불량',\n defectQty: 5,\n totalQty: 30,\n description: '모터 케이스 도장 벗겨짐 및 찍힘 발견',\n vendorName: '모터공급사',\n status: '처리대기',\n registeredBy: '품질팀 박검사',\n images: [],\n approvalLine: {\n type: 'APR-3LINE',\n roles: [\n { id: 'writer', label: '작성', name: '품질팀 박검사', date: '2025-03-07', status: 'approved' },\n { id: 'reviewer', label: '검토', name: '', date: '', status: 'pending' },\n { id: 'approver', label: '승인', name: '', date: '', status: 'pending' },\n ]\n }\n },\n {\n id: 2,\n ncrNo: 'NCR-250308-001',\n registDate: '2025-03-08',\n source: 'PQC',\n sourceNo: 'KD-WO-240115-02',\n itemName: '슬랫 코일절단',\n defectType: '치수불량',\n defectQty: 2,\n totalQty: 5,\n description: '코일 절단 길이 규격 초과 (3010mm, 기준 3000±5mm)',\n vendorName: null,\n status: '처리대기',\n registeredBy: '품질팀 박검사',\n images: [],\n processType: '재작업',\n approvalLine: {\n type: 'APR-3LINE',\n roles: [\n { id: 'writer', label: '작성', name: '품질팀 박검사', date: '2025-03-08', status: 'approved' },\n { id: 'reviewer', label: '검토', name: '생산팀 김팀장', date: '2025-03-08', status: 'approved' },\n { id: 'approver', label: '승인', name: '', date: '', status: 'pending' },\n ]\n }\n },\n {\n id: 3,\n ncrNo: 'NCR-250305-001',\n registDate: '2025-03-05',\n source: 'IQC',\n sourceNo: 'PO-250301-002',\n itemName: '방충망 원단 1524',\n defectType: '이물질혼입',\n defectQty: 10,\n totalQty: 50,\n description: '원단 중간에 금속 이물질 혼입 확인',\n vendorName: '한국망사',\n status: '처리완료',\n processResult: '반품',\n processDate: '2025-03-06',\n processBy: '자재팀 강대리',\n registeredBy: '품질팀 이검사',\n images: [],\n approvalLine: {\n type: 'APR-3LINE',\n roles: [\n { id: 'writer', label: '작성', name: '품질팀 이검사', date: '2025-03-05', status: 'approved' },\n { id: 'reviewer', label: '검토', name: '품질팀 김팀장', date: '2025-03-05', status: 'approved' },\n { id: 'approver', label: '승인', name: '공장장 박상무', date: '2025-03-06', status: 'approved' },\n ]\n }\n },\n {\n id: 4,\n ncrNo: 'NCR-250309-001',\n registDate: '2025-03-09',\n source: 'FQC',\n sourceNo: 'KD-SA-250309-01',\n itemName: '스크린 셔터 (프리미엄)',\n defectType: '기능불량',\n defectQty: 1,\n totalQty: 2,\n description: '모터 작동 시 소음 발생 및 간헐적 정지 현상',\n vendorName: null,\n status: '승인대기',\n registeredBy: '품질팀 최검사',\n images: [],\n processType: '재작업',\n approvalLine: {\n type: 'APR-3LINE',\n roles: [\n { id: 'writer', label: '작성', name: '품질팀 최검사', date: '2025-03-09', status: 'approved' },\n { id: 'reviewer', label: '검토', name: '품질팀 김팀장', date: '2025-03-09', status: 'approved' },\n { id: 'approver', label: '승인', name: '', date: '', status: 'pending' },\n ]\n }\n },\n ]);\n\n // ═══════════════════════════════════════════════════════════════════\n // LOT 시퀀스 관리 시스템 (자동 번호 생성)\n // ═══════════════════════════════════════════════════════════════════\n const [lotSequences, setLotSequences] = useState({\n // 날짜별 시퀀스 관리 (YYMMDD: seq)\n incoming: {}, // 입고LOT: YYMMDD-##\n production: {}, // 생산LOT: KD-PL-YYMMDD-##\n incomingInsp: {}, // 입고검사LOT: YYMMDD-##\n processInsp: {}, // 중간검사LOT: KD-WE-YYMMDD-##-(step)\n productInsp: {}, // 제품검사LOT: KD-SA-YYMMDD-##\n finished: {}, // 완제품LOT: 수주번호-##\n qc: {}, // 일반검사LOT: KD-QC-YYMMDD-##\n });\n\n // 다음 시퀀스 번호 가져오기\n const getNextSequence = (type, dateKey) => {\n const currentSeq = lotSequences[type]?.[dateKey] || 0;\n return currentSeq + 1;\n };\n\n // 시퀀스 번호 업데이트\n const updateSequence = (type, dateKey) => {\n setLotSequences(prev => ({\n ...prev,\n [type]: {\n ...prev[type],\n [dateKey]: (prev[type]?.[dateKey] || 0) + 1\n }\n }));\n };\n\n // ★ 입고LOT 자동생성: YYMMDD-##\n const autoGenerateIncomingLot = (date = new Date()) => {\n const dateKey = formatDateCode(date);\n const seq = getNextSequence('incoming', dateKey);\n updateSequence('incoming', dateKey);\n return generateIncomingInspectionLot(date, seq);\n };\n\n // ★ 생산LOT 자동생성: KD-PL-YYMMDD-##\n const autoGenerateProductionLot = (date = new Date()) => {\n const dateKey = formatDateCode(date);\n const seq = getNextSequence('production', dateKey);\n updateSequence('production', dateKey);\n return generateProductionLot(date, seq);\n };\n\n // ★ 입고검사LOT 자동생성: YYMMDD-## (입고LOT와 동일 체계)\n const autoGenerateIncomingInspLot = (date = new Date()) => {\n const dateKey = formatDateCode(date);\n const seq = getNextSequence('incomingInsp', dateKey);\n updateSequence('incomingInsp', dateKey);\n return generateIncomingInspectionLot(date, seq);\n };\n\n // ★ 중간검사LOT 자동생성: KD-WE-YYMMDD-##-(step)\n const autoGenerateProcessInspLot = (date = new Date(), step = 1) => {\n const dateKey = formatDateCode(date);\n const seq = getNextSequence('processInsp', dateKey);\n updateSequence('processInsp', dateKey);\n return generateProcessInspectionLot(date, seq, step);\n };\n\n // ★ 제품검사LOT 자동생성: KD-SA-YYMMDD-##\n const autoGenerateProductInspLot = (date = new Date()) => {\n const dateKey = formatDateCode(date);\n const seq = getNextSequence('productInsp', dateKey);\n updateSequence('productInsp', dateKey);\n return generateProductInspectionLot(date, seq);\n };\n\n // ★ 완제품LOT 자동생성: 수주번호-## (제품검사 직전)\n const autoGenerateFinishedLot = (orderNo) => {\n const seq = getNextSequence('finished', orderNo);\n updateSequence('finished', orderNo);\n return generateProductLot(orderNo, seq);\n };\n\n // ★ 일반검사LOT 자동생성: KD-QC-YYMMDD-##\n const autoGenerateQCLot = (date = new Date()) => {\n const dateKey = formatDateCode(date);\n const seq = getNextSequence('qc', dateKey);\n updateSequence('qc', dateKey);\n return generateQCLot(date, seq);\n };\n\n // ═══════════════════════════════════════════════════════════════════\n // 회계 자동화 시스템\n // ═══════════════════════════════════════════════════════════════════\n\n // 세금계산서 목록\n const [taxInvoices, setTaxInvoices] = useState([]);\n\n // 거래명세서 목록\n const [transactionStatements, setTransactionStatements] = useState([]);\n\n // 미수금 현황\n const [receivables, setReceivables] = useState([]);\n\n // ★ 세금계산서 자동발행 (출하완료 시 호출)\n const autoGenerateTaxInvoice = (shipment, order) => {\n const invoiceNo = `TI-${formatDateCode(new Date())}-${String(taxInvoices.length + 1).padStart(3, '0')}`;\n const newInvoice = {\n id: taxInvoices.length + 1,\n invoiceNo,\n issueDate: new Date().toISOString().split('T')[0],\n customerId: order.customerId,\n customerName: order.customerName,\n businessNo: order.businessNo || '',\n orderNo: order.orderNo,\n shipmentNo: shipment.shipmentNo,\n items: order.items || [],\n supplyAmount: order.totalAmount || 0,\n taxAmount: Math.round((order.totalAmount || 0) * 0.1),\n totalAmount: Math.round((order.totalAmount || 0) * 1.1),\n status: '발행완료',\n issuedBy: currentUser.name,\n createdAt: new Date().toISOString(),\n };\n setTaxInvoices(prev => [...prev, newInvoice]);\n\n // 미수금 자동 등록\n autoCreateReceivable(newInvoice, order);\n\n return newInvoice;\n };\n\n // ★ 거래명세서 자동생성 (출하완료 시 호출)\n const autoGenerateTransactionStatement = (shipment, order) => {\n const statementNo = `TS-${formatDateCode(new Date())}-${String(transactionStatements.length + 1).padStart(3, '0')}`;\n const newStatement = {\n id: transactionStatements.length + 1,\n statementNo,\n issueDate: new Date().toISOString().split('T')[0],\n customerId: order.customerId,\n customerName: order.customerName,\n orderNo: order.orderNo,\n shipmentNo: shipment.shipmentNo,\n items: order.items || [],\n totalAmount: order.totalAmount || 0,\n status: '발송대기',\n createdAt: new Date().toISOString(),\n };\n setTransactionStatements(prev => [...prev, newStatement]);\n return newStatement;\n };\n\n // ★ 미수금 자동등록\n const autoCreateReceivable = (invoice, order) => {\n const newReceivable = {\n id: receivables.length + 1,\n invoiceNo: invoice.invoiceNo,\n customerId: order.customerId,\n customerName: order.customerName,\n creditGrade: order.creditGrade || 'B',\n orderNo: order.orderNo,\n invoiceDate: invoice.issueDate,\n dueDate: calculateDueDate(invoice.issueDate, order.paymentTerms || 30),\n amount: invoice.totalAmount,\n paidAmount: 0,\n balance: invoice.totalAmount,\n status: '미수',\n createdAt: new Date().toISOString(),\n };\n setReceivables(prev => [...prev, newReceivable]);\n return newReceivable;\n };\n\n // ★ 입금 자동반영\n const autoApplyPayment = (customerId, amount, paymentDate = new Date()) => {\n let remainingAmount = amount;\n const updatedReceivables = receivables.map(r => {\n if (r.customerId === customerId && r.balance > 0 && remainingAmount > 0) {\n const payAmount = Math.min(r.balance, remainingAmount);\n remainingAmount -= payAmount;\n return {\n ...r,\n paidAmount: r.paidAmount + payAmount,\n balance: r.balance - payAmount,\n status: r.balance - payAmount <= 0 ? '완납' : '일부입금',\n lastPaymentDate: paymentDate.toISOString().split('T')[0],\n };\n }\n return r;\n });\n setReceivables(updatedReceivables);\n\n // C등급 고객 출하승인 자동 처리\n const customer = orders.find(o => o.customerId === customerId);\n if (customer?.creditGrade === 'C') {\n autoApproveCGradeShipment(customerId);\n }\n\n return remainingAmount; // 남은 금액 반환 (0이면 전액 처리)\n };\n\n // 결제 만기일 계산\n const calculateDueDate = (issueDate, terms) => {\n const date = new Date(issueDate);\n date.setDate(date.getDate() + terms);\n return date.toISOString().split('T')[0];\n };\n\n // ═══════════════════════════════════════════════════════════════════\n // C등급 출하승인 자동화\n // ═══════════════════════════════════════════════════════════════════\n\n // ★ C등급 고객 입금확인 후 출하승인 자동처리\n const autoApproveCGradeShipment = (customerId) => {\n setShipments(prev => prev.map(s => {\n if (s.customerId === customerId && s.status === '출하보류') {\n return {\n ...s,\n status: '출하준비',\n approvedAt: new Date().toISOString(),\n approvedBy: '시스템(입금확인)',\n approvalNote: '입금확인으로 자동 출하승인',\n };\n }\n return s;\n }));\n };\n\n // ★ 출하보류 자동처리 (C등급 또는 미수금 과다 시)\n const autoHoldShipment = (shipmentId, reason) => {\n setShipments(prev => prev.map(s => {\n if (s.id === shipmentId) {\n return {\n ...s,\n status: '출하보류',\n holdReason: reason,\n holdAt: new Date().toISOString(),\n };\n }\n return s;\n }));\n };\n\n // ═══════════════════════════════════════════════════════════════════\n // 할인정책 자동적용 시스템\n // ═══════════════════════════════════════════════════════════════════\n\n // 할인 정책 정의\n const discountPolicies = {\n // 거래처 등급별 기본 할인율\n gradeDiscount: {\n 'A': 5, // A등급: 5% 기본 할인\n 'B': 3, // B등급: 3% 기본 할인\n 'C': 0, // C등급: 할인 없음\n },\n // 수량별 추가 할인\n volumeDiscount: [\n { minQty: 100, rate: 2 }, // 100개 이상: 2% 추가\n { minQty: 50, rate: 1 }, // 50개 이상: 1% 추가\n { minQty: 0, rate: 0 }, // 기본\n ],\n // 대량주문 할인 (금액 기준)\n bulkDiscount: [\n { minAmount: 50000000, rate: 5 }, // 5천만원 이상: 5% 추가\n { minAmount: 30000000, rate: 3 }, // 3천만원 이상: 3% 추가\n { minAmount: 10000000, rate: 1 }, // 1천만원 이상: 1% 추가\n { minAmount: 0, rate: 0 },\n ],\n // 결제조건별 할인\n paymentDiscount: {\n '선금결제': 3, // 선금결제: 3% 추가\n '현금결제': 2, // 현금결제: 2% 추가\n '30일이내': 1, // 30일 이내 결제: 1% 추가\n },\n };\n\n // ★ 할인율 자동 계산\n const autoCalculateDiscount = (customerId, totalAmount, totalQty, paymentTerms) => {\n const customer = sampleCustomersData.find(c => c.id === customerId);\n if (!customer) return { discountRate: 0, discountAmount: 0, breakdown: [] };\n\n const breakdown = [];\n let totalDiscountRate = 0;\n\n // 1. 거래처 기본 할인율 (거래처에 설정된 값 우선)\n const customerDiscount = customer.discountRate || 0;\n if (customerDiscount > 0) {\n breakdown.push({ type: '거래처 기본', rate: customerDiscount });\n totalDiscountRate += customerDiscount;\n } else {\n // 등급별 기본 할인\n const gradeRate = discountPolicies.gradeDiscount[customer.creditGrade] || 0;\n if (gradeRate > 0) {\n breakdown.push({ type: `${customer.creditGrade}등급 기본`, rate: gradeRate });\n totalDiscountRate += gradeRate;\n }\n }\n\n // 2. 수량별 할인\n const volumeRule = discountPolicies.volumeDiscount.find(r => totalQty >= r.minQty);\n if (volumeRule && volumeRule.rate > 0) {\n breakdown.push({ type: `대량주문(${totalQty}개)`, rate: volumeRule.rate });\n totalDiscountRate += volumeRule.rate;\n }\n\n // 3. 금액별 할인\n const bulkRule = discountPolicies.bulkDiscount.find(r => totalAmount >= r.minAmount);\n if (bulkRule && bulkRule.rate > 0) {\n breakdown.push({ type: `대량금액(${(totalAmount / 10000).toLocaleString()}만원)`, rate: bulkRule.rate });\n totalDiscountRate += bulkRule.rate;\n }\n\n // 4. 결제조건별 할인\n const paymentRate = discountPolicies.paymentDiscount[paymentTerms] || 0;\n if (paymentRate > 0) {\n breakdown.push({ type: `결제조건(${paymentTerms})`, rate: paymentRate });\n totalDiscountRate += paymentRate;\n }\n\n // 최대 할인율 제한 (20%)\n totalDiscountRate = Math.min(totalDiscountRate, 20);\n\n const discountAmount = Math.round(totalAmount * totalDiscountRate / 100);\n\n return {\n discountRate: totalDiscountRate,\n discountAmount,\n finalAmount: totalAmount - discountAmount,\n breakdown,\n };\n };\n\n // ═══════════════════════════════════════════════════════════════════\n // 공정별 작업자 할당 시스템\n // ═══════════════════════════════════════════════════════════════════\n\n // 작업자 목록\n const [workers] = useState([\n { id: 1, name: '김작업', dept: '생산1팀', skills: ['스크린', '슬랫', '조립'], status: '근무중', currentWork: null },\n { id: 2, name: '이생산', dept: '생산1팀', skills: ['스크린', '절곡'], status: '근무중', currentWork: null },\n { id: 3, name: '박공정', dept: '생산2팀', skills: ['슬랫', '조립', '포장'], status: '근무중', currentWork: null },\n { id: 4, name: '최조립', dept: '생산2팀', skills: ['조립', '포장', '검사'], status: '근무중', currentWork: null },\n { id: 5, name: '정설비', dept: '설비팀', skills: ['설비관리', '스크린', '슬랫'], status: '근무중', currentWork: null },\n ]);\n\n // ★ 작업자 자동 할당 (공정 스킬 기반)\n const autoAssignWorker = (workOrder) => {\n const processType = workOrder.processType;\n\n // 해당 공정 스킬을 가진 가용 작업자 찾기\n const availableWorkers = workers.filter(w =>\n w.status === '근무중' &&\n !w.currentWork &&\n w.skills.includes(processType)\n );\n\n if (availableWorkers.length === 0) {\n // 스킬은 있지만 다른 작업 중인 작업자 찾기\n const busyWorkers = workers.filter(w =>\n w.status === '근무중' &&\n w.skills.includes(processType)\n );\n if (busyWorkers.length > 0) {\n return {\n assigned: false,\n message: `${processType} 공정 가능 작업자가 모두 작업 중입니다.`,\n suggestedWorkers: busyWorkers.map(w => w.name),\n };\n }\n return {\n assigned: false,\n message: `${processType} 공정 가능한 작업자가 없습니다.`,\n suggestedWorkers: [],\n };\n }\n\n // 첫 번째 가용 작업자 할당\n const assignedWorker = availableWorkers[0];\n return {\n assigned: true,\n worker: assignedWorker,\n message: `${assignedWorker.name} 작업자가 자동 할당되었습니다.`,\n };\n };\n\n // ═══════════════════════════════════════════════════════════════════\n // 설비 가동률 추적 시스템\n // ═══════════════════════════════════════════════════════════════════\n\n // 설비 목록 및 가동 현황\n const [equipments, setEquipments] = useState([\n { id: 1, code: 'SCR-001', name: '스크린 재단기 1호', type: '스크린', status: '가동중', startTime: null, totalRunTime: 0, totalDownTime: 0 },\n { id: 2, code: 'SCR-002', name: '스크린 재단기 2호', type: '스크린', status: '대기', startTime: null, totalRunTime: 0, totalDownTime: 0 },\n { id: 3, code: 'SLT-001', name: '슬랫 절단기', type: '슬랫', status: '가동중', startTime: null, totalRunTime: 0, totalDownTime: 0 },\n { id: 4, code: 'BND-001', name: '절곡기 1호', type: '절곡', status: '가동중', startTime: null, totalRunTime: 0, totalDownTime: 0 },\n { id: 5, code: 'BND-002', name: '절곡기 2호', type: '절곡', status: '점검중', startTime: null, totalRunTime: 0, totalDownTime: 0 },\n { id: 6, code: 'ASM-001', name: '조립 라인 1', type: '조립', status: '가동중', startTime: null, totalRunTime: 0, totalDownTime: 0 },\n ]);\n\n // 설비 가동 이력\n const [equipmentLogs, setEquipmentLogs] = useState([]);\n\n // ★ 설비 가동 시작\n const startEquipment = (equipmentId, workOrderNo) => {\n const now = new Date();\n setEquipments(prev => prev.map(eq => {\n if (eq.id === equipmentId) {\n return {\n ...eq,\n status: '가동중',\n startTime: now.toISOString(),\n currentWorkOrder: workOrderNo,\n };\n }\n return eq;\n }));\n\n setEquipmentLogs(prev => [...prev, {\n id: Date.now(),\n equipmentId,\n workOrderNo,\n type: '가동시작',\n timestamp: now.toISOString(),\n operator: currentUser.name,\n }]);\n };\n\n // ★ 설비 가동 종료\n const stopEquipment = (equipmentId, reason = '작업완료') => {\n const now = new Date();\n setEquipments(prev => prev.map(eq => {\n if (eq.id === equipmentId && eq.startTime) {\n const runTime = (now - new Date(eq.startTime)) / 1000 / 60; // 분 단위\n return {\n ...eq,\n status: reason === '작업완료' ? '대기' : '비가동',\n startTime: null,\n totalRunTime: eq.totalRunTime + runTime,\n currentWorkOrder: null,\n lastStopReason: reason,\n };\n }\n return eq;\n }));\n\n setEquipmentLogs(prev => [...prev, {\n id: Date.now(),\n equipmentId,\n type: '가동종료',\n reason,\n timestamp: now.toISOString(),\n operator: currentUser.name,\n }]);\n };\n\n // ★ 설비 가동률 계산\n const calculateEquipmentUtilization = (equipmentId, periodDays = 30) => {\n const equipment = equipments.find(eq => eq.id === equipmentId);\n if (!equipment) return 0;\n\n const totalAvailableMinutes = periodDays * 24 * 60; // 총 가용 시간 (분)\n const utilizationRate = (equipment.totalRunTime / totalAvailableMinutes) * 100;\n\n return Math.min(Math.round(utilizationRate * 10) / 10, 100); // 소수점 1자리\n };\n\n // ★ 전체 설비 가동률 요약\n const getEquipmentUtilizationSummary = () => {\n const summary = {\n total: equipments.length,\n running: equipments.filter(eq => eq.status === '가동중').length,\n idle: equipments.filter(eq => eq.status === '대기').length,\n maintenance: equipments.filter(eq => eq.status === '점검중').length,\n down: equipments.filter(eq => eq.status === '비가동').length,\n avgUtilization: 0,\n };\n\n if (summary.total > 0) {\n const totalUtil = equipments.reduce((sum, eq) => sum + calculateEquipmentUtilization(eq.id), 0);\n summary.avgUtilization = Math.round(totalUtil / summary.total);\n }\n\n return summary;\n };\n\n // ═══════════════════════════════════════════════════════════════════\n // 검사성적서 자동생성 시스템\n // ═══════════════════════════════════════════════════════════════════\n\n // 검사성적서 목록\n const [inspectionCertificates, setInspectionCertificates] = useState([]);\n\n // ★ 검사성적서 자동생성 (검사 합격 시 호출)\n const autoGenerateInspectionCertificate = (inspection, inspectionType) => {\n const today = new Date();\n const certNo = `CERT-${formatDateCode(today)}-${String(inspectionCertificates.length + 1).padStart(3, '0')}`;\n\n // 검사 유형별 성적서 템플릿\n const certTypeMap = {\n 'incoming': { type: 'IQC', title: '수입검사 성적서' },\n 'process': { type: 'PQC', title: '중간검사 성적서' },\n 'product': { type: 'FQC', title: '제품검사 성적서' },\n };\n\n const certConfig = certTypeMap[inspectionType] || certTypeMap['product'];\n\n const newCertificate = {\n id: inspectionCertificates.length + 1,\n certNo,\n type: certConfig.type,\n title: certConfig.title,\n inspectionLotNo: inspection.lotNo,\n inspectionDate: inspection.inspectionDate || today.toISOString().split('T')[0],\n itemName: inspection.itemName || inspection.productName || inspection.materialName,\n qty: inspection.qty || inspection.passQty,\n passQty: inspection.passQty || inspection.qty,\n failQty: inspection.failQty || 0,\n result: inspection.result || '합격',\n inspector: inspection.inspector || currentUser.name,\n // 검사 항목 상세\n inspectionItems: inspection.inspectionItems || [\n { item: '외관검사', standard: '표면결함없음', result: '합격', value: 'OK' },\n { item: '치수검사', standard: '규격±0.5mm', result: '합격', value: '규격내' },\n { item: '기능검사', standard: '정상작동', result: '합격', value: 'OK' },\n ],\n // 메타 정보\n orderNo: inspection.orderNo,\n workOrderNo: inspection.workOrderNo,\n customerName: inspection.customerName,\n vendorName: inspection.vendorName,\n // 발행 정보\n issuedBy: currentUser.name,\n issuedAt: today.toISOString(),\n status: '발행완료',\n // 승인 정보\n approvalLine: {\n inspector: { name: currentUser.name, date: today.toISOString().split('T')[0], status: 'approved' },\n reviewer: { name: '', date: '', status: 'pending' },\n approver: { name: '', date: '', status: 'pending' },\n },\n };\n\n setInspectionCertificates(prev => [...prev, newCertificate]);\n\n console.log(`[검사성적서 자동생성] ${certNo} - ${certConfig.title}`);\n return newCertificate;\n };\n\n // ★ 검사 합격 시 성적서 자동 발행 (제품검사 완료 핸들러)\n const handleProductInspectionComplete = (inspectionId, result, inspectorName) => {\n const today = new Date().toISOString().split('T')[0];\n\n setProductInspections(prev => prev.map(insp => {\n if (insp.id === inspectionId) {\n const updatedInsp = {\n ...insp,\n status: result,\n inspectionDate: today,\n inspector: inspectorName || currentUser.name,\n result,\n };\n\n // ★ 합격 시 검사성적서 자동 생성\n if (result === '합격') {\n const cert = autoGenerateInspectionCertificate(updatedInsp, 'product');\n updatedInsp.certificateNo = cert.certNo;\n }\n\n // 불합격 시 NCR 자동 생성\n if (result === '불합격') {\n const ncrNo = `NCR-${formatDateCode(new Date())}-${String(defects.length + 1).padStart(3, '0')}`;\n const newDefect = {\n id: defects.length + 1,\n ncrNo,\n registDate: today,\n source: 'FQC',\n sourceType: '제품검사',\n sourceNo: insp.lotNo,\n itemName: insp.productName,\n defectQty: insp.qty,\n totalQty: insp.qty,\n defectType: '제품불량',\n description: `제품검사 불합격`,\n status: '처리대기',\n registeredBy: currentUser.name,\n };\n setDefects(prev => [...prev, newDefect]);\n updatedInsp.ncrNo = ncrNo;\n }\n\n return updatedInsp;\n }\n return insp;\n }));\n\n if (result === '합격') {\n alert(`✅ 제품검사가 완료되었습니다.\\n\\n결과: 합격\\n\\n📄 검사성적서가 자동 발행되었습니다.\\n[품질관리 > 검사성적서]에서 확인하세요.`);\n } else {\n alert(`⚠️ 제품검사가 완료되었습니다.\\n\\n결과: 불합격\\n\\n📋 NCR(부적합보고서)가 자동 생성되었습니다.\\n[품질관리 > 부적합관리]에서 처리하세요.`);\n }\n };\n\n // 품목유형 정의 (7개 - 모든 회사 공통)\n const itemTypes = [\n { code: 'FG', name: '제품', nameEn: 'Finished Goods' },\n { code: 'SA', name: '조립부품', nameEn: 'Sub-Assembly' },\n { code: 'BP', name: '절곡부품', nameEn: 'Bending Part' },\n { code: 'PP', name: '구매부품', nameEn: 'Purchased Part' },\n { code: 'SM', name: '부자재', nameEn: 'Sub Material' },\n { code: 'RM', name: '원자재', nameEn: 'Raw Material' },\n { code: 'CS', name: '소모품', nameEn: 'Consumables' },\n ];\n\n // ═══════════════════════════════════════════════════════════════════\n // LOT NO 코드 체계 (스크린샷 기반)\n // 형식: [제품]-[종류]-[날짜코드]-[모양&길이]\n // 예: G-I-4A05-53 → 가이드레일-일반-2024년5월5일-C형3000\n // ═══════════════════════════════════════════════════════════════════\n const lotCodeConfig = {\n // 제품 코드 (첫 번째 자리)\n productCodes: [\n { code: 'G', name: '가이드레일', description: 'Guide Rail' },\n { code: 'C', name: '케이스', description: 'Case' },\n { code: 'B', name: '하단마감재', description: 'Bottom Finish' },\n { code: 'S', name: '연기차단재', description: 'Smoke Block' },\n { code: 'L', name: 'L-Bar', description: 'L-Bar' },\n { code: 'M', name: '모터', description: 'Motor' },\n { code: 'T', name: '샤프트', description: 'Shaft' },\n ],\n // 종류 코드 (두 번째 자리)\n typeCodes: [\n { code: 'I', name: '일반', description: 'General' },\n { code: 'S', name: 'SUS마감', description: 'SUS Finish' },\n { code: 'E', name: 'EGI', description: 'EGI' },\n { code: 'D', name: 'D형', description: 'D-Type' },\n { code: 'C', name: 'C형', description: 'C-Type' },\n { code: 'F', name: '전면부', description: 'Front' },\n { code: 'B', name: '후면코너부', description: 'Back Corner' },\n { code: 'L', name: '린텔부', description: 'Lintel' },\n { code: 'P', name: '점검구', description: 'Inspection' },\n ],\n // 날짜 코드 (세 번째 자리) - [연도(A-Z)][월(01-12)][일(01-31)]\n // 연도: 2024=4A, 2025=5B, 2026=6C...\n yearCodes: {\n 2024: '4A', 2025: '5B', 2026: '6C', 2027: '7D', 2028: '8E', 2029: '9F', 2030: '0G'\n },\n // 모양&길이 코드 (네 번째 자리) - 모양코드(1자리) + 길이코드(1-2자리)\n shapeLengthCodes: {\n shapes: [\n { code: '1', name: '본체', description: 'Body' },\n { code: '2', name: 'D형', description: 'D-Type' },\n { code: '3', name: 'C형', description: 'C-Type' },\n { code: '4', name: 'SUS마감', description: 'SUS' },\n { code: '5', name: 'EGI', description: 'EGI' },\n { code: '6', name: '전면부', description: 'Front' },\n { code: '7', name: '후면코너부', description: 'Back' },\n { code: '8', name: '린텔부', description: 'Lintel' },\n { code: '9', name: '점검구', description: 'Inspect' },\n ],\n lengths: [\n { code: '1', length: 1219, display: '1219' },\n { code: '2', length: 2438, display: '2438' },\n { code: '3', length: 3000, display: '3000' },\n { code: '4', length: 3500, display: '3500' },\n { code: '5', length: 4000, display: '4000' },\n { code: '6', length: 4300, display: '4300' },\n { code: '7', length: 4500, display: '4500' },\n ]\n }\n };\n\n // Lot No 생성 함수\n const generateLotNo = (productCode, typeCode, date, shapeCode, lengthCode, sequence = 1) => {\n const d = new Date(date);\n const year = d.getFullYear();\n const month = String(d.getMonth() + 1).padStart(2, '0');\n const day = String(d.getDate()).padStart(2, '0');\n const yearCode = lotCodeConfig.yearCodes[year] || `${year % 10}X`;\n const dateCode = `${yearCode}${month}`;\n const shapeLengthCode = `${shapeCode}${lengthCode}`;\n return `${productCode}-${typeCode}-${dateCode}-${shapeLengthCode}`;\n };\n\n // ═══════════════════════════════════════════════════════════════════\n // 판매 LOT 번호 체계 (스크린샷 기반)\n // 형식: [현장코드]-[수주일자]-[순번]\n // 예: GYS-241105-001 → 광양제철소-2024년11월5일-1번\n // ═══════════════════════════════════════════════════════════════════\n const salesLotConfig = {\n // 현장 코드 예시 (실제 데이터는 sites에서 관리)\n siteCodeExamples: [\n { code: 'GYS', name: '광양제철소' },\n { code: 'POS', name: '포항제철' },\n { code: 'SEC', name: '삼성전자' },\n { code: 'HYN', name: '현대' },\n ],\n // 날짜 형식: YYMMDD\n dateFormat: 'YYMMDD',\n // 순번: 3자리 (001-999)\n sequenceDigits: 3,\n };\n\n // 판매 LOT No 생성 함수\n const generateSalesLotNo = (siteCode, orderDate, sequence) => {\n const d = new Date(orderDate);\n const yy = String(d.getFullYear()).slice(-2);\n const mm = String(d.getMonth() + 1).padStart(2, '0');\n const dd = String(d.getDate()).padStart(2, '0');\n const seq = String(sequence).padStart(3, '0');\n return `${siteCode}-${yy}${mm}${dd}-${seq}`;\n };\n\n // ═══════════════════════════════════════════════════════════════════\n // 인정제품 코드 체계 (스크린샷 기반)\n // KS, KQ 인정 제품 코드\n // ═══════════════════════════════════════════════════════════════════\n const certifiedProductCodes = [\n // KS 인정 제품 (일반형)\n {\n code: 'KSS01', name: 'KS 스크린방화셔터 일반형', certType: 'KS', productType: '스크린', subType: '일반형',\n description: 'KS F 3109 적합 스크린방화셔터', fireRating: '1시간'\n },\n {\n code: 'KSS02', name: 'KS 스크린방화셔터 대형', certType: 'KS', productType: '스크린', subType: '대형',\n description: 'KS F 3109 적합 대형 스크린방화셔터', fireRating: '1시간'\n },\n {\n code: 'KSL01', name: 'KS 슬랫방화셔터 일반형', certType: 'KS', productType: '슬랫', subType: '일반형',\n description: 'KS F 3109 적합 슬랫방화셔터', fireRating: '1시간'\n },\n {\n code: 'KSL02', name: 'KS 슬랫방화셔터 대형', certType: 'KS', productType: '슬랫', subType: '대형',\n description: 'KS F 3109 적합 대형 슬랫방화셔터', fireRating: '1시간'\n },\n {\n code: 'KSB01', name: 'KS 절곡방화셔터 일반형', certType: 'KS', productType: '절곡', subType: '일반형',\n description: 'KS F 3109 적합 절곡방화셔터', fireRating: '1시간'\n },\n\n // KQ (품질인정) 제품\n {\n code: 'KQTS01', name: 'KQ 스크린방화셔터 특수형', certType: 'KQ', productType: '스크린', subType: '특수형',\n description: '품질인정 특수 스크린방화셔터', fireRating: '2시간'\n },\n {\n code: 'KQTS02', name: 'KQ 스크린방화셔터 차연형', certType: 'KQ', productType: '스크린', subType: '차연형',\n description: '품질인정 차연 스크린방화셔터', fireRating: '1시간', smokeBlock: true\n },\n {\n code: 'KQTL01', name: 'KQ 슬랫방화셔터 특수형', certType: 'KQ', productType: '슬랫', subType: '특수형',\n description: '품질인정 특수 슬랫방화셔터', fireRating: '2시간'\n },\n {\n code: 'KQTL02', name: 'KQ 슬랫방화셔터 차연형', certType: 'KQ', productType: '슬랫', subType: '차연형',\n description: '품질인정 차연 슬랫방화셔터', fireRating: '1시간', smokeBlock: true\n },\n {\n code: 'KQTB01', name: 'KQ 절곡방화셔터 특수형', certType: 'KQ', productType: '절곡', subType: '특수형',\n description: '품질인정 특수 절곡방화셔터', fireRating: '2시간'\n },\n {\n code: 'KQTB02', name: 'KQ 절곡방화셔터 차연형', certType: 'KQ', productType: '절곡', subType: '차연형',\n description: '품질인정 차연 절곡방화셔터', fireRating: '1시간', smokeBlock: true\n },\n\n // 기타 인정 제품\n {\n code: 'CERTS01', name: '내화성능 인정 스크린방화셔터', certType: '내화인정', productType: '스크린', subType: '내화형',\n description: '내화성능 인정 스크린방화셔터', fireRating: '3시간'\n },\n {\n code: 'CERTL01', name: '내화성능 인정 슬랫방화셔터', certType: '내화인정', productType: '슬랫', subType: '내화형',\n description: '내화성능 인정 슬랫방화셔터', fireRating: '3시간'\n },\n ];\n\n // 인정제품 코드로 제품 정보 조회\n const getCertifiedProduct = (code) => certifiedProductCodes.find(p => p.code === code);\n\n // ═══════════════════════════════════════════════════════════════════\n // 원자재 입고 LOT 관리 (스크린샷 기반)\n // 형식: [자재코드]-[입고일자]-[순번]\n // ═══════════════════════════════════════════════════════════════════\n const [rawMaterialLots, setRawMaterialLots] = useState([\n {\n id: 1, lotNo: 'EGI155-241101-001', materialCode: 'EGI-1.55T', materialName: 'EGI 철판 1.55T',\n inboundDate: '2024-11-01', qty: 100, remainQty: 85, unit: '매', supplier: '포스코', location: 'D-01'\n },\n {\n id: 2, lotNo: 'EGI155-241105-001', materialCode: 'EGI-1.55T', materialName: 'EGI 철판 1.55T',\n inboundDate: '2024-11-05', qty: 150, remainQty: 150, unit: '매', supplier: '포스코', location: 'D-01'\n },\n {\n id: 3, lotNo: 'SUS12-241103-001', materialCode: 'SUS-1.2T', materialName: 'SUS 철판 1.2T',\n inboundDate: '2024-11-03', qty: 50, remainQty: 42, unit: '매', supplier: '포스코', location: 'D-02'\n },\n {\n id: 4, lotNo: 'SLT-COL-241102-001', materialCode: 'SLT-MAT-001', materialName: '슬랫 코일',\n inboundDate: '2024-11-02', qty: 500, remainQty: 320, unit: 'KG', supplier: '동국제강', location: 'C-01'\n },\n ]);\n\n // ═══════════════════════════════════════════════════════════════════\n // 생산 LOT 관리 (스크린샷 기반)\n // 형식: [제품코드]-[생산일자]-[순번]\n // ═══════════════════════════════════════════════════════════════════\n const [productionLots, setProductionLots] = useState([\n {\n id: 1, lotNo: 'G-I-5B11-33', productCode: 'RC30', productName: '가이드레일(벽면형) C형 3000',\n productionDate: '2025-11-05', qty: 100, rawMaterialLots: ['EGI155-241101-001'], status: '완료'\n },\n {\n id: 2, lotNo: 'G-I-5B11-35', productCode: 'RC35', productName: '가이드레일(벽면형) C형 3500',\n productionDate: '2025-11-05', qty: 80, rawMaterialLots: ['EGI155-241101-001'], status: '완료'\n },\n {\n id: 3, lotNo: 'C-B-5B11-73', productCode: 'CB30', productName: '케이스(후면코너부) 3000',\n productionDate: '2025-11-06', qty: 120, rawMaterialLots: ['EGI155-241105-001'], status: '진행중'\n },\n {\n id: 4, lotNo: 'B-E-5B11-53', productCode: 'BE30', productName: '하단마감재(스크린) EGI 3000',\n productionDate: '2025-11-07', qty: 60, rawMaterialLots: ['EGI155-241105-001'], status: '예정'\n },\n ]);\n\n // 재고 데이터 (확장: itemType, category 필드 추가)\n // ★ 품목코드는 공통코드관리(codeRuleConfig.js) 규칙 준수: {품목명}-{규격}\n const [inventory, setInventory] = useState([\n // ═══════════════════════════════════════════\n // 원자재 (Raw Material) - 패턴: {품목명}-{규격}\n // ═══════════════════════════════════════════\n {\n id: 1, materialCode: '스크린원단-백색-0.3T', materialName: '스크린 원단 (백색)', unit: '㎡', stock: 500, minStock: 100, location: 'A-01', lastUpdated: '2025-03-01',\n itemType: '원자재', category: '원단', subCategory: '스크린용', lotCount: 3, oldestLotDays: 21\n },\n {\n id: 6, materialCode: '슬랫코일-1.0T', materialName: '슬랫 코일', unit: 'KG', stock: 1500, minStock: 300, location: 'C-01', lastUpdated: '2025-03-01',\n itemType: '원자재', category: '코일', subCategory: '슬랫용', lotCount: 4, oldestLotDays: 35\n },\n {\n id: 8, materialCode: '철판-EGI-1.55T', materialName: 'EGI 철판 1.55T', unit: '매', stock: 200, minStock: 50, location: 'D-01', lastUpdated: '2025-03-01',\n itemType: '원자재', category: '철판', subCategory: 'EGI', lotCount: 2, oldestLotDays: 14\n },\n {\n id: 9, materialCode: '철판-SUS-1.2T', materialName: 'SUS 철판 1.2T', unit: '매', stock: 80, minStock: 20, location: 'D-02', lastUpdated: '2025-03-01',\n itemType: '원자재', category: '철판', subCategory: 'SUS', lotCount: 2, oldestLotDays: 7\n },\n\n // ═══════════════════════════════════════════\n // 부자재 (Sub Material) - 패턴: {품목명}-{규격}\n // ═══════════════════════════════════════════\n {\n id: 2, materialCode: '앤드락-표준', materialName: '앤드락', unit: 'EA', stock: 800, minStock: 200, location: 'A-02', lastUpdated: '2025-03-01',\n itemType: '부자재', category: '체결류', lotCount: 2, oldestLotDays: 12\n },\n {\n id: 3, materialCode: '하단바-알루미늄', materialName: '하단바', unit: 'EA', stock: 120, minStock: 30, location: 'A-03', lastUpdated: '2025-03-01',\n itemType: '부자재', category: '프레임류', lotCount: 1, oldestLotDays: 5\n },\n {\n id: 4, materialCode: '미싱실-백색', materialName: '미싱실', unit: 'M', stock: 5000, minStock: 1000, location: 'A-04', lastUpdated: '2025-03-01',\n itemType: '부자재', category: '봉제류', lotCount: 3, oldestLotDays: 28\n },\n {\n id: 7, materialCode: '미미자재-표준', materialName: '미미자재', unit: 'EA', stock: 600, minStock: 150, location: 'C-02', lastUpdated: '2025-03-01',\n itemType: '부자재', category: '마감류', lotCount: 2, oldestLotDays: 18\n },\n {\n id: 50, materialCode: '리벳-4.8×12', materialName: '리벳', unit: 'EA', stock: 5000, minStock: 1000, location: 'B-01', lastUpdated: '2025-03-01',\n itemType: '부자재', category: '체결류', lotCount: 5, oldestLotDays: 45\n },\n {\n id: 51, materialCode: '육각볼트-M8×20', materialName: '육각볼트 M8', unit: 'EA', stock: 3000, minStock: 500, location: 'B-02', lastUpdated: '2025-03-01',\n itemType: '부자재', category: '체결류', lotCount: 4, oldestLotDays: 32\n },\n {\n id: 52, materialCode: '실리콘-투명', materialName: '실리콘', unit: 'EA', stock: 200, minStock: 50, location: 'B-03', lastUpdated: '2025-03-01',\n itemType: '부자재', category: '접착류', lotCount: 1, oldestLotDays: 10\n },\n\n // ═══════════════════════════════════════════\n // 소모품 (Consumables) - 패턴: {품목명}-{규격}\n // ═══════════════════════════════════════════\n {\n id: 5, materialCode: '포장박스-대형', materialName: '포장박스', unit: 'EA', stock: 200, minStock: 50, location: 'B-01', lastUpdated: '2025-03-01',\n itemType: '소모품', category: '포장', lotCount: 2, oldestLotDays: 8\n },\n {\n id: 53, materialCode: '포장테이프-투명', materialName: '포장테이프', unit: 'EA', stock: 100, minStock: 30, location: 'B-04', lastUpdated: '2025-03-01',\n itemType: '소모품', category: '포장', lotCount: 1, oldestLotDays: 3\n },\n\n // ═══════════════════════════════════════════\n // 절곡부품 - 가이드레일 (Bending Part)\n // ═══════════════════════════════════════════\n {\n id: 101, materialCode: 'RC24', materialName: '가이드레일(벽면형) C형 2438', unit: 'EA', stock: 200, minStock: 50, location: 'E-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: 'C형', length: 2438, producedQty: 200, usedQty: 0\n },\n {\n id: 102, materialCode: 'RC30', materialName: '가이드레일(벽면형) C형 3000', unit: 'EA', stock: 737, minStock: 100, location: 'E-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: 'C형', length: 3000, producedQty: 737, usedQty: 0\n },\n {\n id: 103, materialCode: 'RC35', materialName: '가이드레일(벽면형) C형 3500', unit: 'EA', stock: 608, minStock: 100, location: 'E-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: 'C형', length: 3500, producedQty: 608, usedQty: 0\n },\n {\n id: 104, materialCode: 'RC40', materialName: '가이드레일(벽면형) C형 4000', unit: 'EA', stock: 421, minStock: 80, location: 'E-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: 'C형', length: 4000, producedQty: 421, usedQty: 0\n },\n {\n id: 105, materialCode: 'RC43', materialName: '가이드레일(벽면형) C형 4300', unit: 'EA', stock: 550, minStock: 80, location: 'E-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: 'C형', length: 4300, producedQty: 550, usedQty: 0\n },\n {\n id: 106, materialCode: 'RD24', materialName: '가이드레일(벽면형) D형 2438', unit: 'EA', stock: 200, minStock: 50, location: 'E-02', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: 'D형', length: 2438, producedQty: 200, usedQty: 0\n },\n {\n id: 107, materialCode: 'RD30', materialName: '가이드레일(벽면형) D형 3000', unit: 'EA', stock: 650, minStock: 100, location: 'E-02', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: 'D형', length: 3000, producedQty: 650, usedQty: 0\n },\n {\n id: 108, materialCode: 'RD35', materialName: '가이드레일(벽면형) D형 3500', unit: 'EA', stock: 510, minStock: 80, location: 'E-02', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: 'D형', length: 3500, producedQty: 510, usedQty: 0\n },\n {\n id: 109, materialCode: 'RD40', materialName: '가이드레일(벽면형) D형 4000', unit: 'EA', stock: 352, minStock: 80, location: 'E-02', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: 'D형', length: 4000, producedQty: 352, usedQty: 0\n },\n {\n id: 110, materialCode: 'RM24', materialName: '가이드레일(벽면형) 본체 2438', unit: 'EA', stock: 620, minStock: 100, location: 'E-03', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: '본체', length: 2438, producedQty: 620, usedQty: 0\n },\n {\n id: 111, materialCode: 'RM30', materialName: '가이드레일(벽면형) 본체 3000', unit: 'EA', stock: 604, minStock: 100, location: 'E-03', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: '본체', length: 3000, producedQty: 604, usedQty: 0\n },\n {\n id: 112, materialCode: 'RS24', materialName: '가이드레일(벽면형) SUS마감 2438', unit: 'EA', stock: 396, minStock: 80, location: 'E-04', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: 'SUS마감', length: 2438, producedQty: 396, usedQty: 0\n },\n {\n id: 113, materialCode: 'RS30', materialName: '가이드레일(벽면형) SUS마감 3000', unit: 'EA', stock: 804, minStock: 100, location: 'E-04', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '가이드레일', subCategory: 'SUS마감', length: 3000, producedQty: 804, usedQty: 0\n },\n\n // ═══════════════════════════════════════════\n // 절곡부품 - 케이스 (Bending Part)\n // ═══════════════════════════════════════════\n {\n id: 201, materialCode: 'CB12', materialName: '케이스(후면코너부) 1219', unit: 'EA', stock: 652, minStock: 100, location: 'F-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '후면코너부', length: 1219, producedQty: 652, usedQty: 0\n },\n {\n id: 202, materialCode: 'CB24', materialName: '케이스(후면코너부) 2438', unit: 'EA', stock: 1117, minStock: 200, location: 'F-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '후면코너부', length: 2438, producedQty: 1117, usedQty: 0\n },\n {\n id: 203, materialCode: 'CB30', materialName: '케이스(후면코너부) 3000', unit: 'EA', stock: 1081, minStock: 200, location: 'F-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '후면코너부', length: 3000, producedQty: 1081, usedQty: 0\n },\n {\n id: 204, materialCode: 'CB35', materialName: '케이스(후면코너부) 3500', unit: 'EA', stock: 495, minStock: 100, location: 'F-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '후면코너부', length: 3500, producedQty: 715, usedQty: 220\n },\n {\n id: 205, materialCode: 'CB40', materialName: '케이스(후면코너부) 4000', unit: 'EA', stock: 1440, minStock: 200, location: 'F-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '후면코너부', length: 4000, producedQty: 1440, usedQty: 0\n },\n {\n id: 206, materialCode: 'CF12', materialName: '케이스(전면부) 1219', unit: 'EA', stock: 191, minStock: 50, location: 'F-02', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '전면부', length: 1219, producedQty: 191, usedQty: 0\n },\n {\n id: 207, materialCode: 'CF24', materialName: '케이스(전면부) 2438', unit: 'EA', stock: 241, minStock: 50, location: 'F-02', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '전면부', length: 2438, producedQty: 241, usedQty: 0\n },\n {\n id: 208, materialCode: 'CF30', materialName: '케이스(전면부) 3000', unit: 'EA', stock: 271, minStock: 50, location: 'F-02', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '���면부', length: 3000, producedQty: 271, usedQty: 0\n },\n {\n id: 209, materialCode: 'CL12', materialName: '케이스(린텔부) 1219', unit: 'EA', stock: 598, minStock: 100, location: 'F-03', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '린텔부', length: 1219, producedQty: 598, usedQty: 0\n },\n {\n id: 210, materialCode: 'CL24', materialName: '케이스(린텔부) 2438', unit: 'EA', stock: 732, minStock: 100, location: 'F-03', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '린텔부', length: 2438, producedQty: 732, usedQty: 0\n },\n {\n id: 211, materialCode: 'CL30', materialName: '케이스(린텔부) 3000', unit: 'EA', stock: 576, minStock: 100, location: 'F-03', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '린텔부', length: 3000, producedQty: 576, usedQty: 0\n },\n {\n id: 212, materialCode: 'CP12', materialName: '케이스(점검구) 1219', unit: 'EA', stock: 300, minStock: 50, location: 'F-04', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '점검구', length: 1219, producedQty: 300, usedQty: 0\n },\n {\n id: 213, materialCode: 'CP24', materialName: '케이스(점검구) 2438', unit: 'EA', stock: 840, minStock: 100, location: 'F-04', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '점검구', length: 2438, producedQty: 840, usedQty: 0\n },\n {\n id: 214, materialCode: 'CP30', materialName: '케이스(점검구) 3000', unit: 'EA', stock: 766, minStock: 100, location: 'F-04', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '케이스', subCategory: '점검구', length: 3000, producedQty: 766, usedQty: 0\n },\n\n // ═══════════════════════════════════════════\n // 절곡부품 - 하단마감재 (Bending Part)\n // ═══════════════════════════════════════════\n {\n id: 301, materialCode: 'BE30', materialName: '하단마감재(스크린) EGI 3000', unit: 'EA', stock: 396, minStock: 80, location: 'G-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '하단마감재', subCategory: 'EGI', length: 3000, producedQty: 396, usedQty: 0\n },\n {\n id: 302, materialCode: 'BE40', materialName: '하단마감재(스크린) EGI 4000', unit: 'EA', stock: 265, minStock: 50, location: 'G-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '하단마감재', subCategory: 'EGI', length: 4000, producedQty: 265, usedQty: 0\n },\n {\n id: 303, materialCode: 'BS24', materialName: '하단마감재(스크린) SUS 2438', unit: 'EA', stock: 99, minStock: 30, location: 'G-02', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '하단마감재', subCategory: 'SUS', length: 2438, producedQty: 99, usedQty: 0\n },\n {\n id: 304, materialCode: 'BS30', materialName: '하단마감재(스크린) SUS 3000', unit: 'EA', stock: 1144, minStock: 200, location: 'G-02', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '하단마감재', subCategory: 'SUS', length: 3000, producedQty: 1144, usedQty: 0\n },\n {\n id: 305, materialCode: 'BS40', materialName: '하단마감재(스크린) SUS 4000', unit: 'EA', stock: 1150, minStock: 200, location: 'G-02', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '하단마감재', subCategory: 'SUS', length: 4000, producedQty: 1150, usedQty: 0\n },\n {\n id: 306, materialCode: 'TS40', materialName: '하단마감재(철재) SUS 4000', unit: 'EA', stock: 1, minStock: 10, location: 'G-03', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '하단마감재', subCategory: '철재SUS', length: 4000, producedQty: 1, usedQty: 0\n },\n\n // ═══════════════════════════════════════════\n // 절곡부품 - 연기차단재/L-Bar (Bending Part)\n // ═══════════════════════════════════════════\n {\n id: 401, materialCode: 'GI24', materialName: '연기차단재 화이바원단 2438', unit: 'EA', stock: 1412, minStock: 200, location: 'H-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '연기차단재', subCategory: '화이바원단', length: 2438, producedQty: 1412, usedQty: 0\n },\n {\n id: 402, materialCode: 'GI30', materialName: '연기차단재 화이바원단 3000', unit: 'EA', stock: 1984, minStock: 300, location: 'H-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '연기차단재', subCategory: '화이바원단', length: 3000, producedQty: 1984, usedQty: 0\n },\n {\n id: 403, materialCode: 'GI35', materialName: '연기차단재 화이바원단 3500', unit: 'EA', stock: 1165, minStock: 200, location: 'H-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '연기차단재', subCategory: '화이바원단', length: 3500, producedQty: 1165, usedQty: 0\n },\n {\n id: 404, materialCode: 'GI40', materialName: '연기차단재 화이바원단 4000', unit: 'EA', stock: 1171, minStock: 200, location: 'H-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '연기차단재', subCategory: '화이바원단', length: 4000, producedQty: 1171, usedQty: 0\n },\n {\n id: 405, materialCode: 'GI43', materialName: '연기차단재 화이바원단 4300', unit: 'EA', stock: 1000, minStock: 200, location: 'H-01', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '연기차단재', subCategory: '화이바원단', length: 4300, producedQty: 1000, usedQty: 0\n },\n {\n id: 406, materialCode: 'GI83', materialName: '연기차단재 화이바원단 W80×3000', unit: 'EA', stock: 3561, minStock: 500, location: 'H-02', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: '연기차단재', subCategory: '화이바원단(W80)', length: 3000, producedQty: 3561, usedQty: 0\n },\n {\n id: 407, materialCode: 'LA30', materialName: 'L-Bar 스크린용 3000', unit: 'EA', stock: 1443, minStock: 200, location: 'H-03', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: 'L-Bar', subCategory: '스크린용', length: 3000, producedQty: 1443, usedQty: 0\n },\n {\n id: 408, materialCode: 'LA40', materialName: 'L-Bar 스크린용 4000', unit: 'EA', stock: 676, minStock: 100, location: 'H-03', lastUpdated: '2025-03-01',\n itemType: '절곡부품', category: 'L-Bar', subCategory: '스크린용', length: 4000, producedQty: 676, usedQty: 0\n },\n\n // ═══════════════════════════════════════════\n // 구매부품 (Purchased Part) - 코드규칙: {품목명}-{규격} (규격 = 전원+용량+유무선)\n // ═══════════════════════════════════════════\n {\n id: 501, materialCode: '전동개폐기-220V150KG유선', materialName: '전동개폐기 150KG 220V 유선', unit: 'EA', stock: 300, minStock: 50, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '150KG/220V/유선', supplier: '대한', lotCount: 4, oldestLotDays: 25\n },\n {\n id: 502, materialCode: '전동개폐기-220V300KG유선', materialName: '전동개폐기 300KG 220V 유선', unit: 'EA', stock: 508, minStock: 80, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '300KG/220V/유선', supplier: '대한', lotCount: 6, oldestLotDays: 42\n },\n {\n id: 503, materialCode: '전동개폐기-220V400KG유선', materialName: '전동개폐기 400KG 220V 유선', unit: 'EA', stock: 136, minStock: 30, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '400KG/220V/유선', supplier: '대한', lotCount: 2, oldestLotDays: 18\n },\n {\n id: 504, materialCode: '연동제어기-매립형유선', materialName: '연동제어기 매립형 유선', unit: 'EA', stock: 500, minStock: 80, location: 'I-02', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '연동제어기', spec: '매립/유선', supplier: '대한', lotCount: 5, oldestLotDays: 35\n },\n {\n id: 505, materialCode: '연동제어기-노출형유선', materialName: '연동제어기 노출형 유선', unit: 'EA', stock: 500, minStock: 80, location: 'I-02', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '연동제어기', spec: '노출/유선', supplier: '대한', lotCount: 5, oldestLotDays: 28\n },\n {\n id: 506, materialCode: '감기샤프트-114.3×2TL4500', materialName: '감기샤프트 114.3×2T L:4500', unit: 'EA', stock: 41, minStock: 10, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '114.3×2T', length: 4500, lotCount: 2, oldestLotDays: 65\n },\n {\n id: 507, materialCode: '감기샤프트-139.8×2.9TL4500', materialName: '감기샤프트 139.8×2.9T L:4500', unit: 'EA', stock: 35, minStock: 10, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '139.8×2.9T', length: 4500, lotCount: 2, oldestLotDays: 52\n },\n\n // ═══════════════════════════════════════════\n // 구매부품 - 전동개폐기(모터) 확장 - 코드규칙: {품목명}-{전원}{용량}{유무선}\n // ═══════════════════════════════════════════\n // 150KG\n {\n id: 508, materialCode: '전동개폐기-380V150KG유선', materialName: '전동개폐기 150KG 380V 유선', unit: 'EA', stock: 50, minStock: 10, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '150KG/380V/유선', kg: 150, voltage: 380, wireType: '유선', supplier: '대한'\n },\n {\n id: 509, materialCode: '전동개폐기-220V150KG무선', materialName: '전동개폐기 150KG 220V 무선', unit: 'EA', stock: 80, minStock: 15, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '150KG/220V/무선', kg: 150, voltage: 220, wireType: '무선', supplier: '대한'\n },\n {\n id: 510, materialCode: '전동개폐기-380V150KG무선', materialName: '전동개폐기 150KG 380V 무선', unit: 'EA', stock: 30, minStock: 10, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '150KG/380V/무선', kg: 150, voltage: 380, wireType: '무선', supplier: '대한'\n },\n // 300KG\n {\n id: 511, materialCode: '전동개폐기-380V300KG유선', materialName: '전동개폐기 300KG 380V 유선', unit: 'EA', stock: 120, minStock: 20, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '300KG/380V/유선', kg: 300, voltage: 380, wireType: '유선', supplier: '대한'\n },\n {\n id: 512, materialCode: '전동개폐기-220V300KG무선', materialName: '전동개폐기 300KG 220V 무선', unit: 'EA', stock: 150, minStock: 25, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '300KG/220V/무선', kg: 300, voltage: 220, wireType: '무선', supplier: '대한'\n },\n {\n id: 513, materialCode: '전동개폐기-380V300KG무선', materialName: '전동개폐기 300KG 380V 무선', unit: 'EA', stock: 60, minStock: 15, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '300KG/380V/무선', kg: 300, voltage: 380, wireType: '무선', supplier: '대한'\n },\n // 400KG\n {\n id: 514, materialCode: '전동개폐기-380V400KG유선', materialName: '전동개폐기 400KG 380V 유선', unit: 'EA', stock: 80, minStock: 15, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '400KG/380V/유선', kg: 400, voltage: 380, wireType: '유선', supplier: '대한'\n },\n {\n id: 515, materialCode: '전동개폐기-220V400KG무선', materialName: '전동개폐기 400KG 220V 무선', unit: 'EA', stock: 45, minStock: 10, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '400KG/220V/무선', kg: 400, voltage: 220, wireType: '무선', supplier: '대한'\n },\n {\n id: 516, materialCode: '전동개폐기-380V400KG무선', materialName: '전동개폐기 400KG 380V 무선', unit: 'EA', stock: 35, minStock: 10, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '400KG/380V/무선', kg: 400, voltage: 380, wireType: '무선', supplier: '대한'\n },\n // 500KG\n {\n id: 517, materialCode: '전동개폐기-220V500KG유선', materialName: '전동개폐기 500KG 220V 유선', unit: 'EA', stock: 60, minStock: 15, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '500KG/220V/유선', kg: 500, voltage: 220, wireType: '유선', supplier: '대한'\n },\n {\n id: 518, materialCode: '전동개폐기-380V500KG유선', materialName: '전동개폐기 500KG 380V 유선', unit: 'EA', stock: 40, minStock: 10, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '500KG/380V/유선', kg: 500, voltage: 380, wireType: '유선', supplier: '대한'\n },\n {\n id: 519, materialCode: '전동개폐기-220V500KG무선', materialName: '전동개폐기 500KG 220V 무선', unit: 'EA', stock: 25, minStock: 8, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '500KG/220V/무선', kg: 500, voltage: 220, wireType: '무선', supplier: '대한'\n },\n {\n id: 520, materialCode: '전동개폐기-380V500KG무선', materialName: '전동개폐기 500KG 380V 무선', unit: 'EA', stock: 20, minStock: 5, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '500KG/380V/무선', kg: 500, voltage: 380, wireType: '무선', supplier: '대한'\n },\n // 600KG\n {\n id: 521, materialCode: '전동개폐기-220V600KG유선', materialName: '전동개폐기 600KG 220V 유선', unit: 'EA', stock: 35, minStock: 10, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '600KG/220V/유선', kg: 600, voltage: 220, wireType: '유선', supplier: '대한'\n },\n {\n id: 522, materialCode: '전동개폐기-380V600KG유선', materialName: '전동개폐기 600KG 380V 유선', unit: 'EA', stock: 30, minStock: 8, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '600KG/380V/유선', kg: 600, voltage: 380, wireType: '유선', supplier: '대한'\n },\n {\n id: 523, materialCode: '전동개폐기-220V600KG무선', materialName: '전동개폐기 600KG 220V 무선', unit: 'EA', stock: 15, minStock: 5, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '600KG/220V/무선', kg: 600, voltage: 220, wireType: '무선', supplier: '대한'\n },\n {\n id: 524, materialCode: '전동개폐기-380V600KG무선', materialName: '전동개폐기 600KG 380V 무선', unit: 'EA', stock: 12, minStock: 5, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '600KG/380V/무선', kg: 600, voltage: 380, wireType: '무선', supplier: '대한'\n },\n // 800KG\n {\n id: 525, materialCode: '전동개폐기-220V800KG유선', materialName: '전동개폐기 800KG 220V 유선', unit: 'EA', stock: 20, minStock: 5, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '800KG/220V/유선', kg: 800, voltage: 220, wireType: '유선', supplier: '대한'\n },\n {\n id: 526, materialCode: '전동개폐기-380V800KG유선', materialName: '전동개폐기 800KG 380V 유선', unit: 'EA', stock: 18, minStock: 5, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '800KG/380V/유선', kg: 800, voltage: 380, wireType: '유선', supplier: '대한'\n },\n {\n id: 527, materialCode: '전동개폐기-220V800KG무선', materialName: '전동개폐기 800KG 220V 무선', unit: 'EA', stock: 10, minStock: 3, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '800KG/220V/무선', kg: 800, voltage: 220, wireType: '무선', supplier: '대한'\n },\n {\n id: 528, materialCode: '전동개폐기-380V800KG무선', materialName: '전동개폐기 800KG 380V 무선', unit: 'EA', stock: 8, minStock: 3, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '800KG/380V/무선', kg: 800, voltage: 380, wireType: '무선', supplier: '대한'\n },\n // 1000KG\n {\n id: 529, materialCode: '전동개폐기-220V1000KG유선', materialName: '전동개폐기 1000KG 220V 유선', unit: 'EA', stock: 12, minStock: 3, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '1000KG/220V/유선', kg: 1000, voltage: 220, wireType: '유선', supplier: '대한'\n },\n {\n id: 530, materialCode: '전동개폐기-380V1000KG유선', materialName: '전동개폐기 1000KG 380V 유선', unit: 'EA', stock: 10, minStock: 3, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '1000KG/380V/유선', kg: 1000, voltage: 380, wireType: '유선', supplier: '대한'\n },\n {\n id: 531, materialCode: '전동개폐기-220V1000KG무선', materialName: '전동개폐기 1000KG 220V 무선', unit: 'EA', stock: 5, minStock: 2, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '1000KG/220V/무선', kg: 1000, voltage: 220, wireType: '무선', supplier: '대한'\n },\n {\n id: 532, materialCode: '전동개폐기-380V1000KG무선', materialName: '전동개폐기 1000KG 380V 무선', unit: 'EA', stock: 5, minStock: 2, location: 'I-01', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '전동개폐기', spec: '1000KG/380V/무선', kg: 1000, voltage: 380, wireType: '무선', supplier: '대한'\n },\n\n // ═══════════════════════════════════════════\n // 구매부품 - 연동제어기 확장 - 코드규칙: {품목명}-{설치형태}{유무선}\n // ═══════════════════════════════════════════\n {\n id: 533, materialCode: '연동제어기-매립형무선', materialName: '연동제어기 매립형 무선', unit: 'EA', stock: 200, minStock: 50, location: 'I-02', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '연동제어기', spec: '매립/무선', installType: '매립', wireType: '무선', supplier: '대한'\n },\n {\n id: 534, materialCode: '연동제어기-노출형무선', materialName: '연동제어기 노출형 무선', unit: 'EA', stock: 200, minStock: 50, location: 'I-02', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '연동제어기', spec: '노출/무선', installType: '노출', wireType: '무선', supplier: '대한'\n },\n\n // ═══════════════════════════════════════════\n // 구매부품 - 감기샤프트 확장 - 코드규칙: {품목명}-{규격}L{길이}\n // 예: 감기샤프트-60.5*2.9TL3000\n // ═══════════════════════════════════════════\n // 60.5*2.9T\n {\n id: 535, materialCode: '감기샤프트-60.5*2.9TL3000', materialName: '감기샤프트 60.5*2.9T L:3000', unit: 'EA', stock: 100, minStock: 20, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '60.5*2.9T', diameter: 60.5, thickness: 2.9, length: 3000\n },\n {\n id: 536, materialCode: '감기샤프트-60.5*2.9TL3500', materialName: '감기샤프트 60.5*2.9T L:3500', unit: 'EA', stock: 80, minStock: 15, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '60.5*2.9T', diameter: 60.5, thickness: 2.9, length: 3500\n },\n {\n id: 537, materialCode: '감기샤프트-60.5*2.9TL4000', materialName: '감기샤프트 60.5*2.9T L:4000', unit: 'EA', stock: 60, minStock: 15, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '60.5*2.9T', diameter: 60.5, thickness: 2.9, length: 4000\n },\n {\n id: 538, materialCode: '감기샤프트-60.5*2.9TL4500', materialName: '감기샤프트 60.5*2.9T L:4500', unit: 'EA', stock: 50, minStock: 10, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '60.5*2.9T', diameter: 60.5, thickness: 2.9, length: 4500\n },\n // 76.3*2.8T\n {\n id: 539, materialCode: '감기샤프트-76.3*2.8TL3000', materialName: '감기샤프트 76.3*2.8T L:3000', unit: 'EA', stock: 90, minStock: 18, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '76.3*2.8T', diameter: 76.3, thickness: 2.8, length: 3000\n },\n {\n id: 540, materialCode: '감기샤프트-76.3*2.8TL3500', materialName: '감기샤프트 76.3*2.8T L:3500', unit: 'EA', stock: 70, minStock: 15, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '76.3*2.8T', diameter: 76.3, thickness: 2.8, length: 3500\n },\n {\n id: 541, materialCode: '감기샤프트-76.3*2.8TL4000', materialName: '감기샤프트 76.3*2.8T L:4000', unit: 'EA', stock: 55, minStock: 12, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '76.3*2.8T', diameter: 76.3, thickness: 2.8, length: 4000\n },\n {\n id: 542, materialCode: '감기샤프트-76.3*2.8TL4500', materialName: '감기샤프트 76.3*2.8T L:4500', unit: 'EA', stock: 45, minStock: 10, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '76.3*2.8T', diameter: 76.3, thickness: 2.8, length: 4500\n },\n // 89.1*2.8T\n {\n id: 543, materialCode: '감기샤프트-89.1*2.8TL3000', materialName: '감기샤프트 89.1*2.8T L:3000', unit: 'EA', stock: 80, minStock: 15, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '89.1*2.8T', diameter: 89.1, thickness: 2.8, length: 3000\n },\n {\n id: 544, materialCode: '감기샤프트-89.1*2.8TL3500', materialName: '감기샤프트 89.1*2.8T L:3500', unit: 'EA', stock: 65, minStock: 12, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '89.1*2.8T', diameter: 89.1, thickness: 2.8, length: 3500\n },\n {\n id: 545, materialCode: '감기샤프트-89.1*2.8TL4000', materialName: '감기샤프트 89.1*2.8T L:4000', unit: 'EA', stock: 50, minStock: 10, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '89.1*2.8T', diameter: 89.1, thickness: 2.8, length: 4000\n },\n {\n id: 546, materialCode: '감기샤프트-89.1*2.8TL4500', materialName: '감기샤프트 89.1*2.8T L:4500', unit: 'EA', stock: 40, minStock: 8, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '89.1*2.8T', diameter: 89.1, thickness: 2.8, length: 4500\n },\n // 101.6*2.8T\n {\n id: 547, materialCode: '감기샤프트-101.6*2.8TL3000', materialName: '감기샤프트 101.6*2.8T L:3000', unit: 'EA', stock: 70, minStock: 15, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '101.6*2.8T', diameter: 101.6, thickness: 2.8, length: 3000\n },\n {\n id: 548, materialCode: '감기샤프트-101.6*2.8TL3500', materialName: '감기샤프트 101.6*2.8T L:3500', unit: 'EA', stock: 55, minStock: 12, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '101.6*2.8T', diameter: 101.6, thickness: 2.8, length: 3500\n },\n {\n id: 549, materialCode: '감기샤프트-101.6*2.8TL4000', materialName: '감기샤프트 101.6*2.8T L:4000', unit: 'EA', stock: 45, minStock: 10, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '101.6*2.8T', diameter: 101.6, thickness: 2.8, length: 4000\n },\n {\n id: 550, materialCode: '감기샤프트-101.6*2.8TL4500', materialName: '감기샤프트 101.6*2.8T L:4500', unit: 'EA', stock: 38, minStock: 8, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '101.6*2.8T', diameter: 101.6, thickness: 2.8, length: 4500\n },\n // 114.3*2T\n {\n id: 551, materialCode: '감기샤프트-114.3*2TL3000', materialName: '감기샤프트 114.3*2T L:3000', unit: 'EA', stock: 60, minStock: 12, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '114.3*2T', diameter: 114.3, thickness: 2.0, length: 3000\n },\n {\n id: 552, materialCode: '감기샤프트-114.3*2TL3500', materialName: '감기샤프트 114.3*2T L:3500', unit: 'EA', stock: 50, minStock: 10, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '114.3*2T', diameter: 114.3, thickness: 2.0, length: 3500\n },\n {\n id: 553, materialCode: '감기샤프트-114.3*2TL4000', materialName: '감기샤프트 114.3*2T L:4000', unit: 'EA', stock: 42, minStock: 8, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '114.3*2T', diameter: 114.3, thickness: 2.0, length: 4000\n },\n // 139.8*2.9T\n {\n id: 554, materialCode: '감기샤프트-139.8*2.9TL3000', materialName: '감기샤프트 139.8*2.9T L:3000', unit: 'EA', stock: 50, minStock: 10, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '139.8*2.9T', diameter: 139.8, thickness: 2.9, length: 3000\n },\n {\n id: 555, materialCode: '감기샤프트-139.8*2.9TL3500', materialName: '감기샤프트 139.8*2.9T L:3500', unit: 'EA', stock: 40, minStock: 8, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '139.8*2.9T', diameter: 139.8, thickness: 2.9, length: 3500\n },\n {\n id: 556, materialCode: '감기샤프트-139.8*2.9TL4000', materialName: '감기샤프트 139.8*2.9T L:4000', unit: 'EA', stock: 35, minStock: 8, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '139.8*2.9T', diameter: 139.8, thickness: 2.9, length: 4000\n },\n // 165.2*3.3T\n {\n id: 557, materialCode: '감기샤프트-165.2*3.3TL3000', materialName: '감기샤프트 165.2*3.3T L:3000', unit: 'EA', stock: 30, minStock: 8, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '165.2*3.3T', diameter: 165.2, thickness: 3.3, length: 3000\n },\n {\n id: 558, materialCode: '감기샤프트-165.2*3.3TL3500', materialName: '감기샤프트 165.2*3.3T L:3500', unit: 'EA', stock: 25, minStock: 6, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '165.2*3.3T', diameter: 165.2, thickness: 3.3, length: 3500\n },\n {\n id: 559, materialCode: '감기샤프트-165.2*3.3TL4000', materialName: '감기샤프트 165.2*3.3T L:4000', unit: 'EA', stock: 20, minStock: 5, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '165.2*3.3T', diameter: 165.2, thickness: 3.3, length: 4000\n },\n {\n id: 560, materialCode: '감기샤프트-165.2*3.3TL4500', materialName: '감기샤프트 165.2*3.3T L:4500', unit: 'EA', stock: 18, minStock: 5, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '165.2*3.3T', diameter: 165.2, thickness: 3.3, length: 4500\n },\n // 190.7*4.0T\n {\n id: 561, materialCode: '감기샤프트-190.7*4.0TL3000', materialName: '감기샤프트 190.7*4.0T L:3000', unit: 'EA', stock: 20, minStock: 5, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '190.7*4.0T', diameter: 190.7, thickness: 4.0, length: 3000\n },\n {\n id: 562, materialCode: '감기샤프트-190.7*4.0TL3500', materialName: '감기샤프트 190.7*4.0T L:3500', unit: 'EA', stock: 18, minStock: 5, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '190.7*4.0T', diameter: 190.7, thickness: 4.0, length: 3500\n },\n {\n id: 563, materialCode: '감기샤프트-190.7*4.0TL4000', materialName: '감기샤프트 190.7*4.0T L:4000', unit: 'EA', stock: 15, minStock: 4, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '190.7*4.0T', diameter: 190.7, thickness: 4.0, length: 4000\n },\n {\n id: 564, materialCode: '감기샤프트-190.7*4.0TL4500', materialName: '감기샤프트 190.7*4.0T L:4500', unit: 'EA', stock: 12, minStock: 3, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '190.7*4.0T', diameter: 190.7, thickness: 4.0, length: 4500\n },\n // 216.3*4.2T\n {\n id: 565, materialCode: '감기샤프트-216.3*4.2TL3000', materialName: '감기샤프트 216.3*4.2T L:3000', unit: 'EA', stock: 15, minStock: 4, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '216.3*4.2T', diameter: 216.3, thickness: 4.2, length: 3000\n },\n {\n id: 566, materialCode: '감기샤프트-216.3*4.2TL3500', materialName: '감기샤프트 216.3*4.2T L:3500', unit: 'EA', stock: 12, minStock: 3, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '216.3*4.2T', diameter: 216.3, thickness: 4.2, length: 3500\n },\n {\n id: 567, materialCode: '감기샤프트-216.3*4.2TL4000', materialName: '감기샤프트 216.3*4.2T L:4000', unit: 'EA', stock: 10, minStock: 3, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '216.3*4.2T', diameter: 216.3, thickness: 4.2, length: 4000\n },\n {\n id: 568, materialCode: '감기샤프트-216.3*4.2TL4500', materialName: '감기샤프트 216.3*4.2T L:4500', unit: 'EA', stock: 8, minStock: 2, location: 'I-03', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '감기샤프트', spec: '216.3*4.2T', diameter: 216.3, thickness: 4.2, length: 4500\n },\n\n // ═══════════════════════════════════════════\n // 구매부품 - 앵글 (스크린샷 기반)\n // ═══════════════════════════════════════════\n {\n id: 569, materialCode: 'ANG-50-30', materialName: '앵글 50×50 L:3000', unit: 'EA', stock: 200, minStock: 50, location: 'I-04', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '앵글', spec: '50×50', length: 3000\n },\n {\n id: 570, materialCode: 'ANG-50-40', materialName: '앵글 50×50 L:4000', unit: 'EA', stock: 150, minStock: 40, location: 'I-04', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '앵글', spec: '50×50', length: 4000\n },\n {\n id: 571, materialCode: 'ANG-65-30', materialName: '앵글 65×65 L:3000', unit: 'EA', stock: 180, minStock: 45, location: 'I-04', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '앵글', spec: '65×65', length: 3000\n },\n {\n id: 572, materialCode: 'ANG-65-40', materialName: '앵글 65×65 L:4000', unit: 'EA', stock: 120, minStock: 30, location: 'I-04', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '앵글', spec: '65×65', length: 4000\n },\n {\n id: 573, materialCode: 'ANG-75-30', materialName: '앵글 75×75 L:3000', unit: 'EA', stock: 100, minStock: 25, location: 'I-04', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '앵글', spec: '75×75', length: 3000\n },\n {\n id: 574, materialCode: 'ANG-75-40', materialName: '앵글 75×75 L:4000', unit: 'EA', stock: 80, minStock: 20, location: 'I-04', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '앵글', spec: '75×75', length: 4000\n },\n\n // ═══════════════════════════════════════════\n // 구매부품 - 각파이프 (스크린샷 기반)\n // ═══════════════════════════════════════════\n {\n id: 575, materialCode: 'SQP-30-30', materialName: '각파이프 30×30 L:3000', unit: 'EA', stock: 250, minStock: 60, location: 'I-05', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '각파이프', spec: '30×30', length: 3000\n },\n {\n id: 576, materialCode: 'SQP-30-40', materialName: '각파이프 30×30 L:4000', unit: 'EA', stock: 180, minStock: 45, location: 'I-05', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '각파이프', spec: '30×30', length: 4000\n },\n {\n id: 577, materialCode: 'SQP-40-30', materialName: '각파이프 40×40 L:3000', unit: 'EA', stock: 200, minStock: 50, location: 'I-05', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '각파이프', spec: '40×40', length: 3000\n },\n {\n id: 578, materialCode: 'SQP-40-40', materialName: '각파이프 40×40 L:4000', unit: 'EA', stock: 150, minStock: 40, location: 'I-05', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '각파이프', spec: '40×40', length: 4000\n },\n {\n id: 579, materialCode: 'SQP-50-30', materialName: '각파이프 50×50 L:3000', unit: 'EA', stock: 150, minStock: 40, location: 'I-05', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '각파이프', spec: '50×50', length: 3000\n },\n {\n id: 580, materialCode: 'SQP-50-40', materialName: '각파이프 50×50 L:4000', unit: 'EA', stock: 120, minStock: 30, location: 'I-05', lastUpdated: '2025-03-01',\n itemType: '구매부품', category: '각파이프', spec: '50×50', length: 4000\n },\n ]);\n\n // 자재 투입 이력\n const [materialUsage, setMaterialUsage] = useState([]);\n\n // ═══════════════════════════════════════════\n // 자재 LOT 데이터 (입고 LOT별 재고 관리)\n // ═══════════════════════════════════════════\n const [materialLots, setMaterialLots] = useState([\n // ============================================================\n // 원자재 LOT (YYMMDD-## 형식)\n // ============================================================\n // EGI 철판 LOT (절곡용)\n {\n id: 1, lotNo: '240910-01', materialCode: 'EGI-1.55T', materialName: 'EGI 철판 1.55T',\n itemType: '원자재', category: '철판', unit: '매', supplier: '현대제철', poNo: 'KD-SO-240908-01',\n inboundDate: '2024-09-10', expiryDate: null, initialQty: 50, remainingQty: 12, usedQty: 38,\n location: 'D-01-01', status: 'AVAILABLE', spec: '1.55T', lotType: 'RAW'\n },\n {\n id: 2, lotNo: '240920-02', materialCode: 'EGI-1.55T', materialName: 'EGI 철판 1.55T',\n itemType: '원자재', category: '철판', unit: '매', supplier: '현대제철', poNo: 'KD-SO-240918-01',\n inboundDate: '2024-09-20', expiryDate: null, initialQty: 80, remainingQty: 45, usedQty: 35,\n location: 'D-01-02', status: 'AVAILABLE', spec: '1.55T', lotType: 'RAW'\n },\n {\n id: 3, lotNo: '240927-06', materialCode: 'EGI-1.55T', materialName: 'EGI 철판 1.55T',\n itemType: '원자재', category: '철판', unit: '매', supplier: '현대제철', poNo: 'KD-SO-240925-01',\n inboundDate: '2024-09-27', expiryDate: null, initialQty: 100, remainingQty: 100, usedQty: 0,\n location: 'D-01-03', status: 'AVAILABLE', spec: '1.55T', lotType: 'RAW'\n },\n {\n id: 4, lotNo: '241115-01', materialCode: 'EGI-1.15T', materialName: 'EGI 철판 1.15T',\n itemType: '원자재', category: '철판', unit: '매', supplier: '현대제철', poNo: 'KD-SO-241113-01',\n inboundDate: '2024-11-15', expiryDate: null, initialQty: 60, remainingQty: 35, usedQty: 25,\n location: 'D-02-01', status: 'AVAILABLE', spec: '1.15T', lotType: 'RAW'\n },\n // SUS 철판 LOT (가이드레일용)\n {\n id: 5, lotNo: '241120-01', materialCode: 'SUS-1.2T', materialName: 'SUS 철판 1.2T',\n itemType: '원자재', category: '철판', unit: '매', supplier: '스테인리스코리아', poNo: 'KD-SO-241118-01',\n inboundDate: '2024-11-20', expiryDate: null, initialQty: 30, remainingQty: 8, usedQty: 22,\n location: 'D-03-01', status: 'AVAILABLE', spec: '1.2T', lotType: 'RAW'\n },\n {\n id: 6, lotNo: '250102-01', materialCode: 'SUS-1.2T', materialName: 'SUS 철판 1.2T',\n itemType: '원자재', category: '철판', unit: '매', supplier: '스테인리스코리아', poNo: 'KD-SO-241228-01',\n inboundDate: '2025-01-02', expiryDate: null, initialQty: 50, remainingQty: 50, usedQty: 0,\n location: 'D-03-02', status: 'AVAILABLE', spec: '1.2T', lotType: 'RAW'\n },\n // 슬랫 코일 LOT\n {\n id: 7, lotNo: '241201-01', materialCode: 'SLT-MAT-001', materialName: '슬랫 코일',\n itemType: '원자재', category: '코일', unit: 'KG', supplier: '포스코', poNo: 'KD-SO-241128-01',\n inboundDate: '2024-12-01', expiryDate: null, initialQty: 500, remainingQty: 320, usedQty: 180,\n location: 'C-01-01', status: 'AVAILABLE', lotType: 'RAW'\n },\n {\n id: 8, lotNo: '250105-01', materialCode: 'SLT-MAT-001', materialName: '슬랫 코일',\n itemType: '원자재', category: '코일', unit: 'KG', supplier: '포스코', poNo: 'KD-SO-250102-01',\n inboundDate: '2025-01-05', expiryDate: null, initialQty: 800, remainingQty: 800, usedQty: 0,\n location: 'C-01-02', status: 'AVAILABLE', lotType: 'RAW'\n },\n\n // ============================================================\n // 원단 LOT (YYMMDD-## 형식)\n // ============================================================\n // 스크린 원단 LOT\n {\n id: 9, lotNo: '240829-01', materialCode: 'SCR-MAT-001', materialName: '스크린 원단 (백색)',\n itemType: '원자재', category: '원단', unit: '㎡', supplier: '대한섬유', poNo: 'KD-SO-240825-01',\n inboundDate: '2024-08-29', expiryDate: '2025-02-28', initialQty: 100, remainingQty: 35, usedQty: 65,\n location: 'A-01-01', status: 'AVAILABLE', lotType: 'FABRIC'\n },\n {\n id: 10, lotNo: '241015-01', materialCode: 'SCR-MAT-001', materialName: '스크린 원단 (백색)',\n itemType: '원자재', category: '원단', unit: '㎡', supplier: '대한섬유', poNo: 'KD-SO-241010-01',\n inboundDate: '2024-10-15', expiryDate: '2025-04-15', initialQty: 150, remainingQty: 80, usedQty: 70,\n location: 'A-01-02', status: 'AVAILABLE', lotType: 'FABRIC'\n },\n {\n id: 11, lotNo: '250110-01', materialCode: 'SCR-MAT-001', materialName: '스크린 원단 (백색)',\n itemType: '원자재', category: '원단', unit: '㎡', supplier: '한국섬유', poNo: 'KD-SO-250105-01',\n inboundDate: '2025-01-10', expiryDate: '2025-07-10', initialQty: 200, remainingQty: 200, usedQty: 0,\n location: 'A-01-03', status: 'AVAILABLE', lotType: 'FABRIC'\n },\n // 연기차단재 원단 LOT (화이바원단)\n {\n id: 12, lotNo: '241201-01', materialCode: 'SMK-FB-001', materialName: '연기차단재 원단 (W50)',\n itemType: '원자재', category: '원단', unit: 'M', supplier: '방재소재', poNo: 'KD-SO-241125-02',\n inboundDate: '2024-12-01', expiryDate: '2025-06-01', initialQty: 200, remainingQty: 145, usedQty: 55,\n location: 'A-05-01', status: 'AVAILABLE', spec: 'W50', lotType: 'FABRIC'\n },\n {\n id: 13, lotNo: '250110-02', materialCode: 'SMK-FB-001', materialName: '연기차단재 원단 (W50)',\n itemType: '원자재', category: '원단', unit: 'M', supplier: '방재소재', poNo: 'KD-SO-250105-02',\n inboundDate: '2025-01-10', expiryDate: '2025-07-10', initialQty: 300, remainingQty: 300, usedQty: 0,\n location: 'A-05-02', status: 'AVAILABLE', spec: 'W50', lotType: 'FABRIC'\n },\n\n // ============================================================\n // 생산 LOT ({품목}{종류}{Y}{M}{DD}-{규격} 형식)\n // 예: TS4B19-40 = T(하단마감재)+S(SUS마감)+4(2024)+B(11월)+19(일)-40(4000mm)\n // ============================================================\n // 하단마감재(철재) 생산 LOT\n {\n id: 14, lotNo: 'TS4B19-40', materialCode: 'T-SUS-4000', materialName: '하단마감재(철재) SUS마감 4000',\n itemType: '반제품', category: '절곡부품', unit: 'EA', supplier: '자체생산', poNo: null,\n inboundDate: '2024-11-19', expiryDate: null, initialQty: 20, remainingQty: 12, usedQty: 8,\n location: 'E-01-01', status: 'AVAILABLE', spec: '4000mm', lotType: 'PROD',\n itemCode: 'T', typeCode: 'S', specCode: '40'\n },\n {\n id: 15, lotNo: 'TT4C05-43', materialCode: 'T-IRON-4300', materialName: '하단마감재(철재) 본체철재 4300',\n itemType: '반제품', category: '절곡부품', unit: 'EA', supplier: '자체생산', poNo: null,\n inboundDate: '2024-12-05', expiryDate: null, initialQty: 15, remainingQty: 15, usedQty: 0,\n location: 'E-01-02', status: 'AVAILABLE', spec: '4300mm', lotType: 'PROD',\n itemCode: 'T', typeCode: 'T', specCode: '43'\n },\n // 케이스 생산 LOT\n {\n id: 16, lotNo: 'CL4B08-24', materialCode: 'C-LINTEL-2438', materialName: '케이스 린텔부 2438',\n itemType: '반제품', category: '절곡부품', unit: 'EA', supplier: '자체생산', poNo: null,\n inboundDate: '2024-11-08', expiryDate: null, initialQty: 25, remainingQty: 18, usedQty: 7,\n location: 'E-02-01', status: 'AVAILABLE', spec: '2438mm', lotType: 'PROD',\n itemCode: 'C', typeCode: 'L', specCode: '24'\n },\n {\n id: 17, lotNo: 'CP4A24-40', materialCode: 'C-INSP-4000', materialName: '케이스 점검구 4000',\n itemType: '반제품', category: '절곡부품', unit: 'EA', supplier: '자체생산', poNo: null,\n inboundDate: '2024-10-24', expiryDate: null, initialQty: 30, remainingQty: 22, usedQty: 8,\n location: 'E-02-02', status: 'AVAILABLE', spec: '4000mm', lotType: 'PROD',\n itemCode: 'C', typeCode: 'P', specCode: '40'\n },\n {\n id: 18, lotNo: 'CF4A15-40', materialCode: 'C-FRONT-4000', materialName: '케이스 전면부 4000',\n itemType: '반제품', category: '절곡부품', unit: 'EA', supplier: '자체생산', poNo: null,\n inboundDate: '2024-10-15', expiryDate: null, initialQty: 28, remainingQty: 20, usedQty: 8,\n location: 'E-02-03', status: 'AVAILABLE', spec: '4000mm', lotType: 'PROD',\n itemCode: 'C', typeCode: 'F', specCode: '40'\n },\n // 가이드레일 생산 LOT\n {\n id: 19, lotNo: 'RT4A15-43', materialCode: 'R-BODY-4300', materialName: '가이드레일(벽면형) 본체 4300',\n itemType: '반제품', category: '절곡부품', unit: 'EA', supplier: '자체생산', poNo: null,\n inboundDate: '2024-10-15', expiryDate: null, initialQty: 40, remainingQty: 28, usedQty: 12,\n location: 'E-03-01', status: 'AVAILABLE', spec: '4300mm', lotType: 'PROD',\n itemCode: 'R', typeCode: 'T', specCode: '43'\n },\n {\n id: 20, lotNo: 'RS4B10-40', materialCode: 'R-SUS-4000', materialName: '가이드레일(벽면형) SUS마감 4000',\n itemType: '반제품', category: '절곡부품', unit: 'EA', supplier: '자체생산', poNo: null,\n inboundDate: '2024-11-10', expiryDate: null, initialQty: 35, remainingQty: 35, usedQty: 0,\n location: 'E-03-02', status: 'AVAILABLE', spec: '4000mm', lotType: 'PROD',\n itemCode: 'R', typeCode: 'S', specCode: '40'\n },\n // 연기차단재 생산 LOT\n {\n id: 21, lotNo: 'GI4A05-53', materialCode: 'G-FIBER-5000', materialName: '연기차단재 화이바원단 W50×3000',\n itemType: '반제품', category: '연기차단재', unit: 'EA', supplier: '자체생산', poNo: null,\n inboundDate: '2024-10-05', expiryDate: null, initialQty: 50, remainingQty: 32, usedQty: 18,\n location: 'E-04-01', status: 'AVAILABLE', spec: 'W50×3000', lotType: 'PROD',\n itemCode: 'G', typeCode: 'I', specCode: '53'\n },\n {\n id: 22, lotNo: 'GW4B12-53', materialCode: 'G-SHEET-5000', materialName: '연기차단재 화이바시트 W50×3000',\n itemType: '반제품', category: '연기차단재', unit: 'EA', supplier: '자체생산', poNo: null,\n inboundDate: '2024-11-12', expiryDate: null, initialQty: 45, remainingQty: 45, usedQty: 0,\n location: 'E-04-02', status: 'AVAILABLE', spec: 'W50×3000', lotType: 'PROD',\n itemCode: 'G', typeCode: 'W', specCode: '53'\n },\n\n // ============================================================\n // 부자재 LOT (YYMMDD-## 형식)\n // ============================================================\n // 앤드락 LOT\n {\n id: 23, lotNo: '241210-01', materialCode: 'SCR-MAT-002', materialName: '앤드락',\n itemType: '부자재', category: '체결류', unit: 'EA', supplier: '부자재상사', poNo: 'KD-SO-241205-01',\n inboundDate: '2024-12-10', expiryDate: null, initialQty: 500, remainingQty: 380, usedQty: 120,\n location: 'A-02-01', status: 'AVAILABLE', lotType: 'RAW'\n },\n {\n id: 24, lotNo: '250108-01', materialCode: 'SCR-MAT-002', materialName: '앤드락',\n itemType: '부자재', category: '체결류', unit: 'EA', supplier: '부자재상사', poNo: 'KD-SO-250103-01',\n inboundDate: '2025-01-08', expiryDate: null, initialQty: 300, remainingQty: 300, usedQty: 0,\n location: 'A-02-02', status: 'AVAILABLE', lotType: 'RAW'\n },\n // 미싱실 LOT\n {\n id: 25, lotNo: '241201-02', materialCode: 'SCR-MAT-004', materialName: '미싱실',\n itemType: '부자재', category: '봉제류', unit: 'M', supplier: '봉제자재', poNo: 'KD-SO-241128-04',\n inboundDate: '2024-12-01', expiryDate: null, initialQty: 3000, remainingQty: 1800, usedQty: 1200,\n location: 'A-04-01', status: 'AVAILABLE', lotType: 'RAW'\n },\n // 미미자재 LOT (슬랫용)\n {\n id: 26, lotNo: '241215-01', materialCode: 'SLT-MAT-002', materialName: '미미자재',\n itemType: '부자재', category: '마감류', unit: 'EA', supplier: '마감자재', poNo: 'KD-SO-241210-02',\n inboundDate: '2024-12-15', expiryDate: null, initialQty: 400, remainingQty: 280, usedQty: 120,\n location: 'C-02-01', status: 'AVAILABLE', lotType: 'RAW'\n },\n ]);\n\n // 메뉴 설정 (기본값: 관리자 - 전체 메뉴)\n const [menuConfig, setMenuConfig] = useState({\n master: { enabled: true, subMenus: { process: true, item: true, 'inspection-standard': true, 'number-rule': true, 'code-rule': true } },\n sales: { enabled: true, subMenus: { customer: true, quote: true, order: true, site: true, price: true } },\n outbound: { enabled: true, subMenus: { shipment: true } },\n production: { enabled: true, subMenus: { 'production-dashboard': true, 'work-order': true, 'work-result': true, 'worker-task': true, item: true } },\n quality: { enabled: true, subMenus: { inspection: true, defect: true } },\n inventory: { enabled: true, subMenus: { stock: true, inbound: true, outbound: true } },\n accounting: { enabled: true, subMenus: { 'acc-customer': true, 'sales-account': true, purchase: true, cashbook: true, collection: true, 'cost-analysis': true } },\n });\n\n const navigate = (viewName, item = null) => {\n setView(viewName);\n setSelectedItem(item);\n setSelectedData(item);\n };\n\n const handleMenuChange = (menuId) => {\n setActiveMenu(menuId);\n // 메뉴에 따라 기본 뷰 설정\n switch (menuId) {\n case 'dashboard':\n setView('dashboard');\n break;\n case 'item-master':\n setView('item-master-list');\n break;\n case 'process-master':\n setView('process-master-list');\n break;\n case 'quality-master':\n setView('quality-master-list');\n break;\n case 'customer-master':\n setView('customer-master-list');\n break;\n case 'site-master':\n setView('site-master-list');\n break;\n case 'order-master':\n setView('order-master-list');\n break;\n case 'production-master':\n setView('production-master-list');\n break;\n case 'outbound-master':\n setView('outbound-master-list');\n break;\n case 'process':\n setView('process-list');\n break;\n case 'inspection-standard':\n setView('inspection-standard-list');\n break;\n case 'number-rule':\n setView('number-rule-list');\n break;\n case 'code-rule':\n setView('code-rule-list');\n break;\n case 'quote-formula':\n setView('quote-formula-list');\n break;\n case 'document-template':\n setView('document-template-list');\n break;\n case 'quote':\n setView('quote-list');\n break;\n case 'order':\n setView('order-list');\n break;\n case 'price':\n setView('price-list');\n break;\n case 'item':\n setView('item-list');\n break;\n case 'production-dashboard':\n setView('production-status-board');\n break;\n case 'work-order':\n setView('work-order-list');\n break;\n case 'work-result':\n setView('work-result-list');\n break;\n case 'worker-task':\n setView('worker-task-view');\n break;\n case 'traceability':\n setView('traceability-list');\n break;\n case 'shipment':\n setView('shipment-list');\n break;\n // 판매관리 - 거래처관리 (sales 메뉴 그룹)\n case 'customer':\n setView('customer-list');\n break;\n // 판매관리 - 현장관리\n case 'site':\n setView('site-list');\n break;\n case 'inspection':\n setView('inspection-list');\n break;\n case 'defect':\n setView('defect-list');\n break;\n // 품질관리 - IQC, PQC, FQC\n case 'iqc':\n setView('iqc-list');\n break;\n case 'pqc':\n setView('pqc-list');\n break;\n case 'fqc':\n setView('fqc-list');\n break;\n case 'stock':\n setView('stock-list');\n break;\n case 'inbound':\n setView('inbound-list');\n break;\n case 'stock-adjustment':\n setView('stock-adjustment-list');\n break;\n case 'delivery':\n setView('delivery-status-list');\n break;\n case 'integrated-test':\n setView('integrated-test');\n break;\n case 'common-ux':\n setView('common-ux');\n break;\n case 'process-flowchart':\n setView('process-flowchart');\n break;\n case 'detailed-process-flow':\n setView('detailed-process-flow');\n break;\n case 'policy-guide':\n setView('policy-guide');\n break;\n // 회계관리 - 2단계 메뉴 클릭 시 기본 목록 뷰 설정\n case 'acc-customer':\n setView('acc-customer-list');\n break;\n case 'sales-account':\n setView('sales-list');\n break;\n case 'purchase':\n setView('purchase-list');\n break;\n case 'cashbook':\n setView('cashbook-list');\n break;\n case 'collection':\n setView('collection-list');\n break;\n case 'cost-analysis':\n setView('cost-list');\n break;\n // 회계관리 - 거래처관리 (A0)\n case 'acc-customer-list':\n setView('acc-customer-list');\n break;\n case 'acc-customer-register':\n setView('acc-customer-register');\n break;\n case 'acc-customer-detail':\n setSelectedData(data);\n setView('acc-customer-detail');\n break;\n case 'acc-customer-edit':\n setSelectedData(data);\n setView('acc-customer-edit');\n break;\n // 회계관리 - 매출관리 (A1)\n case 'sales-list':\n setView('sales-list');\n break;\n case 'sales-statement':\n setView('sales-statement');\n break;\n case 'sales-tax-invoice':\n setView('sales-tax-invoice');\n break;\n // 회계관리 - 매입관리 (A2)\n case 'purchase-list':\n setView('purchase-list');\n break;\n case 'purchase-register':\n setView('purchase-register');\n break;\n case 'expense-list':\n setView('expense-list');\n break;\n case 'expense-register':\n setView('expense-register');\n break;\n case 'expense-estimate':\n setView('expense-estimate');\n break;\n // 회계관리 - 금전출납부 (A3)\n case 'cashbook-list':\n setView('cashbook-list');\n break;\n case 'cashbook-register':\n setView('cashbook-register');\n break;\n case 'cashbook-edit':\n setView('cashbook-edit');\n break;\n // 회계관리 - 수금관리 (A4)\n case 'collection-list':\n setView('collection-list');\n break;\n case 'collection-register':\n setView('collection-register');\n break;\n case 'receivable-list':\n setView('receivable-list');\n break;\n case 'bill-list':\n setView('bill-list');\n break;\n // 회계관리 - 원가관리 (A5)\n case 'cost-list':\n setView('cost-list');\n break;\n case 'cost-detail':\n setView('cost-detail');\n break;\n // 시스템관리\n case 'users':\n setView('users');\n break;\n case 'roles':\n setView('roles');\n break;\n case 'settings':\n setView('settings');\n break;\n default:\n setView(`${menuId}-list`);\n }\n };\n\n // 견적 등록\n const handleSaveQuote = (newQuote) => {\n const quote = {\n ...newQuote,\n id: quotes.length + 1,\n };\n setQuotes(prev => [...prev, quote]);\n navigate('quote-list');\n };\n\n // 수주 등록\n const handleSaveOrder = (newOrder) => {\n const order = {\n ...newOrder,\n id: orders.length + 1,\n };\n setOrders(prev => [...prev, order]);\n };\n\n // 수주 업데이트\n const handleUpdateOrder = (updatedOrder) => {\n setOrders(prev => prev.map(o =>\n o.id === updatedOrder.id ? updatedOrder : o\n ));\n setSelectedItem(updatedOrder);\n };\n\n // 수주 취소\n const handleCancelOrder = (orderId, cancelInfo) => {\n setOrders(prev => prev.map(o => {\n if (o.id === orderId) {\n const newHistory = {\n id: Date.now(),\n changedAt: cancelInfo.cancelledAt,\n changeType: '취소',\n description: `수주 취소 - 사유: ${cancelInfo.reason}${cancelInfo.reasonDetail ? ` (${cancelInfo.reasonDetail})` : ''}`,\n changedBy: cancelInfo.cancelledBy,\n };\n return {\n ...o,\n status: '취소',\n cancelReason: cancelInfo.reason,\n cancelReasonDetail: cancelInfo.reasonDetail,\n cancelledAt: cancelInfo.cancelledAt,\n cancelledBy: cancelInfo.cancelledBy,\n changeHistory: [...(o.changeHistory || []), newHistory],\n };\n }\n return o;\n }));\n alert('수주가 취소되었습니다.');\n };\n\n // 작업지시 등록 - ★ 생산LOT 자동생성\n const handleSaveWorkOrder = (newWorkOrder) => {\n // ★ 생산LOT 자동 생성 (KD-PL-YYMMDD-## 형식)\n const productionLot = newWorkOrder.productionLot || autoGenerateProductionLot(new Date());\n\n const workOrder = {\n ...newWorkOrder,\n id: workOrders.length + 1,\n productionLot, // ★ 생산LOT 자동 부여\n lotNo: productionLot, // ★ 목록에서 표시하는 필드명과 일치\n productionLotNo: productionLot, // ★ 호환성 유지\n workPriority: newWorkOrder.workPriority || 5, // ★ 이미 숫자로 전달됨\n materialInputs: [], // ★ 자재 투입 이력 초기화 (LOT 추적용)\n createdAt: new Date().toISOString(),\n };\n setWorkOrders(prev => [...prev, workOrder]);\n\n // 해당 수주의 분할 상태 업데이트\n if (newWorkOrder.splitNo) {\n setOrders(prev => prev.map(o => {\n if (o.orderNo === newWorkOrder.orderNo) {\n return {\n ...o,\n splits: o.splits?.map(s =>\n s.splitNo === newWorkOrder.splitNo\n ? { ...s, productionStatus: '작업대기', productionOrderNo: newWorkOrder.workOrderNo, productionLot }\n : s\n ),\n };\n }\n return o;\n }));\n }\n\n console.log(`[작업지시 생성] ${workOrder.workOrderNo} - 생산LOT: ${productionLot}`);\n };\n\n // 작업지시 승인 처리\n const handleApproveWorkOrder = (workOrderId) => {\n const today = new Date().toISOString().split('T')[0];\n setWorkOrders(prev => prev.map(wo => {\n if (wo.id === workOrderId) {\n return {\n ...wo,\n approvalStatus: '승인완료',\n status: '작업대기', // 승인되면 작업대기로 변경\n approval: {\n ...wo.approval,\n approver: { name: '박생산', date: today, dept: '생산관리' },\n },\n };\n }\n return wo;\n }));\n alert('✅ 작업지시가 승인되었습니다.\\n\\n회계팀에 참조 알림이 발송됩니다.');\n };\n\n // 작업자: 작업 시작\n const handleStartWork = (workOrderId) => {\n const now = new Date();\n setWorkOrders(prev => prev.map(wo => {\n if (wo.id === workOrderId) {\n return {\n ...wo,\n status: '작업중',\n startTime: now.toISOString(),\n worker: currentUser.name,\n };\n }\n return wo;\n }));\n alert('✅ 작업을 시작합니다.');\n };\n\n // 작업자: 작업일지(실적) 입력\n const handleInputWorkLog = (workOrderId, workLog) => {\n setWorkOrders(prev => prev.map(wo => {\n if (wo.id === workOrderId) {\n return {\n ...wo,\n completedQty: workLog.completedQty,\n defectQty: workLog.defectQty,\n workNote: workLog.note,\n lastUpdated: new Date().toISOString(),\n };\n }\n return wo;\n }));\n alert('✅ 작업 실적이 저장되었습니다.');\n };\n\n // 작업자: 작업 완료 (workOrderId 또는 task 객체 모두 지원) - ★ 제품검사LOT 자동생성\n const handleCompleteWork = (taskOrId) => {\n const now = new Date();\n const isObject = typeof taskOrId === 'object';\n const workOrderId = isObject ? taskOrId.id : taskOrId;\n const taskData = isObject ? taskOrId : null;\n\n // ★ 제품검사LOT 자동 생성 (KD-SA-YYMMDD-## 형식)\n const productInspLot = autoGenerateProductInspLot(now);\n\n // 현재 작업지시 정보 가져오기\n const currentWO = workOrders.find(wo => wo.id === workOrderId);\n\n // 작업지시 상태 업데이트\n setWorkOrders(prev => prev.map(wo => {\n if (wo.id === workOrderId) {\n return {\n ...wo,\n status: '작업완료',\n endTime: now.toISOString(),\n completedDate: now.toISOString().split('T')[0],\n completedQty: taskData?.goodQty || wo.completedQty || wo.totalQty,\n productInspLot, // ★ 제품검사LOT 자동 부여\n };\n }\n return wo;\n }));\n\n // 작업실적 데이터 자동 생성 (task 객체로 전달된 경우)\n if (isObject && taskData) {\n const resultNo = `WR-${now.toISOString().slice(2, 10).replace(/-/g, '')}-${String(workResults.length + 1).padStart(3, '0')}`;\n const newResult = {\n id: Date.now(),\n resultNo,\n workOrderNo: taskData.workOrderNo,\n workDate: now.toISOString().split('T')[0],\n processType: taskData.processType,\n productName: taskData.items?.[0]?.productName || taskData.productName || '방충망',\n goodQty: taskData.goodQty || taskData.totalQty || 0,\n defectQty: taskData.defectQty || 0,\n status: '작업완료',\n worker: taskData.assignee || '작업자',\n completedAt: now.toISOString(),\n note: taskData.note || '',\n customerName: taskData.customerName,\n siteName: taskData.siteName,\n productInspLot, // ★ 제품검사LOT 연결\n };\n setWorkResults(prev => [...prev, newResult]);\n }\n\n // ★ 제품검사(FQC) 자동 생성\n const newProductInspection = {\n id: productInspections.length + 1,\n lotNo: productInspLot,\n workOrderNo: currentWO?.workOrderNo || taskData?.workOrderNo,\n productName: currentWO?.productName || taskData?.productName || '제품',\n qty: taskData?.goodQty || currentWO?.totalQty || 0,\n status: '검사대기',\n requestDate: now.toISOString().split('T')[0],\n requester: currentUser.name,\n orderNo: currentWO?.orderNo,\n customerName: currentWO?.customerName || taskData?.customerName,\n };\n setProductInspections(prev => [...prev, newProductInspection]);\n\n console.log(`[작업완료] 제품검사LOT: ${productInspLot} 자동 생성`);\n alert(`✅ 작업이 완료되었습니다.\\n\\n📋 제품검사LOT: ${productInspLot}\\n\\n✅ 제품검사(FQC)가 자동 생성되었습니다.\\n[품질관리 > 제품검사]에서 검사를 진행하세요.`);\n };\n\n // 회계팀: C등급 출하 승인\n const handleApproveShipment = (orderId) => {\n const today = new Date().toISOString().split('T')[0];\n setOrders(prev => prev.map(o => {\n if (o.id === orderId) {\n return {\n ...o,\n accountingStatus: '회계확인완료',\n accountingApprovalDate: today,\n accountingApprover: currentUser.name,\n };\n }\n return o;\n }));\n alert('✅ C등급 출하가 승인되었습니다.\\n\\n출하 진행이 가능합니다.');\n };\n\n // 회계팀: C등급 출하 반려\n const handleRejectShipment = (orderId, reason) => {\n const today = new Date().toISOString().split('T')[0];\n setOrders(prev => prev.map(o => {\n if (o.id === orderId) {\n return {\n ...o,\n accountingStatus: '출하반려',\n accountingRejectDate: today,\n accountingRejectReason: reason,\n accountingApprover: currentUser.name,\n };\n }\n return o;\n }));\n alert(`❌ 출하가 반려되었습니다.\\n\\n사유: ${reason}`);\n };\n\n // 구매팀: 발주요청 생성\n const handleCreatePO = (material) => {\n const poNo = `PO-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-${String(purchaseOrders.length + 1).padStart(3, '0')}`;\n const newPO = {\n id: purchaseOrders.length + 1,\n poNo,\n materialCode: material.materialCode,\n materialName: material.materialName,\n requestQty: material.minStock * 2, // 안전재고의 2배 발주\n unit: material.unit,\n vendor: '미정',\n status: '발주대기',\n requestDate: new Date().toISOString().split('T')[0],\n requester: currentUser.name,\n };\n setPurchaseOrders(prev => [...prev, newPO]);\n alert(`✅ 발주요청이 생성되었습니다.\\n\\n발주번호: ${poNo}\\n품목: ${material.materialName}\\n수량: ${material.minStock * 2}${material.unit}`);\n };\n\n // 작업지시 업데이트\n const handleUpdateWorkOrder = (updatedWorkOrder) => {\n setWorkOrders(prev => prev.map(wo =>\n wo.id === updatedWorkOrder.id ? updatedWorkOrder : wo\n ));\n setSelectedItem(updatedWorkOrder);\n };\n\n const handleSaveWorkResult = (newResult) => {\n const result = {\n ...newResult,\n id: workResults.length + 1,\n createdAt: new Date().toISOString(),\n };\n setWorkResults(prev => [...prev, result]);\n\n // 작업지시 완료수량 업데이트\n setWorkOrders(prev => prev.map(wo => {\n if (wo.workOrderNo === newResult.workOrderNo) {\n const newCompletedQty = wo.completedQty + newResult.goodQty;\n return {\n ...wo,\n completedQty: newCompletedQty,\n status: newCompletedQty >= wo.totalQty ? '작업완료' : wo.status,\n };\n }\n return wo;\n }));\n };\n\n // 출하 등록\n const handleSaveShipment = (newShipment) => {\n const shipment = {\n ...newShipment,\n id: shipments.length + 1,\n };\n setShipments(prev => [...prev, shipment]);\n };\n\n // 출하 업데이트 (상태 변경, 상차 체크 등) - ★ 자동화 연동\n const handleUpdateShipment = (shipmentOrNo, updates = null) => {\n // 두 가지 호출 방식 지원:\n // 1. handleUpdateShipment(updatedShipment) - 전체 객체로 업데이트\n // 2. handleUpdateShipment(shipmentNo, { paymentConfirmed: true }) - 부분 업데이트\n\n if (updates !== null && typeof shipmentOrNo === 'string') {\n // 부분 업데이트 방식 (수금관리, 세금계산서 발행에서 사용)\n setShipments(prev => prev.map(s =>\n s.shipmentNo === shipmentOrNo ? { ...s, ...updates } : s\n ));\n // 현재 선택된 아이템도 업데이트\n if (selectedItem?.shipmentNo === shipmentOrNo) {\n setSelectedItem(prev => ({ ...prev, ...updates }));\n }\n } else {\n // 전체 객체 업데이트 방식 (기존 방식)\n const updatedShipment = shipmentOrNo;\n const prevShipment = shipments.find(s => s.id === updatedShipment.id);\n\n // ★ 배송완료로 상태 변경 시 세금계산서/거래명세서 자동 발행\n if (prevShipment?.status !== '배송완료' && updatedShipment.status === '배송완료') {\n const relatedOrder = orders.find(o => o.orderNo === updatedShipment.orderNo);\n if (relatedOrder) {\n // A등급: 세금계산서 자동 발행\n if (relatedOrder.creditGrade === 'A' || !relatedOrder.creditGrade) {\n const invoice = autoGenerateTaxInvoice(updatedShipment, relatedOrder);\n const statement = autoGenerateTransactionStatement(updatedShipment, relatedOrder);\n updatedShipment.taxInvoiceNo = invoice.invoiceNo;\n updatedShipment.taxInvoiceIssued = true;\n updatedShipment.transactionStatementNo = statement.statementNo;\n console.log(`[자동발행] 세금계산서: ${invoice.invoiceNo}, 거래명세서: ${statement.statementNo}`);\n }\n // B/C등급: 거래명세서만 자동 발행\n else {\n const statement = autoGenerateTransactionStatement(updatedShipment, relatedOrder);\n updatedShipment.transactionStatementNo = statement.statementNo;\n console.log(`[자동발행] 거래명세서: ${statement.statementNo} (세금계산서는 입금 후 발행)`);\n }\n }\n }\n\n setShipments(prev => prev.map(s =>\n s.id === updatedShipment.id ? updatedShipment : s\n ));\n setSelectedItem(updatedShipment);\n }\n };\n\n // ★ 출하 생성 시 C등급 자동 보류 체크\n const handleCreateShipmentWithCheck = (shipmentData) => {\n const relatedOrder = orders.find(o => o.orderNo === shipmentData.orderNo);\n\n // C등급 고객은 입금 확인 전까지 자동 보류\n if (relatedOrder?.creditGrade === 'C') {\n const customerReceivables = receivables.filter(r =>\n r.customerId === relatedOrder.customerId && r.balance > 0\n );\n const totalBalance = customerReceivables.reduce((sum, r) => sum + r.balance, 0);\n\n if (totalBalance > 0) {\n shipmentData.status = '출하보류';\n shipmentData.holdReason = `C등급 고객 - 미수금 ${totalBalance.toLocaleString()}원 존재`;\n shipmentData.holdAt = new Date().toISOString();\n console.log(`[출하보류] ${shipmentData.shipmentNo}: C등급 고객 미수금 ${totalBalance.toLocaleString()}원`);\n }\n }\n\n // B등급 고객도 미수금 과다 시 보류\n if (relatedOrder?.creditGrade === 'B') {\n const customerReceivables = receivables.filter(r =>\n r.customerId === relatedOrder.customerId && r.balance > 0\n );\n const totalBalance = customerReceivables.reduce((sum, r) => sum + r.balance, 0);\n const creditLimit = relatedOrder.creditLimit || 10000000; // 기본 1천만원\n\n if (totalBalance > creditLimit) {\n shipmentData.status = '출하보류';\n shipmentData.holdReason = `신용한도 초과 - 미수금 ${totalBalance.toLocaleString()}원 / 한도 ${creditLimit.toLocaleString()}원`;\n shipmentData.holdAt = new Date().toISOString();\n console.log(`[출하보류] ${shipmentData.shipmentNo}: 신용한도 초과`);\n }\n }\n\n handleSaveShipment(shipmentData);\n };\n\n // 입고 처리 (구매발주 입고) - ★ 입고LOT 자동생성\n const handleReceivePO = (poId, receiveData) => {\n const today = new Date().toISOString().split('T')[0];\n // ★ LOT 자동생성 함수 사용 (YYMMDD-## 형식)\n const lotNo = autoGenerateIncomingLot(new Date());\n // ★ 입고검사LOT도 자동 생성 (IQC 연동)\n const inspectionLotNo = autoGenerateIncomingInspLot(new Date());\n\n // 발주 상태 업데이트\n const po = purchaseOrders.find(p => p.id === poId);\n setPurchaseOrders(prev => prev.map(p => {\n if (p.id === poId) {\n return {\n ...p,\n status: receiveData.qty >= p.requestQty ? '검사대기' : '부분입고', // ★ 입고완료 → 검사대기로 변경\n receivedQty: (p.receivedQty || 0) + receiveData.qty,\n receivedDate: today,\n lotNo,\n inspectionLotNo, // ★ 검사LOT 자동 연결\n };\n }\n return p;\n }));\n\n // 재고 추가\n if (po) {\n setInventory(prev => prev.map(item =>\n item.materialCode === po.materialCode\n ? { ...item, stock: item.stock + receiveData.qty, lastUpdated: today }\n : item\n ));\n\n // ★★★ 입고 LOT를 materialLots에 추가 (작업지시 자재투입 시 FIFO 선택 가능하도록)\n const newMaterialLot = {\n id: Date.now(),\n lotNo: lotNo,\n materialCode: po.materialCode,\n materialName: po.materialName,\n itemType: po.itemType || '원자재',\n category: po.category || '원자재',\n unit: po.unit || 'EA',\n supplier: po.supplierName || po.vendor || '',\n poNo: po.poNo,\n inboundDate: today,\n expiryDate: null, // 유통기한이 있는 품목의 경우 별도 설정\n initialQty: receiveData.qty,\n remainingQty: receiveData.qty,\n usedQty: 0,\n location: receiveData.location || 'A-01-01',\n status: 'AVAILABLE',\n spec: po.spec || '',\n lotType: 'RAW', // 입고 LOT는 원자재 타입\n };\n setMaterialLots(prev => [...prev, newMaterialLot]);\n\n // ★ 입고검사(IQC) 자동 생성\n const newInspection = {\n id: allInspections.length + 1,\n type: 'incoming',\n lotNo: inspectionLotNo,\n targetNo: po.poNo,\n itemName: po.materialName,\n qty: receiveData.qty,\n status: '검사대기',\n requestDate: today,\n requester: currentUser.name,\n vendorName: po.supplierName,\n };\n setAllInspections(prev => [...prev, newInspection]);\n }\n\n alert(`✅ 입고가 완료되었습니다.\\n\\n📦 입고LOT: ${lotNo}\\n🔍 검사LOT: ${inspectionLotNo}\\n입고수량: ${receiveData.qty}\\n\\n✅ 수입검사(IQC)가 자동 생성되었습니다.\\n✅ 자재투입용 LOT가 자동 등록되었습니다.\\n[품질관리 > 수입검사]에서 검사를 진행하세요.`);\n };\n\n // 수입검사 저장 - ★ LOT 자동생성 및 NCR 연동\n const handleSaveIncomingInspection = (inspection) => {\n const today = new Date().toISOString().split('T')[0];\n // ★ 검사LOT가 없으면 자동 생성\n const inspLotNo = inspection.lotNo || autoGenerateIncomingInspLot(new Date());\n\n const newInspection = {\n ...inspection,\n id: allInspections.length + 1,\n type: 'incoming',\n inspectionType: '수입검사',\n lotNo: inspLotNo,\n inspectionDate: today,\n inspector: currentUser.name,\n passQty: inspection.result === '합격' ? inspection.qty : (inspection.passQty || 0),\n failQty: inspection.result === '불합격' ? inspection.qty : (inspection.failQty || 0),\n };\n setAllInspections(prev => [...prev, newInspection]);\n\n // 발주 상태 업데이트\n if (inspection.targetNo) {\n setPurchaseOrders(prev => prev.map(po => {\n if (po.poNo === inspection.targetNo) {\n return {\n ...po,\n status: inspection.result === '합격' ? '입고완료' : '불합격',\n inspectionResult: inspection.result,\n inspectionDate: today,\n };\n }\n return po;\n }));\n }\n\n // ★ 불합격 시 NCR(부적합보고서) 자동 등록\n if (inspection.result === '불합격') {\n const ncrNo = `NCR-${formatDateCode(new Date())}-${String(defects.length + 1).padStart(3, '0')}`;\n const newDefect = {\n id: defects.length + 1,\n ncrNo,\n registDate: today,\n source: 'IQC',\n sourceType: '수입검사',\n sourceNo: inspection.targetNo || inspLotNo,\n itemName: inspection.materialName || inspection.itemName,\n defectQty: inspection.failQty || inspection.defectQty || inspection.qty,\n totalQty: inspection.qty,\n defectType: inspection.defectType || '품질불량',\n description: inspection.defectDescription || `수입검사 불합격 - ${inspection.defectType || '품질불량'}`,\n vendorName: inspection.vendorName,\n status: '처리대기',\n registeredBy: currentUser.name,\n approvalLine: {\n type: 'APR-3LINE',\n roles: [\n { id: 'writer', label: '작성', name: currentUser.name, date: today, status: 'approved' },\n { id: 'reviewer', label: '검토', name: '', date: '', status: 'pending' },\n { id: 'approver', label: '승인', name: '', date: '', status: 'pending' },\n ]\n }\n };\n setDefects(prev => [...prev, newDefect]);\n\n alert(`✅ 수입검사가 등록되었습니다.\\n\\n검사LOT: ${inspLotNo}\\n결과: ${inspection.result}\\n\\n⚠️ NCR(부적합보고서)가 자동 생성되었습니다.\\nNCR번호: ${ncrNo}\\n\\n[품질관리 > 부적합관리]에서 처리하세요.`);\n } else {\n alert(`✅ 수입검사가 등록되었습니다.\\n\\n검사LOT: ${inspLotNo}\\n결과: ${inspection.result}\\n합격수량: ${newInspection.passQty}`);\n }\n };\n\n // 중간검사 저장 - ★ LOT 자동생성 및 NCR 연동\n const handleSaveProcessInspection = (inspection) => {\n const today = new Date().toISOString().split('T')[0];\n // ★ 중간검사LOT 자동 생성 (공정단계 포함)\n const processStep = inspection.processStep || 1;\n const inspLotNo = inspection.lotNo || autoGenerateProcessInspLot(new Date(), processStep);\n\n const newInspection = {\n ...inspection,\n id: allInspections.length + 1,\n type: 'process',\n inspectionType: '중간검사',\n lotNo: inspLotNo,\n inspectionDate: today,\n inspector: currentUser.name,\n passQty: inspection.result === '합격' ? inspection.qty : (inspection.passQty || 0),\n failQty: inspection.result === '불합격' ? inspection.qty : (inspection.failQty || 0),\n };\n setAllInspections(prev => [...prev, newInspection]);\n\n // 작업지시 상태 업데이트\n if (inspection.workOrderNo) {\n setWorkOrders(prev => prev.map(wo => {\n if (wo.workOrderNo === inspection.workOrderNo) {\n return {\n ...wo,\n inspectionStatus: inspection.result,\n lastInspectionDate: today,\n };\n }\n return wo;\n }));\n }\n\n // ★ 불합격 시 NCR(부적합보고서) 자동 등록\n if (inspection.result === '불합격') {\n const ncrNo = `NCR-${formatDateCode(new Date())}-${String(defects.length + 1).padStart(3, '0')}`;\n const newDefect = {\n id: defects.length + 1,\n ncrNo,\n registDate: today,\n source: 'PQC',\n sourceType: '중간검사',\n sourceNo: inspection.workOrderNo,\n itemName: inspection.productName || inspection.itemName,\n defectQty: inspection.failQty || inspection.defectQty,\n totalQty: inspection.qty,\n defectType: inspection.defectType || '공정불량',\n description: inspection.defectDescription || `중간검사 불합격 - ${inspection.defectType || '공정불량'}`,\n vendorName: null,\n status: '처리대기',\n processType: '재작업',\n registeredBy: currentUser.name,\n approvalLine: {\n type: 'APR-3LINE',\n roles: [\n { id: 'writer', label: '작성', name: currentUser.name, date: today, status: 'approved' },\n { id: 'reviewer', label: '검토', name: '', date: '', status: 'pending' },\n { id: 'approver', label: '승인', name: '', date: '', status: 'pending' },\n ]\n }\n };\n setDefects(prev => [...prev, newDefect]);\n\n alert(`✅ 중간검사가 등록되었습니다.\\n\\n검사LOT: ${inspLotNo}\\n결과: ${inspection.result}\\n\\n⚠️ NCR(부적합보고서)가 자동 생성되었습니다.\\nNCR번호: ${ncrNo}\\n\\n[품질관리 > 부적합관리]에서 처리하세요.`);\n } else {\n alert(`✅ 중간검사가 등록되었습니다.\\n\\n검사LOT: ${inspLotNo}\\n결과: ${inspection.result}\\n\\n다음 공정으로 진행 가능합니다.`);\n }\n };\n\n // 부적합품 처리\n const handleProcessDefect = (defectId, processData) => {\n const today = new Date().toISOString().split('T')[0];\n\n setDefects(prev => prev.map(d => {\n if (d.id === defectId) {\n return {\n ...d,\n status: '처리완료',\n processType: processData.processType, // 재작업, 폐기, 특채\n processDate: today,\n processBy: currentUser.name,\n processNote: processData.note,\n };\n }\n return d;\n }));\n\n alert(`✅ 부적합품이 처리되었습니다.\\n\\n처리유형: ${processData.processType}`);\n };\n\n // 배송상태 업데이트\n const handleUpdateDeliveryStatus = (shipmentId, statusData) => {\n setShipments(prev => prev.map(s => {\n if (s.id === shipmentId) {\n return {\n ...s,\n deliveryStatus: statusData.status,\n deliveryProgress: statusData.progress,\n currentLocation: statusData.location,\n lastUpdateTime: new Date().toISOString(),\n };\n }\n return s;\n }));\n };\n\n const renderContent = () => {\n switch (view) {\n // 공통 UX 가이드\n case 'common-ux':\n return
;\n // 통합 테스트 대시보드\n case 'integrated-test':\n return
;\n case 'policy-guide':\n return
;\n case 'process-flowchart':\n // 플로우차트 screenId → 실제 view 매핑\n const screenToViewMap = {\n 'customer': 'customer-master-list',\n 'quote': 'quote-list',\n 'order': 'order-list',\n 'site': 'site-master-list',\n 'price': 'price-list',\n 'shipment': 'shipment-list',\n 'item': 'item-list',\n 'work-order': 'work-order-list',\n 'work-result': 'work-result-list',\n 'worker-task': 'worker-task-view',\n 'iqc': 'inspection-list',\n 'pqc': 'inspection-list',\n 'fqc': 'inspection-list',\n 'defect': 'defect-list',\n 'stock': 'stock-list',\n 'inbound': 'inbound-list',\n 'stock-adjustment': 'stock-adjustment-list',\n 'sales-list': 'sales-list',\n 'sales-statement': 'sales-statement',\n 'sales-tax-invoice': 'sales-tax-invoice',\n 'collection-list': 'collection-list',\n 'purchase-list': 'purchase-list',\n 'expense-list': 'expense-list',\n 'cashbook-list': 'cashbook-list',\n 'cost-list': 'cost-list',\n };\n return (\n
\n
\n
\n
\n
\n
{\n const mappedView = screenToViewMap[screen] || screen;\n setView(mappedView);\n }} />\n \n );\n case 'production-process-flowchart':\n return
;\n case 'detailed-process-flow':\n return
;\n // 대시보드\n case 'dashboard':\n case 'ceo-dashboard':\n return
;\n case 'sales-dashboard':\n return
;\n case 'production-dashboard':\n return
;\n case 'quality-dashboard':\n return
;\n case 'accounting-dashboard':\n return
;\n case 'purchase-dashboard':\n return
;\n case 'worker-dashboard':\n return
;\n // 품목관리 (생산관리 메뉴) - 공유 상태 연동\n case 'item-list':\n return
;\n case 'item-create':\n return
;\n case 'item-edit':\n return
;\n case 'item-detail':\n return
;\n // 품목기준관리\n case 'item-master-list':\n return
;\n // 자재기준관리\n case 'material-master':\n case 'material-master-list':\n return
;\n // 공정기준관리\n case 'process-master-list':\n return
;\n // 품질기준관리 (마스터)\n case 'quality-master-list':\n return
;\n // 거래처기준관리\n case 'customer-master-list':\n return
;\n // 현장기준관리\n case 'site-master-list':\n return
;\n // 수주기준관리\n case 'order-master-list':\n return
;\n // 생산기준관리\n case 'production-master-list':\n return
;\n // 출고기준관리\n case 'outbound-master-list':\n return
;\n // 공정관리\n case 'process-list':\n return
;\n case 'process-detail':\n return
navigate('process-list')}\n />;\n case 'process-create':\n return {\n const newCode = `P-${String(sampleProcesses.length + 1).padStart(3, '0')}`;\n const newProcess = {\n id: sampleProcesses.length + 1,\n processCode: newCode,\n ...formData,\n createdAt: new Date().toISOString().split('T')[0],\n updatedAt: new Date().toISOString().split('T')[0],\n };\n sampleProcesses.push(newProcess);\n navigate('process-list');\n }}\n onBack={() => navigate('process-list')}\n />;\n case 'process-edit':\n return {\n const idx = sampleProcesses.findIndex(p => p.id === selectedItem.id);\n if (idx !== -1) {\n sampleProcesses[idx] = { ...sampleProcesses[idx], ...formData, updatedAt: new Date().toISOString().split('T')[0] };\n }\n navigate('process-list');\n }}\n onBack={() => navigate('process-list')}\n />;\n // 품질기준관리\n case 'quality-standard-list':\n return ;\n // 채번관리\n case 'number-rule-list':\n return ;\n // 공통코드관리\n case 'code-rule-list':\n return ;\n // 견적수식관리\n case 'quote-formula-list':\n return ;\n // 문서양식관리\n case 'document-template-list':\n return ;\n // 견적관리\n case 'quote-list':\n return {\n // 수주 리스트에 새 수주 추가\n setOrders(prev => [newOrder, ...prev]);\n }}\n onUpdateQuote={(updatedQuote) => {\n // 견적 상태 업데이트\n setQuotes(prev => prev.map(q =>\n q.id === updatedQuote.id ? updatedQuote : q\n ));\n }}\n />;\n case 'quote-create':\n return navigate('quote-list')} onSave={handleSaveQuote} />;\n case 'quote-detail':\n return navigate('quote-list')}\n onConvertToOrder={(quote, deliveryInfo) => {\n // 수주번호 생성 - 채번관리 규칙 적용\n const productType = quote.productType || quote.productName || '';\n const newOrderNo = generateNumber('수주번호', productType, []);\n\n // 새 수주 데이터 생성\n const newOrder = {\n id: Date.now(),\n orderNo: newOrderNo,\n quoteNo: quote.quoteNo,\n orderDate: new Date().toISOString().split('T')[0],\n customerName: quote.customerName,\n siteName: quote.siteName,\n siteAddress: quote.siteAddress || deliveryInfo?.deliveryAddress,\n manager: quote.manager,\n contact: quote.contact,\n productName: quote.productName,\n qty: quote.qty,\n totalAmount: quote.finalAmount || quote.totalAmount,\n status: '수주등록',\n dueDate: deliveryInfo?.dueDate || quote.dueDate,\n deliveryMethod: deliveryInfo?.deliveryMethod || '상차',\n freightCost: deliveryInfo?.freightCost || '선불',\n receiverName: deliveryInfo?.receiverName || '',\n receiverPhone: deliveryInfo?.receiverPhone || '',\n deliveryAddress: deliveryInfo?.deliveryAddress || '',\n note: deliveryInfo?.note || '',\n creditGrade: quote.creditGrade,\n items: quote.items || [],\n calculatedItems: quote.calculatedItems || [],\n bomData: quote.bomData || {},\n splits: [],\n history: [{\n id: Date.now(),\n changedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n changeType: '수주등록',\n description: `견적(${quote.quoteNo})에서 수주전환`,\n changedBy: '현재 사용자',\n }],\n };\n\n // 수주 추가\n setOrders(prev => [newOrder, ...prev]);\n // 견적 상태 업데이트\n setQuotes(prev => prev.map(q => q.id === quote.id ? { ...q, status: '수주전환', convertedOrderNo: newOrderNo } : q));\n }}\n onUpdateQuote={(updatedQuote) => {\n setQuotes(prev => prev.map(q => q.id === updatedQuote.id ? updatedQuote : q));\n setSelectedItem(updatedQuote);\n }}\n onCreateOrder={(newOrder) => {\n setOrders(prev => [newOrder, ...prev]);\n }}\n />;\n // 수주관리\n case 'order-list':\n return {\n // 수주에서 바로 공정별 작업지시 자동 생성 (구매품/서비스 제외)\n const now = new Date();\n const bomItems = order.items || [];\n const processGroups = {};\n\n // BOM 품목을 공정별로 그룹화 (구매품/서비스 제외)\n bomItems.forEach(item => {\n const mapping = itemProcessMapping[item.itemCode];\n if (mapping && mapping.processCode && !mapping.isPurchased && !mapping.isService) {\n const key = mapping.processCode;\n if (!processGroups[key]) {\n processGroups[key] = {\n processCode: mapping.processCode,\n processName: mapping.processName,\n workflowCode: mapping.workflowCode,\n processSeq: mapping.processSeq,\n items: []\n };\n }\n processGroups[key].items.push(item);\n }\n });\n\n // 공정별 작업지시서 생성\n const newWorkOrders = Object.values(processGroups)\n .sort((a, b) => a.processSeq - b.processSeq)\n .map((group, idx) => {\n const woDate = new Date(now);\n woDate.setDate(woDate.getDate() + idx); // 공정 순서대로 1일씩 추가\n\n return {\n id: Date.now() + idx,\n workOrderNo: `WO-${order.orderNo.replace('SO-', '')}-${String(idx + 1).padStart(2, '0')}`,\n workOrderDate: woDate.toISOString().split('T')[0],\n status: '대기',\n orderId: order.id,\n orderNo: order.orderNo,\n quoteNo: order.quoteNo,\n customerId: order.customerId,\n customerName: order.customerName,\n siteId: order.siteId,\n siteName: order.siteName,\n productType: order.productType,\n qty: order.qty,\n processCode: group.processCode,\n processName: group.processName,\n workflowCode: group.workflowCode,\n processSeq: group.processSeq,\n items: group.items,\n dueDate: order.dueDate,\n plannedStartDate: woDate.toISOString().split('T')[0],\n plannedEndDate: woDate.toISOString().split('T')[0],\n createdBy: '판매팀',\n createdAt: now.toISOString(),\n note: `${order.siteName} - ${group.processName} 공정`,\n };\n });\n\n // 작업지시 추가\n setWorkOrders(prev => [...newWorkOrders, ...prev]);\n\n // 수주 상태 업데이트 (생산지시 생성 완료)\n setOrders(prev => prev.map(o =>\n o.id === order.id ? { ...o, status: '생산지시완료' } : o\n ));\n\n // 알림 표시 및 작업지시 관리 페이지로 이동\n alert(`✅ ${newWorkOrders.length}개의 작업지시서가 공정별로 자동 생성되었습니다.\\n\\n생성된 작업지시서:\\n${newWorkOrders.map(wo => `- ${wo.workOrderNo} (${wo.processName})`).join('\\n')}\\n\\n작업지시 관리 페이지로 이동합니다.`);\n navigate('work-order-list');\n }}\n onCancelOrder={handleCancelOrder}\n />;\n case 'order-create':\n return navigate('order-list')} onSave={handleSaveOrder} />;\n case 'order-create-from-quote':\n return navigate('quote-detail', selectedItem)}\n onSave={(order) => {\n handleSaveOrder(order);\n // 견적 상태 업데이트\n if (selectedItem) {\n setQuotes(prev => prev.map(q =>\n q.id === selectedItem.id\n ? { ...q, status: '수주전환', convertedOrderNo: order.orderNo }\n : q\n ));\n }\n }}\n />;\n case 'order-create-additional':\n // 추가분 수주 생성\n const additionalData = selectedItem; // { quote, pendingItems, relatedOrders }\n return navigate('quote-detail', additionalData?.quote)}\n onSave={(order) => {\n // 추가분 수주 저장\n const newOrder = {\n ...order,\n orderType: 'additional',\n parentOrderNo: additionalData?.relatedOrders?.[0]?.orderNo,\n };\n handleSaveOrder(newOrder);\n // 견적 품목 상태 업데이트\n if (additionalData?.quote) {\n setQuotes(prev => prev.map(q =>\n q.id === additionalData.quote.id\n ? {\n ...q,\n relatedOrders: [...(q.relatedOrders || []), newOrder.orderNo],\n items: q.items.map(item =>\n additionalData.pendingItems.find(pi => pi.id === item.id)\n ? { ...item, orderStatus: 'ordered', orderId: newOrder.id }\n : item\n )\n }\n : q\n ));\n }\n navigate('quote-detail', additionalData?.quote);\n }}\n />;\n case 'order-detail':\n return navigate('order-list')}\n onUpdate={(updatedOrder) => {\n setOrders(prev => prev.map(o => o.id === updatedOrder.id ? updatedOrder : o));\n setSelectedItem(updatedOrder);\n }}\n onCreateWorkOrder={(newWorkOrder) => {\n setWorkOrders(prev => [...prev, newWorkOrder]);\n }}\n onCreateProductionOrder={(order) => {\n // 수주에서 생산지시 생성 (구매품/서비스 제외)\n const now = new Date();\n const bomItems = order.items || [];\n const processGroups = {};\n\n bomItems.forEach(item => {\n const mapping = itemProcessMapping[item.itemCode];\n if (mapping && mapping.processCode && !mapping.isPurchased && !mapping.isService) {\n const key = mapping.processCode;\n if (!processGroups[key]) {\n processGroups[key] = {\n processCode: mapping.processCode,\n processName: mapping.processName,\n workflowCode: mapping.workflowCode,\n processSeq: mapping.processSeq,\n items: []\n };\n }\n processGroups[key].items.push(item);\n }\n });\n\n const newPO = {\n id: Date.now(),\n productionOrderNo: `PO-${order.orderNo.replace('SO-', '')}`,\n productionOrderDate: now.toISOString().split('T')[0],\n status: '생산대기',\n orderId: order.id,\n orderNo: order.orderNo,\n quoteNo: order.quoteNo,\n customerId: order.customerId,\n customerName: order.customerName,\n siteId: order.siteId,\n siteName: order.siteName,\n productType: order.productType,\n qty: order.qty,\n dueDate: order.dueDate,\n processGroups: Object.values(processGroups).sort((a, b) => a.processSeq - b.processSeq),\n totalProcesses: Object.keys(processGroups).length,\n completedProcesses: 0,\n workOrdersGenerated: false,\n createdBy: '판매팀',\n createdAt: now.toISOString(),\n note: `${order.siteName} 생산지시`,\n };\n\n setProductionOrders(prev => [newPO, ...prev]);\n setOrders(prev => prev.map(o =>\n o.id === order.id ? { ...o, productionOrderNo: newPO.productionOrderNo, status: '생산지시완료' } : o\n ));\n\n alert(`✅ 생산지시가 생성되었습니다.\\n\\n생산지시번호: ${newPO.productionOrderNo}\\n\\n생산관리 > 생산지시 관리에서 작업지시서를 생성하세요.`);\n navigate('production-order-detail', newPO);\n }}\n onCancelOrder={handleCancelOrder}\n />;\n case 'order-edit':\n return navigate('order-detail', selectedItem)} onSave={handleUpdateOrder} />;\n // 생산지시 생성 페이지\n case 'production-order-create':\n return navigate('order-detail', selectedItem)}\n onNavigate={navigate}\n onConfirm={(order) => {\n // 수주에서 생산지시 생성 (구매품/서비스 제외)\n const now = new Date();\n const bomItems = order.items || [];\n const processGroups = {};\n\n bomItems.forEach(item => {\n const mapping = itemProcessMapping[item.itemCode];\n if (mapping && mapping.processCode && !mapping.isPurchased && !mapping.isService) {\n const key = mapping.processCode;\n if (!processGroups[key]) {\n processGroups[key] = {\n processCode: mapping.processCode,\n processName: mapping.processName,\n workflowCode: mapping.workflowCode,\n processSeq: mapping.processSeq,\n items: []\n };\n }\n processGroups[key].items.push(item);\n }\n });\n\n const newPO = {\n id: Date.now(),\n productionOrderNo: `PO-${order.orderNo.replace('SO-', '')}`,\n productionOrderDate: now.toISOString().split('T')[0],\n status: '생산대기',\n orderId: order.id,\n orderNo: order.orderNo,\n quoteNo: order.quoteNo,\n customerId: order.customerId,\n customerName: order.customerName,\n siteId: order.siteId,\n siteName: order.siteName,\n productType: order.productType,\n qty: order.qty,\n dueDate: order.dueDate,\n processGroups: Object.values(processGroups).sort((a, b) => a.processSeq - b.processSeq),\n totalProcesses: Object.keys(processGroups).length,\n completedProcesses: 0,\n workOrdersGenerated: false,\n createdBy: '판매팀',\n createdAt: now.toISOString(),\n note: `${order.siteName} 생산지시`,\n };\n\n setProductionOrders(prev => [newPO, ...prev]);\n setOrders(prev => prev.map(o =>\n o.id === order.id ? { ...o, productionOrderNo: newPO.productionOrderNo, status: '생산지시완료' } : o\n ));\n\n alert(`✅ 생산지시가 생성되었습니다.\\n\\n생산지시번호: ${newPO.productionOrderNo}\\n\\n생산관리 > 생산지시 관리에서 작업지시서를 생성하세요.`);\n navigate('production-order-detail', newPO);\n }}\n />;\n // 생산관리 - 현황판\n case 'production-status-board':\n return ;\n // 생산관리 - 작업자 화면\n case 'worker-task-view':\n return navigate('production-dashboard')}\n onStartWork={(task) => {\n setWorkOrders(prev => prev.map(wo =>\n wo.id === task.id ? { ...wo, status: '작업중', startTime: new Date().toISOString() } : wo\n ));\n }}\n onCompleteWork={(task) => {\n // 작업 완료 + 실적 자동 등록\n handleCompleteWork(task);\n }}\n onReportIssue={(task, issue) => {\n console.log('Issue reported:', task.workOrderNo, issue);\n alert(`이슈가 보고되었습니다.\\n작업: ${task.workOrderNo}\\n유형: ${issue.type}`);\n }}\n onMaterialInput={(task) => {\n // 자재투입: 작업자 화면에서 바로 자재투입 모달 열기\n setWorkerMaterialTask(task);\n setShowWorkerMaterialModal(true);\n }}\n />;\n // 생산관리 - 제품 이력 추적\n case 'traceability-list':\n return ;\n // 생산지시 관리\n case 'production-order-list':\n return {\n // BOM 기반 공정별 작업지시서 자동 생성\n const newWorkOrders = [];\n const now = new Date();\n const baseId = Date.now();\n\n po.processGroups?.forEach((pg, idx) => {\n const woNo = `WO-${po.orderNo.replace('SO-', '')}-${String(idx + 1).padStart(2, '0')}`;\n newWorkOrders.push({\n id: baseId + idx,\n workOrderNo: woNo,\n workOrderDate: now.toISOString().split('T')[0],\n status: '대기',\n // 생산지시/수주 연결\n productionOrderNo: po.productionOrderNo,\n orderId: po.orderId,\n orderNo: po.orderNo,\n // 공정 정보\n processCode: pg.processCode,\n processName: pg.processName,\n processSeq: pg.processSeq,\n // 제품 정보\n productType: po.productType,\n siteName: po.siteName,\n customerName: po.customerName,\n qty: po.qty,\n totalQty: po.qty,\n completedQty: 0,\n defectQty: 0,\n // 품목 목록\n items: pg.items,\n // 작업 계획\n plannedStartDate: now.toISOString().split('T')[0],\n plannedEndDate: new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],\n plannedQty: po.qty,\n // 메타\n createdBy: '생산관리',\n createdAt: now.toISOString(),\n note: `${po.siteName} ${pg.processName} 공정`,\n });\n });\n\n setWorkOrders(prev => [...newWorkOrders, ...prev]);\n\n // 생산지시 상태 업데이트\n setProductionOrders(prev => prev.map(p =>\n p.id === po.id ? { ...p, workOrdersGenerated: true, status: '생산지시완료' } : p\n ));\n\n // 수주 상태 업데이트 (생산지시 생성 완료)\n setOrders(prev => prev.map(o =>\n o.orderNo === po.orderNo ? { ...o, status: '생산지시완료' } : o\n ));\n\n alert(`✅ ${newWorkOrders.length}개의 작업지시서가 생성되었습니다.\\n\\n생성된 작업지시서:\\n${newWorkOrders.map(wo => `- ${wo.workOrderNo} (${wo.processName})`).join('\\n')}`);\n }}\n />;\n case 'production-order-detail':\n return navigate('production-order-list')}\n onCreateWorkOrders={(po) => {\n // BOM 기반 공정별 작업지시서 자동 생성 (상세화면에서도 동일 로직)\n const newWorkOrders = [];\n const now = new Date();\n const baseId = Date.now();\n\n po.processGroups?.forEach((pg, idx) => {\n const woNo = `WO-${po.orderNo.replace('SO-', '')}-${String(idx + 1).padStart(2, '0')}`;\n newWorkOrders.push({\n id: baseId + idx,\n workOrderNo: woNo,\n workOrderDate: now.toISOString().split('T')[0],\n status: '대기',\n productionOrderNo: po.productionOrderNo,\n orderId: po.orderId,\n orderNo: po.orderNo,\n processCode: pg.processCode,\n processName: pg.processName,\n processSeq: pg.processSeq,\n productType: po.productType,\n siteName: po.siteName,\n customerName: po.customerName,\n qty: po.qty,\n totalQty: po.qty,\n completedQty: 0,\n defectQty: 0,\n items: pg.items,\n plannedStartDate: now.toISOString().split('T')[0],\n plannedEndDate: new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],\n plannedQty: po.qty,\n createdBy: '생산관리',\n createdAt: now.toISOString(),\n note: `${po.siteName} ${pg.processName} 공정`,\n });\n });\n\n setWorkOrders(prev => [...newWorkOrders, ...prev]);\n setProductionOrders(prev => prev.map(p =>\n p.id === po.id ? { ...p, workOrdersGenerated: true, status: '생산지시완료' } : p\n ));\n setOrders(prev => prev.map(o =>\n o.orderNo === po.orderNo ? { ...o, status: '생산지시완료' } : o\n ));\n\n // 작업지시 생성 완료 후 작업지시 관리 페이지로 이동\n alert(`✅ ${newWorkOrders.length}개의 작업지시서가 공정별로 자동 생성되었습니다.\\n\\n생성된 작업지시서:\\n${newWorkOrders.map(wo => `- ${wo.workOrderNo} (${wo.processName})`).join('\\n')}\\n\\n작업지시 관리 페이지로 이동합니다.`);\n navigate('work-order-list');\n }}\n />;\n case 'work-order-list':\n return {\n // 담당자 배정 처리\n setWorkOrders(prev => prev.map(wo =>\n wo.id === workOrderId\n ? { ...wo, assignee: assignees.join(', '), assignees: assignees }\n : wo\n ));\n }}\n onStartWork={(workOrderId) => {\n // ★ 작업 시작 처리\n const now = new Date();\n setWorkOrders(prev => prev.map(wo =>\n wo.id === workOrderId\n ? {\n ...wo,\n status: '작업중',\n startedAt: now.toISOString(),\n currentStep: wo.processType === '스크린' ? '원단절단' :\n wo.processType === '슬랫' ? '코일절단' :\n wo.processType === '절곡' ? '절단' : '작업준비'\n }\n : wo\n ));\n alert('✅ 작업이 시작되었습니다.');\n }}\n onCompleteWork={(workOrderId) => {\n // ★ 작업 완료 처리\n const wo = workOrders.find(w => w.id === workOrderId);\n if (wo && wo.completedQty < wo.totalQty) {\n if (!confirm(`완료 수량(${wo.completedQty})이 총수량(${wo.totalQty})보다 적습니다.\\n그래도 완료 처리하시겠습니까?`)) {\n return;\n }\n }\n handleCompleteWork(workOrderId);\n }}\n onProgressWork={(workOrderId, progressData) => {\n // ★ 공정 진행 저장 처리\n const now = new Date();\n setWorkOrders(prev => prev.map(wo =>\n wo.id === workOrderId\n ? {\n ...wo,\n completedQty: progressData.completedQty,\n defectQty: progressData.defectQty,\n currentStep: progressData.currentStep,\n lastProgressAt: now.toISOString(),\n progressNote: progressData.note,\n // 완료 수량이 총 수량에 도달하면 자동 완료 처리하지 않고 상태만 유지\n }\n : wo\n ));\n\n // 불량 발생 시 NCR 자동 생성\n if (progressData.defectQty > 0) {\n const wo = workOrders.find(w => w.id === workOrderId);\n if (wo) {\n const ncrNo = `NCR-${now.toISOString().slice(0, 10).replace(/-/g, '')}-${String(ncrs.length + 1).padStart(3, '0')}`;\n const newNcr = {\n id: Date.now(),\n ncrNo,\n workOrderNo: wo.workOrderNo,\n productName: wo.siteName,\n defectQty: progressData.defectQty,\n defectType: '공정불량',\n description: progressData.note || `${progressData.currentStep} 공정에서 불량 발생`,\n reportDate: now.toISOString().split('T')[0],\n reporter: wo.assignee || '생산팀',\n status: '접수',\n approvalLine: [\n { step: 1, role: '품질담당', approver: '', status: '대기', date: null },\n { step: 2, role: '품질팀장', approver: '', status: '대기', date: null },\n ]\n };\n setNcrs(prev => [...prev, newNcr]);\n }\n }\n\n alert(`✅ 진행상황이 저장되었습니다.\\n- 현재 단계: ${progressData.currentStep}\\n- 완료: ${progressData.completedQty}개\\n- 불량: ${progressData.defectQty}개`);\n }}\n />;\n case 'work-order-detail':\n return navigate('work-order-list')}\n onUpdate={(updatedWorkOrder) => {\n setWorkOrders(prev => prev.map(wo => wo.id === updatedWorkOrder.id ? updatedWorkOrder : wo));\n setSelectedItem(updatedWorkOrder);\n }}\n onUpdateOrder={(orderNo, updates) => {\n setOrders(prev => prev.map(o => o.orderNo === orderNo ? { ...o, ...updates } : o));\n }}\n onUseMaterial={(materialCode, qty, usage) => {\n // 재고 감소\n setInventory(prev => prev.map(item =>\n item.materialCode === materialCode\n ? { ...item, stock: Math.max(0, item.stock - qty), lastUpdated: new Date().toISOString().split('T')[0] }\n : item\n ));\n // 사용 이력 추가\n setMaterialUsage(prev => [...prev, {\n id: Date.now(),\n ...usage,\n materialCode,\n qty,\n usedAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n }]);\n }}\n setActiveModal={setActiveModal}\n showFeatureDescription={showFeatureDescription}\n featureBadges={featureBadges}\n selectedBadge={selectedBadge}\n setSelectedBadge={setSelectedBadge}\n updateFeatureBadge={updateFeatureBadge}\n setEditingBadge={setEditingBadge}\n />;\n case 'work-order-register':\n return navigate('work-order-list')} onSave={handleSaveWorkOrder} />;\n case 'work-order-edit':\n return navigate('work-order-detail', selectedItem)} onSave={handleUpdateWorkOrder} />;\n case 'work-result-list':\n return ;\n case 'work-result-input':\n return navigate('work-order-detail', selectedItem)} onSave={handleSaveWorkResult} />;\n case 'work-result-create':\n return navigate('worker-task-view')} onSave={handleSaveWorkResult} />;\n case 'shipment-list':\n return ;\n case 'shipment-create':\n return navigate('shipment-list')} onSave={handleSaveShipment} />;\n case 'shipment-detail':\n return navigate('shipment-list')} onUpdate={handleUpdateShipment} />;\n case 'shipment-edit':\n return navigate('shipment-detail', selectedItem)} onSave={handleUpdateShipment} />;\n // 품질관리 - 검사관리 (통합)\n case 'inspection-list':\n return ;\n case 'inspection-detail':\n return navigate('inspection-list')} onUpdate={(updated) => setProductInspections(prev => prev.map(i => i.id === updated.id ? updated : i))} />;\n // 품질관리 - 수입검사(IQC)\n case 'iqc-list':\n return ;\n // 품질관리 - 중간검사(PQC)\n case 'pqc-list':\n return {\n // 검사 완료 시 다음 공정 진행 허용\n setWorkOrders(prev => prev.map(w =>\n w.id === wo.id ? { ...w, inspectionStatus: result.passed ? '검사합격' : '검사불합격', pqcResult: result } : w\n ));\n }}\n />;\n // 품질관리 - 제품검사(FQC)\n case 'fqc-list':\n return ;\n // 품질관리 - 통합 검사 등록 화면\n case 'inspection-register':\n return {\n // 검사 유형별 저장\n if (inspectionData.type === 'incoming') {\n setAllInspections(prev => [...prev, { ...inspectionData, id: prev.length + 1 }]);\n } else if (inspectionData.type === 'process') {\n setAllInspections(prev => [...prev, { ...inspectionData, id: prev.length + 1 }]);\n } else if (inspectionData.type === 'final') {\n setAllInspections(prev => [...prev, { ...inspectionData, id: prev.length + 1 }]);\n setProductInspections(prev => [...prev, { ...inspectionData, id: prev.length + 1 }]);\n }\n navigate('inspection-list');\n }}\n />;\n // 품질관리 - 검사 등록 화면 (개별)\n case 'iqc-register':\n return {\n // 검사 결과 저장\n setAllInspections(prev => [...prev, { ...inspectionData, id: prev.length + 1, type: 'incoming' }]);\n\n // 발주 데이터에 검사 결과 반영 및 합격 시 자동 입고 처리\n let updatedInbound = null;\n setPurchaseOrders(prev => prev.map(po => {\n if (po.poNo === inspectionData.targetNo) {\n const isPass = inspectionData.result === '합격';\n updatedInbound = {\n ...po,\n status: isPass ? '입고완료' : '검사불합격',\n receivedQty: isPass ? po.requestQty : po.receivedQty, // 합격 시 입고 수량 자동 반영\n receivedDate: isPass ? inspectionData.inspectionDate : po.receivedDate,\n inspectionLot: inspectionData.lotNo, // 검사 LOT 저장\n inspectionResult: {\n result: inspectionData.result,\n inspector: inspectionData.inspector,\n inspectionDate: inspectionData.inspectionDate,\n inspectionItems: inspectionData.inspectionItems,\n }\n };\n\n // 합격 시 재고에 자동 추가\n if (isPass) {\n setInventory(prev => [...prev, {\n id: prev.length + 1,\n itemCode: po.materialCode,\n itemName: po.materialName,\n category: '원자재',\n lotNo: inspectionData.lotNo,\n warehouseCode: 'WH-001',\n warehouseName: '원자재창고',\n locationCode: 'A-01',\n locationName: 'A구역-01',\n quantity: po.requestQty,\n unit: po.unit,\n receivedDate: inspectionData.inspectionDate,\n expiryDate: null,\n supplier: po.vendor,\n poNo: po.poNo,\n status: '정상'\n }]);\n }\n\n return updatedInbound;\n }\n return po;\n }));\n\n alert(`✅ 수입검사가 ${inspectionData.result} 처리되었습니다.\\n\\nLOT번호: ${inspectionData.lotNo}${inspectionData.result === '합격' ? '\\n입고 처리가 완료되었습니다.' : ''}`);\n // 입고 상세로 돌아가기\n navigate('inbound-detail', updatedInbound || selectedItem);\n }}\n />;\n case 'pqc-register':\n return {\n setAllInspections(prev => [...prev, { ...inspectionData, id: prev.length + 1, type: 'process' }]);\n navigate('inspection-list');\n }}\n />;\n case 'fqc-register':\n return {\n setAllInspections(prev => [...prev, { ...inspectionData, id: prev.length + 1, type: 'final' }]);\n setProductInspections(prev => [...prev, { ...inspectionData, id: prev.length + 1, type: 'final' }]);\n navigate('inspection-list');\n }}\n />;\n // 거래처 관리 (판매관리 - 회계정보 숨김)\n case 'customer-list':\n return ;\n case 'customer-detail':\n return ;\n case 'customer-register':\n return ;\n case 'customer-edit':\n return ;\n // 현장 관리\n case 'site-list':\n return ;\n case 'site':\n return ;\n case 'site-detail':\n return ;\n case 'site-register':\n return ;\n case 'site-edit':\n return ;\n // 단가 관리 (단일 목록 + 이력관리 + 거래처그룹 할인) - 품목마스터 연동\n case 'price-list':\n return ;\n // 자재관리 - 재고현황\n case 'stock-list':\n return ;\n case 'stock-detail':\n return ;\n // 자재관리 - 입고관리\n case 'inbound-list':\n return ;\n case 'inbound-detail':\n return ;\n // 자재관리 - 재고조정\n case 'stock-adjustment-list':\n return ;\n // 품질관리 - 부적합품관리\n case 'defect-list':\n return ;\n // 물류관리 - 배송현황\n case 'delivery-status-list':\n return s.status === '출하확정' || s.status === '배송중' || s.status === '배송완료')}\n onNavigate={navigate}\n onUpdateStatus={handleUpdateDeliveryStatus}\n />;\n // 회계관리 - 거래처관리\n case 'acc-customer-list':\n return ;\n case 'acc-customer-register':\n return ;\n case 'acc-customer-detail':\n return ;\n case 'acc-customer-edit':\n return ;\n case 'acc-customer-statement':\n return ;\n case 'acc-customer-tax':\n return ;\n // 회계관리 - 매출관리\n case 'sales-list':\n case 'sales-account':\n return ;\n case 'sales-statement':\n return ;\n case 'sales-tax-invoice':\n return ;\n // 회계관리 - 매입관리\n case 'purchase':\n case 'purchase-list':\n return ;\n case 'purchase-register':\n return ;\n case 'expense-list':\n return ;\n case 'expense-register':\n return ;\n case 'expense-estimate':\n return ;\n // 회계관리 - 금전출납부\n case 'cashbook':\n case 'cashbook-list':\n return ;\n case 'cashbook-register':\n return ;\n case 'cashbook-edit':\n return ;\n // 회계관리 - 수금관리\n case 'collection-list':\n return ;\n case 'collection-register':\n return ;\n case 'receivable-list':\n case 'outstanding':\n case 'receivable':\n return ;\n case 'bill-list':\n return ;\n // 회계관리 - 원가관리\n case 'cost-analysis':\n case 'cost-list':\n return ;\n case 'cost-detail':\n return ;\n // 회계관리 - 기존 호환\n case 'invoice':\n return ;\n case 'collection':\n return ;\n // 시스템관리\n case 'users':\n return ;\n case 'roles':\n return ;\n case 'settings':\n return ;\n default:\n return (\n \n
\n
준비 중인 기능입니다
\n
해당 메뉴는 곧 구현될 예정입니다.
\n
\n );\n }\n };\n\n // 그레이스케일 와이어프레임 스타일\n const grayscaleStyle = wireframeMode ? {\n filter: 'grayscale(100%)',\n WebkitFilter: 'grayscale(100%)',\n } : {};\n\n return (\n <>\n {/* C키 누른 후 클릭 대기 상태 표시 - 컬러 유지 */}\n {isPlacingBadge && (\n \n \n {nextBadgeNumber}\n \n 클릭하여 #{nextBadgeNumber} 뱃지 배치\n (ESC 취소)\n
\n )}\n\n {/* 인라인 입력창 - 컬러 유지 */}\n {inlineInputPosition && (\n \n
\n setInlineInputPosition(null)}\n />\n
\n
\n )}\n\n {/* 인라인 뱃지 수정 팝업 - 컬러 유지 */}\n {editingBadge && (\n \n
\n setEditingBadge(null)}\n onDelete={(badgeId) => {\n deleteWithPassword(badgeId, () => setEditingBadge(null));\n }}\n />\n
\n
\n )}\n\n {/* 기능정의서 활성화 시: 화면 정보 헤더 - 그레이스케일 외부, fixed로 다이얼로그 위에 표시 */}\n {!isMobile && showFeatureDescription && (\n \n
\n \n \n | 화면명 | \n {getScreenName()} | \n 화면ID | \n \n {getScreenId()}\n \n {/* 화면ID 코드 체계 설명 팝업 */}\n {showIdCodeHelp && (\n \n \n {idCodeHelpContent.title}\n \n \n \n {idCodeHelpContent.sections.map((section, idx) => (\n \n {section.category} \n {section.description} \n \n {section.examples.map((ex, i) => (\n {ex}\n ))}\n \n \n ))}\n \n {idCodeHelpContent.suffix}\n \n \n \n )}\n | \n
\n \n | 화면경로 | \n {getScreenPath()} | \n
\n \n
\n
\n )}\n\n \n {/* 좌측: 그레이스케일 적용 영역 (사이드바 + 메인 컨텐츠) */}\n
\n {/* 사이드바 + 메인 컨텐츠 영역 */}\n
\n {/* 데스크탑/태블릿: 사이드바 */}\n {!isMobile && (\n
setShowSettings(true)}\n />\n )}\n\n {/* 기능정의문서 패널 - 좌측에서 슬라이드 */}\n {!isMobile && showFeatureDocPanel && (\n setShowFeatureDocPanel(false)}\n screenFeatures={getFeaturesByCurrentScreen(activeMenu, view)}\n currentScreenName={activeMenu}\n currentView={view}\n onBadgeHover={setHoveredBadgeNumber}\n />\n )}\n\n {/* 메인 컨텐츠 */}\n \n {/* 모바일: 상단 헤더 */}\n {isMobile && (\n
setShowMobileMenu(true)}\n showBack={view !== 'order-list'}\n onBack={() => navigate('order-list')}\n />\n )}\n\n {/* 데스크탑/태블릿: 상단 헤더 */}\n {!isMobile && (\n setShowVersionHistory(true)}\n showFlowPanel={showFlowPanel}\n onToggleFlowPanel={() => setShowFlowPanel(!showFlowPanel)}\n userNickname={userNickname}\n showFeatureDocPanel={showFeatureDocPanel}\n onToggleFeatureDocPanel={() => setShowFeatureDocPanel(!showFeatureDocPanel)}\n showComprehensiveFlowPanel={showComprehensiveFlowPanel}\n onToggleComprehensiveFlowPanel={() => setShowComprehensiveFlowPanel(!showComprehensiveFlowPanel)}\n showAllMenuFeatureDocPanel={showAllMenuFeatureDocPanel}\n onToggleAllMenuFeatureDocPanel={() => setShowAllMenuFeatureDocPanel(!showAllMenuFeatureDocPanel)}\n isFeatureAdmin={isFeatureAdmin}\n />\n )}\n\n {/* 메인 컨텐츠 영역 */}\n \n \n {renderContent()}\n
\n\n {/* 기능정의문서 자동 번호 뱃지 오버레이 */}\n {!isMobile && showFeatureDocPanel && (\n {\n console.log('Badge clicked:', badge);\n }}\n />\n )}\n \n \n \n
\n\n {/* 뱃지 오버레이 - 그레이스케일 밖에서 컬러 유지, 메인 영역 위에 고정 */}\n {/* 다이얼로그 dim 레이어(z-50~z-9999) 위에 표시되도록 z-[10001] 설정 */}\n {!isMobile && showFeatureDescription && contentAreaBounds.width > 0 && (\n
\n
\n updateFeatureBadge(badgeId, pos)}\n onAnalyzeAndUpdate={(badgeId, analysis) => {\n updateFeatureBadge(badgeId, {\n ...(analysis.label && !getCurrentScreenBadges().find(b => b.id === badgeId)?.label ? { label: analysis.label } : {}),\n uiInfo: analysis.uiInfo,\n funcInfo: analysis.funcInfo,\n dataInfo: analysis.dataInfo,\n });\n }}\n onEditBadge={(badge) => setEditingBadge(badge)}\n />\n
\n
\n )}\n\n {/* 기능정의서 + 플로우 우측 패널 영역 - 반응형 레이아웃 */}\n {!isMobile && (showFeatureDescription || showFlowPanel || showComprehensiveFlowPanel) && (\n
\n {/* 기능정의서 패널 */}\n {showFeatureDescription && (\n
\n setShowFeatureDescription(false)}\n badges={getCurrentScreenBadges()}\n selectedBadge={selectedBadge}\n onSelectBadge={setSelectedBadge}\n onUpdateBadge={updateFeatureBadge}\n onDeleteBadge={deleteFeatureBadge}\n onAddBadge={addFeatureBadge}\n isAddingBadge={isAddingBadge}\n setIsAddingBadge={setIsAddingBadge}\n badgeEditMode={badgeEditMode}\n setBadgeEditMode={setBadgeEditMode}\n screenName={getScreenName()}\n screenPath={getScreenPath()}\n screenId={getScreenId()}\n allBadges={featureBadges}\n onImportBadges={setFeatureBadges}\n userNickname={userNickname}\n onChangeNickname={() => setShowNicknameModal(true)}\n onLoadTemplate={loadTemplateToCurrentScreen}\n currentScreenKey={getCurrentScreenKey()}\n hasTemplate={checkHasTemplate()}\n onExtractFeatures={extractFeatures}\n onAddComment={addBadgeComment}\n onDeleteComment={deleteBadgeComment}\n isAdmin={isFeatureAdmin}\n />\n
\n )}\n\n {/* 유저플로우 패널 - 기능정의서 우측에 표시 */}\n {showFlowPanel && (\n
\n setShowFlowPanel(false)}\n onNavigate={(menu, viewName) => {\n setActiveMenu(menu);\n setView(viewName);\n }}\n />\n
\n )}\n\n {/* 종합 플로우 패널 - 드래그로 너비 조절 가능 */}\n {showComprehensiveFlowPanel && (\n
setShowComprehensiveFlowPanel(false)}\n width={comprehensiveFlowPanelWidth}\n onWidthChange={setComprehensiveFlowPanelWidth}\n isInline={true}\n />\n )}\n \n )}\n
\n\n {/* 모바일 메뉴 */}\n {isMobile && (\n setShowMobileMenu(false)}\n activeMenu={activeMenu}\n onMenuChange={handleMenuChange}\n menuConfig={menuConfig}\n />\n )}\n\n {/* 메뉴 설정 모달 */}\n {showSettings && (\n setShowSettings(false)}\n />\n )}\n\n {/* 닉네임 입력 모달 */}\n {showNicknameModal && (\n \n )}\n\n {/* 기능정의서 어드민 비밀번호 모달 */}\n {showAdminPasswordModal && (\n \n
\n
기능정의서 접근
\n
\n 어드민 비밀번호를 입력하면 모든 사용자의 글을 볼 수 있습니다.
\n 빈칸으로 입력하면 일반 사용자로 접근합니다.\n
\n
{\n setAdminPassword(e.target.value);\n setAdminPasswordError('');\n }}\n onKeyDown={(e) => e.key === 'Enter' && handleAdminPasswordSubmit()}\n className=\"w-full border rounded-lg px-4 py-3 mb-2 text-sm\"\n autoFocus\n />\n {adminPasswordError && (\n
{adminPasswordError}
\n )}\n
\n \n \n
\n {isFeatureAdmin && (\n
\n 현재 어드민 모드가 활성화되어 있습니다.\n
\n )}\n
\n
\n )}\n\n {/* 작업자 화면 자재투입 모달 */}\n {showWorkerMaterialModal && workerMaterialTask && (\n {\n setShowWorkerMaterialModal(false);\n setWorkerMaterialTask(null);\n }}\n onSubmit={(materialCode, qty, lotNo, inputBy, note) => {\n // 재고 감소 처리\n handleUseMaterial?.(materialCode, qty, {\n workOrderNo: workerMaterialTask.workOrderNo,\n lotNo,\n inputBy,\n note,\n });\n\n // 작업지시에 투입 이력 추가\n const material = inventory.find(m => m.materialCode === materialCode);\n const newInput = {\n id: Date.now(),\n materialCode,\n materialName: material?.materialName || '',\n lotNo,\n qty,\n unit: material?.unit || '',\n inputBy,\n inputAt: new Date().toISOString().replace('T', ' ').slice(0, 16),\n note,\n };\n\n // 작업지시 업데이트\n setWorkOrders(prev => prev.map(wo =>\n wo.id === workerMaterialTask.id\n ? { ...wo, materialInputs: [...(wo.materialInputs || []), newInput] }\n : wo\n ));\n\n alert(`✅ 자재 투입이 등록되었습니다.\\n\\n자재: ${material?.materialName}\\n투입량: ${qty} ${material?.unit}\\nLOT: ${lotNo}`);\n setShowWorkerMaterialModal(false);\n setWorkerMaterialTask(null);\n }}\n />\n )}\n\n {/* 버전 히스토리 모달 */}\n setShowVersionHistory(false)}\n history={versionHistory}\n onCommit={commitVersion}\n onRollback={rollbackToVersion}\n currentVersion={currentVersion}\n hasChanges={hasChanges}\n autoMessage={autoCommitMessage}\n />\n\n {/* 유저플로우 네비게이터 (레거시 모달 - 패널 방식으로 대체됨) */}\n {/* 패널 방식: 헤더의 플로우 버튼 클릭 시 우측 패널로 표시됨 (showFlowPanel) */}\n {/* 아래 모달은 다른 곳에서 showUserFlow를 사용하는 경우를 위해 유지 */}\n {showUserFlow && (\n setShowUserFlow(false)}\n onNavigate={(menu, viewName) => {\n setActiveMenu(menu);\n setView(viewName);\n }}\n />\n )}\n\n {/* 상세 플로우 다이어그램 (ReactFlow 기반) */}\n {showDetailedFlowDiagram && (\n setShowDetailedFlowDiagram(false)}\n />\n )}\n\n {/* 종합 플로우차트 패널은 이제 메인 레이아웃에 인라인으로 통합됨 (우측 푸시 패널 방식) */}\n\n {/* 전체 메뉴 기능정의서 패널 - 모든 메뉴/페이지/섹션/항목 순차 정의 */}\n setShowAllMenuFeatureDocPanel(false)}\n />\n >\n );\n}\n","import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\n\nconst root = ReactDOM.createRoot(document.getElementById('root'));\nroot.render(\n \n \n \n);\n"],"names":["module","exports","f","require","k","Symbol","for","l","m","Object","prototype","hasOwnProperty","n","__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED","ReactCurrentOwner","p","key","ref","__self","__source","q","c","a","g","b","d","e","h","call","defaultProps","$$typeof","type","props","_owner","current","Fragment","jsx","jsxs","r","t","u","v","w","x","y","z","iterator","B","isMounted","enqueueForceUpdate","enqueueReplaceState","enqueueSetState","C","assign","D","E","this","context","refs","updater","F","G","isReactComponent","setState","Error","forceUpdate","H","constructor","isPureReactComponent","I","Array","isArray","J","K","L","M","arguments","length","children","O","P","Q","replace","escape","toString","R","N","push","A","next","done","value","String","keys","join","S","T","_status","_result","then","default","U","V","transition","W","ReactCurrentDispatcher","ReactCurrentBatchConfig","X","Children","map","forEach","apply","count","toArray","only","Component","Profiler","PureComponent","StrictMode","Suspense","act","cloneElement","createContext","_currentValue","_currentValue2","_threadCount","Provider","Consumer","_defaultValue","_globalName","_context","createElement","createFactory","bind","createRef","forwardRef","render","isValidElement","lazy","_payload","_init","memo","compare","startTransition","unstable_act","useCallback","useContext","useDebugValue","useDeferredValue","useEffect","useId","useImperativeHandle","useInsertionEffect","useLayoutEffect","useMemo","useReducer","useRef","useState","useSyncExternalStore","useTransition","version","pop","sortIndex","id","performance","now","unstable_now","Date","setTimeout","clearTimeout","setImmediate","callback","startTime","expirationTime","priorityLevel","navigator","scheduling","isInputPending","MessageChannel","port2","port1","onmessage","postMessage","unstable_IdlePriority","unstable_ImmediatePriority","unstable_LowPriority","unstable_NormalPriority","unstable_Profiling","unstable_UserBlockingPriority","unstable_cancelCallback","unstable_continueExecution","unstable_forceFrameRate","console","error","Math","floor","unstable_getCurrentPriorityLevel","unstable_getFirstCallbackNode","unstable_next","unstable_pauseExecution","unstable_requestPaint","unstable_runWithPriority","unstable_scheduleCallback","delay","unstable_shouldYield","unstable_wrapCallback","React","objectIs","is","checkIfSnapshotChanged","inst","latestGetSnapshot","getSnapshot","nextValue","shim","window","document","subscribe","_useState","createRoot","hydrateRoot","useSyncExternalStoreWithSelector","getServerSnapshot","selector","isEqual","instRef","hasValue","memoizedSelector","nextSnapshot","hasMemo","memoizedSnapshot","currentSelection","memoizedSelection","nextSelection","maybeGetServerSnapshot","aa","ca","encodeURIComponent","da","Set","ea","fa","ha","add","ia","ja","ka","la","ma","acceptsBooleans","attributeName","attributeNamespace","mustUseProperty","propertyName","sanitizeURL","removeEmptyString","split","toLowerCase","ra","sa","toUpperCase","ta","slice","pa","isNaN","qa","test","oa","removeAttribute","setAttribute","setAttributeNS","xlinkHref","ua","va","wa","ya","za","Aa","Ba","Ca","Da","Ea","Fa","Ga","Ha","Ia","Ja","Ka","La","Ma","stack","trim","match","Na","Oa","prepareStackTrace","defineProperty","set","Reflect","construct","displayName","includes","name","Pa","tag","Qa","Ra","Sa","Ta","nodeName","Va","_valueTracker","getOwnPropertyDescriptor","get","configurable","enumerable","getValue","setValue","stopTracking","Ua","Wa","checked","Xa","activeElement","body","Ya","defaultChecked","defaultValue","_wrapperState","initialChecked","Za","initialValue","controlled","ab","bb","cb","db","ownerDocument","eb","fb","options","selected","defaultSelected","disabled","gb","dangerouslySetInnerHTML","hb","ib","jb","textContent","kb","lb","mb","nb","namespaceURI","innerHTML","valueOf","firstChild","removeChild","appendChild","MSApp","execUnsafeLocalFunction","ob","lastChild","nodeType","nodeValue","pb","animationIterationCount","aspectRatio","borderImageOutset","borderImageSlice","borderImageWidth","boxFlex","boxFlexGroup","boxOrdinalGroup","columnCount","columns","flex","flexGrow","flexPositive","flexShrink","flexNegative","flexOrder","gridArea","gridRow","gridRowEnd","gridRowSpan","gridRowStart","gridColumn","gridColumnEnd","gridColumnSpan","gridColumnStart","fontWeight","lineClamp","lineHeight","opacity","order","orphans","tabSize","widows","zIndex","zoom","fillOpacity","floodOpacity","stopOpacity","strokeDasharray","strokeDashoffset","strokeMiterlimit","strokeOpacity","strokeWidth","qb","rb","sb","style","indexOf","setProperty","charAt","substring","tb","menuitem","area","base","br","col","embed","hr","img","input","keygen","link","meta","param","source","track","wbr","ub","vb","wb","xb","target","srcElement","correspondingUseElement","parentNode","yb","zb","Ab","Bb","Cb","stateNode","Db","Eb","Fb","Gb","Hb","Ib","Jb","Kb","Lb","Mb","addEventListener","removeEventListener","Nb","onError","Ob","Pb","Qb","Rb","Sb","Tb","Vb","alternate","return","flags","Wb","memoizedState","dehydrated","Xb","Zb","child","sibling","Yb","$b","ac","bc","cc","dc","ec","fc","gc","hc","ic","jc","kc","lc","oc","clz32","pc","qc","log","LN2","rc","sc","tc","uc","pendingLanes","suspendedLanes","pingedLanes","entangledLanes","entanglements","vc","xc","yc","zc","Ac","eventTimes","Cc","Dc","Ec","Fc","Gc","Hc","Ic","Jc","Kc","Lc","Mc","Nc","Oc","Map","Pc","Qc","Rc","Sc","delete","pointerId","Tc","nativeEvent","blockedOn","domEventName","eventSystemFlags","targetContainers","Vc","Wc","priority","isDehydrated","containerInfo","Xc","Yc","dispatchEvent","shift","Zc","$c","ad","bd","cd","dd","ed","fd","gd","hd","Uc","stopPropagation","jd","kd","ld","md","nd","od","keyCode","charCode","pd","qd","rd","_reactName","_targetInst","currentTarget","isDefaultPrevented","defaultPrevented","returnValue","isPropagationStopped","preventDefault","cancelBubble","persist","isPersistent","wd","xd","yd","sd","eventPhase","bubbles","cancelable","timeStamp","isTrusted","td","ud","view","detail","vd","Ad","screenX","screenY","clientX","clientY","pageX","pageY","ctrlKey","shiftKey","altKey","metaKey","getModifierState","zd","button","buttons","relatedTarget","fromElement","toElement","movementX","movementY","Bd","Dd","dataTransfer","Fd","Hd","animationName","elapsedTime","pseudoElement","Id","clipboardData","Jd","Ld","data","Md","Esc","Spacebar","Left","Up","Right","Down","Del","Win","Menu","Apps","Scroll","MozPrintableKey","Nd","Od","Alt","Control","Meta","Shift","Pd","Qd","fromCharCode","code","location","repeat","locale","which","Rd","Td","width","height","pressure","tangentialPressure","tiltX","tiltY","twist","pointerType","isPrimary","Vd","touches","targetTouches","changedTouches","Xd","Yd","deltaX","wheelDeltaX","deltaY","wheelDeltaY","wheelDelta","deltaZ","deltaMode","Zd","$d","ae","be","documentMode","ce","de","ee","fe","ge","he","ie","le","color","date","datetime","email","month","number","password","range","search","tel","text","time","url","week","me","ne","oe","event","listeners","pe","qe","re","se","te","ue","ve","we","xe","ye","ze","oninput","Ae","detachEvent","Be","Ce","attachEvent","De","Ee","Fe","He","Ie","Je","Ke","node","offset","nextSibling","Le","contains","compareDocumentPosition","Me","HTMLIFrameElement","contentWindow","href","Ne","contentEditable","Oe","focusedElem","selectionRange","documentElement","start","end","selectionStart","selectionEnd","min","defaultView","getSelection","extend","rangeCount","anchorNode","anchorOffset","focusNode","focusOffset","createRange","setStart","removeAllRanges","addRange","setEnd","element","left","scrollLeft","top","scrollTop","focus","Pe","Qe","Re","Se","Te","Ue","Ve","We","animationend","animationiteration","animationstart","transitionend","Xe","Ye","Ze","animation","$e","af","bf","cf","df","ef","ff","gf","hf","lf","mf","concat","nf","Ub","instance","listener","of","has","pf","qf","rf","random","sf","capture","passive","tf","uf","parentWindow","vf","wf","na","xa","$a","ba","je","char","ke","unshift","xf","yf","zf","Af","Bf","Cf","Df","Ef","__html","Ff","Gf","Hf","Promise","Jf","queueMicrotask","resolve","catch","If","Kf","Lf","Mf","previousSibling","Nf","Of","Pf","Qf","Rf","Sf","Tf","Uf","Vf","Wf","Xf","Yf","contextTypes","__reactInternalMemoizedUnmaskedChildContext","__reactInternalMemoizedMaskedChildContext","Zf","childContextTypes","$f","ag","bg","getChildContext","cg","__reactInternalMemoizedMergedChildContext","dg","eg","fg","gg","hg","jg","kg","lg","mg","ng","og","pg","qg","rg","sg","tg","ug","vg","wg","xg","yg","zg","Ag","Bg","elementType","deletions","Cg","pendingProps","overflow","treeContext","retryLane","Dg","mode","Eg","Fg","Gg","memoizedProps","Hg","Ig","Jg","Kg","Lg","_stringRef","Mg","Ng","Og","index","Pg","Qg","Rg","implementation","Sg","Tg","Ug","Vg","Wg","Xg","Yg","Zg","$g","ah","bh","childLanes","ch","dependencies","firstContext","lanes","dh","eh","memoizedValue","fh","gh","hh","interleaved","ih","jh","kh","updateQueue","baseState","firstBaseUpdate","lastBaseUpdate","shared","pending","effects","lh","mh","eventTime","lane","payload","nh","oh","ph","qh","rh","sh","th","uh","vh","wh","xh","yh","tagName","zh","Ah","Bh","Ch","revealOrder","Dh","Eh","_workInProgressVersionPrimary","Fh","Gh","Hh","Ih","Jh","Kh","Lh","Mh","Nh","Oh","Ph","Qh","Rh","Sh","Th","baseQueue","queue","Uh","Vh","Wh","lastRenderedReducer","action","hasEagerState","eagerState","lastRenderedState","dispatch","Xh","Yh","Zh","$h","ai","bi","ci","di","lastEffect","stores","ei","fi","gi","hi","ii","create","destroy","deps","ji","ki","li","mi","ni","oi","pi","qi","ri","si","ti","ui","vi","wi","xi","yi","zi","Ai","Bi","readContext","useMutableSource","unstable_isNewReconciler","identifierPrefix","Ci","Di","Ei","_reactInternals","Fi","shouldComponentUpdate","Gi","contextType","state","Hi","componentWillReceiveProps","UNSAFE_componentWillReceiveProps","Ii","getDerivedStateFromProps","getSnapshotBeforeUpdate","UNSAFE_componentWillMount","componentWillMount","componentDidMount","Ji","message","digest","Ki","Li","Mi","WeakMap","Ni","Oi","Pi","Qi","getDerivedStateFromError","componentDidCatch","Ri","componentStack","Si","pingCache","Ti","Ui","Vi","Wi","Xi","Yi","Zi","$i","aj","bj","cj","dj","baseLanes","cachePool","transitions","ej","fj","gj","hj","ij","UNSAFE_componentWillUpdate","componentWillUpdate","componentDidUpdate","jj","kj","pendingContext","lj","zj","Aj","Bj","Cj","mj","nj","oj","fallback","pj","qj","sj","dataset","dgst","tj","uj","_reactRetry","rj","subtreeFlags","vj","wj","isBackwards","rendering","renderingStartTime","last","tail","tailMode","xj","Dj","Ej","Fj","wasMultiple","multiple","suppressHydrationWarning","onClick","onclick","size","createElementNS","autoFocus","createTextNode","Gj","Hj","Ij","Jj","Kj","WeakSet","Lj","Mj","Nj","Pj","Qj","Rj","Sj","Tj","Uj","Vj","insertBefore","_reactRootContainer","Wj","Xj","Yj","Zj","onCommitFiberUnmount","componentWillUnmount","ak","bk","ck","dk","ek","isHidden","fk","gk","display","hk","ik","jk","kk","__reactInternalSnapshotBeforeUpdate","src","Vk","lk","ceil","mk","nk","ok","Y","Z","pk","qk","rk","sk","tk","Infinity","uk","vk","wk","xk","yk","zk","Ak","Bk","Ck","Dk","callbackNode","expirationTimes","expiredLanes","wc","callbackPriority","ig","Ek","Fk","Gk","Hk","Ik","Jk","Kk","Lk","Mk","Nk","Ok","finishedWork","finishedLanes","Pk","timeoutHandle","Qk","Rk","Sk","Tk","Uk","mutableReadLanes","Bc","Oj","onCommitFiberRoot","mc","onRecoverableError","Wk","onPostCommitFiberRoot","Xk","Yk","$k","pendingChildren","al","mutableSourceEagerHydrationData","bl","cache","pendingSuspenseBoundaries","dl","el","fl","gl","hl","il","yj","Zk","kl","reportError","ll","_internalRoot","ml","nl","ol","pl","rl","ql","unmount","unstable_scheduleHydration","splice","querySelectorAll","JSON","stringify","form","sl","usingClientEntryPoint","Events","tl","findFiberByHostInstance","bundleType","rendererPackageName","ul","rendererConfig","overrideHookState","overrideHookStateDeletePath","overrideHookStateRenamePath","overrideProps","overridePropsDeletePath","overridePropsRenamePath","setErrorHandler","setSuspenseHandler","scheduleUpdate","currentDispatcherRef","findHostInstanceByFiber","findHostInstancesForRefresh","scheduleRefresh","scheduleRoot","setRefreshHandler","getCurrentFiber","reconcilerVersion","__REACT_DEVTOOLS_GLOBAL_HOOK__","vl","isDisabled","supportsFiber","inject","createPortal","cl","unstable_strictMode","findDOMNode","flushSync","hydrate","hydratedSources","_getVersion","_source","unmountComponentAtNode","unstable_batchedUpdates","unstable_renderSubtreeIntoContainer","checkDCE","err","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","__webpack_modules__","_typeof","o","toPropertyKey","i","toPrimitive","TypeError","Number","_defineProperty","writable","ownKeys","getOwnPropertySymbols","filter","_objectSpread2","getOwnPropertyDescriptors","defineProperties","propertyIsEnumerable","defaultAttributes","xmlns","viewBox","fill","stroke","strokeLinecap","strokeLinejoin","toKebabCase","string","createLucideIcon$1","createLucideIcon","iconName","iconNode","_ref","absoluteStrokeWidth","rest","_objectWithoutProperties","_excluded","_objectSpread","className","_ref2","attrs","Settings","cx","cy","DollarSign","x1","x2","y1","y2","BarChart3","Clock","points","CreditCard","rx","Zap","Users","Factory","ShieldCheck","ShoppingCart","Box","User","Calendar","ry","Truck","Trash2","Plus","FileText","ClipboardList","CheckCircle","Package","AlertTriangle","TrendingUp","TrendingDown","ArrowDownCircle","PenLine","PlayCircle","Printer","Save","Search","ChevronLeft","Home","Sun","GitBranch","Activity","Check","BookOpen","Bell","Layers","MapPin","Calculator","Building","Upload","Clipboard","XCircle","Warehouse","Download","ChevronDown","ChevronRight","Database","ArrowLeft","RefreshCw","Play","AlertCircle","Pen","ChevronsLeft","ChevronsRight","PenSquare","List","Hash","GripVertical","Eraser","ArrowRight","Eye","Shield","Send","Phone","Mail","ArrowUpCircle","Key","HelpCircle","Copy","MessageCircle","title","description","icon","tabs","label","entityTypes","sortOrder","isActive","categories","parentId","level","path","processWorkflows","SCREEN","steps","stepNo","stepCode","stepName","estimatedTime","requiredEquipment","inputMaterials","outputMaterials","checkPoints","isQCStep","note","SLAT","calculationMethod","subSteps","FOLD","STOCK","workflowRelations","mergePoint","flows","from","to","materialInputMethods","unit","calculationNote","masterFields","fieldKey","fieldName","inputType","isRequired","validations","maxLength","items","dataSource","valueField","labelField","category","masterSections","sectionKey","sectionName","sectionType","applicableTypes","fields","pageTemplates","pageCode","pageName","pagePath","itemTypes","sections","sectionId","fieldId","conditions","sampleProcesses","processCode","processName","processType","workflowCode","department","equipment","personnel","createdAt","trigger","approvalLine","inspectionFlow","IQC","performer","inputFrom","outputTo","documentCode","samplingRule","resultAction","pass","fail","PQC","processTypes","checkPoint","method","standard","measurePoints","partTypes","FQC","cycle","hasMeasurement","isExternal","ksStandards","inspectionItems","item","toleranceByThickness","max","tolerance","minValue","samplingPlans","levels","lotSizeMin","lotSizeMax","sampleSize","acceptanceNumber","rejectionNumber","defaultSampleSize","measurementPoints","inspectionItemTemplates","EGI","frequency","measurementType","judgementType","subItems","toleranceRanges","CGI","templateName","diagram","measurements","inspectionStandard","relatedStandard","point","dataTable","judgement","subParts","hasMultipleItems","itemColumns","JOINT_BAR","PACK","DEFAULT","SCREEN_SHUTTER","inspectionCycle","isExternalTest","SLAT_SHUTTER","isMultiRow","measurementColumns","sampleInspectionStandards","inspectionCode","inspectionName","inspectionType","targetItem","samplingLevel","aqlLevel","seq","spec","ksStandard","targetProcess","tenantConfig","systemDefaults","paperSize","orientation","margins","right","bottom","fontSize","fontFamily","language","currency","currencySymbol","dateFormat","numberFormat","approval","useApproval","showSignature","showDate","signatureType","logo","maxWidth","maxHeight","position","showCompanyName","numbering","resetCycle","sequenceDigits","includeDateInNo","tenants","fullName","englishName","isDefault","companyInfo","businessNo","ceo","businessType","businessItem","address","phone","fax","website","primary","secondary","textLogo","main","sub","subSub","seals","companySeal","ceoSeal","useSeal","approvalLines","role","worklog","highlight","highlightColor","showRole","sales","quality","numberingRules","prefix","pattern","documentStyles","header","logoStyle","titleLetterSpacing","subtitleBg","footerHighlightBg","quote","footer","showBankInfo","bankInfo","bank","account","holder","customBlocks","tenantSchema","required","properties","minLength","format","enum","additionalProperties","CURRENT_TENANT_KEY","SEQUENCE_KEY_PATTERN","MASTER_DATA_KEY_PATTERN","masterDataSchema","tables","keyField","getCurrentTenant","currentId","localStorage","getItem","values","find","getTenantCompanyInfo","tenantId","tenant","getTenantNumberingRule","docType","_tenant$numberingRule","tenantRule","getTenantLogo","documentTemplateConfig","blockLibrary","headers","preview","config","showLogo","logoPosition","titlePosition","titleStyle","usedIn","letterSpacing","showDocNo","dateLabel","layout","subText","subSubText","subtitle","defaultBg","rows","labels","highlightLast","highlightBg","styles","borderColor","headerBg","cellPadding","partyBlocks","row","field","colSpan","useCompanyInfo","tableStyle","borderCollapse","labelCellStyle","backgroundColor","textAlign","padding","border","valueCellStyle","approvalBlocks","showLotNo","lotNoLabel","approverRow","roles","showSign","nameRow","defaultNames","deptRows","depts","cellSize","visible","amountBlocks","showKoreanAmount","itemTableBlocks","align","showRowNo","showSubtotal","showTax","showTotal","emptyRows","showTotalQty","editable","titleIcon","inspectionTableBlocks","headerCellStyle","dataCellStyle","showOverallJudgement","minRows","measurementCellStyle","remarksBlocks","minHeight","showCondition","isFixed","judgementBlocks","signatureBlocks","leadText","showCompany","companyPrefix","showSeal","sealText","sealSize","workLogBlocks","leftSection","titleBg","labelBg","rightSection","valueBg","showTotalRow","totalFields","gridColumns","showPartInfo","showDimensions","svgConfig","viewBoxWidth","viewBoxHeight","strokeColor","fillColor","dimensionColor","dimensionFontSize","showBendLines","bendLineColor","bendLineStyle","drawings","partCode","partName","dimensions","svgType","headerGroups","startCol","specCalculation","baseSpec","topPlateSpec","productionSizeOffset","additionalHeight","specRanges","overlapTable","overlap","productionSize","withTopPlate","heightRanges","specId","calculateSpec","remainHeight","spec1210Count","ranges","specs","spec1210","spec900","spec600","spec400","spec300","footerSections","subFields","subFields2","usageCalculation","multiplier","calculation","developedPartsTable","enabled","sampleData","itemCode","itemName","material","totalWidth","qty","weight","developedDrawing","drawing","partCodes","rowSpan","parts","subDimensions","footerSummary","remarks","sectionSummary","summaryFields","grandTotal","calculateSectionTotal","sectionData","reduce","sum","parseFloat","calculateGrandTotal","total","calculate","workLogData","result","sectionTotals","wallType","bottomFinish","case","smokeBarrier","entries","sectionSum","calculated","slateCalculation","slatePitch","calculateSheetCount","calculateJointBar","sheetCount","calculateCoilUsage","round","calculateArea","jointBarQty","coilUsage","headerInfo","table","inspectionItem","subItem","inspectionData","subLabel","parentLabel","defectRemarks","finalJudgement","highlightStyle","linkToInventory","requesterSection","contentSection","deliverySection","subSections","voltage","lotNoSection","miscBlocks","placeholder","acceptTypes","maxSize","checkStyle","borderTop","paddingTop","marginTop","stylePresets","documentCommon","titleStyles","marginBottom","docNoStyles","headerBarStyles","infoTableStyles","container","labelCell","valueCell","tableStyles","headerCell","dataCell","verticalAlign","cellStyles","amountStyles","highlightBox","approvalStyles","lotNoValue","roleHeader","nameCell","deptLabel","deptValue","sealStyles","alignItems","justifyContent","borderRadius","footerStyles","noticeBox","noticeTitle","noticeItem","marginLeft","contactFooter","dividerStyles","double","margin","single","judgementStyles","gap","optionItem","radioCircle","radioChecked","passLabel","failLabel","documentPresets","headerBarStyle","approvalPosition","approvalStyle","tableHeaderStyle","tableCellStyle","judgementStyle","showSealBox","showContactFooter","documentTypes","linkedWorkLog","linkedWorkOrder","logoPath","templates","docNoFormat","blocks","blockId","cellHeight","measurementCellWidth","stylePreset","headerHeight","infoTableHeight","inspectionTableMinHeight","remarksHeight","judgementHeight","diagramHeight","showProcessInfo","autoCreateWorkLog","calculations","showSectionTotal","showGrandTotal","approvalConfig","fastTrack","autoSign","showHeader","svgStyle","subtitleStyle","showBulletList","inputStyle","tableConfig","externalApproval","requestedBy","sentTo","dataMapping","autoFill","fromShipment","fromOrder","fromCustomer","itemLists","materials","motors","slat","folding","consumables","dataBindings","customer","customerName","contactPerson","contactPhone","site","siteName","siteAddress","orderNo","lotNo","productName","totalAmount","dueDate","workOrder","workOrderNo","orderDate","teamName","inspection","supplierName","lotSize","materialNo","inspectionDate","inspector","workLog","workLogNo","workDate","requestContent","writer","totalProduction","totalDefect","goodRate","status","approvalStatus","inventory","materialCode","materialName","currentStock","purchaseOrder","orderCompanyName","totalInstallCount","requestDueDate","shipDate","deliveryMethod","deliveryAddress","recipient","recipientPhone","totalOrderCount","currentOrderCount","specialNotes","shipment","totalShutterQty","guideRailLotNo","caseLotNo","bottomBarLotNo","approvalSystem","statuses","features","timeLimit","escalation","afterHours","applicableTo","notifications","channels","events","delegation","maxDays","requireReason","autoSignature","signatureTypes","printSettings","showApproverName","showApprovalDate","showSignatureImage","showStampImage","signaturePosition","stampSize","signatureSize","security","requirePassword","requireBiometric","auditLog","preventTampering","completedDocument","showWatermark","watermarkText","watermarkOpacity","showQRCode","qrCodePosition","showVerificationInfo","editor","blockCategories","tools","addBlock","removeBlock","reorderBlock","configBlock","duplicateBlock","previewDocument","showGrid","showMargins","defaultZoom","patterns","resetPeriod","lotPatterns","example","variables","lotItemCodes","lotTypeCodes","monthCodes","specCodes","sampleTemplates","documentType","updatedAt","approvalLineIntegration","documentsWithApproval","approvalDisplay","showApproverInfo","showTime","showStatus","approved","rejected","renderRules","pendingCell","content","approvedCell","showName","signatureOpacity","rejectedCell","showRejectionMark","printOptions","showAllSignatures","showApprovalDates","showFinalApprovalMark","inProgress","showCompletedSignatures","showPendingAsBlanks","printWarning","draft","showAllAsBlanks","integrationAPI","submit","endpoint","requiredFields","process","signature","signatureManagement","registration","allowedFormats","maxFileSize","recommendedSize","transparentBackground","handwritten","canvasInput","imageUpload","stamp","shape","initial","font","encryptStorage","accessLog","requireAuth","workOrderToWorkLogSystem","processMapping","workOrderType","workLogType","workSheetCode","autoCreateTriggers","onWorkOrderApproved","createWorkLog","notifyWorker","onWorkStart","setWorkLogStatus","workLogAutoCreate","copyFields","defaults","workerName","docNoRule","workerTaskIntegration","taskFilter","sortBy","startConditions","workOrderApproved","materialAvailable","onComplete","updateWorkLogStatus","createInspectionRequest","notifyQC","workLogPrintSystem","printConditions","requireComplete","requireApproval","requireWorkStarted","printFormats","portrait","landscape","processPrintConfig","includeSpecCalculation","showOverlapTable","includeSheetCalculation","showJointBarQty","showCoilUsage","includeSectionSummary","showDrawings","printButtons","previewConfig","showPageBreaks","zoomLevel","showPrintMargins","workerTaskConfig","displayModes","list","sortable","filterable","kanban","cardFields","statusTransitions","startInputs","completeInputs","endTime","goodQty","defectQty","processSpecificInputs","workLogPrintButton","enabledStates","deliveryConfirmationSystem","centerSection","summaryRow","sumColumns","dynamicColumns","fixedColumns","caseColumns","headerLabel","cellWidth","guideRailColumns","summarySection","sumOf","variants","imageField","drawingStyle","labelStyle","defaultItems","template","pageBreaks","beforeBlocks","autoBreak","minSpaceForBlock","dataBinding","productCategory","certificationNo","deliveryDate","deliveryQty","deliveryManager","vehicleNo","shipmentItems","openWidth","openHeight","productionWidth","productionHeight","guideRailType","shaftType","caseType","motorType","bendingMatrix","bendingDrawings","wallTypeDrawing","sideTypeDrawing","subMaterials","printSystem","requireShipmentComplete","allowDraft","multiPage","shipmentIntegration","buttonPlacement","enableConditions","hasItems","autoDataMapping","fromWorkOrder","calculateBending","applyTenantToBlock","block","_block$config","resolvedData","companyName","getApprovalLineForDocument","_tenant$approvalLines","_tenant$approvalLines2","lineType","getTenantApprovalLine","getApprovalLineType","generateDocumentNo","_docNo$match","_docNo$match$","rule","year","getFullYear","yy","mm","getMonth","padStart","getDate","dateKey","sequenceKey","nextSeq","parseInt","setItem","docNo","digits","generateTenantDocumentNo","createTenantContext","_tenant$documentStyle","numberingRule","company","menuGroups","subMenus","screenId","desc","masterConfigs","apiUrl","sourceFieldKey","conditionType","conditionValue","displayType","targetSectionKey","bendingPartsSampleData","partType","lengthOption","totalKg","developedView","rate","afterRate","angles","drawingImage","lengthOptions","highlight2","workLogMapping","headerFields","workDetailColumns","developedViewFields","foldingWorkLogSampleData","customerCode","siteCode","productLotNo","productionManager","lengthSpec","sign","reviewer","approver","totalWeight","sampleItems","itemType","productType","fireRating","bomRequired","partSubType","assemblyPartType","assemblyPartItemName","remark","bendingPartItemName","purchasedPartItemName","power","capacity","subMaterialItemName","subMaterialSpec","rawMaterialItemName","rawMaterialSpec","consumableItemName","consumableSpec","processMasterConfig","inspectionMasterConfig","supplyTypes","condition","customerType","sampleCustomers","ceoName","creditGrade","paymentTerm","supplyType","supplyItems","partnerType","siteStatuses","refType","readOnly","sampleSites","customerId","siteType","startDate","endDate","siteManager","managerPhone","totalOrderAmount","isAutoGenerate","isCalculated","formula","pageType","workOrderMaster","workOrderTypes","workOrderStatuses","workerScreenMaster","workerRoles","permissions","actionButtons","actionCode","actionName","requiredRole","equipmentMaster","equipmentCategories","equipmentStatuses","sampleEquipments","equipmentCode","equipmentName","manufacturer","model","installDate","workLineMaster","factories","productionLines","factoryId","workStations","lineId","equipmentIds","workerCount","lineFields","bomMaster","bomTypes","materialCategories","bomDetailFields","sampleBoms","bomCode","productCode","bomType","effectiveDate","details","materialCategory","requiredQty","lossRate","Layout","ChevronUp","Grid","Image","Type","MousePointer","Terminal","Code","Bot","Cpu","CheckSquare","AlignLeft","_activePage$layers2","_logicSections$find","_logicSections$find2","_logicSections$find3","_activePage$layers$fi","_activePage$layers$fi2","_activePage$layers$fi3","_activePage$layers$fi4","_activePage$layers$fi5","_activePage$grid$dyna2","_activePage$grid$cell3","_activePage$grid$cell4","_activePage$grid$cell5","_activePage$grid$cell6","_activePage$grid$cell7","_activePage$grid$cell8","_activePage$grid$cell9","_activePage$grid$cell0","_activePage$grid$cell13","_activePage$grid$cell14","_activePage$grid$cell15","_activePage$grid$cell16","_activePage$grid$cell17","_activePage$grid$cell18","_activePage$grid$cell19","onBack","initialTemplate","safeRender","val","PAGE_SIZES","pageConfig","setPageConfig","pages","setPages","grid","cols","rowHeights","colWidths","cells","dynamicRows","layers","activePageId","setActivePageId","selectedCell","setSelectedCell","selectedLayerId","setSelectedLayerId","setSelectionRange","isSelecting","setIsSelecting","scale","setScale","tool","setTool","docName","setDocName","showStructure","setShowStructure","viewMode","setViewMode","scanStep","setScanStep","logicSections","setLogicSections","isDynamic","selectedSectionId","setSelectedSectionId","isAnalyzing","setIsAnalyzing","registeredSchema","setRegisteredSchema","borderSettings","setBorderSettings","history","setHistory","historyIndex","setHistoryIndex","clipboard","setClipboard","contextMenu","setContextMenu","saveToHistory","newPages","stateToSave","parse","newHistory","closeMenu","activePage","updateLayer","updates","newLayers","updateGrid","updateCell","newCells","cell","handleGlobalKeyDown","undo","nextIdx","redo","handleCopy","minR","maxR","minC","maxC","handlePaste","startR","startC","cellData","targetR","targetC","updatedPages","nr","nc","handleMergeCells","masterKey","masterCell","handleUnmergeCells","updateSelectedRange","isResizing","setIsResizing","isDragging","setIsDragging","resizer","setResizer","resizeStart","setResizeStart","initialPos","setInitialPos","handleResizeStart","layerId","layer","addRow","newRowHeights","dr","addCol","newColWidths","_ref3","evaluateFormula","evaluated","startsWith","rangeRegex","fn","startRow","endCol","endRow","sC","charCodeAt","sR","eC","eR","_cells","cellVal","varRegex","cellWithKey","dataKey","cellRefRegex","colStr","rowStr","Function","isFinite","isInteger","toFixed","_jsxs","onMouseMove","newWidths","newHeights","onMouseUp","handleMouseUp","_jsx","onChange","orient","asNew","templateData","toISOString","saved","existingIdx","findIndex","alert","handleSave","ImageIcon","page","idx","deletePage","addPage","newId","transform","transformOrigin","acc","limit","prevAcc","onMouseDown","_activePage$grid$dyna","gridTemplateColumns","gridTemplateRows","_","_cell$style","_cell$style2","_cell$style3","_cell$style4","_cell$style5","isShadowed","_ref4","mKey","mCell","mr","rs","cs","isSelected","isInRange","handleCellMouseDown","onMouseEnter","handleCellMouseEnter","prev","onContextMenu","handleContextMenu","opt","alt","_layer$style","handleLayerDragStart","cursor","outline","boxShadow","_Fragment","sec","s","newSections","cellK","prompt","some","accept","file","files","reader","FileReader","onloadend","readAsDataURL","htmlFor","step","_activePage$layers$fi6","deleteLayer","_activePage$grid$dyna3","newDynamicRows","_activePage$grid$cell","_activePage$grid$cell2","newOpts","currentOpts","applyBorders","borderStyle","newStyle","borderBottom","borderLeft","borderRight","_activePage$grid$cell1","_activePage$grid$cell10","isBold","_activePage$grid$cell11","_activePage$grid$cell12","onNavigate","setMode","setTemplates","IQC_TEMPLATE_PRESET","contact","dept","basicFields","guidelineSections","image","regulation","inspectionColumns","group","remarkLabel","judgementLabel","judgementOptions","defaultTemplate","subLabels","currentTemplate","setCurrentTemplate","selectedTemplate","setSelectedTemplate","showPreview","setShowPreview","handleCreate","InputField","PreviewModal","onClose","groupName","groupKeys","getRowSpans","spans","currentVal","startIndex","app","fIdx","section","sIdx","categorySpans","methodSpans","totalSpan","span","rowId","matchedGuideline","flatMap","standardValue","handleDesignerBack","DocumentDesigner","newTemplate","handleEdit","newSec","handleImageUpload","iIdx","newCols","InspectionTemplateManager","templateMode","setTemplateMode","setInspectionType","setInspectionItems","validation","docForm","itemCount","handleAddItem","newItem","handleDeleteItem","Paperclip","InspectionManagement","selectedInspection","setSelectedInspection","statusFilter","setStatusFilter","searchTerm","setSearchTerm","sampleInspections","requestDate","sampleInspectionItems","getStatusBadge","badges","class","progress","completed","badge","getResultBadge","getTypeBadge","filteredData","matchesStatus","matchesSearch","statusCounts","all","Edit","featureDefinitions","screenName","uiInfo","componentType","interaction","funcInfo","purpose","dataInfo","relatedTable","getFeatureDefinition","screenKey","screenKeyMapping","screenFeatureTemplates","menuPath","screenType","elements","behavior","navigation","states","api","lotProductCodes","parentProduct","lotDateCodeRules","yearCodes","dayFormat","lotSizeCodes","generateItemCode","formData","subType","installType","sideSpecWidth","sideWidth","sideSpecHeight","sideHeight","lengthCode","Boolean","_lotProductCodes$item","_lotTypeCodes$partTyp","bendingPartType","sizeLength","bendingSizeLength","typeCode","sizeCode","generateDocNo","dateCode","formatDateCode","getOrderPrefix","productProcessMapping","primaryProcess","requiredProcesses","materialLotMaster","receivedDate","supplier","generateE2ETestData","customerMaster","siteMaster","testData","quotes","orders","productionOrders","workOrders","workResults","pqcInspections","packaging","shipments","materialLotUsage","workflows","sizeOptions","W0","H0","productTypes","statusFlow","siteIdx","manager","discountRate","sizeOpt","baseDate","orderStatus","basePrice","quoteId","quoteNo","bomItems","generateBOMItems","quoteDate","validUntil","getTime","siteId","productionSizeW","productionSizeH","supplyAmount","discountAmount","finalAmount","createdBy","orderId","orderPrefix","orderAmount","poDate","productionOrderId","productionOrderNo","processGroups","groupItemsByProcess","productionOrderDate","totalProcesses","completedProcesses","workOrderSeq","procIdx","workflow","woBaseDate","stepIdx","_processMasterConfig$","workOrderId","stepStartDate","stepEndDate","woStatus","isMaterialInputStep","assignedLots","lotKey","availableLots","selectedLot","usedQty","unitLabel","usedDate","usedBy","workOrderDate","workflowName","stepDescription","plannedStartDate","plannedEndDate","plannedQty","actualStartDate","actualEndDate","actualQty","worker","workCenter","resultStartTime","resultEndTime","resultQty","resultNo","resultDate","workingMinutes","scrapQty","materialInputs","equipmentUsed","checkResults","cp","checkItem","confirmedBy","confirmedAt","inspDate","pqcId","reworkInfo","_workflow$steps$find","reworkStartDate","reworkEndDate","reworkOrderNo","reworkType","reworkReason","reworkStepCode","reworkStepName","reworkQty","reworkStatus","reworkResult","reworkInspectionNo","reworkBy","inspectionNo","inspectedQty","passedQty","failedQty","checkItems","packDate","mergedProcesses","wfCode","_workflows$wfCode","packStepCode","completedDate","packagingNo","packagingDate","totalMergedProcesses","packedQty","packagingType","packageCount","labelInfo","fqcPassed","fqcDate","shipStatus","shipmentNo","shipPackagingNo","shipmentDate","expectedDeliveryDate","actualDeliveryDate","packagingId","siteTel","shippedQty","carrier","driverName","driverTel","lineNo","unitPrice","amount","slatCount","isPurchased","groups","_processMasterConfig$2","_processMasterConfig$3","_processMasterConfig$4","_processMasterConfig$5","totalSteps","generateTestDataSummary","_testData$workResults","_testData$materialLot","totalQuotes","totalOrders","totalProductionOrders","totalWorkOrders","totalWorkResults","totalPQCInspections","totalPackaging","totalShipments","totalMaterialLotUsage","orderStatusDistribution","workOrdersByProcess","wo","workResultStatusDistribution","wr","pqcResultDistribution","insp","reworkCount","materialLotUsageSummary","mu","shipmentStatusDistribution","packagingMergeStats","pkg","totalMerged","validateE2EDataIntegrity","_testData$workResults2","issues","po","ship","failedPQC","pqc","isValid","issueCount","validationStats","totalPQC","reworkCompleted","_p$reworkInfo","defaultNumberRules","seqDigits","separator","dateFormats","resetCycleOptions","targetTemplates","yyyy","generateNumberByRule","existingNumbers","sep","filteredNumbers","monthCode","yearCode","maxSeq","num","lastPart","findRuleByTarget","rules","getOrderTarget","NUMBER_RULES_STORAGE_KEY","NUMBER_SEQUENCES_STORAGE_KEY","loadNumberRules","saveNumberRules","loadNumberSequences","saveNumberSequences","sequences","getNextSequence","generateNumber","actualTarget","Scissors","Wrench","Monitor","Info","RotateCcw","ClipboardCheck","ProcessFlowLine","bgColor","lightBg","ProcessFlowChart","_selectedStep$require","_selectedStep$inputMa","_selectedStep$outputM","_selectedStep$checkPo","_workflowRelations$fl","activeProcess","setActiveProcess","selectedStep","setSelectedStep","animating","setAnimating","activeStepIndex","setActiveStepIndex","processStyles","FORMING","resetAnimation","currentWorkflow","currentStyle","currentMaterial","Icon","startAnimation","interval","setInterval","clearInterval","isCurrent","eq","ss","_processWorkflows$cod","flow","screenDataFlows","outputs","inputs","businessProcesses","menuGroup","endToEndFlows","processes","phase","getMenuGroupFlows","screens","menu","subMenu","getScreenInfo","groupLabel","parent","getConnectedScreens","processStates","iconMap","colorMap","blue","light","green","purple","orange","indigo","teal","red","gray","menuGroupColors","master","logistics","production","accounting","system","E2EFlowDiagram","onScreenClick","phases","phaseColors","stepsInPhase","minWidth","screenInfo","ProcessFlowSteps","_ref5","MenuGroupFlow","_ref6","_flow$outputs","MenuScreenButton","screen","outputId","outputInfo","_ref7","_flow$inputs","_flow$outputs2","hasConnections","ScreenDetailView","_ref8","connections","output","_endToEndFlows$find","activeView","setActiveView","selectedProcess","setSelectedProcess","selectedScreen","setSelectedScreen","selectedE2E","setSelectedE2E","expandedGroups","setExpandedGroups","handleScreenClick","viewTabs","hidden","tab","isExpanded","toggleGroup","groupId","colorKey","_allNodes$startPoint","_allNodes$endPoint","screenToViewMap","handleNavigateToScreen","mapping","startPoint","setStartPoint","endPoint","setEndPoint","showFlow","setShowFlow","allNodes","quotation","productionPlan","materialInput","foldingWorkLog","workResult","defectProcess","shipping","salesSlip","collection","itemMaster","documentTemplate","flowPaths","actions","question","yes","no","dimensionTable","nodesByCategory","grouped","selectedPath","availableEndPoints","handleStartSelect","nodeId","nodes","handleEndSelect","handleShowFlow","handleReset","renderStep","canNavigate","aIdx","dIdx","dim","hIdx","rIdx","cIdx","ExternalLink","Diamond","mainUserFlows","branches","menuFlows","screenFlows","flowCategories","allScreens","findFlowPath","startScreen","endScreen","paths","startIdx","endIdx","_flow$branches","flowId","flowName","departmentFlows","scenarios","dataFlow","dataLink","dataRegistrationFlow","requiredBefore","outputData","dependencyDiagram","depends","minimumRequiredData","checklist","minCount","departmentList","MiniFlowStep","isLast","isDecision","FlowCard","onToggle","onStepClick","CategoryIcon","branch","MenuFlowCard","menuId","menuData","arr","ScreenActionCard","screenData","detailedProcessFlow","stages","documents","timing","templateRef","masterRef","accountingRef","processClassification","inspectionStandards","types","standardRef","criteria","dataMatrix","masterDataMatrix","usage","accountingIntegration","매출","targetMenu","autoCreate","매입","수금","원가","isOpen","activeTab","setActiveTab","selectedCategory","setSelectedCategory","expandedFlows","setExpandedFlows","expandedMenus","setExpandedMenus","searchQuery","setSearchQuery","setStartScreen","setEndScreen","foundPaths","setFoundPaths","filteredFlows","matchCategory","matchSearch","screenGroups","stage","doc","mIdx","cat","toggleFlow","toggleMenu","handleFindPath","handleResetPath","_flowCategories$find","bIdx","names","out","tmp","createStoreImpl","createState","partial","nextState","previousState","getState","getInitialState","initialState","warn","clear","createStore","ReactExports","useSyncExternalStoreExports","identity","arg","useStoreWithEqualityFn","equalityFn","getServerState","createWithEqualityFnImpl","defaultEqualityFn","useBoundStoreWithEqualityFn","shallow$1","objA","objB","keysA","keyA","noop","Dispatch","on","typename","copy","that","args","none","querySelector","empty","arrayAll","select","matches","childMatcher","childFirst","firstElementChild","update","EnterNode","datum","_next","_parent","__data__","bindIndex","enter","exit","groupLength","dataLength","bindKey","keyValue","nodeByKeyValue","keyValues","arraylike","ascending","NaN","xhtml","svg","xlink","xml","namespaces","space","local","attrRemove","attrRemoveNS","fullname","removeAttributeNS","attrConstant","attrConstantNS","attrFunction","attrFunctionNS","styleRemove","removeProperty","styleConstant","styleFunction","styleValue","getPropertyValue","getComputedStyle","propertyRemove","propertyConstant","propertyFunction","classArray","classList","ClassList","_node","_names","getAttribute","classedAdd","classedRemove","remove","classedTrue","classedFalse","classedFunction","textRemove","textConstant","textFunction","htmlRemove","htmlConstant","htmlFunction","raise","lower","creatorInherit","uri","creatorFixed","namespace","constantNull","selection_cloneShallow","clone","cloneNode","selection_cloneDeep","onRemove","__on","j","onAdd","contextListener","params","CustomEvent","createEvent","initEvent","dispatchConstant","dispatchFunction","root","Selection","parents","_groups","_parents","selection","subgroups","subnode","subgroup","selectAll","selectorAll","selectChild","childFind","selectChildren","childrenFilter","matcher","enterGroup","updateGroup","previous","i0","i1","_enter","_exit","sparse","onenter","onupdate","onexit","append","merge","groups0","groups1","m0","m1","merges","group0","group1","sort","compareNode","sortgroups","sortgroup","each","attr","getAttributeNS","property","classed","html","creator","insert","before","deep","typenames","parseTypenames","nonpassive","nonpassivecapture","nopropagation","stopImmediatePropagation","noevent","__noselect","MozUserSelect","yesdrag","noclick","cosh","exp","zoomRho","rho","rho2","rho4","p0","p1","ux0","uy0","w0","ux1","uy1","w1","dx","dy","d2","d1","sqrt","b0","b1","r0","r1","coshr0","sinh","duration","SQRT2","_1","_2","sourceEvent","ownerSVGElement","createSVGPoint","matrixTransform","getScreenCTM","inverse","getBoundingClientRect","rect","clientLeft","clientTop","taskHead","taskTail","frame","timeout","clockLast","clockNow","clockSkew","clock","setFrame","requestAnimationFrame","clearNow","Timer","_call","_time","timer","restart","wake","timerFlush","t0","t2","t1","sleep","nap","poke","elapsed","stop","emptyOn","emptyTween","schedules","__transition","self","tween","schedule","tick","ease","init","active","svgNode","degrees","PI","translateX","translateY","rotate","skewX","scaleX","scaleY","atan2","atan","interpolateTransform","pxComma","pxParen","degParen","translate","interpolateTransformCss","DOMMatrix","WebKitCSSMatrix","isIdentity","decompose","interpolateTransformSvg","baseVal","consolidate","matrix","tweenRemove","tween0","tween1","tweenFunction","tweenValue","_id","factory","definition","Color","darker","brighter","reI","reN","reP","reHex","reRgbInteger","RegExp","reRgbPercent","reRgbaInteger","reRgbaPercent","reHslPercent","reHslaPercent","named","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","greenyellow","grey","honeydew","hotpink","indianred","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","rebeccapurple","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen","color_formatHex","rgb","formatHex","color_formatRgb","formatRgb","exec","rgbn","Rgb","rgba","hsla","rgb_formatHex","hex","rgb_formatRgb","clampa","clampi","Hsl","hslConvert","clamph","clampt","hsl2rgb","m2","basis","v0","v1","v2","v3","t3","define","displayable","formatHex8","formatHsl","pow","clamp","linear","gamma","nogamma","exponential","constant","rgbGamma","colorRgb","rgbSpline","spline","colors","reA","reB","am","bm","bs","lastIndex","one","zero","interpolateNumber","interpolateRgb","interpolateString","interpolate","value1","string00","interpolate0","string1","string0","string10","attrTweenNS","attrInterpolateNS","_value","attrTween","attrInterpolate","delayFunction","delayConstant","durationFunction","durationConstant","Transition","_name","selection_prototype","inherit","id0","id1","on0","on1","sit","every","onFunction","styleTween","styleNull","listener0","styleMaybeRemove","styleInterpolate","textTween","textInterpolate","removeFunction","easeConstant","easeVarying","reject","cancel","interrupt","defaultTiming","ZoomEvent","Transform","applyX","applyY","invert","invertX","invertY","rescaleX","domain","rescaleY","defaultFilter","defaultExtent","SVGElement","hasAttribute","clientWidth","clientHeight","defaultTransform","__zoom","defaultWheelDelta","defaultTouchable","maxTouchPoints","defaultConstrain","extent","translateExtent","dx0","dx1","dy0","dy1","touchstarting","touchfirst","touchending","constrain","touchable","scaleExtent","interpolateZoom","clickDistance2","tapDistance","wheeled","mousedowned","dblclicked","touchstarted","touchmoved","touchended","centroid","gesture","clean","__zooming","Gesture","taps","_len","_key","pointer","wheel","mouse","_len2","_key2","moved","x0","y0","dragEnable","dragDisable","_len3","_key3","k1","_len4","_key4","started","identifier","touch0","touch1","_len5","_key5","l0","l1","dp","_len6","_key6","hypot","scaleBy","scaleTo","translateBy","translateTo","emit","clickDistance","DragEvent","subject","defaultContainer","defaultSubject","StoreContext","Provider$1","errorMessages","error004","error005","error006","error008","sourceHandle","edge","targetHandle","error010","edgeType","zustandErrorMessage","error001","useStore","store","useStoreApi","selector$g","userSelectionActive","Panel","pointerEvents","positionClasses","Attribution","proOptions","hideAttribution","rel","EdgeText$1","labelShowBg","labelBgStyle","labelBgPadding","labelBgBorderRadius","_excluded2","edgeRef","edgeTextBbox","setEdgeTextBbox","edgeTextClasses","textBbox","getBBox","visibility","getDimensions","offsetWidth","offsetHeight","clampPosition","calcAutoPanVelocity","abs","calcAutoPan","pos","bounds","getHostForElement","_element$getRootNode","_window","getRootNode","getBoundsOfBoxes","box1","box2","rectToBox","boxToRect","nodeToRect","positionAbsolute","getOverlappingArea","rectA","rectB","xOverlap","yOverlap","isNumeric","internalsSymbol","elementSelectionKeys","devWarn","isInputDOMNode","_kbEvent$composedPath","kbEvent","isReactKeyboardEvent","composedPath","closest","isMouseEvent","getEventPosition","_event$touches","_event$touches2","_bounds$left","_bounds$top","isMouseTriggered","evtX","evtY","isMacOs","_navigator","userAgent","BaseEdge","labelX","labelY","markerEnd","markerStart","interactionWidth","getMouseHandler$1","handler","edges","getEdgeCenter","sourceX","sourceY","targetX","targetY","xOffset","centerX","yOffset","getBezierEdgeCenter","sourceControlX","sourceControlY","targetControlX","targetControlY","centerY","ConnectionMode","PanOnScrollMode","SelectionMode","ConnectionLineType","MarkerType","Position","getControl","_ref9","getSimpleBezierPath","_ref0","sourcePosition","Bottom","targetPosition","Top","offsetX","offsetY","SimpleBezierEdge","_ref1","handleDirections","distance","getPoints","_ref11","center","sourceDir","targetDir","sourceGapped","targetGapped","dir","_ref10","getDirection","dirAccessor","currDir","sourceGapOffset","targetGapOffset","defaultCenterX","defaultCenterY","defaultOffsetX","defaultOffsetY","_center$x","_center$y","verticalSplit","horizontalSplit","sourceTarget","targetSource","diff","gapOffset","dirAccessorOpposite","isSameDir","sourceGtTargetOppo","sourceLtTargetOppo","sourceGapPoint","targetGapPoint","getSmoothStepPath","_ref12","res","segment","bendSize","xDir","yDir","getBend","SmoothStepEdge","_ref13","pathOptions","StepEdge","_props$pathOptions2","_props$pathOptions","StraightEdge","_ref15","_ref14","getStraightPath","calculateControlOffset","curvature","getControlWithCurvature","_ref16","getBezierPath","_ref17","BezierEdge","_ref18","NodeIdContext","getEdgeId","_ref19","getMarkerId","marker","rfId","idPrefix","pointToRendererPoint","_ref20","_ref21","snapToGrid","_ref22","tx","ty","tScale","snapX","snapY","rendererPointToPoint","_ref23","_ref24","getNodePositionWithOrigin","_node$width","_node$height","nodeOrigin","getNodesBounds","box","currBox","getNodesInside","nodeInternals","partially","excludeNonSelectableNodes","paneRect","visibleNodes","selectable","nodeRect","overlappingArea","dragging","getConnectedEdges","nodeIds","getViewportForBounds","minZoom","maxZoom","xZoom","yZoom","clampedZoom","getD3Transition","getHandles","handleBounds","currentHandle","_node$positionAbsolut","_node$positionAbsolut2","_node$positionAbsolut3","_node$positionAbsolut4","nullConnection","defaultResult","handleDomNode","connection","endHandle","isValidHandle","handle","connectionMode","fromNodeId","fromHandleId","fromType","isValidConnection","isTarget","handleToCheck","handleType","getHandleType","handleNodeId","handleId","connectable","connectableEnd","Strict","edgeUpdaterType","resetRecentHandle","getConnectionStatus","isInsideConnectionRadius","isHandleValid","connectionStatus","handlePointerDown","_ref29","onConnect","onReconnectEnd","domNode","autoPanOnConnect","connectionRadius","onConnectStart","panBy","getNodes","cancelConnection","closestHandle","autoPanId","clickedHandle","elementFromPoint","containerBounds","prevActiveHandle","connectionPosition","autoPanStarted","handleLookup","_ref28","sourceHandles","targetHandles","getHandleLookup","autoPan","xMovement","yMovement","onPointerMove","validHandleResult","handles","validator","handleBelow","elementsFromPoint","closestHandles","minDistance","hasValidHandle","_ref25","hasTargetHandle","_ref26","_ref27","getClosestHandle","connectionEndHandle","toggle","onPointerUp","_getState$onConnectEn","_getState","onConnectEnd","cancelAnimationFrame","connectionNodeId","connectionHandleId","connectionHandleType","connectionStartHandle","alwaysValid","selector$f","connectOnClick","noPanClassName","Handle","_ref30","isConnectable","isConnectableStart","isConnectableEnd","onTouchStart","_excluded4","shallow","connecting","clickConnecting","connectingSelector","startHandle","connectionClickStartHandle","clickHandle","_store$getState$onErr","_store$getState","onConnectExtended","defaultEdgeOptions","onConnectAction","hasDefaultEdges","edgeParams","setEdges","addEdge","connectionExists","onPointerDown","connectablestart","connectableend","connectionindicator","onClickConnectStart","onClickConnectEnd","isValidConnectionStore","isValidConnectionHandler","Handle$1","DefaultNode","_ref31","DefaultNode$1","InputNode","_ref32","InputNode$1","OutputNode","_ref33","OutputNode$1","GroupNode","selector$e","selectedNodes","selectedEdges","selectId","obj","areEqual","SelectionListener","_ref34","onSelectionChange","changeSelector","Wrapper$1","_ref35","storeHasSelectionChange","selector$d","setNodes","setDefaultNodesAndEdges","setMinZoom","setMaxZoom","setTranslateExtent","setNodeExtent","reset","useStoreUpdater","setStoreState","useDirectStoreUpdater","StoreUpdater","_ref36","defaultNodes","defaultEdges","nodesDraggable","nodesConnectable","nodesFocusable","edgesFocusable","edgesUpdatable","elevateNodesOnSelect","nodeExtent","onNodesChange","onEdgesChange","elementsSelectable","snapGrid","fitView","fitViewOptions","onNodesDelete","onEdgesDelete","onNodeDrag","onNodeDragStart","onNodeDragStop","onSelectionDrag","onSelectionDragStart","onSelectionDragStop","autoPanOnNodeDrag","nodeDragThreshold","edgesWithDefaults","ariaLiveStyle","clip","clipPath","ARIA_NODE_DESC_KEY","ARIA_EDGE_DESC_KEY","selector$c","ariaLiveMessage","AriaLiveMessage","_ref37","A11yDescriptions","_ref38","disableKeyboardA11y","useKeyPress","actInsideInputWithModifier","keyPressed","setKeyPressed","modifierPressed","pressedKeys","keyCodes","keysToWatch","keysFlat","downHandler","keyOrCode","useKeyOrCode","isMatchingKey","upHandler","resetHandler","isUp","eventCode","calculateXYZPosition","_result$x","_result$y","_parentNode$internals","_parentNode$internals2","_result$z","_parentNode$internals3","_parentNode$internals4","_result$z2","parentNodePosition","updateAbsoluteNodePositions","parentNodes","_node$internalsSymbol","_node$internalsSymbol2","isParent","createNodeInternals","nextNodeInternals","selectedNodeZ","_currInternals$intern","currInternals","internals","resetHandleBounds","d3Zoom","d3Selection","fitViewOnInitDone","fitViewOnInit","isInitialFitView","_options$nodes","isVisible","includeHiddenNodes","optionNode","nodesInitialized","_options$minZoom","_options$maxZoom","_options$padding","nextTransform","zoomIdentity","handleControlledNodeSelectionChange","nodeChanges","change","handleControlledEdgeSelectionChange","edgeChanges","updateNodesAndEdgesSelections","_ref39","changedNodes","changedEdges","hasDefaultNodes","initialViewportHelper","zoomIn","zoomOut","zoomTo","getZoom","setViewport","getViewport","setCenter","fitBounds","project","screenToFlowPosition","flowToScreenPosition","viewportInitialized","selector$b","useReactFlow","viewportHelper","useViewportHelper","viewportHelperFunctions","_transform$x","_transform$y","_transform$zoom","nextZoom","_options$padding2","domX","domY","relativePosition","rendererPosition","getNode","getEdges","getEdge","nextNodes","changes","nextEdges","addNodes","addEdges","toObject","viewport","deleteElements","_ref40","nodesDeleted","edgesDeleted","edgeIds","nodesToRemove","parentHit","deletable","deletableEdges","initialHitEdges","connectedEdges","edgesToRemove","edgeIdsToRemove","getNodeRect","nodeOrRect","isRect","getIntersectingNodes","currNodeRect","isNodeIntersecting","deleteKeyOptions","containerStyle","eventToFlowTransform","eventTransform","isWrappedWithClass","isRightClickPan","panOnDrag","usedButton","factor","selector$a","d3ZoomHandler","ZoomPane","_ref42","onMove","onMoveStart","onMoveEnd","onPaneContextMenu","zoomOnScroll","zoomOnPinch","panOnScroll","panOnScrollSpeed","panOnScrollMode","Free","zoomOnDoubleClick","defaultViewport","zoomActivationKeyCode","preventScrolling","noWheelClassName","timerId","isZoomingOrPanning","zoomedWithRightMouseButton","zoomPane","prevTransform","zoomActivationKeyPressed","mouseButton","isPanScrolling","panScrollTimeout","rendererNode","resizeObserver","updateDimensions","_store$getState$onErr2","_store$getState2","ResizeObserver","observe","unobserve","useResizeHandler","bbox","d3ZoomInstance","updatedTransform","constrainedTransform","currentZoom","pinchDelta","deltaNormalize","Vertical","Horizontal","internal","nextViewport","onViewportChangeStart","onViewportChange","onViewportChangeEnd","_event$sourceEvent","_event$sourceEvent2","flowTransform","paneDragging","_mouseButton$current","_event$sourceEvent3","_mouseButton$current2","prevViewport","viewChanged","zoomScroll","pinchZoom","buttonAllowed","selector$9","userSelectionRect","UserSelection","handleParentExpand","updateItem","extendWidth","extendHeight","_parent$style$width","_parent$style$height","xDiff","yDiff","applyChanges","initElements","currentChanges","currentChange","expandParent","updateStyle","resizing","applyNodeChanges","createSelectionChange","getSelectionChanges","selectedIds","willBeSelected","wrapHandler","containerRef","selector$8","Pane","_ref43","selectionMode","Full","onSelectionStart","onSelectionEnd","onPaneClick","onPaneScroll","onPaneMouseEnter","onPaneMouseMove","onPaneMouseLeave","prevSelectedNodesCount","prevSelectedEdgesCount","resetUserSelection","resetSelectedElements","nodesSelectionActive","onWheel","hasActiveSelection","startX","startY","_userSelectionRect$st","_userSelectionRect$st2","mousePos","nextUserSelectRect","Partial","selectedEdgeIds","selectedNodeIds","onMouseLeave","isParentSelected","hasSelector","nodeRef","_current","parentElement","getDragItems","draggable","_n$positionAbsolute$x","_n$positionAbsolute","_n$positionAbsolute$y","_n$positionAbsolute2","delta","calcNextPosition","nextPosition","clampedNodeExtent","clampNodeExtent","currentExtent","parentX","parentY","parentPosition","getEventHandlerParams","_ref44","dragItems","extentedDragItems","getHandleBounds","nodeElement","handlesArray","nodeBounds","nodeOffset","getMouseHandler","handleNodeClick","_ref45","unselect","addSelectedNodes","unselectNodesAndEdges","multiSelectionActive","_nodeRef$current","blur","wrapSelectionDragFunc","selectionFunc","useDrag","_ref47","noDragClassName","handleSelector","isSelectable","selectNodesOnDrag","setDragging","lastPos","mousePosition","dragEvent","dragStarted","abortDrag","getPointerPosition","_ref46","pointerPos","xSnapped","ySnapped","useGetPointerPosition","updateNodes","_ref48","updateNodePositions","hasChange","nodesBox","adjustedNodeExtent","_n$width","_n$height","updatedPos","onDrag","currentNode","_lastPos$current$x","_lastPos$current$y","startDrag","onStart","_nodeInternals$get","dragHandler","mousedownx","mousedowny","mousemoving","gestures","drag","beforestart","mousemoved","mouseupped","nodrag","touch","_lastPos$current$x2","_lastPos$current","_lastPos$current$y2","_lastPos$current2","onStop","useUpdateNodePositions","xVelo","yVelo","isShiftPressed","positionDiffX","positionDiffY","arrowKeyDiffs","ArrowUp","ArrowDown","wrapNode","NodeComponent","NodeWrapper","_ref49","xPos","yPos","xPosOrigin","yPosOrigin","onDoubleClick","isDraggable","isFocusable","dragHandle","initialized","ariaLabel","hasHandleBounds","prevNodeRef","prevSourcePosition","prevTargetPosition","prevType","hasPointerEvents","updatePositions","onMouseEnterHandler","onMouseMoveHandler","onMouseLeaveHandler","onContextMenuHandler","onDoubleClickHandler","currNode","typeChanged","sourcePosChanged","targetPosChanged","updateNodeDimensions","onKeyDown","tabIndex","selector$7","transformString","NodesSelection$1","_ref50","onSelectionContextMenu","_nodeRef$current2","preventScroll","selector$6","FlowRenderer","_ref51","deleteKeyCode","selectionKeyCode","selectionOnDrag","multiSelectionKeyCode","panActivationKeyCode","_panOnScroll","_panOnDrag","selectionKeyPressed","panActivationKeyPressed","_ref41","deleteKeyPressed","multiSelectionKeyPressed","useGlobalKeyHandler","FlowRenderer$1","createNodeTypes","nodeTypes","standardTypes","specialTypes","selector$5","NodeRenderer","onlyRenderVisible","onlyRenderVisibleElements","resizeObserverRef","observer","entry","_resizeObserverRef$cu","disconnect","_clampedPosition$x","_clampedPosition$y","_node$width2","_node$height2","_node$internalsSymbol3","_node$internalsSymbol4","_node$internalsSymbol5","_node$internalsSymbol6","focusable","clampedPosition","posX","posY","posOrigin","_ref52","origin","getPositionWithOrigin","onNodeClick","onNodeMouseEnter","onNodeMouseMove","onNodeMouseLeave","onNodeContextMenu","onNodeDoubleClick","NodeRenderer$1","shiftX","shiftY","EdgeUpdaterClassName","EdgeAnchor","_ref53","radius","onMouseOut","alwaysValidConnection","wrapEdge","EdgeComponent","EdgeWrapper","_ref54","onEdgeDoubleClick","animated","sourceHandleId","targetHandleId","reconnectRadius","onReconnect","onReconnectStart","isReconnectable","updateHover","setUpdateHover","updating","setUpdating","markerStartUrl","markerEndUrl","onEdgeDoubleClickHandler","onEdgeContextMenu","onEdgeMouseEnter","onEdgeMouseMove","onEdgeMouseLeave","handleEdgeUpdater","isSourceHandle","evt","onEdgeUpdaterMouseEnter","onEdgeUpdaterMouseOut","inactive","addSelectedEdges","_edgeRef$current","_edgeRef$current2","createEdgeTypes","edgeTypes","straight","bezier","smoothstep","simplebezier","getHandlePosition","getHandle","getNodeData","_node$internalsSymbol7","_node$positionAbsolut5","_node$positionAbsolut6","_node$positionAbsolut7","_node$positionAbsolut8","defaultEdgeTree","isMaxLevel","useVisibleEdges","elevateEdgesOnSelect","sourceNode","targetNode","_ref55","sourcePos","targetPos","sourceWidth","sourceHeight","targetWidth","targetHeight","edgeBox","isEdgeVisible","maxLevel","levelLookup","tree","hasZIndex","_sourceNode$internals","_targetNode$internals","edgeOrConnectedNodeSelected","selectedZIndex","edgeTree","_ref56","groupEdgesByZLevel","MarkerSymbols","Arrow","_ref57","ArrowClosed","_ref58","Marker","_ref59","markerUnits","_store$getState$onErr3","_store$getState3","useMarkerSymbol","markerWidth","markerHeight","refX","refY","MarkerDefinitions","_ref61","defaultColor","markers","_ref60","ids","markerId","localeCompare","markerSelector","MarkerDefinitions$1","selector$4","EdgeRenderer","_ref62","defaultMarkerColor","onEdgeClick","_ref63","_targetHandleBounds$t","_targetHandleBounds$s","sourceNodeRect","sourceHandleBounds","sourceIsValid","targetNodeRect","targetHandleBounds","targetIsValid","targetNodeHandles","edgeReconnectable","reconnectable","updatable","getEdgePositions","sourceHandlePos","targetHandlePos","EdgeRenderer$1","selector$3","Viewport","_ref64","oppositePosition","ConnectionLine","_ref65","_fromNode$internalsSy","_fromNode$width","_fromNode$height","_fromNode$positionAbs","_fromNode$positionAbs2","_fromNode$positionAbs3","_fromNode$positionAbs4","Bezier","CustomComponent","fromNode","toX","toY","fromHandleBounds","Loose","fromHandle","fromHandleX","fromHandleY","fromX","fromY","fromPosition","toPosition","connectionLineType","connectionLineStyle","dAttr","pathParams","Step","SmoothStep","SimpleBezier","selector$2","ConnectionLineWrapper","_ref66","component","useNodeOrEdgeTypes","nodeOrEdgeTypes","createTypes","GraphView","_ref67","onInit","connectionLineComponent","connectionLineContainerStyle","nodeTypesWrapped","edgeTypesWrapped","rfInstance","isInitialized","useOnInitHandler","GraphView$1","infiniteExtent","NEGATIVE_INFINITY","POSITIVE_INFINITY","fitViewOnInitOptions","createRFStore","createWithEqualityFn","viewportNode","m22","DOMMatrixReadOnly","nextFitViewOnInitDone","nodeDragItems","positionChanged","triggerNodeChanges","edgeId","storeEdges","edgesToUnselect","_get$d3Zoom","ReactFlowProvider","_ref68","storeRef","Wrapper","_ref69","defaultNodeTypes","defaultEdgeTypes","initNodeOrigin","initSnapGrid","initDefaultViewport","wrapperStyle","ReactFlow","_ref70","onEdgeUpdate","onEdgeUpdateStart","onEdgeUpdateEnd","edgeUpdaterRadius","attributionPosition","_excluded5","createUseItemsState","initialItems","setItems","onItemsChange","useNodesState","useEdgesState","BackgroundVariant","LinePattern","lineWidth","DotPattern","Dots","Lines","Cross","defaultSize","patternId","Background","variant","patternColor","patternSize","isDots","isCross","gapXY","scaledGap","scaledSize","patternDimensions","patternOffset","patternUnits","patternTransform","Background$1","PlusIcon","MinusIcon","FitViewIcon","LockIcon","UnlockIcon","ControlButton","isInteractive","minZoomReached","maxZoomReached","Controls","showZoom","showFitView","showInteractive","onZoomIn","onZoomOut","onFitView","onInteractiveChange","setIsVisible","onZoomInHandler","onZoomOutHandler","onFitViewHandler","onToggleInteractivity","Controls$1","MiniMapNode","shapeRendering","background","MiniMapNode$1","selector$1","selectorNodes","getAttrFunction","func","MiniMapNodes$1","nodeStrokeColor","nodeColor","nodeClassName","nodeBorderRadius","nodeStrokeWidth","nodeComponent","nodeColorFunc","nodeStrokeColorFunc","nodeClassNameFunc","chrome","viewBB","boundingRect","rect1","rect2","getBoundsOfRects","MiniMap","_style$width","_style$height","maskColor","maskStrokeColor","maskStrokeWidth","pannable","zoomable","inversePan","zoomStep","offsetScale","elementWidth","elementHeight","scaledWidth","scaledHeight","viewScale","viewWidth","viewHeight","labelledBy","viewScaleRef","zoomHandler","panHandler","moveScale","zoomAndPanHandler","onSvgClick","rfCoord","onSvgNodeClick","fillRule","MiniMap$1","bgColors","decision","colorClass","terminal","isStart","database","flowDefinitions","flowType","selectedFlow","setSelectedFlow","flowDef","handleFlowChange","newFlow","newDef","def","_node$data","defaultPriceData","purchasePrice","processingCost","marginRate","sellingPrice","validFrom","PriceEditModal","onSave","setFormData","subtotal","lossAppliedCost","toLocaleString","newSellingPrice","handleMarginRateChange","price","newMarginRate","handleSellingPriceChange","externalItems","onUpdatePrice","setSearch","itemTypeFilter","setItemTypeFilter","showModal","setShowModal","selectedItem","setSelectedItem","localPriceData","setLocalPriceData","priceData","setPriceData","newData","customerGroups","setCustomerGroups","groupCode","customerCount","itemTypeTabs","_p$spec","handleSelect","handleSelectAll","clearSelection","isAllSelected","hasSelection","isMultiSelect","setSelectedIds","useListSelection","totalItems","activeItems","handleDeleteSelected","confirm","openEditModal","handleDelete","updatedItem","Book","MessageSquare","documentStatusPolicy","createsDocument","autoTrigger","validityDays","productionOrder","documentLinkagePolicy","relationships","relationship","linkField","inheritFields","statusSync","newStatus","requiresAction","processFlowPolicy","estimatedDays","mergePoints","requiredWorkflows","outputDocument","ASSEMBLY","sequenceConstraints","materialPolicy","lotManagement","trackingRequired","lotNumberFormat","expiryManagement","fifoEnforced","inventoryPolicy","safetyStockLevel","reorderPoint","maxStockLevel","stockLocations","transactionPolicy","inbound","requiresInspection","requiresLotAssignment","outbound","requiresApproval","lotSelectionMethod","pricingPolicy","costPrice","marginAmount","customerGroupDiscount","VIP","GOLD","SILVER","NORMAL","historyPolicy","trackChanges","retentionPeriod","changeReasonRequired","approvalRequired","autoRecalculation","triggers","notifyOnChange","itemCodePolicy","제품","부품_조립","부품_절곡","부품_구매","부자재","원자재","소모품","lotNumberPattern","components","PRODUCT","TYPE","YYMDD","SIZE","authorizationPolicy","ADMIN","SALES_MANAGER","PRODUCTION_MANAGER","QUALITY_MANAGER","WORKER","VIEWER","approvalPolicy","approvers","always","priceChange","marginBelow","terminologyPolicy","korean","abbreviation","product","part","subMaterial","rawMaterial","consumable","SCR","SLT","FLD","STK","inspectionTypes","notificationPolicy","recipients","channel","OverviewSection","DocumentStatusSection","policy","expanded","toggleSection","statusColorMap","filteredDocs","_doc$statuses","term","docKey","docPolicy","trans","_trans$requiredFields","typeInfo","DocumentLinkageSection","sync","uidx","ProcessFlowSection","mp","MaterialSection","PricingSection","info","ItemCodeSection","AuthorizationSection","roleCode","perm","TerminologySection","terms","_val$description","filterTerms","NotificationSection","activeCategory","setActiveCategory","expandedSections","setExpandedSections","lastUpdated","QuoteSheetDialog","_quote$totalAmount","_quote$totalAmount2","_quote$totalAmount3","_Math$round","_Math$round2","_item$unitPrice","_item$amount","CalculationSheetDialog","_quote$totalAmount4","_item$unitPrice2","_item$amount2","PurchaseOrderDialog","today","poNo","formatDate","dateStr","receiver","receiverContact","cumulativeCount","currentCount","specialNote","InboundSlipDialog","day","inboundNo","incomingLot","vendor","supplierLot","vendorContact","orderQty","receivedQty","requestQty","Card","Button","sizeClasses","variantClasses","StatusBadge","_quote$items","_quote$items2","_quote$unitPrice","onConvertToOrder","onUpdateQuote","onCreateOrder","showQuoteSheet","setShowQuoteSheet","showCalculationSheet","setShowCalculationSheet","showPurchaseOrder","setShowPurchaseOrder","showConvertModal","setShowConvertModal","showItemAddModal","setShowItemAddModal","setNewItem","convertForm","setConvertForm","shipDateUndecided","dueDateUndecided","freightCost","receiverName","receiverPhone","postalCode","baseAddress","detailAddress","pendingItems","Edit3","handleConvertToOrder","handleConvertConfirm","newOrderNo","newOrder","deliveryAddressDetail","changedAt","changeType","changedBy","convertedOrderNo","Square","ToggleLeft","CommonUXGuide","activeSection","setActiveSection","statusColors","waiting","complete","docColors","work","ProcessNode","direction","activePhase","setActivePhase","elementTypeLabels","card","checkbox","column","textarea","modal","screenFeatures","currentScreenName","onBadgeHover","expandedItems","setExpandedItems","hoveredNumber","setHoveredNumber","numberedElements","globalIndex","sectionIdx","_section$elements","_section$elements2","sectionIndex","elementCount","subElement","handleItemLeave","isSubElement","hasDetails","isHovered","handleItemHover","st","ELEMENT_SELECTORS","AREA_SELECTORS","onBadgeClick","setBadges","containerRect","setContainerRect","findElement","_item$position","_item$position2","getElementById","_selector$match","_selector$match2","tagMatch","_el$textContent","areaSelector","btn","_btn$textContent","_label$textContent","forId","siblingInput","nextElementSibling","calculateBadges","mainContainer","mainRect","newBadges","handleScroll","mutationObserver","MutationObserver","childList","subtree","attributes","isHighlighted","allScreenFeatures","dependsOn","screenIdMapping","getFeaturesByCurrentScreen","activeMenu","getScreenFeatures","screenFeatureDocuments","propertyType","stateManagement","stateName","getScreenFeatureDocument","FileCode","NUMBERING_RULES","useOrderRef","resetAllSequences","removeItem","getSequenceStorage","stored","seqKey","setSequenceStorage","seqStr","subSeq","generateIncomingLotNo","materialType","getLotSequenceStorage","setLotSequenceStorage","generateProductionLotNo","typeName","_lotRules$lotItemCode","_lotRules$lotItemCode2","_lotRules$lotTypeCode","_lotRules$lotTypeCode2","_lotTypeCodes$typeNam","_lotRules$monthCodes","lotRules","specCode","generateItemCodeAuto","createMaterialReceipt","supplierLotNo","multiUnits","attachments","incomingLotNo","receiptNo","receiptDate","materialId","standardPrice","warehouseCode","warehouseName","calculateProductionSize","W1","H1","calculateAreaWeight","motorCapacityTable","maxWeight","shaft","lookupMotorCapacity","bracketTable","bracket","angle","guideRailLengthTable","maxG","calculateFullBOM","motorPower","wireType","controllerType","motorCapacity","shaftInch","bracketSpec","grSpec","productionLotRequired","incomingLotRequired","summary","totalItemCount","vatAmount","isQC","team","workers","getWorkLogTemplate","documentTemplates","processConfig","createWorkLogData","stepResults","workLogDate","templateCode","productSpec","planQty","totalQty","completedQty","_stepResults$steps","qcResult","signDate","getInspectionTemplate","createInspectionData","defaultCheckItems","getDefaultCheckItems","hasMillSheet","productionLotNo","inspectionQty","passQty","failQty","inspectorName","_checkResults$items","_checkResults$items$i","_checkResults$items2","_checkResults$items2$","_checkResults$items3","_checkResults$items3$","measuredValue","baseItems","runCompleteProcess","testNo","results","success","errors","timeline","timestamp","bom","materialReceipts","receipt","procType","_processConfig$steps$","grLength","stepStatus","currentStep","workLogs","wl","inspections","ins","depositAmount","deposit","depositNo","depositDate","depositType","bankName","accountNo","workOrderIds","workOrderNos","lotNos","shipmentQty","deliveryType","inspectionRequest","requestNo","shipmentId","requestedDate","finalInspection","salesNo","salesDate","inspectionId","remainingAmount","invoiceDate","workOrderCount","workLogNos","productionLotNos","incomingLotNos","inspectionNos","masterData","customers","contactName","paymentTerms","creditLimit","sites","totalUnits","products","rawMaterials","safetyStock","runAllTests","testCase","totalTests","successCount","failCount","CompleteIntegrationTestTab","_completeMasterData$c","_completeMasterData$s","_completeMasterData$p","_completeMasterData$r","_completeMasterData$p2","_completeMasterData$c2","_testResults$summary","_testResults$summary2","_testResults$summary3","_testResults$summary4","_testResults$summary5","_testResults$results","testResults","setTestResults","isRunning","setIsRunning","selectedTest","setSelectedTest","selectedWorkLog","setSelectedWorkLog","completeMasterData","handleRunTests","runCompleteTests","_result$data","_result$data$quote","_result$data2","_result$data2$product","_result$data3","_result$data3$incomin","_result$data$quote2","_result$data$order","_result$data$workOrde","_result$data$shipment","_result$data$incoming","_result$data$producti","_result$data$quote3","_result$data$quote3$t","_result$data$depositP","_result$data$depositP2","_result$data$sales","_result$data$sales$re","_result$data$sales2","testName","documentNo","productionLots","incomingLots","lot","_lot$materialName","_lot$itemName","processInspection","depositPayment","renderWorkLogDialog","_workLog$steps","_workLog$materialLots","quantity","assignee","materialLots","mat","EyeOff","STORAGE_KEYS","deposits","clearAllTestData","saveToStorage","getFromStorage","appendToStorage","existing","updated","passedTests","failedTests","addTestResult","passed","tests","testMasterDataProcess","valid","customersValid","expectedPattern","sitesValid","expectedCode","subMaterialsValid","bendingParts","expected","bendingPartsValid","numberingValidation","expectedPrefix","generated","numberingValid","testQuoteProcess","testCustomers","testSites","quoteNumbers","quoteNumbersValid","bomResults","sizeTests","_bom$variables","_bom$variables2","_bom$items","_bom$summary","_bom$items2","_bom$summary2","_bom$summary2$totalAm","hasTotal","hasVariables","bomValid","sizeTest","hasQuoteNo","hasCustomer","hasSite","hasAmount","quotesValid","testOrderProcess","orderNumbers","orderNumbersValid","workOrderNumbers","woNo","woNumbersValid","productionLotsValid","ordersValid","testMaterialReceiptProcess","incomingLotsValid","receipts","hasLotNo","hasReceiptNo","hasQty","receiptsValid","iqcLots","iqcNo","iqcLotsValid","testWorkProgressProcess","workLogNumbers","wlNo","wlNumbersValid","_workLog$stepResults","_processConfig$worker","_processConfig$worker2","hasWorkLogNo","hasSteps","hasLinkedWO","workLogsValid","processSteps","_workflow$steps","_workflow$steps2","_workflow$steps3","hasQcStep","hasTeam","stepCount","processStepsValid","testPQCProcess","pqcLots","pqcNo","pqcLotsValid","_workOrders$i","_inspection$checkItem","_inspection$checkItem2","mockWorkOrder","hasInspectionNo","hasCheckItems","inspectionsValid","testShipmentProcess","shipmentNumbers","shipmentNumbersValid","outboundNumbers","outNo","outboundNo","outboundNumbersValid","shipmentsValid","testFQCAndSalesProcess","fqcLots","fqcNo","fqcLotsValid","fqcInspections","_inspection$checkItem3","_inspection$checkItem4","hasFQCItems","_item$code","_item$name","fqcInspectionsValid","salesList","_orders$i","_orders$i2","_shipments$i","_shipments$i2","_fqcInspections$i","_fqcInspections$i2","_orders$i3","_orders$i4","hasSalesNo","hasCorrectAmounts","salesValid","runAllRealProcessTests","getTestResults","validateNumberingRules","generator","failed","PROCESSES","ProcessIntegrationTestTab","_testResults$summary6","_testResults$summary7","_testResults$summary8","_testResults$summary9","_testResults$summary0","testMode","setTestMode","numberingResults","setNumberingResults","expandedProcesses","setExpandedProcesses","expandedTests","setExpandedTests","showRawData","setShowRawData","savedResults","handleClearData","_result$tests","handleDownloadResults","dataStr","blob","Blob","URL","createObjectURL","download","click","revokeObjectURL","pIdx","_process$tests","processInfo","allPassed","toggleProcessExpand","tIdx","_test$items","testKey","isTestExpanded","toggleTestExpand","STATE_TRANSITIONS","COMPREHENSIVE_MENU_FEATURES","register","autoGenerate","linkedTo","stateTransitions","searchTarget","dashboard","traceability","iqc","fqc","defect","stock","creditGradeLogic","canShip","users","settings","FULL_WORKFLOW","dataConnections","creditGradeRule","masterDataFlow","targets","StateTransitionDiagram","entity","fromStates","SectionItemDetail","badgeNumber","ScreenSection","_section$items","startBadge","setExpanded","ScreenView","_screen$sections","badgeCounter","_section$items2","currentStart","PageView","_page$workflow$steps","_page$workflow$dataFl","_page$workflow$dataFl2","_page$workflow$dataFl3","MenuGroup","menuKey","pageKey","FlowNode","getNodeStyle","FlowConnector","ScenarioFlowChart","scenario","DepartmentFlowDiagram","deptData","FullWorkflowDiagram","_FULL_WORKFLOW$stages","_stage$screens","DataRegistrationFlowDiagram","_dataRegistrationFlow","_dataRegistrationFlow2","_dataRegistrationFlow3","expandedPhases","setExpandedPhases","expandedSteps","setExpandedSteps","_layer$items","phaseIdx","_phase$steps","_phase$steps2","togglePhase","phaseId","toggleStep","stepId","secIdx","_section$items3","itemIdx","_dataRegistrationFlow5","onWidthChange","isInline","handleMouseDown","startWidth","handleMouseMove","newWidth","_deptData$scenarios","_dataRegistrationFlow4","_deptData$scenarios2","_d$scenarios","typeLabels","FeatureItemDetail","globalBadgeNumber","typeLabel","paddingBottom","paddingLeft","SectionBlock","globalBadgeStart","ScreenBlock","_screen$sections2","currentBadge","screenTypeLabel","sectionStart","PageBlock","_screen$sections3","_sec$items","_screen$sections4","screenStart","screenItems","_sec$items2","MenuGroupBlock","_screen$sections5","_sec$items3","pageStart","pageItems","_screen$sections6","_sec$items4","scrollRef","stats","totalMenus","totalPages","totalScreens","totalSections","_section$items4","filteredPages","filteredScreens","filteredSections","_section$items5","filteredItems","_item$label","_item$id","_item$type","_item$description","getGlobalBadgeStart","_section$items6","filteredCount","_section$items7","inset","flexDirection","boxSizing","overflowY","processClassificationRules","ruleName","ruleCode","operator","conditionLogic","targetProcessName","workSheetType","classifyItemToProcess","matched","conditionResults","cond","fieldValue","processId","matchedRule","logs","maxLogs","logEntry","substr","quoteCalc","orderConvert","orderConfirm","confirmType","processSplit","classificationResults","completedCount","errorData","getLogs","getLogsByStep","getLogsByType","storageKey","currentSeq","getCurrentSequence","generateDocumentNumber","_documentTemplateConf","seq2","seq3","angleTable","caseLengthTable","maxS","shaftLengthTable","maxW","_table$shaftInch2","inch","_table$shaftInch","calculateBOM","_guideRailLengthTable","_shaftLengthTable$fin","angleSpec","caseSpec","shaftLength","smokeBlockQty","caseSmokeQty","generateBendingItemCode","_lotProductCodes$prod","_lotTypeCodes$type","processAssignees","members","initializeStepStatus","createWorkOrderFromOrder","_order$items","_order$items$","_order$items2","_order$items2$","_order$items3","_order$items3$","_order$items4","_processSteps$process","splitNo","assigneeInfo","shipRequestDate","assignees","createInspectionFromWorkOrder","createShipmentFromInspection","constructionType","expectedEndDate","testProducts","leadTime","minOrderQty","testParts","purchased","assembly","bending","testRawMaterials","testConsumables","runFullProcessTest","logStep","bomVariables","bomCount","updatedWorkOrders","updatedStepStatus","completedSteps","completedWorkOrders","completedStepStatus","depositor","inspectionCount","processFlow","_results$timeline","runAllIntegrationTests","_r$summary$workOrderN","getAllMasterData","factoryCodes","generateQuoteNo","generateProductionLot","generateProductLot","splitSeq","generateIncomingInspectionLot","generateProcessInspectionLot","generateProductInspectionLot","DashboardCard","StatCard","_colorClasses$color","_colorClasses$color2","colorClasses","statusStyles","xs","sm","CEODashboard","userRole","showWidgetManager","setShowWidgetManager","widgetConfig","setWidgetConfig","dashboardData","dailySales","dailySalesChange","dailySalesMonthChange","profitMargin","operatingMargin","totalReceivable","overdueReceivable","overdueRate","dailyProfit","salesAchievement","daily","actual","collected","receivable","monthly","yearly","monthlySales","monthlyReceivable","topCustomers","rank","productionStatus","utilization","qualityStatus","defectRate","inspected","defects","byProcess","receivableTop5","days","cashflow","opening","closing","transactions","purchaseStatus","purchaseRate","byCategory","inventoryStatus","normal","shortage","hrStatus","totalEmployees","attendanceRate","onLeave","onTrip","recentActivity","period","todaySchedule","attendees","renderWidget","widget","DailyMetricsWidget","ProcessPipelineWidget","SalesAchievementWidget","SalesTrendWidget","ReceivableTrendWidget","TopCustomersWidget","ProductionStatusWidget","QualityStatusWidget","ReceivableTop5Widget","CashflowWidget","PurchaseStatusWidget","InventoryStatusWidget","HRStatusWidget","CalendarWidget","enabledWidgets","WidgetManagerModal","widgets","newConfig","badgeColor","maxAmount","proc","daysInMonth","firstDayOfMonth","getDay","todayDate","weekDays","setConfig","enabledCount","totalCount","toggleWidget","setWidgetWidth","resetToDefault","SalesDashboard","pendingQuotes","confirmedOrders","pendingProduction","readyToShip","shipmentStatus","myPendingApprovals","_o$splits","splits","ProductionDashboard","onApprove","inProgressOrders","waitingOrders","completedOrders","urgentOrders","diffDays","PurchaseDashboard","purchaseOrders","onCreatePO","lowStockMaterials","minStock","pendingPOs","inTransitPOs","catMaterials","lowStock","QualityDashboard","pendingInspection","inspectionStatus","incomingInspection","monthlyDefectRate","pendingInspections","defectTypes","AccountingDashboard","onApproveShipment","onRejectShipment","accountingStatus","cGradeOrders","cGradeTotal","collectSchedule","paymentSchedule","WorkerDashboard","_selectedWork$product","currentUser","onStartWork","onInputWorkLog","onCompleteWork","showWorkLogModal","setShowWorkLogModal","selectedWork","setSelectedWork","setWorkLog","myWorks","inProgressWorks","waitingWorks","todayCompleted","totalCompleted","totalDefects","toLocaleDateString","workNote","handleOpenWorkLog","handleCompleteWork","handleStartWork","print","handleSaveWorkLog","TabFilter","onTabChange","SearchInput","danger","ghost","useResponsive","screenSize","setScreenSize","checkSize","innerWidth","isMobile","isTablet","isDesktop","isMobileOrTablet","MobileHeader","onMenuToggle","showBack","reload","DesktopHeader","user","roleName","onUserChange","showFeatureDescription","onToggleFeatureDescription","currentVersion","hasChanges","onOpenHistory","showFlowPanel","onToggleFlowPanel","userNickname","showFeatureDocPanel","onToggleFeatureDocPanel","showComprehensiveFlowPanel","onToggleComprehensiveFlowPanel","showAllMenuFeatureDocPanel","onToggleAllMenuFeatureDocPanel","isFeatureAdmin","MobileMenu","onMenuChange","menuConfig","_menuConfig$g$id","menuTitleMap","getElementAtPoint","_el$getAttribute","_el$className","_el$className2","_el$className3","_el$className4","NicknameModal","currentNickname","nickname","setNickname","inputRef","isEditMode","onSubmit","VersionHistoryModal","onCommit","onRollback","autoMessage","handleQuickCommit","author","InlineBadgeInput","_existingBadge$uiInfo","_existingBadge$uiInfo2","onCancel","existingBadge","onDelete","setLabel","setDescription","showDetails","setShowDetails","uiType","setUiType","setInteraction","selectedColor","setSelectedColor","labelInputRef","handleKeyDown","badgeData","getPopupStyle","WebkitFilter","FeatureDocumentModal","_featureDoc$sections","filterType","setFilterType","featureDoc","filterColors","function","propertyTypeColors","sectionTypeIcons","itemTypeIcons","radio","hasFilteredProperties","_item$properties","prop","hasFilteredItems","itemIndex","itemKey","isItemExpanded","filteredProps","toggleItem","propIndex","FeatureDescriptionPanel","selectedBadge","onSelectBadge","onUpdateBadge","onDeleteBadge","onAddBadge","isAddingBadge","setIsAddingBadge","badgeEditMode","setBadgeEditMode","screenPath","allBadges","onImportBadges","onChangeNickname","onLoadTemplate","currentScreenKey","hasTemplate","onExtractFeatures","onAddComment","onDeleteComment","isAdmin","fileInputRef","showBackupMenu","setShowBackupMenu","newComment","setNewComment","expandedComments","setExpandedComments","commentInputs","setCommentInputs","editingBadgeId","setEditingBadgeId","editFormData","setEditFormData","lastSaved","setLastSaved","autoBackupEnabled","setAutoBackupEnabled","lastAutoBackup","setLastAutoBackup","showVersionModal","setShowVersionModal","showVersionHistory","setShowVersionHistory","versionChangeLog","setVersionChangeLog","showFeatureDocModal","setShowFeatureDocModal","screenVersions","setScreenVersions","upgradeVersion","major","minor","patch","newVersion","updatedVersionData","previousVersion","changeLog","updatedBy","badgeCount","updatedVersions","prevBadgesRef","autoSaveTimeoutRef","prevBadges","prevCount","changeDetail","autoHistoryEntry","isAutoSave","lastModified","handleDeleteWithPassword","badgeId","startEditing","saveEditing","_editFormData$label","cancelEditing","performAutoBackup","exportData","exportedAt","autoBackup","totalBadges","flat","newBadge","setNewBadge","badgeColors","generateDescription","resetNewBadge","onload","readAsText","manualSave","exportBadges","_fileInputRef$current","_badgeColors$find","_editFormData$label2","_badgeColors$find2","comments","comment","hour","minute","FeatureBadgeOverlay","onUpdatePosition","onAnalyzeAndUpdate","onEditBadge","dragStart","setDragStart","hoveredElement","setHoveredElement","badgeColorHex","targetElement","outlineOffset","clickedBadge","analysis","_element$tagName","_element$getAttribute","_element$className","_element$className2","_element$className3","_element$className4","_element$innerText","_element$innerText$tr","_element$getAttribute2","_element$getAttribute3","innerText","reactKey","currentFiber","onClickStr","navigateMatch","targetTitle","setViewMatch","targetView","menuMatch","modalMatch","analyzeElement","movedBadge","userSelect","absoluteX","absoluteY","ModalFeatureBadgeOverlay","modalId","overlayRef","OrderCard","getQuoteStatusBadge","quoteStatus","modificationCount","productionOrdered","FormField","hint","errorMessage","useFormValidation","validationRules","setErrors","touched","setTouched","validateField","patternMessage","validate","customError","validateForm","newErrors","allTouched","handleBlur","clearFieldError","getFieldError","hasError","resetErrors","getInputClassName","TextInput","Select","InfoRow","PageHeader","leftActions","bizNo","bizType","bizItem","creditNote","requirePaymentBeforeShip","generateProductionSpec","openW","openH","prodWidth","prodHeight","drawingNo","guideRailSpec","motorBracket","finish","generateBomData","guideRails","lengths","cases","mainSpec","sideCover","topCover","extension","quoteProducts","formulaCount","formulaCategories","initialQuoteFormulas","productId","categoryId","variable","resultType","lookupTable","qtyFormula","motor","steel","shaftInchTableSteel","w1Min","w1Max","kMin","kMax","shaftInchTableScreen","gMin","gMax","sMin","sMax","combo","bottomBarTable","bMin","bMax","hwanbongQtyTable","squarePipeTableScreen","lMin","lMax","mat3000","mat6000","squarePipeTableSteel","itemPriceMaster","integratedCustomerMaster","currentBalance","managerTel","integratedSiteMaster","integratedQuoteMaster","voltages","wireTypes","ctTypes","gtTypes","calculateQuote","calcResult","statusList","qYY","qMM","qDD","qSeq","guideType","totalWithVat","testQuote1Date","developedParts","testQuote2Date","integratedOrderMaster","setDate","oYY","oMM","oDD","oSeq","isFoldProduct","openSizeW","openSizeH","approvedBy","approvedAt","itemProcessMapping","processSeq","isService","integratedProductionOrderMaster","hasFoldProcess","workOrdersGenerated","integratedWorkOrderMaster","orderIdx","woDate","woDocType","wYY","wMM","wDD","wSeq","integratedQualityMaster","iYY","iMM","iDD","iSeq","inspPrefix","integratedShipmentMaster","_relatedWorkOrders$","_relatedInspections$","_relatedWorkOrders$2","sYY","sMM","sDD","sSeq","relatedWorkOrders","relatedWorkOrderNo","relatedInspections","relatedInspectionNo","departureTime","arrivalTime","integratedSalesMaster","salesType","invoiceNo","invoiceStatus","paidAmount","unpaidAmount","integratedCollectionMaster","collections","sale","collectDate","collectionNo","collectionDate","collectionType","salesId","billNo","billDueDate","integratedDataSummary","qualityInspections","totalQuoteAmount","totalSalesAmount","totalCollectedAmount","IntegratedTestDashboard","selectedCustomer","setSelectedCustomer","selectedQuote","setSelectedQuote","formatCurrency","Intl","NumberFormat","getStatusColor","FullIntegrationTestTab","_result$summary$total","_result$summary$remai","_result$summary$workO","WorkflowTestTab","_integratedCustomerMa","_integratedSiteMaster","_testScenarios$find","_testScenarios$find2","_steps","_workflowData$documen","_workflowData$documen2","_workflowData$documen3","_workflowData$documen4","_workflowData$documen5","_workflowData$documen6","_workflowData$documen7","_workflowData$approva","_workflowData$approva2","_workflowData$approva3","_workflowData$approva4","_workflowData$approva5","_workflowData$approva6","_workflowData$approva7","_workflowData$approva8","_workflowData$approva9","_workflowData$fullDoc","_workflowData$fullDoc2","_workflowData$fullDoc3","_workflowData$fullDoc4","_workflowData$fullDoc5","_workflowData$fullDoc6","setCurrentStep","isProcessing","setIsProcessing","workflowData","setWorkflowData","PC","QTY","GT","WIRE","CT","symbol","calculatedResult","customerSites","selectedSite","handleInputChange","openSize","resetWorkflow","_integratedCustomerMa2","_integratedSiteMaster2","isAutoRunning","setIsAutoRunning","selectedScenario","setSelectedScenario","testScenarios","isE2E","allTestResults","setAllTestResults","testSummary","setTestSummary","runSingleScenario","async","seqCounters","cancelReason","cancelDate","expireDate","expireReason","_prev$calculatedResul","_prev$calculatedResul2","_prev$calculatedResul3","_prev$calculatedResul4","_prev$calculatedResul5","_prev$calculatedResul6","isRushOrder","_prev$quote","_prev$quote2","_prev$quote3","_prev$quote4","_prev$quote5","_prev$quote6","_prev$quote7","_prev$quote8","_prev$quote9","_prev$quote0","_prev$quote1","generateOrderNo","modelType","productionLot","isRush","rushReason","newWorkOrders","_prev$order","_prev$order2","_prev$order3","_prev$order4","processInspLot","processInspectionLot","processStep","producedQty","sizeChangeLog","originalSize","newSize","changeDate","reason","qualityResult","_prev$order5","completedWO","isFinalInspection","inspLot","inspectionLot","defectType","defectDescription","conditionalNote","productLot","finishedProductLot","installationLot","_prev$workOrders$","reworkOrder","reworkNo","originalWorkOrderNo","reworkProcess","reinspectionDate","reinspectionNote","isPartialShipment","_prev$order6","_prev$order7","_prev$order8","_prev$order9","_prev$order0","_prev$order1","remainingQty","partialReason","_prev$order10","_prev$order11","shipment2","isPaymentDelay","_prev$order12","_prev$order13","_prev$order14","_prev$order15","_prev$order16","_prev$order17","_prev$order18","_prev$order19","_prev$order20","paymentStatus","paymentDueDate","paymentMethod","_prev$sales","_prev$sales2","_prev$order21","_prev$order22","_prev$order23","delayDays","registeredDate","registeredBy","yymmdd","testStatus","codeReferences","codeGroups","codes","accountingSales","testCases","after","accountingPurchase","purchaseNo","accountingCashbook","accountingCollection","overdueDays","groupBy","accountingCost","_prev$calculatedResul7","documentTest","docTypeCode","toLocaleTimeString","copies","includeLogo","includeStamp","_prev$order24","_prev$order25","_prev$order26","linkedDoc","_prev$order27","_prev$inputs","_prev$order28","_prev$order29","overallResult","_prev$order30","attachment","_prev$order31","_prev$order32","_prev$order33","stampType","representative","issueDate","_prev$order34","_prev$order35","_prev$order36","signType","ntsSendResult","approvalNo","taxType","issueType","buyer","ntsApprovalNo","approvalTest","testType","actor","nextApprover","notifyType","beforeStatus","afterStatus","approvalFlow","totalDuration","sentAt","rejectionHistory","rejectedBy","rejectReason","rejectDate","modifiedFields","absentee","delegate","originalApprover","logged","delegateInfo","delegateReason","delegatePeriod","delegateRegisteredAt","urgentLevel","originalSteps","reducedSteps","responseTime","totalTime","urgentInfo","isUrgent","urgentReason","originalApprovalLine","reducedApprovalLine","skippedSteps","notificationChannels","totalProcessTime","normalProcessTime","timeSaved","fullDocumentFlow","testPhases","approvalTime","approvalCount","totalApprovals","totalDocuments","totalApprovalTime","avgApprovalTime","approvalRoles","_prev$inputs2","_prev$inputs3","_prev$inputs4","_prev$inputs5","_prev$inputs6","_prev$inputs7","mvpCoreTest","testSteps","woCount","resultCount","inspCount","shipQty","mvpMaterialInput","available","mvpWorkResult","mvpWorkerScreen","workerId","assignedWO","currentQty","nextWO","workerInfo","assignedProcess","todayTasks","completedTasks","mvpProductionBoard","totalWO","bottleneck","refreshRate","lastUpdate","delayedOrders","warningOrders","activeWorkers","idleWorkers","planned","achievement","todayOrders","inProgressWO","delayedWO","processStatus","alerts","mvpStockStatus","belowSafety","warningItems","warehouses","mainStock","subStock","movements","stockData","mvpInbound","expectedItems","inspResult","warehouse","updatedItems","totalIncreased","totalInbound","inboundData","inboundDate","_prev$inputs8","mvpFullFlow","mvpMenus","orderInfo","passRate","coverage","detailedResults","passCount","scenarioStartTime","scenarioId","totalScenarios","completedAt","newQuote","newShipment","newSales","newCollection","cidx","rejection","_rejection$modifiedFi","notif","E2ETestTab","e2eData","setE2eData","setSummary","setValidation","selectedOrder","setSelectedOrder","OverviewView","issue","pqcCount","DetailView","_flow$order","_flow$order2","_flow$order3","_flow$order4","_flow$order5","_flow$quote","_flow$quote2","_flow$productionOrder","_flow$productionOrder2","_flow$productionOrder3","getOrderFlow","processMasterConfigModule","workflowWOs","IssuesView","SummaryTab","quoteStats","견적중","제출완료","수주전환","취소","만료","orderStats","수주확정","생산지시","생산중","생산완료","생산지시완료","workOrderStats","대기","진행중","완료","qualityStats","integratedQualityInspection","합격","조건부합격","불합격","collectionStats","uncollected","collectionRate","creditStats","CustomersTab","siteCount","QuotesTab","quoteFilter","setQuoteFilter","quoteSearch","setQuoteSearch","quotePage","setQuotePage","filteredQuotes","matchStatus","paginatedQuotes","handleFilterChange","handleBulkDelete","Edit2","pageNum","OrdersTab","ProductionTab","displayData","QualityTab","_insp$inspectionItems","ShipmentTab","AccountingTab","FlowTab","_integratedOrderMaste","selectedOrderId","setSelectedOrderId","relatedQuote","relatedCustomer","relatedSite","relatedQuality","relatedShipment","relatedSales","relatedCollection","initialPriceClassifications","initialPriceFormulas","classificationId","classificationName","qtyCalc","formulaEngine","lookupTables","subShaftTable","setVariable","getVariable","evaluateCondition","evalCondition","regex","evaluate","evalFormula","evaluateLookup","_motorCapacityTable$p","_table","getShaftInch","_bracketTable$product","motorKG","inchTable","primaryMat","barQty","elbarQty","reinforceQty","qty3000","qty6000","calculateQuoteItems","formulas","priceList","lookupParams","motorResult","MOTOR_KG","SHAFT_INCH","bracketResult","bracketCode","angleCode","grResult","caseResult","bottomResult","hwanbongQty","sqpResult","TOP_COVER_QTY","CASE_SMOKE_QTY","JOINTBAR_QTY","WEIGHT_FLAT_QTY","SUB_SHAFT_LEN","MOTOR_CODE","CTL_CODE","outputItems","addItem","priceItem","itemQty","smokeQty","workSteps","assignedWorkers","sampleDetailedOrderData","certNo","receiverManager","orderManager","motorSpec","motors220V","motors380V","brackets","heatSinks","controllers","bomData","initialOrders","orderType","parentLotNo","scheduledShipDate","accountingConfirmedBy","accountingConfirmedAt","invoiceIssued","invoiceIssuedAt","taxInvoiceIssued","taxInvoiceIssuedAt","productionSpec","splitOrder","splitType","itemIds","documentHistory","sentBy","sentMethod","changeHistory","shipmentHold","shipmentHoldReason","approvalRequestedAt","productionHold","productionHoldReason","depositRequired","qualityIssue","qualityIssueNote","accountingApproval","productionProgress","controller","hasQualityIssue","qualityIssueDescription","initialWorkOrders","workPriority","workSequence","instruction","movedToShippingArea","movedToShippingAreaAt","materialLotNo","drafter","startedAt","reworkStartedAt","failReason","reworkRequired","reportedBy","reportedAt","resolvedBy","resolvedAt","resolution","actionTaken","hasIssue","expectedCompletionDate","resolved","bomCalculated","guideRail","cornerBracket","ncrNo","ncrInfo","rootCause","correctiveAction","preventiveAction","shippedAt","initialWorkResults","productionQty","inspectionCompleted","packingCompleted","initialShipments","releaseNo","paymentConfirmed","shipmentPriority","isSplitShipment","dispatchType","logisticsCompany","vehicleType","driverPhone","scheduledArrival","confirmedArrival","shippingCost","loadingCompleted","loadingCompletedAt","loadingWorker","arrivedAtLoadingArea","loadingChecked","accessoryLotNo","beforeValue","afterValue","canceledAt","canceledBy","initialProductInspections","setLotNo","scheduledDate","scheduledTime","completedItems","passedItems","failedItems","orderSpec","installedSpec","specMatch","inspectedAt","scheduleHistory","ncrStatus","ncrNote","initialCustomers","zipCode","addressDetail","contacts","balance","receivables","overdue","registeredAt","modifiedAt","modifiedBy","initialSites","siteContact","installManager","installManagerPhone","installScheduledDate","installCompletedDate","by","sampleQuotesData","relatedOrders","wingIndex","inspectionFee","autoCalcParams","autoCalcResult","MAIN_SHAFT_LEN","GR_MAT_LEN","CASE_MAT_LEN","HJ_MAT_LEN","HWANBONG_QTY","SQP3000_QTY","SQP6000_QTY","CreditGradeBadge","grade","showLabel","ProcessList","processList","setProcessList","filtered","handleView","_process$autoClassify","autoClassifyRules","requiredWorkers","handleToggleActive","ProcessForm","_process$isActive","_process$workSteps","workSheetTemplates","ruleConditionOperators","ruleTypes","equipmentInfo","showRuleModal","setShowRuleModal","editingRule","setEditingRule","ruleForm","setRuleForm","registrationMethod","matchedItems","isSearching","setIsSearching","sampleItemsData","getAssignedItemIds","assignedIds","savedItems","assignedItems","selectedItemIds","setSelectedItemIds","showAssignedWarning","setShowAssignedWarning","individualSearchTerm","setIndividualSearchTerm","individualTypeFilter","setIndividualTypeFilter","searchMatchingItems","searchValue","allMatchedItems","endsWith","isAssigned","excludedCount","openRuleModal","_rule$savedItems","toggleItemSelection","itemId","newSet","toggleSelectAll","getFilteredItemsForIndividual","searchLower","toggleSelectAllIndividual","handleSubmit","tpl","_ruleTypes$find","_ruleConditionOperato","savedCount","matchedCount","op","removeRule","ruleId","saveRule","selectedItems","ruleData","DocumentTemplatePreviewModal","_sampleData$inspectio","docCode","samples","managerName","finishType","finishSpec","requestCompany","requestManager","requestPhone","deliveryPhone","itemSpec","judgment","notes","finalJudgment","defectContent","getSampleData","catIdx","_item$standard","_item$measurements","blockInfo","_documentTemplateConf2","_documentTemplateConf3","_documentTemplateConf4","_documentTemplateConf5","_documentTemplateConf6","approvals","footers","getBlockInfo","WorkLogDataPreviewModal","_workOrder$materials","_workOrder$materialIn","getProcessName","workItems","renderBendingSvg","getProcessColor","instructionDate","itemStatus","guideRailQty","sideGuideQty","bottomSusQty","bottomEgiQty","steelSusQty","steelEgiQty","caseQty","caseWidth","totalSus","totalEgi","fabricType","fabricWidth","cuttingQty","sewingQty","endlockQty","packingQty","coilType","coilColor","coilCuttingQty","mimiQty","ProcessDetail","_process$autoClassify2","showTemplatePreview","setShowTemplatePreview","workSheetNameMap","equals","initialInspectionStandards","targetType","targetCode","targetName","critical","samplingQty","responsibleTeam","InspectionStandardList","standards","showPanel","setShowPanel","panelMode","setPanelMode","selectedStandard","setSelectedStandard","localStandards","setLocalStandards","filteredStandards","matchesTab","handleOpenDetail","InspectionStandardPanel","onEdit","ProductionMasterManagement","MasterConfigurationManager","configId","OutboundMasterManagement","NumberRuleManagement","_dateFormats$find","showHelp","setShowHelp","numberRules","setNumberRules","numberTargetTemplates","numberDateFormats","generatePreview","filteredRules","activeCount","docTypeCount","lotTypeCount","openCreateModal","_resetCycleOptions$fi","toggleStatus","handleTargetChange","commonInputTypeOptions","commonConditionTypeOptions","itemMasterConfig","_config$ksStandards","_config$samplingPlans","_config$tabs$find2","_categories$find","setEntityTypes","setCategories","setMasterFields","setMasterSections","setPageTemplates","showPageModal","setShowPageModal","showSectionModal","setShowSectionModal","showFieldModal","setShowFieldModal","showCategoryModal","setShowCategoryModal","selectedPage","setSelectedPage","selectedSection","setSelectedSection","selectedField","setSelectedField","pageForm","setPageForm","sectionForm","setSectionForm","useMaster","fieldForm","setFieldForm","categoryForm","setCategoryForm","getIcon","HeaderIcon","buildCategoryTree","categoryTree","handleAddSection","handleAddField","handleAddCategory","renderCategoryTree","_config$tabs$find","depth","_node$children","_node$children2","TabIcon","handleAddPage","_entityTypes$","it","updatedPage","handleDeleteSection","_commonInputTypeOptio","handleDeleteField","updatedSections","_entityTypes$find","_commonInputTypeOptio2","ks","plan","handleSavePage","newPage","masterSectionId","handleSaveSection","newSection","_commonInputTypeOptio3","masterFieldId","handleSaveField","newField","handleSaveCategory","newCategory","_item$itemName$match","ItemList","onAddItem","onUpdateItem","onDeleteItem","localItems","setLocalItems","showDeleteModal","setShowDeleteModal","typeCount","ItemForm","isEdit","lotPrefix","certNumber","certStartDate","certEndDate","needsBom","finishing","drawingInputType","drawingFile","setBomItems","showDrawingEditor","setShowDrawingEditor","drawingDetails","setDrawingDetails","updateDrawingDetailRow","bendingPartItemNames","bendingPartTypes","bendingSizeLengths","units","isProductType","isPartType","isSubMaterialType","isRawMaterialType","isConsumableType","isAssemblyPart","isBendingPart","isPurchasedPart","subMaterialItems","rawMaterialItems","addBomItem","removeBomItem","updateBomItem","itemData","onBlur","_bendingPartItemNames","_bendingPartTypes$fin","_bendingSizeLengths$f","getSubMaterialSpecs","getSubMaterialCode","getRawMaterialSpecs","getRawMaterialCode","getConsumableCode","cap","getPurchasedPartCode","addDrawingDetailRow","newRow","elongation","elongationCalc","shadow","removeDrawingDetailRow","getAutoItemCode","bomItem","ItemDetail","_ref71","currentDate","ItemMasterManagement","_ref72","MaterialMasterManagement","_ref73","CodeRuleManagement","_ref74","_dateFormats$find2","codeRules","setCodeRules","codeType","useDate","useProcess","filteredCodeRules","structure","getStructureDescription","StockList","_ref75","selectedItemType","setSelectedItemType","filteredByType","handleTypeChange","lowStockItems","noStockItems","normalItems","getTypeStats","getStockStatus","handleRowClick","lotCount","oldestLotDays","StockAdjustment","_ref76","showDetailModal","setShowDetailModal","selectedAdjustment","setSelectedAdjustment","showRegisterModal","setShowRegisterModal","adjustmentTypes","adjustments","setAdjustments","adjustNo","adjustType","adjustDate","beforeQty","adjustQty","afterQty","registerForm","setRegisterForm","fromLocation","toLocation","getTypeStyle","getStatusStyle","filteredAdjustments","adj","matchType","thisMonth","getTypeName","handleOpenRegister","handleViewDetail","handleSelectItem","handleRegister","newAdjustment","InboundManagement","_ref77","onReceive","onCreateInspection","selectedPO","setSelectedPO","showReceiveModal","setShowReceiveModal","showInboundSlip","setShowInboundSlip","slipData","setSlipData","receiveForm","setReceiveForm","pendingList","receivedList","todayReceived","getStatusCount","filteredList","received","_po$poNo","_po$materialCode","_po$materialName","_po$vendor","handleSelectOne","handleOpenReceive","handleReceive","receiveData","handleDeleteConfirm","InboundDetail","_ref78","onUpdateStatus","inspectionResult","InspectionRegisterForm","_ref79","selectedTarget","setSelectedTarget","templateType","setTemplateType","inspectionForm","setInspectionForm","loadInspectionTemplate","_config$inspectionIte","_config$inspectionIte2","measurement","measurementValue","targetList","hasFailure","targetNo","newItems","StockDetail","_ref81","lots","generateLots","remainingStock","daysAgo","suppliers","locations","daysInStock","IQCRegisterForm","_ref82","initialData","generateLotNo","vendorName","PQCRegisterForm","_ref83","selectedWO","setSelectedWO","pendingWOs","FQCRegisterForm","_ref84","IncomingInspectionList","_ref85","_selectedForApproval$","_selectedForApproval$2","_selectedForApproval$3","_selectedForApproval$4","onApprovalRequest","onStockRegister","showCreateModal","setShowCreateModal","showApprovalModal","setShowApprovalModal","selectedForApproval","setSelectedForApproval","completedInspections","defaultInspectionItems","sampleValues","thickness","handleOpenInspection","_item$sampleValues","sample","handleSaveInspection","specification","nextAction","_document$getElementB","approverName","handleApproval","updatedApprovalLine","allApproved","updatedInspection","_inspection$ncrInfo","ProcessInspectionList","_ref86","_selectedWorkOrder$it4","_selectedWorkOrder$it5","_selectedWorkOrder$it6","_selectedWorkOrder$it7","_selectedWorkOrder$it8","_selectedForApproval$5","onProcessPermit","inspectionRequests","selectedWorkOrder","setSelectedWorkOrder","pendingWorkOrders","getCurrentApprovalStep","_wo$items4","_wo$items4$","_wo$items5","_wo$items5$","_wo$items","_wo$items$","_wo$items2","_wo$items2$","_wo$items3","_wo$items3$","average","processStepName","numVals","avg","_processSteps$find","_selectedWorkOrder$it","_selectedWorkOrder$it2","_selectedWorkOrder$it3","requestedFrom","requestedAt","defectiveQty","approverInput","_pendingRoles$","permittedAt","DefectManagement","_ref88","_selectedForApproval$6","onDispose","onRework","onVendorClaim","selectedDefect","setSelectedDefect","showActionModal","setShowActionModal","actionForm","setActionForm","allDefects","_insp$ncrInfo","_insp$ncrInfo2","_insp$ncrInfo3","_insp$inspectionItems2","_insp$ncrInfo4","_insp$ncrInfo5","_insp$ncrInfo6","defectNo","sourceType","sourceLot","defectReason","detectedDate","pendingDefects","processedDefects","reworkDefects","disposeDefects","filteredDefects","aId","handleOpenAction","option","handleProcessDefect","processedData","rework","dispose","processedDate","processNote","handleNCRApproval","_defect$approvalLine","_updatedApprovalLine$","_pendingRoles$2","claimDate","DeliveryStatusList","_ref89","selectedShipment","setSelectedShipment","inProgressShipments","completedShipments","todayShipments","getStatusProgress","driver","vehicle","handleUpdateDeliveryStatus","_ref95","isReadOnly","handleRemoveItem","SalesAccountManagement","_ref96","dateRange","setDateRange","selectedSale","setSelectedSale","salesData","setSalesData","saleNo","vat","saleDate","accountingApprovalRequired","accountingApprovalStatus","accountingApprovalDate","accountingApprover","paymentConfirmRequired","paymentConfirmStatus","paymentConfirmDate","paymentConfirmAmount","totalSales","collectedAmount","outstandingAmount","invoicedCount","salesSelectedIds","handleSalesSelect","handleSalesSelectAll","isSalesAllSelected","hasSalesSelection","isSalesMultiSelect","isSalesSelected","approvalFiltered","approvalSelectedIds","handleApprovalSelect","handleApprovalSelectAll","isApprovalAllSelected","hasApprovalSelection","isApprovalMultiSelect","isApprovalSelected","paymentFiltered","paymentSelectedIds","handlePaymentSelect","handlePaymentSelectAll","isPaymentAllSelected","hasPaymentSelection","isPaymentMultiSelect","isPaymentSelected","handleViewSale","handleAccountingApproval","pendingApprovalCount","pendingPaymentCount","handleCreateSale","handleIssueInvoice","handlePaymentConfirm","percent","outstanding","processPaymentConfirm","isFullPayment","InvoiceManagement","selectedInvoice","setSelectedInvoice","invoices","setInvoices","reportDate","issuedTotal","receivedTotal","pendingCount","transmittedCount","invoice","handleSendInvoice","CollectionManagement","filterStatus","setFilterStatus","setCollections","saleAmount","remainAmount","bankAccount","totalSaleAmount","totalCollected","totalRemain","handleRegisterCollection","OutstandingManagement","filterOverdue","setFilterOverdue","outstandingData","lastCollectionDate","totalOutstanding","overdueAmount","overdueCount","matchOverdue","outstandingSelectedIds","handleOutstandingSelect","handleOutstandingSelectAll","isOutstandingAllSelected","hasOutstandingSelection","isOutstandingMultiSelect","isOutstandingSelected","accCustomerData","usedCredit","receivableTotal","receivableOverdue","isCreditWarning","AccCustomerList","_ref97","warningCount","creditCustomers","avgCreditUsage","AccCustomerDetail","_ref98","availableCredit","creditUsageRate","AccCustomerRegister","_ref99","handleChange","TransactionStatementIssue","_ref101","_documentTemplateConf7","_templateConfig$block","onUpdateShipment","setSelectedItems","templateConfig","statements","setStatements","statementNo","statementDate","statementStatus","sentDate","statusFilters","handleIssueStatement","handleSendStatement","handleBulkIssue","TaxInvoiceIssue","_ref102","ntsStatus","Receipt","handleBulkSendNTS","pendingNTS","handleSendNTS","PurchaseRequestManagement","_ref103","selectedRequest","setSelectedRequest","requests","setRequests","requester","approvedDate","purchaseType","processApproval","request","isAccountingStep","needsCeoApproval","newApprovalFlow","newCurrentStep","newApprovedDate","newApprover","requestSelectedIds","handleRequestSelect","handleRequestSelectAll","isRequestAllSelected","hasRequestSelection","isRequestMultiSelect","isRequestSelected","accountingPendingCount","ceoPendingCount","approvedAmount","req","ExpenseApprovalManagement","_ref104","selectedExpense","setSelectedExpense","expenses","setExpenses","expenseNo","purchaseRequestNo","expenseDate","vendorAccount","scheduledPaymentDate","actualPaymentDate","processExpenseApproval","expense","expenseSelectedIds","handleExpenseSelect","handleExpenseSelectAll","isExpenseAllSelected","hasExpenseSelection","isExpenseMultiSelect","isExpenseSelected","accountingPendingAmount","ceoPendingAmount","processPayment","CashbookManagement","_ref105","mainTab","setMainTab","typeFilter","setTypeFilter","selectedEntry","setSelectedEntry","selectedAccount","setSelectedAccount","accounts","cards","cardNo","used","setEntries","entryNo","entryDate","counterpart","accountId","registrant","relatedNo","cardEntries","setCardEntries","cardId","merchant","totalIncome","totalExpense","totalBalance","totalCardUsed","categoryStats","income","dailyStats","monthlyStats","matchAccount","ledgerSelectedIds","handleLedgerSelect","handleLedgerSelectAll","isLedgerAllSelected","hasLedgerSelection","isLedgerMultiSelect","isLedgerSelected","cardSelectedIds","handleCardSelect","handleCardSelectAll","isCardAllSelected","hasCardSelection","isCardMultiSelect","isCardSelected","_cards$find","_ref106","_ref107","_ref108","_document$querySelect","_accounts$find","newEntry","handleRegisterEntry","CollectionRegister","_ref109","showRegisterPanel","setShowRegisterPanel","showReminderModal","setShowReminderModal","selectedForReminder","setSelectedForReminder","reminderHistory","setReminderHistory","sender","pendingFiltered","pendingSelectedIds","handlePendingSelect","handlePendingSelectAll","isPendingAllSelected","hasPendingSelection","isPendingMultiSelect","isPendingSelected","historyFiltered","collectionHistory","historySelectedIds","handleHistorySelect","handleHistorySelectAll","isHistoryAllSelected","hasHistorySelection","isHistoryMultiSelect","isHistorySelected","setCollectionHistory","registrar","customerSummary","maxOverdueDays","overdueItems","newCollectedAmount","newRemainAmount","isFullyPaid","_document$querySelect2","newReminders","handleSendReminder","BillManagement","bills","setBills","bill","CostAnalysisManagement","_tabs$find","costData","materialCost","laborCost","outsourcingCost","overheadCost","totalCost","profit","profitRate","totalSale","totalProfit","avgProfitRate","UserManagement","selectedUser","setSelectedUser","setUsers","userId","userName","lastLogin","inactiveCount","deptGroups","handleCreateUser","handleEditUser","handleToggleStatus","RoleManagement","selectedRole","setSelectedRole","userCount","menus","SystemSettings","setSettings","quotePrefix","sessionTimeout","maxLoginAttempts","passwordExpireDays","autoLogout","emailNotification","smsNotification","overdueAlert","stockAlert","CustomerList","_ref110","hideAccountingInfo","customerList","setCustomerList","_c$ceo","_c$businessNo","totalReceivables","_c$receivables","overdueReceivables","_c$receivables2","gradeACount","_customer$receivables","_customer$receivables2","CustomerDetail","_ref111","_customer$receivables3","_customer$receivables4","_customer$receivables5","CustomerRegister","_ref112","firstErrorField","errorElement","scrollIntoView","SiteList","_ref115","siteList","setSiteList","SiteDetail","_ref118","_site$orders","_site$shipments","_site$history","SiteRegister","_ref119","generateSiteCode","QuoteFormulaManagement","_ref120","_quoteProducts$find","_categories$find2","selectedProduct","setSelectedProduct","showProductDropdown","setShowProductDropdown","editMode","setEditMode","showFormulaModal","setShowFormulaModal","showClassificationModal","setShowClassificationModal","showPriceFormulaModal","setShowPriceFormulaModal","setFormulas","classifications","setClassifications","priceFormulas","setPriceFormulas","editingItem","setEditingItem","formulaForm","setFormulaForm","classificationForm","setClassificationForm","selectedCategories","priceFormulaForm","setPriceFormulaForm","filteredFormulas","getCategoryFormulaCount","handleProductSelect","handleDeleteFormula","classification","AutoQuoteCalculator","handleAddFormula","newFormula","handleAddClassification","newClassification","handleAddPriceFormula","newPriceFormula","_ref121","setInputs","setCalculatedResult","showSummary","setShowSummary","groupByProcess","setGroupByProcess","showQuoteModal","setShowQuoteModal","groupedItems","categorySummary","getCategorySummary","handleCalculate","_ref122","QuoteList","_ref131","onDeleteQuotes","editing","thisMonthQuotes","thisMonthAmount","progressAmount","thisWeekNew","conversionRate","rowNumber","calculatedItems","QuoteCreate","_ref137","setCalculatedItems","showCalculatedItems","setShowCalculatedItems","guideTypes","motorPowers","priceTable","pricePerMm","pricePerSqm","handleItemChange","handleDuplicateItem","groupedByProcess","processColors","_items$","_items$2","handleCustomerSelect","_products$item$catego","_item$outputItems","allCalculatedItems","totalProductAmount","calculatedItemsDetails","W1_스크린","H1_스크린","W1_철재","H1_철재","AREA","GR_L","GR_BASE","CASE_L","SHAFT_L","SHAFT_D","BF_L","screenPrice","grPrice","gbPrice","csPrice","sideCoverQty","bfPrice","bbPrice","shPrice","diameter","sbPrice","itemTotal","inspectionAmount","calculatedVariables","_ref138","processItems","OrderList","_ref139","onCreateWorkOrders","onDeleteOrders","onCancelOrder","showWOCreateModal","setShowWOCreateModal","selectedOrderForWO","setSelectedOrderForWO","showCancelModal","setShowCancelModal","cancelTargetOrder","setCancelTargetOrder","setCancelReason","cancelReasonDetail","setCancelReasonDetail","getWorkOrdersByOrder","canCancelOrder","registered","confirmed","cancelled","productionPending","_o$splits2","shipmentPending","getShipmentPercentByOrder","needSplit","getProductionStatus","instructed","_order$splits","shipmentInfo","getShipmentInfoByOrder","isOnlySelected","handleCancelClick","handleConfirmCreateWO","handleCancelConfirm","reasonDetail","cancelledAt","cancelledBy","OrderDetail","_ref141","_order$splits3","_order$createdAt","_order$createdAt2","_order$items4$","_order$createdAt3","_order$items5","_productionCompleteIn","_productionCompleteIn2","onUpdate","onCreateWorkOrder","onCreateProductionOrder","activeDocTab","setActiveDocTab","showSplitModal","setShowSplitModal","showProductionModal","setShowProductionModal","showPOCreateModal","setShowPOCreateModal","showDocumentModal","setShowDocumentModal","setDocumentType","showProductionSheet","setShowProductionSheet","sheetTitle","setSheetTitle","showProductionCompleteDialog","setShowProductionCompleteDialog","productionCompleteInfo","setProductionCompleteInfo","splitForm","setSplitForm","relatedPO","isPrePayment","isPaid","getUnassignedItems","finalTotal","handleDocTabClick","tabId","shippingDate","deliveryRequestDate","deliveryZipcode","deliveryAddressBase","deliveryNote","handleAddSplit","_order$splits2","newSplitOrder","newSplit","ProductionOrderSheet","ProductionOrderModal","onConfirm","handleFullProductionOrder","inferCategory","hasScreen","hasSlat","getStepStatus","seqNum","itemsByProcess","_ref142","processAssignee","getDefaultAssignee","bendingItems","_bomData$guideRails2","_bomData$cases2","_bomData$bottomFinish2","screenWoNo","screenAssignee2","slatItems","slatWoNo","slatAssignee2","bendWoNo","_bomData$guideRails$s","_bomData$cases$sideCo","_bomData$cases$topCov","rail","_rail$lengths2","len","caseItem","_finish$lengths2","bendAssignee2","productionOrderedAt","productionOrderedBy","onPreview","_item$spec","OrderCreate","_ref144","_relatedOrders$2","_selectedQuote$totalA","fromQuote","additionalItems","isAdditional","showQuotePanel","setShowQuotePanel","directItems","setDirectItems","_fromQuote$items","initialDueDate","initialAmount","availableQuotes","showItemModal","setShowItemModal","itemForm","setItemForm","calculateTotal","canSubmit","shippingDateUndecided","setShippingDateUndecided","deliveryDateUndecided","setDeliveryDateUndecided","setFreightCost","setDeliveryZipcode","setDeliveryAddressBase","setDeliveryAddressDetail","_relatedOrders$","_initialCustomers$fin","orderItems","generateAdditionalOrderNo","baseOrderNo","existingAdditionals","suffix","generateMotorSpec","parentOrderNo","_item$amount4","handleClearQuote","_item$productionSpec4","_item$productionSpec5","handleSaveItem","_quote$items4","_quote$items3","handleQuoteSelect","OrderEdit","_ref145","_formData$deliveryAdd","isProductionStarted","canEditItems","salesperson","_item$productionSpec6","_item$productionSpec7","_item$amount5","handleEditItem","ProductTraceability","_ref146","searchType","setSearchType","setSearchValue","traceResult","setTraceResult","selectedNode","setSelectedNode","sampleTraceData","productionDate","iqcResult","issuedBy","machine","sampleQty","palletNo","packedBy","qualityRecords","handleSearch","getStageIcon","onKeyPress","_ref147","qr","ProductionStatusBoard","_ref148","selectedFactory","setSelectedFactory","workerStats","working","urgentWorks","delayedWorks","currentStats","urgent","delayed","getFactoryStats","_ref149","WorkerTaskView","_ref150","onReportIssue","onUpdateItemStatus","onPrintWorkLog","onMaterialInput","activeProcesses","selectedWorker","setSelectedWorker","selectedTeam","setSelectedTeam","sortOption","setSortOption","allAssignedWorkers","teamProcessMap","showCompleteToast","setShowCompleteToast","showIssueModal","setShowIssueModal","selectedTask","setSelectedTask","quickInputTask","setQuickInputTask","quickResult","setQuickResult","expandedTask","setExpandedTask","stepProgress","setStepProgress","stepNameMapping","handleExpandTask","taskId","task","getTaskSteps","newProgress","statusKey","stepData","initializeStepProgressFromStatus","workLogTask","setWorkLogTask","showWorkLogDataPreview","setShowWorkLogDataPreview","workLogDataPreviewTask","setWorkLogDataPreviewTask","myTasks","isAssignedToWorker","isInTeam","aPriority","bPriority","toDateString","urgentCount","inProgressCount","needsMaterialInput","isStepComplete","_stepProgress$taskId","isAllStepsComplete","_task$assignedWorkers","_task$assignedWorkers2","isWorking","taskNo","handleQuickComplete","getStepCompletedCount","_stepProgress$taskId2","isComplete","prevStep","isPrevComplete","requestInspection","_stepProgress$task$id","_stepProgress$task$id2","isChecked","itemDetail","getItemDetails","_task$assignedLots","_task$assignedLots$","_task$processType","baseW","baseH","variation","itemNo","toggleItemCheck","taskProgress","newChecks","submitQuickResult","IssueReportModal","WorkLogTemplateModal","_ref153","setIssueType","_ref154","_order$items6","_motorSpec$motors220V","_motorSpec$motors380V","_motorSpec$brackets","_motorSpec$heatSinks","_motorSpec$heatSinks$","_motorSpec$controller","_bomData$guideRails3","_bomData$guideRails3$","_bomData$guideRails4","_bomData$guideRails4$","_bomData$guideRails5","_bomData$guideRails5$","_bomData$guideRails6","_bomData$guideRails6$","_bomData$guideRails6$2","_bomData$cases3","_bomData$cases4","_bomData$cases4$items","_bomData$cases6","_bomData$cases6$sideC","_bomData$cases7","_bomData$cases7$sideC","_bomData$cases8","_bomData$cases8$topCo","_bomData$cases9","_bomData$cases9$smoke","_bomData$cases9$smoke2","_bomData$cases0","_bomData$cases0$smoke","_bomData$bottomFinish3","_bomData$bottomFinish4","_bomData$bottomFinish5","_bomData$bottomFinish6","_bomData$bottomFinish7","_bomData$bottomFinish8","_bomData$bottomFinish9","_bomData$bottomFinish0","_bomData$bottomFinish1","_bomData$bottomFinish10","_bomData$bottomFinish11","_bomData$bottomFinish12","_bomData$bottomFinish13","_bomData$bottomFinish14","_bomData$bottomFinish15","_bomData$bottomFinish16","_bomData$bottomFinish17","_bomData$bottomFinish18","_bomData$bottomFinish19","_bomData$bottomFinish20","_bomData$bottomFinish21","_bomData$bottomFinish22","_bomData$bottomFinish23","_bomData$bottomFinish24","_bomData$bottomFinish25","_bomData$bottomFinish26","_bomData$bottomFinish27","_bomData$bottomFinish28","_bomData$bottomFinish29","_bomData$bottomFinish30","setPriority","setNote","showDetailTab","setShowDetailTab","previewWorkOrders","materialsWithStock","_inventoryItem$stock","_inventoryItem$stock2","inventoryItem","isShort","_item$spec4","_item$spec5","_item$spec6","_item$spec7","_item$spec8","_item$spec9","GuideRailFrontSVG","textAnchor","GuideRailSideSVG","CaseBoxSVG","hasShortage","sidx","_item$openWidth","_item$openHeight","_item$prodWidth","_item$prodHeight","ctrl","_item$lengths","lidx","_len$length","_item$lengths2","_len$length2","_item$lengths3","_item$lengths3$","_len$length3","_bomData$cases5","_item$length","ProductionOrderCreatePage","_ref155","_order$items7","_completeInfo$workOrd","_completeInfo$workOrd2","showCompleteDialog","setShowCompleteDialog","completeInfo","setCompleteInfo","handleConfirm","classifyItem","_ref156","_inventoryItem$stock3","_inventoryItem$stock4","_item$spec0","_item$spec1","_item$lengths$","_item$lengths$2","_item$lengths$3","_item$lengths$4","_ref157","_data$orderInfo$appro","_data$orderInfo$appro2","_data$bomData$guideRa","_data$bomData$guideRa2","_data$bomData$guideRa3","_data$bomData$guideRa4","orderData","printRef","convertToSheetData","sourceOrder","handleDownloadPDF","printContent","printWindow","open","write","close","handlePrint","createWorkLogDataLocal","_items$3","findInputLotNo","baseData","checker","screenMaterialMap","fabric","inputLotNo","itemDetails","fabricLotNo","productionResult","totalProduced","slatMaterialMap","coil","cuttingDetails","bendingMaterialMap","railType","caseBox","sus","egi","ScreenWorkLogEditor","_ref158","setData","SlatWorkLogEditor","_ref159","fireGlassQty","installFloor","BendingWorkLogEditor","_ref160","getMaterialLotByCode","egiLot","susLot","_updated$caseBox$mate","_item$material","_item$material2","updateField","lastKey","newGuideRail","ridx","newRows","_ref161","_data$materialInputs","_data$materialInputs2","_data$materialInputs3","_data$materialInputs4","_data$approvalLine","_data$approvalLine$wr","_data$workItems","_data$workItems2","_data$workDate","templateOptions","generateWorkLogDataFromWorkOrder","_workOrder$workOrderN","_workOrder$items2","getInputLotNo","inputLotNos","yieldRate","signed","appearance","screenProductionMatrix","widthSizes","meshSize","widthSize","ws","slatProductionLots","good","totalProd","qty1220","qty900","qty600","qty400","qty300","fireLotNo","usage1220m","usage400m","totalUsageM2","usage900m","usage300m","usage600m","MaterialCheckModal","_ref163","MaterialInputModal","_ref164","allMaterialLots","featureBadges","setSelectedBadge","updateFeatureBadge","setEditingBadge","selectedMaterial","setSelectedMaterial","setSelectedLot","setQty","inputBy","setInputBy","processMaterials","getProcessMaterials","remainQty","expiryDate","fifoRank","isRecommended","getMaterialLots","modalBadges","ProductionOrderList","_ref167","_selectedPO$processGr","onDeleteProductionOrders","filteredOrders","matchTab","getWorkOrderCount","_pg$items","confirmCreateWorkOrders","ProductionOrderDetail","_ref168","_productionOrder$proc","_productionOrder$proc2","_productionOrder$proc3","_productionOrder$proc4","relatedOrder","getProcessStatusColor","_pg$items2","_pg$items3","_pg$items4","handleCreateWorkOrders","WorkOrderList","_ref169","onAssign","onProgressWork","onDeleteWorkOrders","getOrderLotNo","processFilter","setProcessFilter","showProgressModal","setShowProgressModal","progressData","setProgressData","showAssignModal","setShowAssignModal","selectedForAssign","setSelectedForAssign","selectedAssignees","setSelectedAssignees","teams","unassignedCount","unassigned","_ref170","processOrders","noMaterial","materialInputted","noAssignee","allInTeam","othersCount","formatAssigneeDisplay","handleOpenAssignModal","currentStepDisplay","getCurrentStep","inProgressStep","_ref171","lastCompleted","_ref172","assignStatus","inputStatus","isInputted","startStatus","isStarted","isFullySelected","isPartiallySelected","allSelected","handleTeamSelect","handleWorkerToggle","handleSaveAssignment","handleSaveProgress","WorkOrderCreate","_ref173","_activeProcesses$","_activeProcesses$2","_activeProcesses$3","_selectedOrder$items","_selectedOrder$splits","_selectedOrder$items2","salesCustomers","registrationMode","setRegistrationMode","selectedSplit","setSelectedSplit","showOrderPanel","setShowOrderPanel","orderSearch","setOrderSearch","manualItems","showAssigneePanel","setShowAssigneePanel","filteredSites","updateManualItem","handleModeChange","availableOrders","_order$orderNo","_order$customerName","_order$siteName","exists","isTeamFullySelected","isTeamPartiallySelected","handleSplitSelect","isSubmitDisabled","newWorkOrder","registrationType","handleClearOrder","removeManualItem","addManualItem","handleProcessTypeChange","_order$items8","_order$splits4","handleOrderSelect","currentAssignees","newAssignees","member","WorkOrderEdit","_ref174","_workOrder$items3","WorkOrderDetail","_ref175","_workOrder$issues","_workOrder$stepStatus8","_workOrder$stepStatus9","_workLogApproval$step","_workLogApproval$step2","_workLogApproval$step3","_workLogApproval$step4","_workLogApproval$step5","_workLogApproval$step6","_workLogApproval$step7","_workLogApproval$step8","_workLogApproval$step9","_workLogApproval$step0","_workLogApproval$step1","_workLogApproval$step10","_workLogApproval$step11","_workLogApproval$step12","_workLogApproval$step13","onUpdateOrder","onUseMaterial","setActiveModal","showMaterialCheck","setShowMaterialCheck","showStepModal","setShowStepModal","showMaterialInputModal","setShowMaterialInputModal","showWorkLogEditor","setShowWorkLogEditor","showWorkLogSheet","setShowWorkLogSheet","showCombinedSheet","setShowCombinedSheet","setWorkLogData","workLogApproval","setWorkLogApproval","step1","submittedAt","submittedBy","step2","step3","rejectedAt","rejectedStep","showMaterialSelectModal","setShowMaterialSelectModal","selectedMaterials","setSelectedMaterials","showInspectionCertModal","setShowInspectionCertModal","inspectionCertData","setInspectionCertData","parseInitialAssignees","workerTeams","_p$assignedWorkers","matchedProcess","relatedResults","getProcessSteps","_matchedProcess$workS","handleStepComplete","additionalData","currentIndex","isLastStep","nextStep","newCompletedQty","handleInspectionComplete","_workOrder$stepStatus","_workOrder$stepStatus2","inspectionIndex","previousStep","newIssue","addItemHistory","_workOrder$stepStatus5","_workOrder$stepStatus6","_workOrder$stepStatus7","canStart","canComplete","handleStepStart","historyEntry","itemHistory","handleItemStart","allCompleted","handleItemComplete","newInput","inputAt","handleTeamToggle","handleAssign","CombinedWorkOrderSheet","iqcStatus","inputQty","_ref176","_workOrder$items4","_workLog$workDate","_workLog$productionRe","_workLog$totalProduct","_workLog$totalProduct2","_workLog$productionRe2","_workLog$productionRe3","_workLog$guideRail","WorkResultInput","_ref177","_workOrder$items5","_workOrder$items5$","_workOrder$items8","prodQty","_workOrder$items6","_workOrder$items6$fin","_workOrder$items7","_workOrder$items7$fin","newResult","WorkResultList","_ref178","totalGood","dispatchTypes","vehicleTypes","logisticsCompanies","priorityOptions","nonShipmentReasons","ShipmentCalendarView","_ref179","calendarDate","setCalendarDate","selectedCalendarDate","setSelectedCalendarDate","startDayOfWeek","getShipmentsForDate","monthStr","_s$shipmentDate","_s$shipmentDate2","_s$shipmentDate3","_s$shipmentDate4","selectedDateShipments","goToToday","goToPrevMonth","goToNextMonth","renderCalendarCells","dayShipments","isToday","dayOfWeek","_shipment$customerNam","ShipmentList","_ref180","onDeleteShipments","dispatchFilter","setDispatchFilter","priorityFilter","setPriorityFilter","selectedForCancel","setSelectedForCancel","showPriorityModal","setShowPriorityModal","selectedForPriority","setSelectedForPriority","newPriority","setNewPriority","ready","loading","_s$splitNo","_s$customerName","_s$siteName","_s$lotNo","unpaidShipments","urgentShipments","personInCharge","handlePriorityChange","_priorityOptions$find","handleCancelShipment","ShipmentCreate","_ref181","_selectedOrder$splits3","_selectedOrder$splits2","ShipmentDetail","_ref182","_shipment$shipmentDat","_shipment$items","_shipment$items2","_shipment$items3","_shipment$items4","_shipment$items5","_shipment$items6","_shipment$items7","_shipment$items8","_shipment$items9","_shipment$items0","_shipment$items1","_shipment$items10","_shipment$items11","_shipment$items12","showShipmentDocModal","setShowShipmentDocModal","showDeliveryConfirmationModal","setShowDeliveryConfirmationModal","showTransactionStatementModal","setShowTransactionStatementModal","linkedOrder","handleStatusChange","_shipment$changeHisto","currentPriority","trackingNo","_shipment$changeHisto2","_priorityOptions$find2","_shipment$changeHisto3","ShipmentEdit","_ref183","changeReason","_priorityOptions$find3","_priorityOptions$find4","oldPriority","_shipment$changeHisto4","updatedShipment","ProductInspectionList","_ref184","_selectedForApproval$7","onShipmentCertify","scheduled","_inspection$approvalL","_updatedApprovalLine$2","_pendingRoles$3","certifiedAt","ProductInspectionDetail","_ref185","_inspection$items","_inspection$items$","_inspection$items2","_inspection$items2$","_inspection$scheduleH","setInspectionData","inspectionSheet","setInspectionSheet","orderValue","overallJudgement","handleInspectionSheetUpdate","calculateOverallJudgement","allItems","judgedItems","handleInspectionResult","beforeDate","afterDate","Sidebar","_ref186","onOpenSettings","isTopLevel","depth3","_menuConfig$group$id","_menuConfig$group$id2","_menuConfig$group$id3","d3","MenuSettings","_ref187","rolePresets","purchase","cashbook","admin","menuDefinitions","_ref188","preset","handleRoleChange","_config$group$id","GroupIcon","isEnabled","_prev$groupId","_config$group$id2","_config$group$id2$sub","isSubEnabled","toggleSubMenu","subId","_prev$groupId2","_prev$groupId3","_prev$groupId3$subMen","App","setActiveMenu","setView","selectedData","setSelectedData","showSettings","setShowSettings","showMobileMenu","setShowMobileMenu","setUserNickname","showNicknameModal","setShowNicknameModal","setShowFeatureDescription","activeModal","setIsFeatureAdmin","showAdminPasswordModal","setShowAdminPasswordModal","adminPassword","setAdminPassword","adminPasswordError","setAdminPasswordError","handleAdminPasswordSubmit","setFeatureBadges","setShowFeatureDocPanel","hoveredBadgeNumber","setHoveredBadgeNumber","showUserFlow","setShowUserFlow","setShowFlowPanel","setShowComprehensiveFlowPanel","comprehensiveFlowPanelWidth","setComprehensiveFlowPanelWidth","setShowAllMenuFeatureDocPanel","showDetailedFlowDiagram","setShowDetailedFlowDiagram","detailedFlowType","setDetailedFlowType","showWorkerMaterialModal","setShowWorkerMaterialModal","workerMaterialTask","setWorkerMaterialTask","handleUseMaterial","setInventory","sharedItems","setSharedItems","_item$itemName$match2","maxId","itemWithId","handleUpdateItem","handleUpdatePrice","priceDataArray","priceInfo","versionHistory","setVersionHistory","lastCommittedSnapshot","setLastCommittedSnapshot","generateChangeSummary","oldBadges","addedCount","removedCount","modifiedCount","changeDetails","screenNameMap","oldList","newList","oldIds","newIds","screenDisplayName","viewType","menuName","viewName","getScreenDisplayName","screenChanges","added","removed","modified","newB","oldB","summaryParts","totalSummary","detailText","autoCommitMessage","loadLatestCommit","committed","isPlacingBadge","setIsPlacingBadge","inlineInputPosition","setInlineInputPosition","nextBadgeNumber","setNextBadgeNumber","contentAreaRef","scrollState","setScrollState","contentAreaBounds","setContentAreaBounds","contentArea","updateState","scrollHeight","getNextBadgeNumber","currentBadges","getCurrentScreenBadges","extractFeatures","_screenId$match","baseId","listViewId","getScreenFeatureTemplate","contentWidth","scrollWidth","contentHeight","feature","stateInfo","autoGenerated","menuMapping","getScreenName","screenTitleMap","showIdCodeHelp","setShowIdCodeHelp","idCodeHelpContent","examples","getScreenPath","menuInfo","getScreenId","modalIdMap","getCurrentScreenKey","editingBadge","deleteFeatureBadge","setCurrentUser","avatar","setOrders","setQuotes","setProductionOrders","setWorkOrders","setWorkResults","setShipments","productInspections","setProductInspections","setPurchaseOrders","arrivalDate","allInspections","setAllInspections","setDefects","registDate","sourceNo","images","processResult","processDate","processBy","lotSequences","setLotSequences","incoming","incomingInsp","processInsp","productInsp","finished","_lotSequences$type","updateSequence","_prev$type","autoGenerateIncomingInspLot","taxInvoices","setTaxInvoices","transactionStatements","setTransactionStatements","setReceivables","autoGenerateTransactionStatement","newStatement","autoCreateReceivable","newReceivable","calculateDueDate","skills","currentWork","equipments","setEquipments","totalRunTime","totalDownTime","equipmentLogs","setEquipmentLogs","inspectionCertificates","setInspectionCertificates","rawMaterialLots","setRawMaterialLots","setProductionLots","subCategory","materialUsage","setMaterialUsage","setMaterialLots","initialQty","lotType","setMenuConfig","navigate","handleMenuChange","handleSaveQuote","handleSaveOrder","handleUpdateOrder","updatedOrder","handleCancelOrder","cancelInfo","handleSaveWorkOrder","autoGenerateProductionLot","_o$splits3","handleApproveWorkOrder","handleInputWorkLog","taskOrId","isObject","taskData","productInspLot","autoGenerateProductInspLot","currentWO","_taskData$items","_taskData$items$","newProductInspection","handleApproveShipment","handleRejectShipment","accountingRejectDate","accountingRejectReason","handleCreatePO","newPO","handleUpdateWorkOrder","updatedWorkOrder","handleSaveWorkResult","handleSaveShipment","handleUpdateShipment","shipmentOrNo","prevShipment","statement","transactionStatementNo","autoGenerateTaxInvoice","newInvoice","taxAmount","taxInvoiceNo","handleReceivePO","poId","autoGenerateIncomingLot","inspectionLotNo","newMaterialLot","newInspection","defectId","processData","statusData","deliveryStatus","deliveryProgress","currentLocation","lastUpdateTime","grayscaleStyle","deleteWithPassword","onSuccess","ex","FeatureDocPanel","currentView","handleToggleFeatureDescription","renderContent","PolicyGuide","BusinessFlowChart","mappedView","DetailedProcessFlowChart","newCode","newProcess","DocumentTemplateManager","updatedQuote","QuoteDetailNew","deliveryInfo","_additionalData$relat","_additionalData$relat2","_po$processGroups","_po$processGroups2","lastProgressAt","progressNote","ncrs","newNcr","reporter","setNcrs","usedAt","onInspectionComplete","pqcResult","updatedInbound","isPass","locationCode","locationName","PriceListSimple","onProcess","FeatureDocBadgeOverlay","_getCurrentScreenBadg","loadTemplateToCurrentScreen","mappedKey","templateBadges","_badge$position","_badge$position2","generateDefaultBadges","existingBadges","maxNumber","checkHasTemplate","addBadgeComment","commentText","deleteBadgeComment","commentId","UserFlowPanel","ComprehensiveFlowPanel","oldNickname","updatedBadges","newCommit","snapshot","commitId","targetCommit","UserFlowNavigator","DetailedFlowDiagram","AllMenuFeatureDocPanel","ReactDOM"],"ignoreList":[],"sourceRoot":""}
\ No newline at end of file
diff --git a/mes기획서_리액트/fix_app.ps1 b/mes기획서_리액트/fix_app.ps1
new file mode 100644
index 0000000..2968741
--- /dev/null
+++ b/mes기획서_리액트/fix_app.ps1
@@ -0,0 +1,13 @@
+
+$path = "src\App.jsx"
+$absPath = Convert-Path $path
+$content = [System.IO.File]::ReadAllText($absPath, [System.Text.Encoding]::UTF8)
+$pattern = "import DocumentTemplateManager from './components/DocumentTemplateManager';\s+import DocumentTemplateManager from './components/DocumentTemplateManager';"
+$replace = "import DocumentTemplateManager from './components/DocumentTemplateManager';"
+$newContent = $content -replace $pattern, $replace
+if ($newContent.Length -ne $content.Length) {
+ Write-Host "Fixed double import via Regex."
+ [System.IO.File]::WriteAllText($absPath, $newContent, [System.Text.Encoding]::UTF8)
+} else {
+ Write-Host "No change made."
+}
diff --git a/mes기획서_리액트/package-lock.json b/mes기획서_리액트/package-lock.json
new file mode 100644
index 0000000..2956da9
--- /dev/null
+++ b/mes기획서_리액트/package-lock.json
@@ -0,0 +1,17831 @@
+{
+ "name": "sam-mes-system",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "sam-mes-system",
+ "version": "1.0.0",
+ "dependencies": {
+ "@google/generative-ai": "^0.24.1",
+ "lucide-react": "^0.263.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "5.0.1",
+ "reactflow": "^11.11.4"
+ },
+ "devDependencies": {
+ "playwright": "^1.57.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/eslint-parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.5.tgz",
+ "integrity": "sha512-fcdRcWahONYo+JRnJg1/AekOacGvKx12Gu0qXJXFi2WBqQA1i7+O5PaxRB7kxE/Op94dExnCiiar6T09pvdHpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
+ "eslint-visitor-keys": "^2.1.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || >=14.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.11.0",
+ "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0"
+ }
+ },
+ "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@babel/eslint-parser/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+ "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz",
+ "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-member-expression-to-functions": "^7.28.5",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/traverse": "^7.28.5",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz",
+ "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "regexpu-core": "^6.3.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-define-polyfill-provider": {
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz",
+ "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "debug": "^4.4.1",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.22.10"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz",
+ "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+ "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz",
+ "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-wrap-function": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
+ "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+ "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-wrap-function": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz",
+ "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.3",
+ "@babel/types": "^7.28.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz",
+ "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz",
+ "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz",
+ "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
+ "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-transform-optional-chaining": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.13.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz",
+ "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-class-properties": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz",
+ "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==",
+ "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-decorators": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz",
+ "integrity": "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-syntax-decorators": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz",
+ "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==",
+ "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.18.6",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-numeric-separator": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz",
+ "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==",
+ "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.18.6",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-optional-chaining": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz",
+ "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==",
+ "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-methods": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz",
+ "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==",
+ "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-async-generators": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
+ "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-bigint": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
+ "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-properties": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
+ "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.12.13"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-static-block": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
+ "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-decorators": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz",
+ "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-flow": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz",
+ "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz",
+ "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
+ "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-meta": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+ "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-json-strings": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
+ "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
+ "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
+ "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-object-rest-spread": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
+ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
+ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-chaining": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-private-property-in-object": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
+ "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-top-level-await": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
+ "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
+ "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-unicode-sets-regex": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
+ "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-arrow-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz",
+ "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-generator-functions": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz",
+ "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-remap-async-to-generator": "^7.27.1",
+ "@babel/traverse": "^7.28.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz",
+ "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-remap-async-to-generator": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoped-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz",
+ "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoping": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz",
+ "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-properties": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz",
+ "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-static-block": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz",
+ "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.28.3",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz",
+ "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/traverse": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-computed-properties": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz",
+ "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/template": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz",
+ "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dotall-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz",
+ "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-keys": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz",
+ "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz",
+ "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dynamic-import": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
+ "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-explicit-resource-management": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz",
+ "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-transform-destructuring": "^7.28.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz",
+ "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-export-namespace-from": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz",
+ "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-flow-strip-types": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz",
+ "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-syntax-flow": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-for-of": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz",
+ "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-function-name": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz",
+ "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-json-strings": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz",
+ "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz",
+ "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-logical-assignment-operators": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz",
+ "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-member-expression-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz",
+ "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-amd": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
+ "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz",
+ "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
+ "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
+ "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz",
+ "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-new-target": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
+ "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz",
+ "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-numeric-separator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz",
+ "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-rest-spread": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz",
+ "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-transform-destructuring": "^7.28.0",
+ "@babel/plugin-transform-parameters": "^7.27.7",
+ "@babel/traverse": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-super": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
+ "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-catch-binding": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz",
+ "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-chaining": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz",
+ "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-parameters": {
+ "version": "7.27.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz",
+ "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-methods": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz",
+ "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-property-in-object": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz",
+ "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-property-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
+ "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-constant-elements": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz",
+ "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-display-name": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz",
+ "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz",
+ "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-development": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz",
+ "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-transform-react-jsx": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-pure-annotations": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz",
+ "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regenerator": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz",
+ "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regexp-modifiers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz",
+ "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-reserved-words": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
+ "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-runtime": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz",
+ "integrity": "sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "babel-plugin-polyfill-corejs2": "^0.4.14",
+ "babel-plugin-polyfill-corejs3": "^0.13.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.5",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-runtime/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/plugin-transform-shorthand-properties": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz",
+ "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-spread": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz",
+ "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-sticky-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz",
+ "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-template-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz",
+ "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typeof-symbol": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
+ "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz",
+ "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-create-class-features-plugin": "^7.28.5",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-syntax-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-escapes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
+ "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-property-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz",
+ "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz",
+ "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-sets-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz",
+ "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/preset-env": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz",
+ "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5",
+ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3",
+ "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
+ "@babel/plugin-syntax-import-assertions": "^7.27.1",
+ "@babel/plugin-syntax-import-attributes": "^7.27.1",
+ "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
+ "@babel/plugin-transform-arrow-functions": "^7.27.1",
+ "@babel/plugin-transform-async-generator-functions": "^7.28.0",
+ "@babel/plugin-transform-async-to-generator": "^7.27.1",
+ "@babel/plugin-transform-block-scoped-functions": "^7.27.1",
+ "@babel/plugin-transform-block-scoping": "^7.28.5",
+ "@babel/plugin-transform-class-properties": "^7.27.1",
+ "@babel/plugin-transform-class-static-block": "^7.28.3",
+ "@babel/plugin-transform-classes": "^7.28.4",
+ "@babel/plugin-transform-computed-properties": "^7.27.1",
+ "@babel/plugin-transform-destructuring": "^7.28.5",
+ "@babel/plugin-transform-dotall-regex": "^7.27.1",
+ "@babel/plugin-transform-duplicate-keys": "^7.27.1",
+ "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1",
+ "@babel/plugin-transform-dynamic-import": "^7.27.1",
+ "@babel/plugin-transform-explicit-resource-management": "^7.28.0",
+ "@babel/plugin-transform-exponentiation-operator": "^7.28.5",
+ "@babel/plugin-transform-export-namespace-from": "^7.27.1",
+ "@babel/plugin-transform-for-of": "^7.27.1",
+ "@babel/plugin-transform-function-name": "^7.27.1",
+ "@babel/plugin-transform-json-strings": "^7.27.1",
+ "@babel/plugin-transform-literals": "^7.27.1",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.28.5",
+ "@babel/plugin-transform-member-expression-literals": "^7.27.1",
+ "@babel/plugin-transform-modules-amd": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/plugin-transform-modules-systemjs": "^7.28.5",
+ "@babel/plugin-transform-modules-umd": "^7.27.1",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1",
+ "@babel/plugin-transform-new-target": "^7.27.1",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
+ "@babel/plugin-transform-numeric-separator": "^7.27.1",
+ "@babel/plugin-transform-object-rest-spread": "^7.28.4",
+ "@babel/plugin-transform-object-super": "^7.27.1",
+ "@babel/plugin-transform-optional-catch-binding": "^7.27.1",
+ "@babel/plugin-transform-optional-chaining": "^7.28.5",
+ "@babel/plugin-transform-parameters": "^7.27.7",
+ "@babel/plugin-transform-private-methods": "^7.27.1",
+ "@babel/plugin-transform-private-property-in-object": "^7.27.1",
+ "@babel/plugin-transform-property-literals": "^7.27.1",
+ "@babel/plugin-transform-regenerator": "^7.28.4",
+ "@babel/plugin-transform-regexp-modifiers": "^7.27.1",
+ "@babel/plugin-transform-reserved-words": "^7.27.1",
+ "@babel/plugin-transform-shorthand-properties": "^7.27.1",
+ "@babel/plugin-transform-spread": "^7.27.1",
+ "@babel/plugin-transform-sticky-regex": "^7.27.1",
+ "@babel/plugin-transform-template-literals": "^7.27.1",
+ "@babel/plugin-transform-typeof-symbol": "^7.27.1",
+ "@babel/plugin-transform-unicode-escapes": "^7.27.1",
+ "@babel/plugin-transform-unicode-property-regex": "^7.27.1",
+ "@babel/plugin-transform-unicode-regex": "^7.27.1",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.27.1",
+ "@babel/preset-modules": "0.1.6-no-external-plugins",
+ "babel-plugin-polyfill-corejs2": "^0.4.14",
+ "babel-plugin-polyfill-corejs3": "^0.13.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.5",
+ "core-js-compat": "^3.43.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-env/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/preset-modules": {
+ "version": "0.1.6-no-external-plugins",
+ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
+ "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/preset-react": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz",
+ "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-transform-react-display-name": "^7.28.0",
+ "@babel/plugin-transform-react-jsx": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-development": "^7.27.1",
+ "@babel/plugin-transform-react-pure-annotations": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz",
+ "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/plugin-transform-typescript": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "license": "MIT"
+ },
+ "node_modules/@csstools/normalize.css": {
+ "version": "12.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz",
+ "integrity": "sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/@csstools/postcss-cascade-layers": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz",
+ "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@csstools/selector-specificity": "^2.0.2",
+ "postcss-selector-parser": "^6.0.10"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-color-function": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz",
+ "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@csstools/postcss-progressive-custom-properties": "^1.1.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-font-format-keywords": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz",
+ "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-hwb-function": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz",
+ "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-ic-unit": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz",
+ "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@csstools/postcss-progressive-custom-properties": "^1.1.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-is-pseudo-class": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz",
+ "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@csstools/selector-specificity": "^2.0.0",
+ "postcss-selector-parser": "^6.0.10"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-nested-calc": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz",
+ "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-normalize-display-values": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz",
+ "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-oklab-function": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz",
+ "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@csstools/postcss-progressive-custom-properties": "^1.1.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-progressive-custom-properties": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz",
+ "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3"
+ }
+ },
+ "node_modules/@csstools/postcss-stepped-value-functions": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz",
+ "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-text-decoration-shorthand": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz",
+ "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-trigonometric-functions": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz",
+ "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/postcss-unset-value": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz",
+ "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==",
+ "license": "CC0-1.0",
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/@csstools/selector-specificity": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz",
+ "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==",
+ "license": "CC0-1.0",
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss-selector-parser": "^6.0.10"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@google/generative-ai": {
+ "version": "0.24.1",
+ "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz",
+ "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@istanbuljs/load-nyc-config": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
+ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/console": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz",
+ "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^27.5.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@jest/core": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz",
+ "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^27.5.1",
+ "@jest/reporters": "^27.5.1",
+ "@jest/test-result": "^27.5.1",
+ "@jest/transform": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "emittery": "^0.8.1",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-changed-files": "^27.5.1",
+ "jest-config": "^27.5.1",
+ "jest-haste-map": "^27.5.1",
+ "jest-message-util": "^27.5.1",
+ "jest-regex-util": "^27.5.1",
+ "jest-resolve": "^27.5.1",
+ "jest-resolve-dependencies": "^27.5.1",
+ "jest-runner": "^27.5.1",
+ "jest-runtime": "^27.5.1",
+ "jest-snapshot": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "jest-validate": "^27.5.1",
+ "jest-watcher": "^27.5.1",
+ "micromatch": "^4.0.4",
+ "rimraf": "^3.0.0",
+ "slash": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/environment": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz",
+ "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/node": "*",
+ "jest-mock": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@jest/fake-timers": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz",
+ "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^27.5.1",
+ "@sinonjs/fake-timers": "^8.0.1",
+ "@types/node": "*",
+ "jest-message-util": "^27.5.1",
+ "jest-mock": "^27.5.1",
+ "jest-util": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@jest/globals": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz",
+ "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "expect": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@jest/reporters": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz",
+ "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==",
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@jest/console": "^27.5.1",
+ "@jest/test-result": "^27.5.1",
+ "@jest/transform": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "exit": "^0.1.2",
+ "glob": "^7.1.2",
+ "graceful-fs": "^4.2.9",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-instrument": "^5.1.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^4.0.0",
+ "istanbul-reports": "^3.1.3",
+ "jest-haste-map": "^27.5.1",
+ "jest-resolve": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "jest-worker": "^27.5.1",
+ "slash": "^3.0.0",
+ "source-map": "^0.6.0",
+ "string-length": "^4.0.1",
+ "terminal-link": "^2.0.0",
+ "v8-to-istanbul": "^8.1.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz",
+ "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.24.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/source-map": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz",
+ "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==",
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9",
+ "source-map": "^0.6.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@jest/source-map/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@jest/test-result": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz",
+ "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz",
+ "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "^27.5.1",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^27.5.1",
+ "jest-runtime": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@jest/transform": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz",
+ "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.1.0",
+ "@jest/types": "^27.5.1",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^1.4.0",
+ "fast-json-stable-stringify": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^27.5.1",
+ "jest-regex-util": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "source-map": "^0.6.1",
+ "write-file-atomic": "^3.0.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@jest/transform/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/@jest/transform/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@jest/types": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz",
+ "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^16.0.0",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@leichtgewicht/ip-codec": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
+ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
+ "license": "MIT"
+ },
+ "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
+ "version": "5.1.1-v1",
+ "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
+ "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==",
+ "license": "MIT",
+ "dependencies": {
+ "eslint-scope": "5.1.1"
+ }
+ },
+ "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@pmmmwh/react-refresh-webpack-plugin": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz",
+ "integrity": "sha512-tXDyE1/jzFsHXjhRZQ3hMl0IVhYe5qula43LDWIhVfjp9G/nT5OQY5AORVOrkEGAUltBJOfOWeETbmhm6kHhuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-html": "^0.0.9",
+ "core-js-pure": "^3.23.3",
+ "error-stack-parser": "^2.0.6",
+ "html-entities": "^2.1.0",
+ "loader-utils": "^2.0.4",
+ "schema-utils": "^4.2.0",
+ "source-map": "^0.7.3"
+ },
+ "engines": {
+ "node": ">= 10.13"
+ },
+ "peerDependencies": {
+ "@types/webpack": "4.x || 5.x",
+ "react-refresh": ">=0.10.0 <1.0.0",
+ "sockjs-client": "^1.4.0",
+ "type-fest": ">=0.17.0 <5.0.0",
+ "webpack": ">=4.43.0 <6.0.0",
+ "webpack-dev-server": "3.x || 4.x || 5.x",
+ "webpack-hot-middleware": "2.x",
+ "webpack-plugin-serve": "0.x || 1.x"
+ },
+ "peerDependenciesMeta": {
+ "@types/webpack": {
+ "optional": true
+ },
+ "sockjs-client": {
+ "optional": true
+ },
+ "type-fest": {
+ "optional": true
+ },
+ "webpack-dev-server": {
+ "optional": true
+ },
+ "webpack-hot-middleware": {
+ "optional": true
+ },
+ "webpack-plugin-serve": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reactflow/background": {
+ "version": "11.3.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
+ "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/controls": {
+ "version": "11.2.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
+ "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/core": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
+ "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3": "^7.4.0",
+ "@types/d3-drag": "^3.0.1",
+ "@types/d3-selection": "^3.0.3",
+ "@types/d3-zoom": "^3.0.1",
+ "classcat": "^5.0.3",
+ "d3-drag": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/minimap": {
+ "version": "11.7.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
+ "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "@types/d3-selection": "^3.0.3",
+ "@types/d3-zoom": "^3.0.1",
+ "classcat": "^5.0.3",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/node-resizer": {
+ "version": "2.2.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
+ "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.4",
+ "d3-drag": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/node-toolbar": {
+ "version": "1.3.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
+ "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@rollup/plugin-babel": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
+ "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.10.4",
+ "@rollup/pluginutils": "^3.1.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0",
+ "@types/babel__core": "^7.1.9",
+ "rollup": "^1.20.0||^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/babel__core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-node-resolve": {
+ "version": "11.2.1",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz",
+ "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^3.1.0",
+ "@types/resolve": "1.17.1",
+ "builtin-modules": "^3.1.0",
+ "deepmerge": "^4.2.2",
+ "is-module": "^1.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0"
+ }
+ },
+ "node_modules/@rollup/plugin-replace": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz",
+ "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^3.1.0",
+ "magic-string": "^0.25.7"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0 || ^2.0.0"
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
+ "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "0.0.39",
+ "estree-walker": "^1.0.1",
+ "picomatch": "^2.2.2"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0"
+ }
+ },
+ "node_modules/@rollup/pluginutils/node_modules/@types/estree": {
+ "version": "0.0.39",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
+ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
+ "license": "MIT"
+ },
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+ "license": "MIT"
+ },
+ "node_modules/@rushstack/eslint-patch": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz",
+ "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==",
+ "license": "MIT"
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.24.51",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
+ "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==",
+ "license": "MIT"
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "1.8.6",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
+ "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz",
+ "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^1.7.0"
+ }
+ },
+ "node_modules/@surma/rollup-plugin-off-main-thread": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
+ "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "ejs": "^3.1.6",
+ "json5": "^2.2.0",
+ "magic-string": "^0.25.0",
+ "string.prototype.matchall": "^4.0.6"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-add-jsx-attribute": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz",
+ "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-remove-jsx-attribute": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz",
+ "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz",
+ "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz",
+ "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-svg-dynamic-title": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz",
+ "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-svg-em-dimensions": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz",
+ "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-transform-react-native-svg": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz",
+ "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/babel-plugin-transform-svg-component": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz",
+ "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/babel-preset": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz",
+ "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==",
+ "license": "MIT",
+ "dependencies": {
+ "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0",
+ "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0",
+ "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1",
+ "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1",
+ "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0",
+ "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0",
+ "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0",
+ "@svgr/babel-plugin-transform-svg-component": "^5.5.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/core": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz",
+ "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@svgr/plugin-jsx": "^5.5.0",
+ "camelcase": "^6.2.0",
+ "cosmiconfig": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/hast-util-to-babel-ast": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz",
+ "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.12.6"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/plugin-jsx": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz",
+ "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@svgr/babel-preset": "^5.5.0",
+ "@svgr/hast-util-to-babel-ast": "^5.5.0",
+ "svg-parser": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/plugin-svgo": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz",
+ "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cosmiconfig": "^7.0.0",
+ "deepmerge": "^4.2.2",
+ "svgo": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@svgr/webpack": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz",
+ "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/plugin-transform-react-constant-elements": "^7.12.1",
+ "@babel/preset-env": "^7.12.1",
+ "@babel/preset-react": "^7.12.5",
+ "@svgr/core": "^5.5.0",
+ "@svgr/plugin-jsx": "^5.5.0",
+ "@svgr/plugin-svgo": "^5.5.0",
+ "loader-utils": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/gregberge"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
+ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@trysound/sax": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
+ "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/bonjour": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz",
+ "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect-history-api-fallback": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz",
+ "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/express-serve-static-core": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/eslint": {
+ "version": "8.56.12",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
+ "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/eslint-scope": {
+ "version": "3.7.7",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint": "*",
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.25",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
+ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "^1"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
+ "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/express/node_modules/@types/express-serve-static-core": {
+ "version": "4.19.7",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz",
+ "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/graceful-fs": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
+ "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/html-minifier-terser": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
+ "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/http-proxy": {
+ "version": "1.17.17",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz",
+ "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
+ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
+ "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "node_modules/@types/istanbul-reports": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
+ "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
+ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/node-forge": {
+ "version": "1.3.14",
+ "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz",
+ "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/parse-json": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/prettier": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz",
+ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/q": {
+ "version": "1.5.8",
+ "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz",
+ "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/resolve": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
+ "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/semver": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-index": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz",
+ "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.10",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
+ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "<1"
+ }
+ },
+ "node_modules/@types/serve-static/node_modules/@types/send": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
+ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/sockjs": {
+ "version": "0.3.36",
+ "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
+ "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/stack-utils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
+ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/yargs": {
+ "version": "16.0.11",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz",
+ "integrity": "sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.3",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
+ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
+ "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.4.0",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/type-utils": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "natural-compare-lite": "^1.4.0",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz",
+ "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/utils": "5.62.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
+ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
+ "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
+ "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
+ "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
+ "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
+ "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@types/json-schema": "^7.0.9",
+ "@types/semver": "^7.3.12",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "eslint-scope": "^5.1.1",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
+ "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+ "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+ "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+ "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+ "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/wasm-gen": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
+ "license": "MIT",
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+ "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/helper-wasm-section": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-opt": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1",
+ "@webassemblyjs/wast-printer": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/abab": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+ "deprecated": "Use your platform's native atob() and btoa() methods instead",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-globals": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
+ "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^7.1.1",
+ "acorn-walk": "^7.1.1"
+ }
+ },
+ "node_modules/acorn-globals/node_modules/acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-phases": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
+ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "peerDependencies": {
+ "acorn": "^8.14.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/address": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz",
+ "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/adjust-sourcemap-loader": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz",
+ "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==",
+ "license": "MIT",
+ "dependencies": {
+ "loader-utils": "^2.0.0",
+ "regex-parser": "^2.2.11"
+ },
+ "engines": {
+ "node": ">=8.9"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-html": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz",
+ "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==",
+ "engines": [
+ "node >= 0.8.0"
+ ],
+ "license": "Apache-2.0",
+ "bin": {
+ "ansi-html": "bin/ansi-html"
+ }
+ },
+ "node_modules/ansi-html-community": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz",
+ "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==",
+ "engines": [
+ "node >= 0.8.0"
+ ],
+ "license": "Apache-2.0",
+ "bin": {
+ "ansi-html": "bin/ansi-html"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
+ "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
+ "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-shim-unscopables": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.reduce": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.8.tgz",
+ "integrity": "sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-array-method-boxes-properly": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "is-string": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "license": "MIT"
+ },
+ "node_modules/ast-types-flow": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+ "license": "MIT"
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "license": "MIT"
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.22",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
+ "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.27.0",
+ "caniuse-lite": "^1.0.30001754",
+ "fraction.js": "^5.3.4",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.11.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz",
+ "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==",
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/babel-jest": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz",
+ "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/transform": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^27.5.1",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.8.0"
+ }
+ },
+ "node_modules/babel-loader": {
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz",
+ "integrity": "sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==",
+ "license": "MIT",
+ "dependencies": {
+ "find-cache-dir": "^3.3.1",
+ "loader-utils": "^2.0.4",
+ "make-dir": "^3.1.0",
+ "schema-utils": "^2.6.5"
+ },
+ "engines": {
+ "node": ">= 8.9"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0",
+ "webpack": ">=2"
+ }
+ },
+ "node_modules/babel-loader/node_modules/schema-utils": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
+ "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.5",
+ "ajv": "^6.12.4",
+ "ajv-keywords": "^3.5.2"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/babel-plugin-istanbul": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
+ "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-instrument": "^5.0.4",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-jest-hoist": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz",
+ "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.0.0",
+ "@types/babel__traverse": "^7.0.6"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/babel-plugin-macros": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/babel-plugin-named-asset-import": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz",
+ "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@babel/core": "^7.1.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz",
+ "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.7",
+ "@babel/helper-define-polyfill-provider": "^0.6.5",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs3": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz",
+ "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.5",
+ "core-js-compat": "^3.43.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz",
+ "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.5"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-transform-react-remove-prop-types": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz",
+ "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==",
+ "license": "MIT"
+ },
+ "node_modules/babel-preset-current-node-syntax": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
+ "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-bigint": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.12.13",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5",
+ "@babel/plugin-syntax-import-attributes": "^7.24.7",
+ "@babel/plugin-syntax-import-meta": "^7.10.4",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
+ "@babel/plugin-syntax-top-level-await": "^7.14.5"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/babel-preset-jest": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz",
+ "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jest-hoist": "^27.5.1",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/babel-preset-react-app": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.1.0.tgz",
+ "integrity": "sha512-f9B1xMdnkCIqe+2dHrJsoQFRz7reChaAHE/65SdaykPklQqhme2WaC08oD3is77x9ff98/9EazAKFDZv5rFEQg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.16.0",
+ "@babel/plugin-proposal-class-properties": "^7.16.0",
+ "@babel/plugin-proposal-decorators": "^7.16.4",
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0",
+ "@babel/plugin-proposal-numeric-separator": "^7.16.0",
+ "@babel/plugin-proposal-optional-chaining": "^7.16.0",
+ "@babel/plugin-proposal-private-methods": "^7.16.0",
+ "@babel/plugin-proposal-private-property-in-object": "^7.16.7",
+ "@babel/plugin-transform-flow-strip-types": "^7.16.0",
+ "@babel/plugin-transform-react-display-name": "^7.16.0",
+ "@babel/plugin-transform-runtime": "^7.16.4",
+ "@babel/preset-env": "^7.16.4",
+ "@babel/preset-react": "^7.16.0",
+ "@babel/preset-typescript": "^7.16.0",
+ "@babel/runtime": "^7.16.3",
+ "babel-plugin-macros": "^3.1.0",
+ "babel-plugin-transform-react-remove-prop-types": "^0.4.24"
+ }
+ },
+ "node_modules/babel-preset-react-app/node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz",
+ "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==",
+ "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.18.6",
+ "@babel/helper-create-class-features-plugin": "^7.21.0",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.4",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.4.tgz",
+ "integrity": "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/batch": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
+ "license": "MIT"
+ },
+ "node_modules/bfj": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz",
+ "integrity": "sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw==",
+ "license": "MIT",
+ "dependencies": {
+ "bluebird": "^3.7.2",
+ "check-types": "^11.2.3",
+ "hoopy": "^0.1.4",
+ "jsonpath": "^1.1.1",
+ "tryer": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/big.js": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
+ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/bonjour-service": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz",
+ "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "multicast-dns": "^7.2.5"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "license": "ISC"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browser-process-hrtime": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
+ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bser": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
+ "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "node-int64": "^0.4.0"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/builtin-modules": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
+ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camel-case": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
+ "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
+ "license": "MIT",
+ "dependencies": {
+ "pascal-case": "^3.1.2",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-api": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
+ "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.0.0",
+ "caniuse-lite": "^1.0.0",
+ "lodash.memoize": "^4.1.2",
+ "lodash.uniq": "^4.5.0"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001759",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz",
+ "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/case-sensitive-paths-webpack-plugin": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz",
+ "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/char-regex": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
+ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/check-types": {
+ "version": "11.2.3",
+ "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz",
+ "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==",
+ "license": "MIT"
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
+ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
+ "license": "MIT"
+ },
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
+ "node_modules/clean-css": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
+ "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "~0.6.0"
+ },
+ "engines": {
+ "node": ">= 10.0"
+ }
+ },
+ "node_modules/clean-css/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">= 1.0.0",
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/coa": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
+ "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/q": "^1.5.1",
+ "chalk": "^2.4.1",
+ "q": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 4.0"
+ }
+ },
+ "node_modules/coa/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/coa/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/coa/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/coa/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "license": "MIT"
+ },
+ "node_modules/coa/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/coa/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/coa/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/collect-v8-coverage": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz",
+ "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==",
+ "license": "MIT"
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/colord": {
+ "version": "2.9.3",
+ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
+ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
+ "license": "MIT"
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/common-tags": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
+ "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+ "license": "MIT"
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.1.0",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "license": "MIT"
+ },
+ "node_modules/confusing-browser-globals": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
+ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==",
+ "license": "MIT"
+ },
+ "node_modules/connect-history-api-fallback": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
+ "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/core-js": {
+ "version": "3.47.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
+ "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-js-compat": {
+ "version": "3.47.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz",
+ "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==",
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-js-pure": {
+ "version": "3.47.0",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz",
+ "integrity": "sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
+ "node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/crypto-random-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
+ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/css-blank-pseudo": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz",
+ "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.9"
+ },
+ "bin": {
+ "css-blank-pseudo": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
+ "node_modules/css-declaration-sorter": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz",
+ "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==",
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.9"
+ }
+ },
+ "node_modules/css-has-pseudo": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz",
+ "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.9"
+ },
+ "bin": {
+ "css-has-pseudo": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
+ "node_modules/css-loader": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz",
+ "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==",
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.1.0",
+ "postcss": "^8.4.33",
+ "postcss-modules-extract-imports": "^3.1.0",
+ "postcss-modules-local-by-default": "^4.0.5",
+ "postcss-modules-scope": "^3.2.0",
+ "postcss-modules-values": "^4.0.0",
+ "postcss-value-parser": "^4.2.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || 1.x",
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/css-minimizer-webpack-plugin": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz",
+ "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "cssnano": "^5.0.6",
+ "jest-worker": "^27.0.2",
+ "postcss": "^8.3.5",
+ "schema-utils": "^4.0.0",
+ "serialize-javascript": "^6.0.0",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@parcel/css": {
+ "optional": true
+ },
+ "clean-css": {
+ "optional": true
+ },
+ "csso": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/css-minimizer-webpack-plugin/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/css-prefers-color-scheme": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz",
+ "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==",
+ "license": "CC0-1.0",
+ "bin": {
+ "css-prefers-color-scheme": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
+ "node_modules/css-select": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
+ "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.0.1",
+ "domhandler": "^4.3.1",
+ "domutils": "^2.8.0",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-select-base-adapter": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
+ "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
+ "license": "MIT"
+ },
+ "node_modules/css-tree": {
+ "version": "1.0.0-alpha.37",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
+ "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.4",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/css-tree/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cssdb": {
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz",
+ "integrity": "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ }
+ ],
+ "license": "CC0-1.0"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cssnano": {
+ "version": "5.1.15",
+ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz",
+ "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==",
+ "license": "MIT",
+ "dependencies": {
+ "cssnano-preset-default": "^5.2.14",
+ "lilconfig": "^2.0.3",
+ "yaml": "^1.10.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/cssnano"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/cssnano-preset-default": {
+ "version": "5.2.14",
+ "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz",
+ "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==",
+ "license": "MIT",
+ "dependencies": {
+ "css-declaration-sorter": "^6.3.1",
+ "cssnano-utils": "^3.1.0",
+ "postcss-calc": "^8.2.3",
+ "postcss-colormin": "^5.3.1",
+ "postcss-convert-values": "^5.1.3",
+ "postcss-discard-comments": "^5.1.2",
+ "postcss-discard-duplicates": "^5.1.0",
+ "postcss-discard-empty": "^5.1.1",
+ "postcss-discard-overridden": "^5.1.0",
+ "postcss-merge-longhand": "^5.1.7",
+ "postcss-merge-rules": "^5.1.4",
+ "postcss-minify-font-values": "^5.1.0",
+ "postcss-minify-gradients": "^5.1.1",
+ "postcss-minify-params": "^5.1.4",
+ "postcss-minify-selectors": "^5.2.1",
+ "postcss-normalize-charset": "^5.1.0",
+ "postcss-normalize-display-values": "^5.1.0",
+ "postcss-normalize-positions": "^5.1.1",
+ "postcss-normalize-repeat-style": "^5.1.1",
+ "postcss-normalize-string": "^5.1.0",
+ "postcss-normalize-timing-functions": "^5.1.0",
+ "postcss-normalize-unicode": "^5.1.1",
+ "postcss-normalize-url": "^5.1.0",
+ "postcss-normalize-whitespace": "^5.1.1",
+ "postcss-ordered-values": "^5.1.3",
+ "postcss-reduce-initial": "^5.1.2",
+ "postcss-reduce-transforms": "^5.1.0",
+ "postcss-svgo": "^5.1.0",
+ "postcss-unique-selectors": "^5.1.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/cssnano-utils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz",
+ "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/csso": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz",
+ "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==",
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/csso/node_modules/css-tree": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
+ "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.14",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/csso/node_modules/mdn-data": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
+ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/csso/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/cssom": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
+ "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==",
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+ "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+ "license": "MIT",
+ "dependencies": {
+ "cssom": "~0.3.6"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cssstyle/node_modules/cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+ "license": "MIT"
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/data-urls": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
+ "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.3",
+ "whatwg-mimetype": "^2.3.0",
+ "whatwg-url": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "license": "MIT"
+ },
+ "node_modules/dedent": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
+ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
+ "license": "MIT"
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "license": "MIT"
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/default-gateway": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz",
+ "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "execa": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-newline": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
+ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+ "license": "MIT"
+ },
+ "node_modules/detect-port-alt": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz",
+ "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "address": "^1.0.1",
+ "debug": "^2.6.0"
+ },
+ "bin": {
+ "detect": "bin/detect-port",
+ "detect-port": "bin/detect-port"
+ },
+ "engines": {
+ "node": ">= 4.2.1"
+ }
+ },
+ "node_modules/detect-port-alt/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/detect-port-alt/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/diff-sequences": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
+ "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "license": "MIT"
+ },
+ "node_modules/dns-packet": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
+ "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@leichtgewicht/ip-codec": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dom-converter": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
+ "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
+ "license": "MIT",
+ "dependencies": {
+ "utila": "~0.4"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
+ "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.2.0",
+ "entities": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domexception": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
+ "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "license": "MIT",
+ "dependencies": {
+ "webidl-conversions": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/domexception/node_modules/webidl-conversions": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
+ "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/domhandler": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
+ "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.2.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
+ "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^1.0.1",
+ "domelementtype": "^2.2.0",
+ "domhandler": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dot-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
+ "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
+ "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/dotenv-expand": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
+ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/duplexer": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
+ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
+ "license": "MIT"
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.266",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz",
+ "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==",
+ "license": "ISC"
+ },
+ "node_modules/emittery": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz",
+ "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/emojis-list": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
+ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
+ "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/error-stack-parser": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
+ "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "stackframe": "^1.3.4"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
+ "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-array-method-boxes-properly": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
+ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
+ "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.3",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.6",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.4",
+ "safe-array-concat": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/escodegen/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-react-app": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz",
+ "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.16.0",
+ "@babel/eslint-parser": "^7.16.3",
+ "@rushstack/eslint-patch": "^1.1.0",
+ "@typescript-eslint/eslint-plugin": "^5.5.0",
+ "@typescript-eslint/parser": "^5.5.0",
+ "babel-preset-react-app": "^10.0.1",
+ "confusing-browser-globals": "^1.0.11",
+ "eslint-plugin-flowtype": "^8.0.3",
+ "eslint-plugin-import": "^2.25.3",
+ "eslint-plugin-jest": "^25.3.0",
+ "eslint-plugin-jsx-a11y": "^6.5.1",
+ "eslint-plugin-react": "^7.27.1",
+ "eslint-plugin-react-hooks": "^4.3.0",
+ "eslint-plugin-testing-library": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^8.0.0"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
+ "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-flowtype": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz",
+ "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "lodash": "^4.17.21",
+ "string-natural-compare": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "@babel/plugin-syntax-flow": "^7.14.5",
+ "@babel/plugin-transform-react-jsx": "^7.14.9",
+ "eslint": "^8.1.0"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.32.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
+ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.9",
+ "array.prototype.findlastindex": "^1.2.6",
+ "array.prototype.flat": "^1.3.3",
+ "array.prototype.flatmap": "^1.3.3",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.12.1",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.16.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.1",
+ "semver": "^6.3.1",
+ "string.prototype.trimend": "^1.0.9",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-jest": {
+ "version": "25.7.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz",
+ "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/experimental-utils": "^5.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@typescript-eslint/eslint-plugin": {
+ "optional": true
+ },
+ "jest": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.10.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
+ "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "aria-query": "^5.3.2",
+ "array-includes": "^3.1.8",
+ "array.prototype.flatmap": "^1.3.2",
+ "ast-types-flow": "^0.0.8",
+ "axe-core": "^4.10.0",
+ "axobject-query": "^4.1.0",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^3.3.5",
+ "language-tags": "^1.0.9",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.includes": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
+ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.9",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
+ "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.5",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
+ "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-testing-library": {
+ "version": "5.11.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz",
+ "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==",
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/utils": "^5.58.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "eslint": "^7.5.0 || ^8.0.0"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-webpack-plugin": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz",
+ "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint": "^7.29.0 || ^8.4.1",
+ "jest-worker": "^28.0.2",
+ "micromatch": "^4.0.5",
+ "normalize-path": "^3.0.0",
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0",
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/eslint-webpack-plugin/node_modules/jest-worker": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz",
+ "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/eslint-webpack-plugin/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/eslint/node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/eslint/node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
+ "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
+ "license": "MIT"
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "license": "MIT"
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expect": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz",
+ "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^27.5.1",
+ "jest-get-type": "^27.5.1",
+ "jest-matcher-utils": "^27.5.1",
+ "jest-message-util": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/faye-websocket": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
+ "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/fb-watchman": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
+ "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bser": "2.1.1"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/file-loader": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
+ "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
+ "license": "MIT",
+ "dependencies": {
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^4.0.0 || ^5.0.0"
+ }
+ },
+ "node_modules/file-loader/node_modules/schema-utils": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
+ "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.8",
+ "ajv": "^6.12.5",
+ "ajv-keywords": "^3.5.2"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/filelist": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
+ "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/filelist/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/filelist/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/filesize": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
+ "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/find-cache-dir": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
+ "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+ "license": "MIT",
+ "dependencies": {
+ "commondir": "^1.0.1",
+ "make-dir": "^3.0.2",
+ "pkg-dir": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/fork-ts-checker-webpack-plugin": {
+ "version": "6.5.3",
+ "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz",
+ "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.8.3",
+ "@types/json-schema": "^7.0.5",
+ "chalk": "^4.1.0",
+ "chokidar": "^3.4.2",
+ "cosmiconfig": "^6.0.0",
+ "deepmerge": "^4.2.2",
+ "fs-extra": "^9.0.0",
+ "glob": "^7.1.6",
+ "memfs": "^3.1.2",
+ "minimatch": "^3.0.4",
+ "schema-utils": "2.7.0",
+ "semver": "^7.3.2",
+ "tapable": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=10",
+ "yarn": ">=1.0.0"
+ },
+ "peerDependencies": {
+ "eslint": ">= 6",
+ "typescript": ">= 2.7",
+ "vue-template-compiler": "*",
+ "webpack": ">= 4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ },
+ "vue-template-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
+ "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.1.0",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.7.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
+ "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.4",
+ "ajv": "^6.12.2",
+ "ajv-keywords": "^3.4.1"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
+ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz",
+ "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/fs-monkey": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz",
+ "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==",
+ "license": "Unlicense"
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-own-enumerable-property-symbols": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
+ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
+ "license": "ISC"
+ },
+ "node_modules/get-package-type": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
+ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/global-modules": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
+ "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
+ "license": "MIT",
+ "dependencies": {
+ "global-prefix": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
+ "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
+ "license": "MIT",
+ "dependencies": {
+ "ini": "^1.3.5",
+ "kind-of": "^6.0.2",
+ "which": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/global-prefix/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "license": "MIT"
+ },
+ "node_modules/gzip-size": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
+ "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "duplexer": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/handle-thing": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
+ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==",
+ "license": "MIT"
+ },
+ "node_modules/harmony-reflect": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz",
+ "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==",
+ "license": "(Apache-2.0 OR MPL-1.1)"
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/hoopy": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
+ "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/hpack.js": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+ "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "obuf": "^1.0.0",
+ "readable-stream": "^2.0.1",
+ "wbuf": "^1.1.0"
+ }
+ },
+ "node_modules/hpack.js/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/hpack.js/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/hpack.js/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/hpack.js/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
+ "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/html-entities": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
+ "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/mdevils"
+ },
+ {
+ "type": "patreon",
+ "url": "https://patreon.com/mdevils"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "license": "MIT"
+ },
+ "node_modules/html-minifier-terser": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
+ "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==",
+ "license": "MIT",
+ "dependencies": {
+ "camel-case": "^4.1.2",
+ "clean-css": "^5.2.2",
+ "commander": "^8.3.0",
+ "he": "^1.2.0",
+ "param-case": "^3.0.4",
+ "relateurl": "^0.2.7",
+ "terser": "^5.10.0"
+ },
+ "bin": {
+ "html-minifier-terser": "cli.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/html-webpack-plugin": {
+ "version": "5.6.5",
+ "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.5.tgz",
+ "integrity": "sha512-4xynFbKNNk+WlzXeQQ+6YYsH2g7mpfPszQZUi3ovKlj+pDmngQ7vRXjrrmGROabmKwyQkcgcX5hqfOwHbFmK5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/html-minifier-terser": "^6.0.0",
+ "html-minifier-terser": "^6.0.2",
+ "lodash": "^4.17.21",
+ "pretty-error": "^4.0.0",
+ "tapable": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/html-webpack-plugin"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || 1.x",
+ "webpack": "^5.20.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
+ "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.0.0",
+ "domutils": "^2.5.2",
+ "entities": "^2.0.0"
+ }
+ },
+ "node_modules/http-deceiver": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+ "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==",
+ "license": "MIT"
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/http-parser-js": {
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
+ "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
+ "license": "MIT"
+ },
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
+ "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/once": "1",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/http-proxy-middleware": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
+ "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-proxy": "^1.17.8",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "is-plain-obj": "^3.0.0",
+ "micromatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "@types/express": "^4.17.13"
+ },
+ "peerDependenciesMeta": {
+ "@types/express": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/icss-utils": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+ "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/idb": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
+ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
+ "license": "ISC"
+ },
+ "node_modules/identity-obj-proxy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
+ "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==",
+ "license": "MIT",
+ "dependencies": {
+ "harmony-reflect": "^1.4.6"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immer": {
+ "version": "9.0.21",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
+ "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-fresh/node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
+ "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
+ "license": "MIT",
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
+ "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "license": "MIT"
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
+ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+ "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
+ "license": "MIT"
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
+ "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+ "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regexp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
+ "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-root": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz",
+ "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
+ "license": "MIT"
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
+ "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
+ "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/iterator.prototype": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+ "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "get-proto": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/jake": {
+ "version": "10.9.4",
+ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
+ "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.6",
+ "filelist": "^1.0.4",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jest": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
+ "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "^27.5.1",
+ "import-local": "^3.0.2",
+ "jest-cli": "^27.5.1"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-changed-files": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz",
+ "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^27.5.1",
+ "execa": "^5.0.0",
+ "throat": "^6.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-circus": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz",
+ "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^27.5.1",
+ "@jest/test-result": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "co": "^4.6.0",
+ "dedent": "^0.7.0",
+ "expect": "^27.5.1",
+ "is-generator-fn": "^2.0.0",
+ "jest-each": "^27.5.1",
+ "jest-matcher-utils": "^27.5.1",
+ "jest-message-util": "^27.5.1",
+ "jest-runtime": "^27.5.1",
+ "jest-snapshot": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "pretty-format": "^27.5.1",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3",
+ "throat": "^6.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-cli": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz",
+ "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/core": "^27.5.1",
+ "@jest/test-result": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "chalk": "^4.0.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "import-local": "^3.0.2",
+ "jest-config": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "jest-validate": "^27.5.1",
+ "prompts": "^2.0.1",
+ "yargs": "^16.2.0"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-config": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz",
+ "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.8.0",
+ "@jest/test-sequencer": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "babel-jest": "^27.5.1",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "deepmerge": "^4.2.2",
+ "glob": "^7.1.1",
+ "graceful-fs": "^4.2.9",
+ "jest-circus": "^27.5.1",
+ "jest-environment-jsdom": "^27.5.1",
+ "jest-environment-node": "^27.5.1",
+ "jest-get-type": "^27.5.1",
+ "jest-jasmine2": "^27.5.1",
+ "jest-regex-util": "^27.5.1",
+ "jest-resolve": "^27.5.1",
+ "jest-runner": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "jest-validate": "^27.5.1",
+ "micromatch": "^4.0.4",
+ "parse-json": "^5.2.0",
+ "pretty-format": "^27.5.1",
+ "slash": "^3.0.0",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ },
+ "peerDependencies": {
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-diff": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
+ "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^27.5.1",
+ "jest-get-type": "^27.5.1",
+ "pretty-format": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-docblock": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz",
+ "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-each": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz",
+ "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^27.5.1",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "pretty-format": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz",
+ "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^27.5.1",
+ "@jest/fake-timers": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/node": "*",
+ "jest-mock": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "jsdom": "^16.6.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-environment-node": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz",
+ "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^27.5.1",
+ "@jest/fake-timers": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/node": "*",
+ "jest-mock": "^27.5.1",
+ "jest-util": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-get-type": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz",
+ "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-haste-map": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz",
+ "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^27.5.1",
+ "@types/graceful-fs": "^4.1.2",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^27.5.1",
+ "jest-serializer": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "jest-worker": "^27.5.1",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.7"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/jest-jasmine2": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz",
+ "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^27.5.1",
+ "@jest/source-map": "^27.5.1",
+ "@jest/test-result": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "co": "^4.6.0",
+ "expect": "^27.5.1",
+ "is-generator-fn": "^2.0.0",
+ "jest-each": "^27.5.1",
+ "jest-matcher-utils": "^27.5.1",
+ "jest-message-util": "^27.5.1",
+ "jest-runtime": "^27.5.1",
+ "jest-snapshot": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "pretty-format": "^27.5.1",
+ "throat": "^6.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-leak-detector": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz",
+ "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^27.5.1",
+ "pretty-format": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz",
+ "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^27.5.1",
+ "jest-get-type": "^27.5.1",
+ "pretty-format": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-message-util": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz",
+ "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^27.5.1",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^27.5.1",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-mock": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
+ "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^27.5.1",
+ "@types/node": "*"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-pnp-resolver": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+ "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "jest-resolve": "*"
+ },
+ "peerDependenciesMeta": {
+ "jest-resolve": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-regex-util": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz",
+ "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-resolve": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz",
+ "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^27.5.1",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^27.5.1",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^27.5.1",
+ "jest-validate": "^27.5.1",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^1.1.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz",
+ "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^27.5.1",
+ "jest-regex-util": "^27.5.1",
+ "jest-snapshot": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-runner": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz",
+ "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^27.5.1",
+ "@jest/environment": "^27.5.1",
+ "@jest/test-result": "^27.5.1",
+ "@jest/transform": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "emittery": "^0.8.1",
+ "graceful-fs": "^4.2.9",
+ "jest-docblock": "^27.5.1",
+ "jest-environment-jsdom": "^27.5.1",
+ "jest-environment-node": "^27.5.1",
+ "jest-haste-map": "^27.5.1",
+ "jest-leak-detector": "^27.5.1",
+ "jest-message-util": "^27.5.1",
+ "jest-resolve": "^27.5.1",
+ "jest-runtime": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "jest-worker": "^27.5.1",
+ "source-map-support": "^0.5.6",
+ "throat": "^6.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-runtime": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz",
+ "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^27.5.1",
+ "@jest/fake-timers": "^27.5.1",
+ "@jest/globals": "^27.5.1",
+ "@jest/source-map": "^27.5.1",
+ "@jest/test-result": "^27.5.1",
+ "@jest/transform": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "execa": "^5.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^27.5.1",
+ "jest-message-util": "^27.5.1",
+ "jest-mock": "^27.5.1",
+ "jest-regex-util": "^27.5.1",
+ "jest-resolve": "^27.5.1",
+ "jest-snapshot": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-serializer": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz",
+ "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "graceful-fs": "^4.2.9"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-snapshot": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz",
+ "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.7.2",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/traverse": "^7.7.2",
+ "@babel/types": "^7.0.0",
+ "@jest/transform": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/babel__traverse": "^7.0.4",
+ "@types/prettier": "^2.1.5",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^27.5.1",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^27.5.1",
+ "jest-get-type": "^27.5.1",
+ "jest-haste-map": "^27.5.1",
+ "jest-matcher-utils": "^27.5.1",
+ "jest-message-util": "^27.5.1",
+ "jest-util": "^27.5.1",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^27.5.1",
+ "semver": "^7.3.2"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-util": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz",
+ "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^27.5.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-validate": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz",
+ "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^27.5.1",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^27.5.1",
+ "leven": "^3.1.0",
+ "pretty-format": "^27.5.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-watch-typeahead": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz",
+ "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^4.3.1",
+ "chalk": "^4.0.0",
+ "jest-regex-util": "^28.0.0",
+ "jest-watcher": "^28.0.0",
+ "slash": "^4.0.0",
+ "string-length": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "jest": "^27.0.0 || ^28.0.0"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/@jest/console": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz",
+ "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/@jest/console/node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/@jest/test-result": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz",
+ "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/console": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/@jest/types": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz",
+ "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^28.1.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/@types/yargs": {
+ "version": "17.0.35",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
+ "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/emittery": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz",
+ "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/jest-message-util": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz",
+ "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^28.1.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^28.1.3",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/jest-message-util/node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/jest-regex-util": {
+ "version": "28.0.2",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz",
+ "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/jest-util": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz",
+ "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/jest-watcher": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz",
+ "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "emittery": "^0.10.2",
+ "jest-util": "^28.1.3",
+ "string-length": "^4.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/string-length": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
+ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
+ "license": "MIT",
+ "dependencies": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/pretty-format": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz",
+ "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^28.1.3",
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "license": "MIT"
+ },
+ "node_modules/jest-watch-typeahead/node_modules/slash": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
+ "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/string-length": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz",
+ "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==",
+ "license": "MIT",
+ "dependencies": {
+ "char-regex": "^2.0.0",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/string-length/node_modules/char-regex": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz",
+ "integrity": "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/jest-watcher": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz",
+ "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/test-result": "^27.5.1",
+ "@jest/types": "^27.5.1",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "jest-util": "^27.5.1",
+ "string-length": "^4.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "16.7.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
+ "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==",
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.5",
+ "acorn": "^8.2.4",
+ "acorn-globals": "^6.0.0",
+ "cssom": "^0.4.4",
+ "cssstyle": "^2.3.0",
+ "data-urls": "^2.0.0",
+ "decimal.js": "^10.2.1",
+ "domexception": "^2.0.1",
+ "escodegen": "^2.0.0",
+ "form-data": "^3.0.0",
+ "html-encoding-sniffer": "^2.0.1",
+ "http-proxy-agent": "^4.0.1",
+ "https-proxy-agent": "^5.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.0",
+ "parse5": "6.0.1",
+ "saxes": "^5.0.1",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.0.0",
+ "w3c-hr-time": "^1.0.2",
+ "w3c-xmlserializer": "^2.0.0",
+ "webidl-conversions": "^6.1.0",
+ "whatwg-encoding": "^1.0.5",
+ "whatwg-mimetype": "^2.3.0",
+ "whatwg-url": "^8.5.0",
+ "ws": "^7.4.6",
+ "xml-name-validator": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "license": "MIT"
+ },
+ "node_modules/json-schema": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
+ "license": "(AFL-2.1 OR BSD-3-Clause)"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonpath": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz",
+ "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==",
+ "license": "MIT",
+ "dependencies": {
+ "esprima": "1.2.2",
+ "static-eval": "2.0.2",
+ "underscore": "1.12.1"
+ }
+ },
+ "node_modules/jsonpath/node_modules/esprima": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz",
+ "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/jsonpointer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
+ "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/klona": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
+ "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.23",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+ "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+ "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
+ "license": "MIT",
+ "dependencies": {
+ "language-subtag-registry": "^0.3.20"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/launch-editor": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz",
+ "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^1.1.1",
+ "shell-quote": "^1.8.3"
+ }
+ },
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+ "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/loader-runner": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
+ "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.11.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/loader-utils": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
+ "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
+ "license": "MIT",
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.memoize": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.sortby": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
+ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.uniq": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lower-case": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
+ "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.263.1",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.263.1.tgz",
+ "integrity": "sha512-keqxAx97PlaEN89PXZ6ki1N8nRjGWtDa4021GFYLNj0RgruM5odbpl8GHTExj0hhPq3sF6Up0gnxt6TSHu+ovw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
+ "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+ "license": "MIT",
+ "dependencies": {
+ "sourcemap-codec": "^1.4.8"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/makeerror": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
+ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tmpl": "1.0.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
+ "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/memfs": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz",
+ "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==",
+ "license": "Unlicense",
+ "dependencies": {
+ "fs-monkey": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mini-css-extract-plugin": {
+ "version": "2.9.4",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz",
+ "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "schema-utils": "^4.0.0",
+ "tapable": "^2.2.1"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "license": "ISC"
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/multicast-dns": {
+ "version": "7.2.5",
+ "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
+ "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
+ "license": "MIT",
+ "dependencies": {
+ "dns-packet": "^5.2.2",
+ "thunky": "^1.0.2"
+ },
+ "bin": {
+ "multicast-dns": "cli.js"
+ }
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "license": "MIT"
+ },
+ "node_modules/natural-compare-lite": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "license": "MIT"
+ },
+ "node_modules/no-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
+ "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
+ "license": "MIT",
+ "dependencies": {
+ "lower-case": "^2.0.2",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/node-forge": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
+ "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
+ "license": "(BSD-3-Clause OR GPL-2.0)",
+ "engines": {
+ "node": ">= 6.13.0"
+ }
+ },
+ "node_modules/node-int64": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
+ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-url": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
+ "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
+ "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.getownpropertydescriptors": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz",
+ "integrity": "sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A==",
+ "license": "MIT",
+ "dependencies": {
+ "array.prototype.reduce": "^1.0.6",
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0",
+ "gopd": "^1.0.1",
+ "safe-array-concat": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+ "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/obuf": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
+ "license": "MIT"
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/open": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-retry": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
+ "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "0.12.0",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/param-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
+ "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
+ "license": "MIT",
+ "dependencies": {
+ "dot-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+ "license": "MIT"
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/pascal-case": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
+ "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-up": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz",
+ "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-up/node_modules/find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pkg-up/node_modules/locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pkg-up/node_modules/p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pkg-up/node_modules/path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
+ "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.57.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
+ "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-attribute-case-insensitive": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz",
+ "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.10"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-browser-comments": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz",
+ "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==",
+ "license": "CC0-1.0",
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "browserslist": ">=4",
+ "postcss": ">=8"
+ }
+ },
+ "node_modules/postcss-calc": {
+ "version": "8.2.4",
+ "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz",
+ "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.9",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.2"
+ }
+ },
+ "node_modules/postcss-clamp": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz",
+ "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=7.6.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.6"
+ }
+ },
+ "node_modules/postcss-color-functional-notation": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz",
+ "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-color-hex-alpha": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz",
+ "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
+ "node_modules/postcss-color-rebeccapurple": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz",
+ "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-colormin": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz",
+ "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "caniuse-api": "^3.0.0",
+ "colord": "^2.9.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-convert-values": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz",
+ "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==",
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-custom-media": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz",
+ "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3"
+ }
+ },
+ "node_modules/postcss-custom-properties": {
+ "version": "12.1.11",
+ "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz",
+ "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-custom-selectors": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz",
+ "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.4"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3"
+ }
+ },
+ "node_modules/postcss-dir-pseudo-class": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz",
+ "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.10"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-discard-comments": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz",
+ "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-discard-duplicates": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz",
+ "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-discard-empty": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz",
+ "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-discard-overridden": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz",
+ "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-double-position-gradients": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz",
+ "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@csstools/postcss-progressive-custom-properties": "^1.1.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-env-function": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz",
+ "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
+ "node_modules/postcss-flexbugs-fixes": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz",
+ "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "postcss": "^8.1.4"
+ }
+ },
+ "node_modules/postcss-focus-visible": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz",
+ "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.9"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
+ "node_modules/postcss-focus-within": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz",
+ "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.9"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
+ "node_modules/postcss-font-variant": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz",
+ "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-gap-properties": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz",
+ "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==",
+ "license": "CC0-1.0",
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-image-set-function": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz",
+ "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-initial": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz",
+ "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-lab-function": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz",
+ "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@csstools/postcss-progressive-custom-properties": "^1.1.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-loader": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz",
+ "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "cosmiconfig": "^7.0.0",
+ "klona": "^2.0.5",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "postcss": "^7.0.0 || ^8.0.1",
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/postcss-logical": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz",
+ "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==",
+ "license": "CC0-1.0",
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
+ "node_modules/postcss-media-minmax": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz",
+ "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-merge-longhand": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz",
+ "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0",
+ "stylehacks": "^5.1.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-merge-rules": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz",
+ "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==",
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "caniuse-api": "^3.0.0",
+ "cssnano-utils": "^3.1.0",
+ "postcss-selector-parser": "^6.0.5"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-minify-font-values": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz",
+ "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-minify-gradients": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz",
+ "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==",
+ "license": "MIT",
+ "dependencies": {
+ "colord": "^2.9.1",
+ "cssnano-utils": "^3.1.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-minify-params": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz",
+ "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==",
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "cssnano-utils": "^3.1.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-minify-selectors": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz",
+ "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.5"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-modules-extract-imports": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
+ "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz",
+ "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==",
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.0.0",
+ "postcss-selector-parser": "^7.0.0",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
+ "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-modules-scope": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
+ "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
+ "license": "ISC",
+ "dependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
+ "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-modules-values": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+ "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+ "license": "ISC",
+ "dependencies": {
+ "icss-utils": "^5.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-nesting": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz",
+ "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@csstools/selector-specificity": "^2.0.0",
+ "postcss-selector-parser": "^6.0.10"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-normalize": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz",
+ "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@csstools/normalize.css": "*",
+ "postcss-browser-comments": "^4",
+ "sanitize.css": "*"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4",
+ "postcss": ">= 8"
+ }
+ },
+ "node_modules/postcss-normalize-charset": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz",
+ "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-display-values": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz",
+ "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-positions": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz",
+ "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-repeat-style": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz",
+ "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-string": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz",
+ "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-timing-functions": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz",
+ "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-unicode": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz",
+ "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-url": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz",
+ "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==",
+ "license": "MIT",
+ "dependencies": {
+ "normalize-url": "^6.0.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-whitespace": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz",
+ "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-opacity-percentage": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz",
+ "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==",
+ "funding": [
+ {
+ "type": "kofi",
+ "url": "https://ko-fi.com/mrcgrtz"
+ },
+ {
+ "type": "liberapay",
+ "url": "https://liberapay.com/mrcgrtz"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-ordered-values": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz",
+ "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cssnano-utils": "^3.1.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-overflow-shorthand": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz",
+ "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-page-break": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz",
+ "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "postcss": "^8"
+ }
+ },
+ "node_modules/postcss-place": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz",
+ "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-preset-env": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz",
+ "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@csstools/postcss-cascade-layers": "^1.1.1",
+ "@csstools/postcss-color-function": "^1.1.1",
+ "@csstools/postcss-font-format-keywords": "^1.0.1",
+ "@csstools/postcss-hwb-function": "^1.0.2",
+ "@csstools/postcss-ic-unit": "^1.0.1",
+ "@csstools/postcss-is-pseudo-class": "^2.0.7",
+ "@csstools/postcss-nested-calc": "^1.0.0",
+ "@csstools/postcss-normalize-display-values": "^1.0.1",
+ "@csstools/postcss-oklab-function": "^1.1.1",
+ "@csstools/postcss-progressive-custom-properties": "^1.3.0",
+ "@csstools/postcss-stepped-value-functions": "^1.0.1",
+ "@csstools/postcss-text-decoration-shorthand": "^1.0.0",
+ "@csstools/postcss-trigonometric-functions": "^1.0.2",
+ "@csstools/postcss-unset-value": "^1.0.2",
+ "autoprefixer": "^10.4.13",
+ "browserslist": "^4.21.4",
+ "css-blank-pseudo": "^3.0.3",
+ "css-has-pseudo": "^3.0.4",
+ "css-prefers-color-scheme": "^6.0.3",
+ "cssdb": "^7.1.0",
+ "postcss-attribute-case-insensitive": "^5.0.2",
+ "postcss-clamp": "^4.1.0",
+ "postcss-color-functional-notation": "^4.2.4",
+ "postcss-color-hex-alpha": "^8.0.4",
+ "postcss-color-rebeccapurple": "^7.1.1",
+ "postcss-custom-media": "^8.0.2",
+ "postcss-custom-properties": "^12.1.10",
+ "postcss-custom-selectors": "^6.0.3",
+ "postcss-dir-pseudo-class": "^6.0.5",
+ "postcss-double-position-gradients": "^3.1.2",
+ "postcss-env-function": "^4.0.6",
+ "postcss-focus-visible": "^6.0.4",
+ "postcss-focus-within": "^5.0.4",
+ "postcss-font-variant": "^5.0.0",
+ "postcss-gap-properties": "^3.0.5",
+ "postcss-image-set-function": "^4.0.7",
+ "postcss-initial": "^4.0.1",
+ "postcss-lab-function": "^4.2.1",
+ "postcss-logical": "^5.0.4",
+ "postcss-media-minmax": "^5.0.0",
+ "postcss-nesting": "^10.2.0",
+ "postcss-opacity-percentage": "^1.1.2",
+ "postcss-overflow-shorthand": "^3.0.4",
+ "postcss-page-break": "^3.0.4",
+ "postcss-place": "^7.0.5",
+ "postcss-pseudo-class-any-link": "^7.1.6",
+ "postcss-replace-overflow-wrap": "^4.0.0",
+ "postcss-selector-not": "^6.0.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-pseudo-class-any-link": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz",
+ "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.10"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-reduce-initial": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz",
+ "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==",
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "caniuse-api": "^3.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-reduce-transforms": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz",
+ "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-replace-overflow-wrap": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz",
+ "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "postcss": "^8.0.3"
+ }
+ },
+ "node_modules/postcss-selector-not": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz",
+ "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.10"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-svgo": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz",
+ "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0",
+ "svgo": "^2.7.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-svgo/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/postcss-svgo/node_modules/css-tree": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
+ "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.14",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/postcss-svgo/node_modules/mdn-data": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
+ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/postcss-svgo/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postcss-svgo/node_modules/svgo": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz",
+ "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==",
+ "license": "MIT",
+ "dependencies": {
+ "@trysound/sax": "0.2.0",
+ "commander": "^7.2.0",
+ "css-select": "^4.1.3",
+ "css-tree": "^1.1.3",
+ "csso": "^4.2.0",
+ "picocolors": "^1.0.0",
+ "stable": "^0.1.8"
+ },
+ "bin": {
+ "svgo": "bin/svgo"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/postcss-unique-selectors": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz",
+ "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.5"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/pretty-bytes": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pretty-error": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
+ "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.20",
+ "renderkid": "^3.0.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
+ "node_modules/promise": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz",
+ "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==",
+ "license": "MIT",
+ "dependencies": {
+ "asap": "~2.0.6"
+ }
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-addr/node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/psl": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
+ "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/lupomontero"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/q": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+ "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
+ "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.0",
+ "teleport": ">=0.2.0"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/raf": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+ "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+ "license": "MIT",
+ "dependencies": {
+ "performance-now": "^2.1.0"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-app-polyfill": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz",
+ "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==",
+ "license": "MIT",
+ "dependencies": {
+ "core-js": "^3.19.2",
+ "object-assign": "^4.1.1",
+ "promise": "^8.1.0",
+ "raf": "^3.4.1",
+ "regenerator-runtime": "^0.13.9",
+ "whatwg-fetch": "^3.6.2"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/react-dev-utils": {
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
+ "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.16.0",
+ "address": "^1.1.2",
+ "browserslist": "^4.18.1",
+ "chalk": "^4.1.2",
+ "cross-spawn": "^7.0.3",
+ "detect-port-alt": "^1.1.6",
+ "escape-string-regexp": "^4.0.0",
+ "filesize": "^8.0.6",
+ "find-up": "^5.0.0",
+ "fork-ts-checker-webpack-plugin": "^6.5.0",
+ "global-modules": "^2.0.0",
+ "globby": "^11.0.4",
+ "gzip-size": "^6.0.0",
+ "immer": "^9.0.7",
+ "is-root": "^2.1.0",
+ "loader-utils": "^3.2.0",
+ "open": "^8.4.0",
+ "pkg-up": "^3.1.0",
+ "prompts": "^2.4.2",
+ "react-error-overlay": "^6.0.11",
+ "recursive-readdir": "^2.2.2",
+ "shell-quote": "^1.7.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/react-dev-utils/node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/react-dev-utils/node_modules/loader-utils": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz",
+ "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
+ "node_modules/react-dev-utils/node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/react-dev-utils/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/react-dev-utils/node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-error-overlay": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz",
+ "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "license": "MIT"
+ },
+ "node_modules/react-refresh": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
+ "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-scripts": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
+ "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.16.0",
+ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
+ "@svgr/webpack": "^5.5.0",
+ "babel-jest": "^27.4.2",
+ "babel-loader": "^8.2.3",
+ "babel-plugin-named-asset-import": "^0.3.8",
+ "babel-preset-react-app": "^10.0.1",
+ "bfj": "^7.0.2",
+ "browserslist": "^4.18.1",
+ "camelcase": "^6.2.1",
+ "case-sensitive-paths-webpack-plugin": "^2.4.0",
+ "css-loader": "^6.5.1",
+ "css-minimizer-webpack-plugin": "^3.2.0",
+ "dotenv": "^10.0.0",
+ "dotenv-expand": "^5.1.0",
+ "eslint": "^8.3.0",
+ "eslint-config-react-app": "^7.0.1",
+ "eslint-webpack-plugin": "^3.1.1",
+ "file-loader": "^6.2.0",
+ "fs-extra": "^10.0.0",
+ "html-webpack-plugin": "^5.5.0",
+ "identity-obj-proxy": "^3.0.0",
+ "jest": "^27.4.3",
+ "jest-resolve": "^27.4.2",
+ "jest-watch-typeahead": "^1.0.0",
+ "mini-css-extract-plugin": "^2.4.5",
+ "postcss": "^8.4.4",
+ "postcss-flexbugs-fixes": "^5.0.2",
+ "postcss-loader": "^6.2.1",
+ "postcss-normalize": "^10.0.1",
+ "postcss-preset-env": "^7.0.1",
+ "prompts": "^2.4.2",
+ "react-app-polyfill": "^3.0.0",
+ "react-dev-utils": "^12.0.1",
+ "react-refresh": "^0.11.0",
+ "resolve": "^1.20.0",
+ "resolve-url-loader": "^4.0.0",
+ "sass-loader": "^12.3.0",
+ "semver": "^7.3.5",
+ "source-map-loader": "^3.0.0",
+ "style-loader": "^3.3.1",
+ "tailwindcss": "^3.0.2",
+ "terser-webpack-plugin": "^5.2.5",
+ "webpack": "^5.64.4",
+ "webpack-dev-server": "^4.6.0",
+ "webpack-manifest-plugin": "^4.0.2",
+ "workbox-webpack-plugin": "^6.4.1"
+ },
+ "bin": {
+ "react-scripts": "bin/react-scripts.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ },
+ "peerDependencies": {
+ "react": ">= 16",
+ "typescript": "^3.2.1 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/reactflow": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
+ "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/background": "11.3.14",
+ "@reactflow/controls": "11.2.14",
+ "@reactflow/core": "11.11.4",
+ "@reactflow/minimap": "11.7.14",
+ "@reactflow/node-resizer": "2.2.14",
+ "@reactflow/node-toolbar": "1.3.14"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/recursive-readdir": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
+ "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==",
+ "license": "MIT",
+ "dependencies": {
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regenerate": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
+ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
+ "license": "MIT"
+ },
+ "node_modules/regenerate-unicode-properties": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz",
+ "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "license": "MIT"
+ },
+ "node_modules/regex-parser": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz",
+ "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==",
+ "license": "MIT"
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpu-core": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
+ "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2",
+ "regenerate-unicode-properties": "^10.2.2",
+ "regjsgen": "^0.8.0",
+ "regjsparser": "^0.13.0",
+ "unicode-match-property-ecmascript": "^2.0.0",
+ "unicode-match-property-value-ecmascript": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regjsgen": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
+ "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
+ "license": "MIT"
+ },
+ "node_modules/regjsparser": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz",
+ "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "jsesc": "~3.1.0"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
+ "node_modules/relateurl": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+ "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/renderkid": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
+ "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
+ "license": "MIT",
+ "dependencies": {
+ "css-select": "^4.1.3",
+ "dom-converter": "^0.2.0",
+ "htmlparser2": "^6.1.0",
+ "lodash": "^4.17.21",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "license": "MIT"
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-url-loader": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz",
+ "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==",
+ "license": "MIT",
+ "dependencies": {
+ "adjust-sourcemap-loader": "^4.0.0",
+ "convert-source-map": "^1.7.0",
+ "loader-utils": "^2.0.0",
+ "postcss": "^7.0.35",
+ "source-map": "0.6.1"
+ },
+ "engines": {
+ "node": ">=8.9"
+ },
+ "peerDependencies": {
+ "rework": "1.0.1",
+ "rework-visit": "1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rework": {
+ "optional": true
+ },
+ "rework-visit": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/resolve-url-loader/node_modules/picocolors": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
+ "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
+ "license": "ISC"
+ },
+ "node_modules/resolve-url-loader/node_modules/postcss": {
+ "version": "7.0.39",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
+ "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/resolve-url-loader/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve.exports": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz",
+ "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "2.79.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
+ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
+ "license": "MIT",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup-plugin-terser": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
+ "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==",
+ "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "jest-worker": "^26.2.1",
+ "serialize-javascript": "^4.0.0",
+ "terser": "^5.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.0.0"
+ }
+ },
+ "node_modules/rollup-plugin-terser/node_modules/jest-worker": {
+ "version": "26.6.2",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
+ "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
+ "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/sanitize.css": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
+ "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/sass-loader": {
+ "version": "12.6.0",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
+ "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==",
+ "license": "MIT",
+ "dependencies": {
+ "klona": "^2.0.4",
+ "neo-async": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "fibers": ">= 3.1.0",
+ "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
+ "sass": "^1.3.0",
+ "sass-embedded": "*",
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "fibers": {
+ "optional": true
+ },
+ "node-sass": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+ "license": "ISC"
+ },
+ "node_modules/saxes": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+ "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/schema-utils": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/schema-utils/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/select-hose": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==",
+ "license": "MIT"
+ },
+ "node_modules/selfsigned": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz",
+ "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node-forge": "^1.3.0",
+ "node-forge": "^1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz",
+ "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/send/node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/serve-index": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
+ "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "batch": "0.6.1",
+ "debug": "2.6.9",
+ "escape-html": "~1.0.3",
+ "http-errors": "~1.6.2",
+ "mime-types": "~2.1.17",
+ "parseurl": "~1.3.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-index/node_modules/http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-index/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
+ "license": "ISC"
+ },
+ "node_modules/serve-index/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/serve-index/node_modules/setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
+ "license": "ISC"
+ },
+ "node_modules/serve-index/node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/serve-static/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/serve-static/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static/node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/serve-static/node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/serve-static/node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/serve-static/node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "license": "MIT"
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sockjs": {
+ "version": "0.3.24",
+ "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
+ "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "faye-websocket": "^0.11.3",
+ "uuid": "^8.3.2",
+ "websocket-driver": "^0.7.4"
+ }
+ },
+ "node_modules/source-list-map": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
+ "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
+ "license": "MIT"
+ },
+ "node_modules/source-map": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+ "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-loader": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz",
+ "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==",
+ "license": "MIT",
+ "dependencies": {
+ "abab": "^2.0.5",
+ "iconv-lite": "^0.6.3",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/source-map-support/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+ "deprecated": "Please use @jridgewell/sourcemap-codec instead",
+ "license": "MIT"
+ },
+ "node_modules/spdy": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
+ "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "handle-thing": "^2.0.0",
+ "http-deceiver": "^1.2.7",
+ "select-hose": "^2.0.0",
+ "spdy-transport": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/spdy-transport": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
+ "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "detect-node": "^2.0.4",
+ "hpack.js": "^2.1.6",
+ "obuf": "^1.1.2",
+ "readable-stream": "^3.0.6",
+ "wbuf": "^1.7.3"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/stable": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
+ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
+ "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility",
+ "license": "MIT"
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+ "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stackframe": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
+ "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
+ "license": "MIT"
+ },
+ "node_modules/static-eval": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz",
+ "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==",
+ "license": "MIT",
+ "dependencies": {
+ "escodegen": "^1.8.1"
+ }
+ },
+ "node_modules/static-eval/node_modules/escodegen": {
+ "version": "1.14.3",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
+ "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^4.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/static-eval/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/static-eval/node_modules/levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/static-eval/node_modules/optionator": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/static-eval/node_modules/prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/static-eval/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/static-eval/node_modules/type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-length": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
+ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
+ "license": "MIT",
+ "dependencies": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/string-natural-compare": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz",
+ "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==",
+ "license": "MIT"
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/string.prototype.includes": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
+ "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+ "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "regexp.prototype.flags": "^1.5.3",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-object-atoms": "^1.0.0",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/stringify-object": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
+ "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "get-own-enumerable-property-symbols": "^3.0.0",
+ "is-obj": "^1.0.1",
+ "is-regexp": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz",
+ "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/style-loader": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
+ "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/stylehacks": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
+ "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==",
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "postcss-selector-parser": "^6.0.4"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/sucrase/node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-hyperlinks": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
+ "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/svg-parser": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
+ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
+ "license": "MIT"
+ },
+ "node_modules/svgo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
+ "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==",
+ "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^2.4.1",
+ "coa": "^2.0.2",
+ "css-select": "^2.0.0",
+ "css-select-base-adapter": "^0.1.1",
+ "css-tree": "1.0.0-alpha.37",
+ "csso": "^4.0.2",
+ "js-yaml": "^3.13.1",
+ "mkdirp": "~0.5.1",
+ "object.values": "^1.1.0",
+ "sax": "~1.2.4",
+ "stable": "^0.1.8",
+ "unquote": "~1.1.1",
+ "util.promisify": "~1.0.0"
+ },
+ "bin": {
+ "svgo": "bin/svgo"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/svgo/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/svgo/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/svgo/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/svgo/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "license": "MIT"
+ },
+ "node_modules/svgo/node_modules/css-select": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
+ "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^3.2.1",
+ "domutils": "^1.7.0",
+ "nth-check": "^1.0.2"
+ }
+ },
+ "node_modules/svgo/node_modules/css-what": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz",
+ "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/svgo/node_modules/dom-serializer": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
+ "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "entities": "^2.0.0"
+ }
+ },
+ "node_modules/svgo/node_modules/domutils": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+ "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "0",
+ "domelementtype": "1"
+ }
+ },
+ "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+ "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/svgo/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/svgo/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/svgo/node_modules/nth-check": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
+ "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "~1.0.0"
+ }
+ },
+ "node_modules/svgo/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "license": "MIT"
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.18",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
+ "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tailwindcss/node_modules/yaml": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
+ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/temp-dir": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
+ "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tempy": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz",
+ "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-stream": "^2.0.0",
+ "temp-dir": "^2.0.0",
+ "type-fest": "^0.16.0",
+ "unique-string": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/tempy/node_modules/type-fest": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
+ "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/terminal-link": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
+ "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^4.2.1",
+ "supports-hyperlinks": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.44.1",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
+ "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.3.15",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.15.tgz",
+ "integrity": "sha512-PGkOdpRFK+rb1TzVz+msVhw4YMRT9txLF4kRqvJhGhCM324xuR3REBSHALN+l+sAhKUmz0aotnjp5D+P83mLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^4.3.0",
+ "serialize-javascript": "^6.0.2",
+ "terser": "^5.31.1"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "license": "MIT"
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "license": "MIT"
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/throat": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz",
+ "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==",
+ "license": "MIT"
+ },
+ "node_modules/thunky": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
+ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tmpl": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
+ "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tough-cookie/node_modules/universalify": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
+ "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tryer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
+ "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
+ "license": "MIT"
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tsconfig-paths/node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/tsconfig-paths/node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/tsutils/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0",
+ "reflect.getprototypeof": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "license": "MIT",
+ "dependencies": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/underscore": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz",
+ "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==",
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "license": "MIT"
+ },
+ "node_modules/unicode-canonical-property-names-ecmascript": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
+ "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-ecmascript": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
+ "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "unicode-canonical-property-names-ecmascript": "^2.0.0",
+ "unicode-property-aliases-ecmascript": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-value-ecmascript": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz",
+ "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-property-aliases-ecmascript": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
+ "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unique-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
+ "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
+ "license": "MIT",
+ "dependencies": {
+ "crypto-random-string": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/unquote": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
+ "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==",
+ "license": "MIT"
+ },
+ "node_modules/upath": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
+ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4",
+ "yarn": "*"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
+ "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/util.promisify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz",
+ "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==",
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.2",
+ "has-symbols": "^1.0.1",
+ "object.getownpropertydescriptors": "^2.1.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/utila": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
+ "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/v8-to-istanbul": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz",
+ "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==",
+ "license": "ISC",
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^1.6.0",
+ "source-map": "^0.7.3"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/v8-to-istanbul/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/w3c-hr-time": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
+ "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
+ "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.",
+ "license": "MIT",
+ "dependencies": {
+ "browser-process-hrtime": "^1.0.0"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
+ "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==",
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/walker": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "makeerror": "1.0.12"
+ }
+ },
+ "node_modules/watchpack": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
+ "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
+ "license": "MIT",
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/wbuf": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
+ "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
+ "license": "MIT",
+ "dependencies": {
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
+ "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=10.4"
+ }
+ },
+ "node_modules/webpack": {
+ "version": "5.103.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz",
+ "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint-scope": "^3.7.7",
+ "@types/estree": "^1.0.8",
+ "@types/json-schema": "^7.0.15",
+ "@webassemblyjs/ast": "^1.14.1",
+ "@webassemblyjs/wasm-edit": "^1.14.1",
+ "@webassemblyjs/wasm-parser": "^1.14.1",
+ "acorn": "^8.15.0",
+ "acorn-import-phases": "^1.0.3",
+ "browserslist": "^4.26.3",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.17.3",
+ "es-module-lexer": "^1.2.1",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.11",
+ "json-parse-even-better-errors": "^2.3.1",
+ "loader-runner": "^4.3.1",
+ "mime-types": "^2.1.27",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^4.3.3",
+ "tapable": "^2.3.0",
+ "terser-webpack-plugin": "^5.3.11",
+ "watchpack": "^2.4.4",
+ "webpack-sources": "^3.3.3"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-middleware": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz",
+ "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "colorette": "^2.0.10",
+ "memfs": "^3.4.3",
+ "mime-types": "^2.1.31",
+ "range-parser": "^1.2.1",
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^4.0.0 || ^5.0.0"
+ }
+ },
+ "node_modules/webpack-dev-server": {
+ "version": "4.15.2",
+ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
+ "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/bonjour": "^3.5.9",
+ "@types/connect-history-api-fallback": "^1.3.5",
+ "@types/express": "^4.17.13",
+ "@types/serve-index": "^1.9.1",
+ "@types/serve-static": "^1.13.10",
+ "@types/sockjs": "^0.3.33",
+ "@types/ws": "^8.5.5",
+ "ansi-html-community": "^0.0.8",
+ "bonjour-service": "^1.0.11",
+ "chokidar": "^3.5.3",
+ "colorette": "^2.0.10",
+ "compression": "^1.7.4",
+ "connect-history-api-fallback": "^2.0.0",
+ "default-gateway": "^6.0.3",
+ "express": "^4.17.3",
+ "graceful-fs": "^4.2.6",
+ "html-entities": "^2.3.2",
+ "http-proxy-middleware": "^2.0.3",
+ "ipaddr.js": "^2.0.1",
+ "launch-editor": "^2.6.0",
+ "open": "^8.0.9",
+ "p-retry": "^4.5.0",
+ "rimraf": "^3.0.2",
+ "schema-utils": "^4.0.0",
+ "selfsigned": "^2.1.1",
+ "serve-index": "^1.9.1",
+ "sockjs": "^0.3.24",
+ "spdy": "^4.0.2",
+ "webpack-dev-middleware": "^5.3.4",
+ "ws": "^8.13.0"
+ },
+ "bin": {
+ "webpack-dev-server": "bin/webpack-dev-server.js"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^4.37.0 || ^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack": {
+ "optional": true
+ },
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-server/node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-manifest-plugin": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz",
+ "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==",
+ "license": "MIT",
+ "dependencies": {
+ "tapable": "^2.0.0",
+ "webpack-sources": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "peerDependencies": {
+ "webpack": "^4.44.2 || ^5.47.0"
+ }
+ },
+ "node_modules/webpack-manifest-plugin/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz",
+ "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==",
+ "license": "MIT",
+ "dependencies": {
+ "source-list-map": "^2.0.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
+ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/webpack/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/websocket-driver": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
+ "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "http-parser-js": ">=0.5.1",
+ "safe-buffer": ">=5.1.0",
+ "websocket-extensions": ">=0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/websocket-extensions": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
+ "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==",
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.4.24"
+ }
+ },
+ "node_modules/whatwg-encoding/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/whatwg-fetch": {
+ "version": "3.6.20",
+ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
+ "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
+ "license": "MIT"
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
+ "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==",
+ "license": "MIT"
+ },
+ "node_modules/whatwg-url": {
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz",
+ "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.7.0",
+ "tr46": "^2.1.0",
+ "webidl-conversions": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+ "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/workbox-background-sync": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz",
+ "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==",
+ "license": "MIT",
+ "dependencies": {
+ "idb": "^7.0.1",
+ "workbox-core": "6.6.0"
+ }
+ },
+ "node_modules/workbox-broadcast-update": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz",
+ "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "6.6.0"
+ }
+ },
+ "node_modules/workbox-build": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz",
+ "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@apideck/better-ajv-errors": "^0.3.1",
+ "@babel/core": "^7.11.1",
+ "@babel/preset-env": "^7.11.0",
+ "@babel/runtime": "^7.11.2",
+ "@rollup/plugin-babel": "^5.2.0",
+ "@rollup/plugin-node-resolve": "^11.2.1",
+ "@rollup/plugin-replace": "^2.4.1",
+ "@surma/rollup-plugin-off-main-thread": "^2.2.3",
+ "ajv": "^8.6.0",
+ "common-tags": "^1.8.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "fs-extra": "^9.0.1",
+ "glob": "^7.1.6",
+ "lodash": "^4.17.20",
+ "pretty-bytes": "^5.3.0",
+ "rollup": "^2.43.1",
+ "rollup-plugin-terser": "^7.0.0",
+ "source-map": "^0.8.0-beta.0",
+ "stringify-object": "^3.3.0",
+ "strip-comments": "^2.0.1",
+ "tempy": "^0.6.0",
+ "upath": "^1.2.0",
+ "workbox-background-sync": "6.6.0",
+ "workbox-broadcast-update": "6.6.0",
+ "workbox-cacheable-response": "6.6.0",
+ "workbox-core": "6.6.0",
+ "workbox-expiration": "6.6.0",
+ "workbox-google-analytics": "6.6.0",
+ "workbox-navigation-preload": "6.6.0",
+ "workbox-precaching": "6.6.0",
+ "workbox-range-requests": "6.6.0",
+ "workbox-recipes": "6.6.0",
+ "workbox-routing": "6.6.0",
+ "workbox-strategies": "6.6.0",
+ "workbox-streams": "6.6.0",
+ "workbox-sw": "6.6.0",
+ "workbox-window": "6.6.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz",
+ "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==",
+ "license": "MIT",
+ "dependencies": {
+ "json-schema": "^0.4.0",
+ "jsonpointer": "^5.0.0",
+ "leven": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "ajv": ">=8"
+ }
+ },
+ "node_modules/workbox-build/node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/workbox-build/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/workbox-build/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/workbox-build/node_modules/source-map": {
+ "version": "0.8.0-beta.0",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
+ "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==",
+ "deprecated": "The work that was done in this beta branch won't be included in future versions",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "whatwg-url": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/workbox-build/node_modules/tr46": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
+ "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/workbox-build/node_modules/webidl-conversions": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
+ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/workbox-build/node_modules/whatwg-url": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz",
+ "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash.sortby": "^4.7.0",
+ "tr46": "^1.0.1",
+ "webidl-conversions": "^4.0.2"
+ }
+ },
+ "node_modules/workbox-cacheable-response": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz",
+ "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==",
+ "deprecated": "workbox-background-sync@6.6.0",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "6.6.0"
+ }
+ },
+ "node_modules/workbox-core": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz",
+ "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==",
+ "license": "MIT"
+ },
+ "node_modules/workbox-expiration": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz",
+ "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==",
+ "license": "MIT",
+ "dependencies": {
+ "idb": "^7.0.1",
+ "workbox-core": "6.6.0"
+ }
+ },
+ "node_modules/workbox-google-analytics": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz",
+ "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==",
+ "deprecated": "It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-background-sync": "6.6.0",
+ "workbox-core": "6.6.0",
+ "workbox-routing": "6.6.0",
+ "workbox-strategies": "6.6.0"
+ }
+ },
+ "node_modules/workbox-navigation-preload": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz",
+ "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "6.6.0"
+ }
+ },
+ "node_modules/workbox-precaching": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz",
+ "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "6.6.0",
+ "workbox-routing": "6.6.0",
+ "workbox-strategies": "6.6.0"
+ }
+ },
+ "node_modules/workbox-range-requests": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz",
+ "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "6.6.0"
+ }
+ },
+ "node_modules/workbox-recipes": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz",
+ "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-cacheable-response": "6.6.0",
+ "workbox-core": "6.6.0",
+ "workbox-expiration": "6.6.0",
+ "workbox-precaching": "6.6.0",
+ "workbox-routing": "6.6.0",
+ "workbox-strategies": "6.6.0"
+ }
+ },
+ "node_modules/workbox-routing": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz",
+ "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "6.6.0"
+ }
+ },
+ "node_modules/workbox-strategies": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz",
+ "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "6.6.0"
+ }
+ },
+ "node_modules/workbox-streams": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz",
+ "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==",
+ "license": "MIT",
+ "dependencies": {
+ "workbox-core": "6.6.0",
+ "workbox-routing": "6.6.0"
+ }
+ },
+ "node_modules/workbox-sw": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz",
+ "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==",
+ "license": "MIT"
+ },
+ "node_modules/workbox-webpack-plugin": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.0.tgz",
+ "integrity": "sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-json-stable-stringify": "^2.1.0",
+ "pretty-bytes": "^5.4.1",
+ "upath": "^1.2.0",
+ "webpack-sources": "^1.4.3",
+ "workbox-build": "6.6.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "webpack": "^4.4.0 || ^5.9.0"
+ }
+ },
+ "node_modules/workbox-webpack-plugin/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
+ "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "source-list-map": "^2.0.0",
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/workbox-window": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz",
+ "integrity": "sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2",
+ "workbox-core": "6.6.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/write-file-atomic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+ "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "node_modules/ws": {
+ "version": "7.5.10",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+ "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
+ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "license": "MIT"
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/mes기획서_리액트/package.json b/mes기획서_리액트/package.json
index 60d5ddc..8e2a1e7 100644
--- a/mes기획서_리액트/package.json
+++ b/mes기획서_리액트/package.json
@@ -3,6 +3,7 @@
"version": "1.0.0",
"private": true,
"dependencies": {
+ "@google/generative-ai": "^0.24.1",
"lucide-react": "^0.263.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/mes기획서_리액트/patch_app.ps1 b/mes기획서_리액트/patch_app.ps1
new file mode 100644
index 0000000..a07dad1
--- /dev/null
+++ b/mes기획서_리액트/patch_app.ps1
@@ -0,0 +1,9 @@
+
+$path = "src\App.jsx"
+$absPath = Convert-Path $path
+Write-Host "Editing: $absPath"
+$content = [System.IO.File]::ReadAllText($absPath, [System.Text.Encoding]::UTF8)
+$content = $content.Replace("import documentTemplateConfig from './configs/documentTemplateConfig';", "import documentTemplateConfig from './configs/documentTemplateConfig';`r`nimport DocumentTemplateManager from './components/DocumentTemplateManager';")
+$content = $content.Replace("const DocumentTemplateManager = ({ onNavigate }) => {", "const OldDocumentTemplateManager = ({ onNavigate }) => {")
+[System.IO.File]::WriteAllText($absPath, $content, [System.Text.Encoding]::UTF8)
+Write-Host "Success"
diff --git a/mes기획서_리액트/public/index.html b/mes기획서_리액트/public/index.html
index ce5acf4..d85d76d 100644
--- a/mes기획서_리액트/public/index.html
+++ b/mes기획서_리액트/public/index.html
@@ -36,13 +36,47 @@
.fixed.inset-0 .bg-white button.bg-blue-600,
.fixed.inset-0 .bg-white button.bg-blue-600 *,
.fixed.inset-0 button[class*="bg-blue"],
- .fixed.inset-0 button[class*="bg-blue"] * {
+ .fixed.inset-0 button[class*="bg-blue"] *,
+ .fixed.inset-0 button[class*="bg-purple"],
+ .fixed.inset-0 button[class*="bg-purple"] *,
+ .fixed.inset-0 button[class*="bg-green"],
+ .fixed.inset-0 button[class*="bg-green"] *,
+ .fixed.inset-0 button[class*="bg-red"],
+ .fixed.inset-0 button[class*="bg-red"] *,
+ .fixed.inset-0 button[class*="bg-indigo"],
+ .fixed.inset-0 button[class*="bg-indigo"] * {
color: #ffffff !important;
}
- /* 모달 내 검정 배경 요소는 흰색 텍스트 유지 (FIFO 뱃지 등) */
+ /* 모달 내 검정/어두운 배경 요소는 흰색 텍스트 유지 - 높은 특이성 적용 */
+ /* 문서 미리보기 (A4) 내 어두운 배경 헤더/섹션 */
+ .fixed.inset-0 div[style*="width: 210mm"] .bg-black,
+ .fixed.inset-0 div[style*="width: 210mm"] .bg-black *,
+ .fixed.inset-0 div[style*="width: 210mm"] .bg-gray-900,
+ .fixed.inset-0 div[style*="width: 210mm"] .bg-gray-900 *,
+ .fixed.inset-0 div[style*="width: 210mm"] .bg-gray-800,
+ .fixed.inset-0 div[style*="width: 210mm"] .bg-gray-800 *,
+ .fixed.inset-0 div[style*="width: 210mm"] .bg-gray-700,
+ .fixed.inset-0 div[style*="width: 210mm"] .bg-gray-700 *,
+ .fixed.inset-0 div[style*="width: 210mm"] .text-white,
+ .fixed.inset-0 div[style*="width: 210mm"] [class*="bg-gray-8"],
+ .fixed.inset-0 div[style*="width: 210mm"] [class*="bg-gray-8"] *,
+ .fixed.inset-0 div[style*="width: 210mm"] [class*="bg-gray-7"],
+ .fixed.inset-0 div[style*="width: 210mm"] [class*="bg-gray-7"] *,
+ .fixed.inset-0 div[style*="width: 210mm"] [class*="bg-black"],
+ .fixed.inset-0 div[style*="width: 210mm"] [class*="bg-black"] * {
+ color: #ffffff !important;
+ }
+
+ /* 일반 모달 내 어두운 배경 요소 */
.fixed.inset-0 .bg-black,
- .fixed.inset-0 .bg-black * {
+ .fixed.inset-0 .bg-black *,
+ .fixed.inset-0 .bg-gray-800,
+ .fixed.inset-0 .bg-gray-800 *,
+ .fixed.inset-0 .bg-gray-900,
+ .fixed.inset-0 .bg-gray-900 *,
+ .fixed.inset-0 .bg-gray-700,
+ .fixed.inset-0 .bg-gray-700 * {
color: #ffffff !important;
}
diff --git a/mes기획서_리액트/src/App.jsx b/mes기획서_리액트/src/App.jsx
index ef5d129..ecd554b 100644
--- a/mes기획서_리액트/src/App.jsx
+++ b/mes기획서_리액트/src/App.jsx
@@ -14,6 +14,9 @@ import {
// Master Config imports
import { masterConfigs } from './configs';
import documentTemplateConfig from './configs/documentTemplateConfig';
+import DocumentTemplateManager from './components/DocumentTemplateManager';
+import InspectionTemplateManager, { TemplateList, TemplateRegister, TemplateDetail, TemplateEdit } from './components/InspectionTemplateManager';
+import InspectionManagement, { InspectionList, InspectionRegister, InspectionDetail, InspectionEdit } from './components/InspectionManagement';
import featureDefinitions, { getFeatureDefinition, generateDefaultBadges, screenKeyMapping, getAllScreenIds } from './configs/featureDefinitions';
import { getScreenFeatureTemplate } from './configs/screenFeatureTemplates';
import { screenActionDefinitions, getScreenActions, convertActionsToBadges, ACTION_TYPES, getBadgeColorByActionType } from './configs/screenActionDefinitions';
@@ -55,14 +58,13 @@ import {
// Component imports
import ProcessFlowChart from './components/ProcessFlowChart';
import BusinessFlowChart from './components/BusinessFlowChart';
-import ProductionUserFlow from './components/ProductionUserFlow';
import UserFlowNavigator from './components/UserFlowNavigator';
import UserFlowPanel from './components/UserFlowPanel';
import UserFlowChartViewer from './components/UserFlowChartViewer';
import DetailedFlowDiagram from './components/DetailedFlowDiagram';
import PriceListSimple from './components/PriceListSimple';
import PolicyGuide from './components/PolicyGuide';
-import { QuoteSheetDialog, CalculationSheetDialog, PurchaseOrderDialog } from './components/QuoteDocumentDialogs';
+import { QuoteSheetDialog, CalculationSheetDialog, PurchaseOrderDialog, DeliveryConfirmDialog, InboundSlipDialog } from './components/QuoteDocumentDialogs';
import QuoteDetailNew from './components/QuoteDetailNew';
import CommonUXGuide from './components/CommonUXGuide';
import DetailedProcessFlowChart from './components/DetailedProcessFlowChart';
@@ -74,10 +76,22 @@ import CompleteIntegrationTestTab from './components/CompleteIntegrationTestTab'
import ProcessIntegrationTestTab from './components/ProcessIntegrationTestTab';
import ComprehensiveFlowPanel from './components/ComprehensiveFlowPanel';
import AllMenuFeatureDocPanel from './components/AllMenuFeatureDocPanel';
+import AllFeatureBadgesViewer from './components/AllFeatureBadgesViewer';
// E2E 테스트 유틸리티
import { processClassificationRules, classifyItemToProcess, e2eDebugLog, runE2EScenario } from './e2eTestUtils';
+// 기능정의서 로컬 파일 저장 유틸리티
+import {
+ loadFeatureBadges,
+ saveFeatureBadges,
+ filterAdminBadges,
+ createBackup,
+ importFromFile,
+ extractBadgesByFilter,
+ generateBadgeStatistics
+} from './utils/featureBadgeStorage';
+
// MES 통합 유틸리티 (채번관리, BOM 자동계산, 공정관리 연동)
import {
generateDocumentNumber,
@@ -800,11 +814,10 @@ const ListPageLayout = ({
-
+
{/* 위젯 그리드 */}
{enabledWidgets.map(widget => (
@@ -1371,7 +1391,7 @@ const CEODashboard = ({ userRole = 'ceo', onNavigate }) => {
))}
-
+
{/* 위젯 관리 모달 */}
{showWidgetManager && (
{
전체 업무 프로세스 실시간 모니터링
-
+
{/* 파이프라인 */}
{stages.map((stage, idx) => (
@@ -1483,7 +1503,7 @@ const ProcessPipelineWidget = ({ data }) => {
))}
-
+
{/* 상세 목록 */}