diff --git a/Jenkinsfile b/Jenkinsfile index 4ccf1c0b..d71b4df2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -26,7 +26,7 @@ pipeline { steps { script { if (env.BRANCH_NAME == 'main') { - // main: Stage 빌드 먼저 (승인 후 Production 재빌드) + // main: Stage 빌드 먼저 → Production 재빌드 sh "cp /var/lib/jenkins/env-files/react/.env.stage .env.production" } else { def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}" @@ -82,18 +82,18 @@ pipeline { } } - // ── 운영 배포 승인 ── - stage('Production Approval') { - when { branch 'main' } - steps { - slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token', - message: "🔔 *react* 운영 배포 승인 대기 중\n${env.GIT_COMMIT_MSG}\nStage: https://stage.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>" - timeout(time: 24, unit: 'HOURS') { - input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage: https://stage.sam.it.kr', - ok: '운영 배포 진행' - } - } - } + // ── 운영 배포 승인 (런칭 후 활성화) ── + // stage('Production Approval') { + // when { branch 'main' } + // steps { + // slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token', + // message: "🔔 *react* 운영 배포 승인 대기 중\n${env.GIT_COMMIT_MSG}\nStage: https://stage.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>" + // timeout(time: 24, unit: 'HOURS') { + // input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage: https://stage.sam.it.kr', + // ok: '운영 배포 진행' + // } + // } + // } // ── main → Production 재빌드 (운영 환경변수) ── stage('Rebuild for Production') { diff --git a/claudedocs/architecture/.DS_Store b/claudedocs/architecture/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/claudedocs/architecture/.DS_Store differ diff --git a/src/components/production/WorkOrders/documents/InspectionReportModal.tsx b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx index e89a0dd8..67ea5df7 100644 --- a/src/components/production/WorkOrders/documents/InspectionReportModal.tsx +++ b/src/components/production/WorkOrders/documents/InspectionReportModal.tsx @@ -22,6 +22,7 @@ import { getInspectionReport, getInspectionTemplate, saveInspectionDocument, + resolveInspectionDocument, } from '../actions'; import type { WorkOrder, ProcessType } from '../types'; import type { InspectionReportData, InspectionReportNodeGroup } from '../actions'; @@ -169,6 +170,14 @@ export function InspectionReportModal({ const [reportSummary, setReportSummary] = useState(null); // 자체 로딩한 템플릿 데이터 (prop으로 안 넘어올 때) const [selfTemplateData, setSelfTemplateData] = useState(null); + // 기존 문서의 document_data EAV 레코드 (bending 복원용) + const [documentRecords, setDocumentRecords] = useState | null>(null); // props에서 목업 제외한 실제 개소만 사용 (WorkerScreen에서 apiItems + mockItems가 합쳐져 전달됨) // ★ 반드시 workItems와 inspectionDataMap을 같은 소스에서 가져와야 key 포맷이 일치함 @@ -233,14 +242,16 @@ export function InspectionReportModal({ setIsLoading(true); setError(null); - // 작업지시 기본정보 + 검사 성적서 + 템플릿 동시 로딩 + // 작업지시 기본정보 + 검사 성적서 + 템플릿 + 기존 문서 동시 로딩 Promise.all([ getWorkOrderById(workOrderId), getInspectionReport(workOrderId), // prop으로 템플릿이 안 넘어왔으면 자체 로딩 !templateData ? getInspectionTemplate(workOrderId) : Promise.resolve(null), + // 기존 저장된 문서 조회 (document_data EAV 복원용) + resolveInspectionDocument(workOrderId), ]) - .then(([orderResult, reportResult, templateResult]) => { + .then(([orderResult, reportResult, templateResult, resolveResult]) => { // 1) WorkOrder 기본정보 if (orderResult.success && orderResult.data) { const orderData = orderResult.data; @@ -277,6 +288,20 @@ export function InspectionReportModal({ if (templateResult && templateResult.success && templateResult.data) { setSelfTemplateData(templateResult.data); } + + // 4) 기존 문서의 document_data EAV 레코드 추출 + if (resolveResult?.success && resolveResult.data) { + const existingDoc = (resolveResult.data as Record).existing_document as + | { data?: Array<{ section_id: number | null; column_id: number | null; row_index: number; field_key: string; field_value: string | null }> } + | null; + if (existingDoc?.data && existingDoc.data.length > 0) { + setDocumentRecords(existingDoc.data); + } else { + setDocumentRecords(null); + } + } else { + setDocumentRecords(null); + } }) .catch(() => { setError('서버 오류가 발생했습니다.'); @@ -290,6 +315,7 @@ export function InspectionReportModal({ setApiInspectionDataMap(null); setReportSummary(null); setSelfTemplateData(null); + setDocumentRecords(null); setError(null); } }, [open, workOrderId, processType, templateData]); @@ -366,6 +392,7 @@ export function InspectionReportModal({ readOnly={readOnly} workItems={effectiveWorkItems} inspectionDataMap={effectiveInspectionDataMap} + documentRecords={documentRecords ?? undefined} /> ); } diff --git a/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx index 3f42cf38..0aac454a 100644 --- a/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx +++ b/src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx @@ -54,6 +54,14 @@ interface TemplateInspectionContentProps { readOnly?: boolean; workItems?: WorkItemData[]; inspectionDataMap?: InspectionDataMap; + /** 기존 document_data EAV 레코드 (문서 로딩 시 복원용) */ + documentRecords?: Array<{ + section_id: number | null; + column_id: number | null; + row_index: number; + field_key: string; + field_value: string | null; + }>; } // ===== 유틸 ===== @@ -301,7 +309,7 @@ function SectionImage({ section }: { section: { id: number; title?: string; name // ===== 컴포넌트 ===== export const TemplateInspectionContent = forwardRef( - function TemplateInspectionContent({ data: order, template, readOnly = false, workItems, inspectionDataMap }, ref) { + function TemplateInspectionContent({ data: order, template, readOnly = false, workItems, inspectionDataMap, documentRecords }, ref) { const fullDate = getFullDate(); const { primaryAssignee } = getOrderInfo(order); @@ -486,6 +494,84 @@ export const TemplateInspectionContent = forwardRef { + if (!isBending || !documentRecords || documentRecords.length === 0 || bendingProducts.length === 0) return; + + const initial: Record = {}; + + // field_key 패턴: b{productIdx}_ok, b{productIdx}_ng, b{productIdx}_p{pointIdx}_n1, b{productIdx}_n{n}, b{productIdx}_judgment + for (const rec of documentRecords) { + const fk = rec.field_key; + if (!fk.startsWith('b')) continue; + const val = rec.field_value; + if (val == null) continue; + + // b{productIdx}_ok / b{productIdx}_ng → check status + const checkMatch = fk.match(/^b(\d+)_(ok|ng)$/); + if (checkMatch && rec.column_id) { + const productIdx = parseInt(checkMatch[1], 10); + const cellKey = `b-${productIdx}-${rec.column_id}`; + if (checkMatch[2] === 'ok' && val === 'OK') { + initial[cellKey] = { ...initial[cellKey], status: 'good' }; + } else if (checkMatch[2] === 'ng' && val === 'NG') { + initial[cellKey] = { ...initial[cellKey], status: 'bad' }; + } + continue; + } + + // b{productIdx}_p{pointIdx}_n1 → gap measurement + const gapMatch = fk.match(/^b(\d+)_p(\d+)_n(\d+)$/); + if (gapMatch && rec.column_id) { + const productIdx = parseInt(gapMatch[1], 10); + const pointIdx = parseInt(gapMatch[2], 10); + const cellKey = `b-${productIdx}-p${pointIdx}-${rec.column_id}`; + initial[cellKey] = { measurements: [val, '', ''] }; + continue; + } + + // b{productIdx}_n{n} → complex measurement (길이/너비) + const complexMatch = fk.match(/^b(\d+)_n(\d+)$/); + if (complexMatch && rec.column_id) { + const productIdx = parseInt(complexMatch[1], 10); + const mIdx = parseInt(complexMatch[2], 10) - 1; + const cellKey = `b-${productIdx}-${rec.column_id}`; + const prev = initial[cellKey]?.measurements || ['', '', '']; + const m: [string, string, string] = [...prev] as [string, string, string]; + m[mIdx] = val; + initial[cellKey] = { ...initial[cellKey], measurements: m }; + continue; + } + + // b{productIdx}_judgment → skip (자동 계산, 복원 불필요) + if (fk.match(/^b\d+_judgment$/)) continue; + + // b{productIdx}_value → fallback value + const valMatch = fk.match(/^b(\d+)_value$/); + if (valMatch && rec.column_id) { + const productIdx = parseInt(valMatch[1], 10); + const cellKey = `b-${productIdx}-${rec.column_id}`; + initial[cellKey] = { value: val }; + continue; + } + } + + // overall_result, remark 복원 + for (const rec of documentRecords) { + if (rec.field_key === 'overall_result' && rec.field_value) { + // overallResult는 자동 계산이므로 별도 처리 불필요 + } + if (rec.field_key === 'remark' && rec.field_value) { + setInadequateContent(rec.field_value); + } + } + + if (Object.keys(initial).length > 0) { + setCellValues(prev => ({ ...prev, ...initial })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [documentRecords, isBending, bendingProducts]); + const updateCell = (key: string, update: Partial) => { setCellValues(prev => ({ ...prev, @@ -563,7 +649,99 @@ export const TemplateInspectionContent = forwardRef { + // Bending 모드: 구성품별 데이터 (개소 단위, field_key에 구성품/포인트 인코딩) + if (isBending && bendingProducts.length > 0) { + bendingProducts.forEach((product, productIdx) => { + for (const col of template.columns) { + const label = col.label.trim(); + const isGapCol = col.id === gapColumnId; + + // text 컬럼 (분류/제품명, 타입) → bendingInfo에서 동적 생성이므로 저장 불필요 + if (col.column_type === 'text') continue; + + // 판정 컬럼 → 자동 계산 결과 저장 + if (isJudgmentColumn(label)) { + const judgment = getBendingProductJudgment(productIdx); + if (judgment) { + records.push({ + section_id: null, column_id: col.id, row_index: 0, + field_key: `b${productIdx}_judgment`, + field_value: judgment === '적' ? 'OK' : 'NG', + }); + } + continue; + } + + // 간격 컬럼 (per-point 데이터) + if (isGapCol) { + product.gapPoints.forEach((_gp, pointIdx) => { + const cellKey = `b-${productIdx}-p${pointIdx}-${col.id}`; + const cell = cellValues[cellKey]; + if (cell?.measurements?.[0]) { + records.push({ + section_id: null, column_id: col.id, row_index: 0, + field_key: `b${productIdx}_p${pointIdx}_n1`, + field_value: cell.measurements[0], + }); + } + }); + continue; + } + + // 비간격 merged 컬럼 + const cellKey = `b-${productIdx}-${col.id}`; + const cell = cellValues[cellKey]; + + // check 컬럼 (절곡상태) + if (col.column_type === 'check') { + records.push({ + section_id: null, column_id: col.id, row_index: 0, + field_key: `b${productIdx}_ok`, + field_value: cell?.status === 'good' ? 'OK' : '', + }); + records.push({ + section_id: null, column_id: col.id, row_index: 0, + field_key: `b${productIdx}_ng`, + field_value: cell?.status === 'bad' ? 'NG' : '', + }); + continue; + } + + // complex 컬럼 (길이/너비 측정) + if (col.column_type === 'complex' && col.sub_labels) { + let inputIdx = 0; + for (const sl of col.sub_labels) { + const slLower = sl.toLowerCase(); + if (slLower.includes('도면') || slLower.includes('기준')) continue; + if (slLower.includes('point') || slLower.includes('포인트')) continue; + const n = inputIdx + 1; + const val = cell?.measurements?.[inputIdx] || null; + if (val) { + records.push({ + section_id: null, column_id: col.id, row_index: 0, + field_key: `b${productIdx}_n${n}`, + field_value: val, + }); + } + inputIdx++; + } + continue; + } + + // fallback + if (cell?.value) { + records.push({ + section_id: null, column_id: col.id, row_index: 0, + field_key: `b${productIdx}_value`, + field_value: cell.value, + }); + } + } + }); + } + + // 비-Bending 모드: 개소(WorkItem)별 데이터 + if (!isBending) effectiveWorkItems.forEach((wi, rowIdx) => { for (const col of template.columns) { // 일련번호 컬럼 → 저장 (mng show에서 표시용) if (isSerialColumn(col.label)) {