diff --git a/backend-node/src/controllers/popProductionController.ts b/backend-node/src/controllers/popProductionController.ts index c5def3e1..a95e55db 100644 --- a/backend-node/src/controllers/popProductionController.ts +++ b/backend-node/src/controllers/popProductionController.ts @@ -3586,6 +3586,11 @@ export const getProcessList = async ( res: Response, ) => { const pool = getPool(); + const __debugTag = `[POP-DEBUG][getProcessList][${req.user!.userId}@${req.user!.companyCode}][${new Date().toISOString()}]`; + console.log(`${__debugTag} ▶ ENTER`, { + query: req.query, + headers_user_agent: req.headers["user-agent"], + }); try { const companyCode = req.user!.companyCode; @@ -3606,8 +3611,15 @@ export const getProcessList = async ( [companyCode], ); const wops = wopRes.rows; + console.log(`${__debugTag} 1) wop SQL → rows:`, { + total: wops.length, + by_process: wops.reduce((m: Record, w: any) => { m[w.process_code] = (m[w.process_code] || 0) + 1; return m; }, {}), + master_count: wops.filter((w: any) => !w.parent_process_id).length, + split_count: wops.filter((w: any) => w.parent_process_id).length, + }); if (wops.length === 0) { + console.log(`${__debugTag} ◀ EXIT (no wop)`); return res.json({ success: true, data: [] }); } @@ -3624,6 +3636,11 @@ export const getProcessList = async ( ORDER BY wop_id, seq ASC`, [wopIds], ); + console.log(`${__debugTag} 2) wopr SQL → rows:`, { + total: resRes.rows.length, + by_status: resRes.rows.reduce((m: Record, r: any) => { m[r.status] = (m[r.status] || 0) + 1; return m; }, {}), + rework_count: resRes.rows.filter((r: any) => r.is_rework === "Y").length, + }); const resultsByWop = new Map(); for (const r of resRes.rows) { @@ -3631,6 +3648,12 @@ export const getProcessList = async ( arr.push(r); resultsByWop.set(r.wop_id, arr); } + const __wopWithoutResult = wops.filter((w: any) => !resultsByWop.has(w.id)); + console.log(`${__debugTag} 2-1) wop ↔ wopr 매핑:`, { + wop_with_result: wops.length - __wopWithoutResult.length, + wop_WITHOUT_result: __wopWithoutResult.length, + missing_sample: __wopWithoutResult.slice(0, 5).map((w: any) => ({ id: w.id, process_code: w.process_code, seq_no: w.seq_no })), + }); // 3) 대표 상태/수량은 첫 실적 기준으로 매핑 (프론트는 accepted_results도 별도 순회) const data = wops.map((w: any) => { @@ -3692,6 +3715,45 @@ export const getProcessList = async ( }; }); + // 3) 응답 데이터 분포 로그 + const masterFirstDist: Record = {}; + const p001WoprDist: Record = {}; + const reworkDist: Record = {}; + let totalWopr = 0; + const p001Cards: any[] = []; + for (const d of data) { + masterFirstDist[d.status] = (masterFirstDist[d.status] || 0) + 1; + if (d.process_code === "P001") { + p001Cards.push({ + id: d.id, + seq_no: d.seq_no, + parent: d.parent_process_id, + master_status: d.status, + wopr_count: d.accepted_results.length, + wopr_statuses: d.accepted_results.map((r: any) => `${r.seq}:${r.status}${r.is_rework === "Y" ? "(R)" : ""}`), + }); + } + for (const ar of d.accepted_results) { + totalWopr++; + if (d.process_code === "P001") { + p001WoprDist[ar.status] = (p001WoprDist[ar.status] || 0) + 1; + } + if (ar.is_rework === "Y") { + reworkDist[`${d.process_code}:${ar.status}`] = (reworkDist[`${d.process_code}:${ar.status}`] || 0) + 1; + } + } + } + console.log(`${__debugTag} 3) 응답 빌드 완료:`, { + data_length: data.length, + wopr_total_in_response: totalWopr, + master_first_status_dist: masterFirstDist, + p001_wopr_status_dist: p001WoprDist, + rework_dist: reworkDist, + }); + console.log(`${__debugTag} 3-1) P001 카드 상세 (${p001Cards.length}개):`); + console.table(p001Cards); + console.log(`${__debugTag} ◀ EXIT`); + return res.json({ success: true, data }); } catch (error: any) { logger.error("[pop/production] processes 조회 오류:", error); diff --git a/frontend/app/(main)/COMPANY_7/pop/_components/production/WorkOrderList.tsx b/frontend/app/(main)/COMPANY_7/pop/_components/production/WorkOrderList.tsx index e94ba04d..5acd50aa 100644 --- a/frontend/app/(main)/COMPANY_7/pop/_components/production/WorkOrderList.tsx +++ b/frontend/app/(main)/COMPANY_7/pop/_components/production/WorkOrderList.tsx @@ -831,7 +831,7 @@ export function WorkOrderList(props: WorkOrderListProps) { const masterProcesses = useMemo(() => { // 마스터 행 + 분할 행(진행중/완료/리워크) — 중복 제거 const seen = new Set(); - return allProcesses.filter((p) => { + const result = allProcesses.filter((p) => { if (seen.has(p.id)) return false; const include = !p.parent_process_id || // 마스터 행 @@ -841,6 +841,32 @@ export function WorkOrderList(props: WorkOrderListProps) { if (include) seen.add(p.id); return include; }); + const __tag = `[POP-DEBUG][WorkOrderList][${new Date().toISOString()}]`; + const dist: Record = {}; + for (const p of result) { + const kind = p.parent_process_id ? "split" : "master"; + const rw = isReworkProcess(p) ? "(R)" : ""; + const k = `${p.process_code}:${kind}${rw}:${p.status}`; + dist[k] = (dist[k] || 0) + 1; + } + console.log(`${__tag} 3) masterProcesses 생성:`, { + input_allProcesses: allProcesses.length, + output_masterProcesses: result.length, + excluded: allProcesses.length - result.length, + distribution: dist, + }); + console.log(`${__tag} 3-1) masterProcesses — P001 + 모든 리워크 상세:`); + console.table(result + .filter((p) => p.process_code === "P001" || isReworkProcess(p)) + .map((p) => ({ + id: p.id, + wo_id: p.wo_id, + process_code: p.process_code, + kind: p.parent_process_id ? "split" : "master", + status: p.status, + is_rework: p.is_rework, + }))); + return result; }, [allProcesses]); const equipmentMap = useMemo(() => { @@ -857,36 +883,56 @@ export function WorkOrderList(props: WorkOrderListProps) { /* ---- Filtered processes ---- */ const filteredProcesses = useMemo(() => { - if (selectedProcess === "__all__") return []; // 공정 미선택 시 빈 목록 - return masterProcesses.filter((proc) => { + const __tag = `[POP-DEBUG][WorkOrderList][${new Date().toISOString()}]`; + if (selectedProcess === "__all__") { + console.log(`${__tag} 4) filteredProcesses: 공정 미선택 — 빈 배열`); + return []; + } + const __excluded: Array<{ id: string; reason: string; status: string; process_code: string }> = []; + const result = masterProcesses.filter((proc) => { const isRework = isReworkProcess(proc); const isMaster = !proc.parent_process_id; - // 완료/진행중 탭에서는 SPLIT만 표시 (마스터 제외) - if ( - isMaster && - !isRework && - (activeTab === "completed" || activeTab === "in_progress") - ) + if (isMaster && !isRework && (activeTab === "completed" || activeTab === "in_progress")) { + __excluded.push({ id: proc.id, reason: "master_in_completed_or_in_progress_tab", status: proc.status, process_code: proc.process_code }); return false; - // 리워크 마스터가 in_progress/completed면 SPLIT이 생성된 것 → 리워크 마스터 숨김 (SPLIT은 표시) - if ( - isRework && - !proc.parent_process_id && - (proc.status === "in_progress" || proc.status === "completed") - ) + } + if (isRework && !proc.parent_process_id && (proc.status === "in_progress" || proc.status === "completed")) { + __excluded.push({ id: proc.id, reason: "rework_master_with_split", status: proc.status, process_code: proc.process_code }); return false; - // 재작업 카드는 공정 필터 무시 (모든 공정에서 표시) - if (!isRework && proc.process_code !== selectedProcess) return false; + } + if (!isRework && proc.process_code !== selectedProcess) { + __excluded.push({ id: proc.id, reason: `process_code_mismatch(${proc.process_code} vs ${selectedProcess})`, status: proc.status, process_code: proc.process_code }); + return false; + } if (selectedEquipment !== "__all__") { const wi = instructionMap[proc.wo_id]; - if (!wi) return false; + if (!wi) { + __excluded.push({ id: proc.id, reason: "no_wi_for_equipment_filter", status: proc.status, process_code: proc.process_code }); + return false; + } const eqId = wi.equipment_id; const eq = equipmentMap[eqId]; - if (!eq || eq.equipment_code !== selectedEquipment) return false; + if (!eq || eq.equipment_code !== selectedEquipment) { + __excluded.push({ id: proc.id, reason: `equipment_mismatch(${eq?.equipment_code} vs ${selectedEquipment})`, status: proc.status, process_code: proc.process_code }); + return false; + } + } + if (activeTab !== "all" && proc.status !== activeTab) { + __excluded.push({ id: proc.id, reason: `tab_mismatch(${proc.status} vs ${activeTab})`, status: proc.status, process_code: proc.process_code }); + return false; } - if (activeTab !== "all" && proc.status !== activeTab) return false; return true; }); + console.log(`${__tag} 4) filteredProcesses 계산:`, { + activeTab, + selectedProcess, + selectedEquipment, + input: masterProcesses.length, + output: result.length, + excluded_count: __excluded.length, + excluded_by_reason: __excluded.reduce((m: Record, e) => { m[e.reason] = (m[e.reason] || 0) + 1; return m; }, {}), + }); + return result; }, [ masterProcesses, selectedProcess, @@ -923,23 +969,36 @@ export function WorkOrderList(props: WorkOrderListProps) { waiting: 0, completed: 0, }; + const __tag = `[POP-DEBUG][WorkOrderList][${new Date().toISOString()}]`; + const breakdown: Array<{ id: string; wo_id: string; process_code: string; status: string; kind: string; is_rework: string; bucket: string }> = []; for (const proc of preFiltered) { const isMaster = !proc.parent_process_id; const isRw = isReworkProcess(proc); - // 리워크 마스터가 in_progress/completed면 SPLIT이 있으므로 카운트 제외 + const kind = isMaster ? "master" : "split"; if ( isRw && !proc.parent_process_id && (proc.status === "in_progress" || proc.status === "completed") - ) + ) { + breakdown.push({ id: proc.id, wo_id: proc.wo_id, process_code: proc.process_code, status: proc.status, kind, is_rework: proc.is_rework || "", bucket: "★SKIPPED" }); continue; - if (proc.status === "acceptable") counts.acceptable++; - else if (proc.status === "in_progress" && (!isMaster || isRw)) - counts.in_progress++; - else if (proc.status === "completed" && (!isMaster || isRw)) - counts.completed++; - else counts.waiting++; + } + let bucket: string; + if (proc.status === "acceptable") { counts.acceptable++; bucket = "acceptable"; } + else if (proc.status === "in_progress" && (!isMaster || isRw)) { counts.in_progress++; bucket = "in_progress"; } + else if (proc.status === "completed" && (!isMaster || isRw)) { counts.completed++; bucket = "completed"; } + else { counts.waiting++; bucket = "waiting"; } + breakdown.push({ id: proc.id, wo_id: proc.wo_id, process_code: proc.process_code, status: proc.status, kind, is_rework: proc.is_rework || "", bucket }); } + console.log(`${__tag} 5) tabCounts 계산:`, { + selectedProcess, + selectedEquipment, + masterProcesses_total: masterProcesses.length, + preFiltered_total: preFiltered.length, + counts, + }); + console.log(`${__tag} 5-1) 카드별 분류 breakdown (${breakdown.length}개):`); + console.table(breakdown); return counts; }, [ masterProcesses, diff --git a/frontend/app/(main)/COMPANY_7/pop/_components/production/useProcessData.ts b/frontend/app/(main)/COMPANY_7/pop/_components/production/useProcessData.ts index 791e59cf..434c2169 100644 --- a/frontend/app/(main)/COMPANY_7/pop/_components/production/useProcessData.ts +++ b/frontend/app/(main)/COMPANY_7/pop/_components/production/useProcessData.ts @@ -77,25 +77,53 @@ export function useProcessData() { if (inFlight.current) return inFlight.current; const task = (async () => { + const __tag = `[POP-DEBUG][useProcessData][${new Date().toISOString()}]`; + console.log(`${__tag} ▶ fetchData 시작`, { withSync: !!opts?.withSync }); setLoading(true); if (opts?.withSync) setSyncing(true); try { if (opts?.withSync) { try { - await apiClient.post("/pop/production/sync-work-instructions"); - } catch { + const __t0 = Date.now(); + const syncRes = await apiClient.post("/pop/production/sync-work-instructions"); + console.log(`${__tag} 0) POST /sync-work-instructions (${Date.now() - __t0}ms)`, syncRes.data); + } catch (e) { + console.warn(`${__tag} 0) sync 실패`, e); toast.warning( "동기화 실패 — 최신 작업지시가 반영되지 않을 수 있습니다", ); } } + const __t1 = Date.now(); const [wiRes, procRes, pmRes, eqRes] = await Promise.all([ apiClient.get("/work-instruction/list"), apiClient.get("/pop/production/processes"), dataApi.getTableData("process_mng", { size: 500 }), dataApi.getTableData("equipment_mng", { size: 500 }), ]); + console.log(`${__tag} 1) 4개 API 병렬 호출 완료 (${Date.now() - __t1}ms)`); + const __wiArr = (Array.isArray(wiRes.data?.data) ? wiRes.data.data : wiRes.data?.data?.rows ?? []) as any[]; + console.log(`${__tag} 1-1) /work-instruction/list 응답:`, { + raw_length: __wiArr.length, + unique_wi_ids: new Set(__wiArr.map((r: any) => r.wi_id || r.id)).size, + sample: __wiArr.slice(0, 3), + }); + console.log(`${__tag} 1-2) /pop/production/processes 응답:`, { + raw_length: procRes.data?.data?.length ?? 0, + by_process_code: (procRes.data?.data ?? []).reduce((m: Record, d: any) => { m[d.process_code] = (m[d.process_code] || 0) + 1; return m; }, {}), + }); + console.log(`${__tag} 1-3) /pop/production/processes — P001 카드 RAW:`); + console.table((procRes.data?.data ?? []) + .filter((d: any) => d.process_code === "P001") + .map((d: any) => ({ + id: d.id, + seq_no: d.seq_no, + parent: d.parent_process_id || "", + master_status: d.status, + wopr_count: d.accepted_results?.length ?? 0, + wopr_statuses: (d.accepted_results || []).map((r: any) => `${r.seq}:${r.status}${r.is_rework === "Y" ? "(R)" : ""}`).join("|"), + }))); // work-instruction: header+detail 조인이라 id 중복 → 첫 행만 취함 let wiRaw: Record[] = []; @@ -175,6 +203,55 @@ export function useProcessData() { setAllProcesses(flat); setProcessList((pmRes.data ?? []) as ProcessMng[]); setEquipmentList((eqRes.data ?? []) as EquipmentMng[]); + + // 2) flat 펼친 후 분포 — useProcessData가 master + 모든 wopr를 split row로 펼친 결과 + const allDist: Record = {}; + const p001Dist: Record = {}; + const reworkDist: Record = {}; + for (const p of flat) { + const kind = p.parent_process_id ? "split" : "master"; + const key = `${kind}:${p.status}`; + allDist[key] = (allDist[key] || 0) + 1; + if (p.process_code === "P001") { + p001Dist[key] = (p001Dist[key] || 0) + 1; + } + if (p.is_rework === "Y") { + reworkDist[`${p.process_code}:${kind}:${p.status}`] = + (reworkDist[`${p.process_code}:${kind}:${p.status}`] || 0) + 1; + } + } + console.log(`${__tag} 2) flat 펼침 완료:`, { + raw_wop_count: rawRows.length, + flat_count: flat.length, + expand_factor: flat.length / Math.max(1, rawRows.length), + all_kind_status_dist: allDist, + p001_kind_status_dist: p001Dist, + rework_dist: reworkDist, + }); + console.log(`${__tag} 2-1) flat — P001 row 상세:`); + console.table(flat + .filter((p) => p.process_code === "P001") + .map((p) => ({ + id: p.id, + wo_id: p.wo_id, + seq_no: p.seq_no, + kind: p.parent_process_id ? "split" : "master", + status: p.status, + is_rework: p.is_rework, + split_no: p.split_no ?? "", + }))); + console.log(`${__tag} 2-2) flat — 모든 리워크 row:`); + console.table(flat + .filter((p) => p.is_rework === "Y") + .map((p) => ({ + id: p.id, + wo_id: p.wo_id, + process_code: p.process_code, + seq_no: p.seq_no, + kind: p.parent_process_id ? "split" : "master", + status: p.status, + }))); + console.log(`${__tag} ◀ fetchData 종료`); } catch { toast.error("데이터 조회 실패"); } finally {