From 0f6f652bedb8ea7757bb94dc1663c8672e2296af Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 2 Apr 2026 11:28:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=83=9D=EC=82=B0=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=A9=94=EB=89=B4=20(=EC=9E=85=EA=B3=A0/?= =?UTF-8?q?=EC=B6=9C=EA=B3=A0=EC=99=80=20=EB=8F=99=EC=9D=BC=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /pop/production → ProductionMain (아이콘 메뉴: 공정실행/작업지시/생산현황/불량관리/실적조회) - /pop/production/process → WorkOrderList (기존 공정 목록 이동) - KPI 실데이터 연동 (작업지시 목록 API) - amber gradient 테마, 최근 생산활동 - cmux 검증 완료 --- frontend/app/(pop)/pop/production/page.tsx | 6 +- .../app/(pop)/pop/production/process/page.tsx | 12 + .../hardcoded/production/ProductionMain.tsx | 389 ++++++++++++++++++ .../pop/hardcoded/production/index.ts | 1 + 4 files changed, 405 insertions(+), 3 deletions(-) create mode 100644 frontend/app/(pop)/pop/production/process/page.tsx create mode 100644 frontend/components/pop/hardcoded/production/ProductionMain.tsx diff --git a/frontend/app/(pop)/pop/production/page.tsx b/frontend/app/(pop)/pop/production/page.tsx index cc27af15..0ad08502 100644 --- a/frontend/app/(pop)/pop/production/page.tsx +++ b/frontend/app/(pop)/pop/production/page.tsx @@ -1,12 +1,12 @@ "use client"; import { PopShell } from "@/components/pop/hardcoded"; -import { WorkOrderList } from "@/components/pop/hardcoded/production"; +import { ProductionMain } from "@/components/pop/hardcoded/production"; export default function ProductionPage() { return ( - - + + ); } diff --git a/frontend/app/(pop)/pop/production/process/page.tsx b/frontend/app/(pop)/pop/production/process/page.tsx new file mode 100644 index 00000000..4c45e6a9 --- /dev/null +++ b/frontend/app/(pop)/pop/production/process/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { PopShell } from "@/components/pop/hardcoded"; +import { WorkOrderList } from "@/components/pop/hardcoded/production"; + +export default function ProductionProcessPage() { + return ( + + + + ); +} diff --git a/frontend/components/pop/hardcoded/production/ProductionMain.tsx b/frontend/components/pop/hardcoded/production/ProductionMain.tsx new file mode 100644 index 00000000..dd72807f --- /dev/null +++ b/frontend/components/pop/hardcoded/production/ProductionMain.tsx @@ -0,0 +1,389 @@ +"use client"; + +import React, { useState, useRef, useCallback, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { apiClient } from "@/lib/api/client"; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +interface ProductionMenuItem { + id: string; + title: string; + gradient: string; + shadowColor: string; + icon: React.ReactNode; + href: string; +} + +interface RecentActivityItem { + id: string; + time: string; + processName: string; + itemName: string; + qty: string; + statusColor: string; + statusLabel: string; +} + +interface KpiData { + todayCompleted: number; + inProgressCount: number; + completedCount: number; +} + +/* ------------------------------------------------------------------ */ +/* Data */ +/* ------------------------------------------------------------------ */ + +const MENU_ITEMS: ProductionMenuItem[] = [ + { + id: "process", + title: "공정실행", + gradient: "linear-gradient(135deg,#f59e0b,#d97706)", + shadowColor: "rgba(245,158,11,.3)", + icon: ( + + + + + ), + href: "/pop/production/process", + }, + { + id: "work-order", + title: "작업지시", + gradient: "linear-gradient(135deg,#f59e0b,#d97706)", + shadowColor: "rgba(245,158,11,.3)", + icon: ( + + + + ), + href: "#", + }, + { + id: "status", + title: "생산현황", + gradient: "linear-gradient(135deg,#f59e0b,#d97706)", + shadowColor: "rgba(245,158,11,.3)", + icon: ( + + + + ), + href: "#", + }, + { + id: "defect", + title: "불량관리", + gradient: "linear-gradient(135deg,#f59e0b,#d97706)", + shadowColor: "rgba(245,158,11,.3)", + icon: ( + + + + ), + href: "#", + }, + { + id: "result", + title: "실적조회", + gradient: "linear-gradient(135deg,#f59e0b,#d97706)", + shadowColor: "rgba(245,158,11,.3)", + icon: ( + + + + ), + href: "#", + }, +]; + +/* ------------------------------------------------------------------ */ +/* Component */ +/* ------------------------------------------------------------------ */ + +export function ProductionMain() { + const router = useRouter(); + + /* KPI carousel */ + const [kpiIdx, setKpiIdx] = useState(0); + const kpiTimerRef = useRef | null>(null); + + /* Data state */ + const [kpi, setKpi] = useState({ + todayCompleted: 0, + inProgressCount: 0, + completedCount: 0, + }); + const [recentItems, setRecentItems] = useState([]); + const [loading, setLoading] = useState(true); + + const startKpiAuto = useCallback(() => { + if (kpiTimerRef.current) clearInterval(kpiTimerRef.current); + kpiTimerRef.current = setInterval(() => setKpiIdx((p) => (p + 1) % 3), 4000); + }, []); + + useEffect(() => { + startKpiAuto(); + return () => { + if (kpiTimerRef.current) clearInterval(kpiTimerRef.current); + }; + }, [startKpiAuto]); + + /* Fetch real data */ + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + + const wiRes = await apiClient.get("/work-instruction/list"); + let wiData: any[] = []; + if (wiRes.data?.data) { + wiData = Array.isArray(wiRes.data.data) + ? wiRes.data.data + : wiRes.data.data.rows || []; + } else if (Array.isArray(wiRes.data)) { + wiData = wiRes.data; + } + + // KPI 계산 + let todayCompleted = 0; + let inProgressCount = 0; + let completedCount = 0; + + for (const wi of wiData) { + const status = wi.progress_status || wi.status || ""; + if (status === "completed" || status === "완료") { + completedCount++; + todayCompleted += Number(wi.completed_qty) || 0; + } else if (status === "in_progress" || status === "진행중") { + inProgressCount++; + } + } + + setKpi({ todayCompleted, inProgressCount, completedCount }); + + // 최근 활동 5건 (최신순) + const sorted = [...wiData].sort((a, b) => { + const da = new Date(a.start_date || a.created_date || "").getTime() || 0; + const db = new Date(b.start_date || b.created_date || "").getTime() || 0; + return db - da; + }); + + const recent: RecentActivityItem[] = sorted.slice(0, 5).map((wi, idx) => { + const dateObj = wi.start_date ? new Date(wi.start_date) : null; + const time = dateObj + ? `${String(dateObj.getMonth() + 1).padStart(2, "0")}/${String(dateObj.getDate()).padStart(2, "0")}` + : "--/--"; + + const status = wi.progress_status || wi.status || ""; + let statusColor = "text-gray-600 bg-gray-50"; + let statusLabel = "대기"; + if (status === "completed" || status === "완료") { + statusColor = "text-green-600 bg-green-50"; + statusLabel = "완료"; + } else if (status === "in_progress" || status === "진행중") { + statusColor = "text-blue-600 bg-blue-50"; + statusLabel = "진행중"; + } else if (status === "acceptable" || status === "접수가능") { + statusColor = "text-amber-600 bg-amber-50"; + statusLabel = "접수가능"; + } + + return { + id: String(wi.id || idx), + time, + processName: wi.routing ? "공정 있음" : "공정 미설정", + itemName: wi.item_name || wi.item_code || wi.work_instruction_no || "-", + qty: `${(Number(wi.qty) || 0).toLocaleString()} EA`, + statusColor, + statusLabel, + }; + }); + + setRecentItems(recent); + } catch { + // 실패 시 0/빈 배열 유지 + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + const handleMenuClick = (item: ProductionMenuItem) => { + if (item.href === "#") { + alert(`${item.title} 화면은 준비 중입니다.`); + } else { + router.push(item.href); + } + }; + + return ( +
+ {/* ===== Back + Title ===== */} +
+ +
+

생산관리

+

메뉴를 선택하세요

+
+
+ + {/* ===== KPI Carousel ===== */} +
+
+ {/* Slide 1 */} +
+
+
+ + + +
+
+
+
+ {/* Single dot (only 1 slide) */} +
+ {[0].map((idx) => ( +
+
+ + {/* ===== Menu Icons ===== */} +
+
+
+

생산 메뉴

+
+
+ {MENU_ITEMS.map((item) => ( + + ))} +
+
+ + {/* ===== Recent Activity ===== */} +
+
+
+

최근 생산활동

+ 최근 5건 +
+
+ {loading ? ( +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+ ))} +
+ ) : recentItems.length === 0 ? ( +
+ 최근 생산활동 내역이 없습니다 +
+ ) : ( + recentItems.map((item) => ( +
+ + {item.time} + +
+
+ {item.itemName} + + {item.statusLabel} + +
+
+ {item.processName} | {item.qty} +
+
+
+ )) + )} +
+
+
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Sub-components */ +/* ------------------------------------------------------------------ */ + +function KpiCell({ value, label, color, unit }: { value: string; label: string; color: string; unit?: string }) { + return ( +
+
+ + {value} + + {unit && {unit}} +
+ {label} +
+ ); +} + +function MenuIcon({ item, onClick }: { item: ProductionMenuItem; onClick: (item: ProductionMenuItem) => void }) { + return ( +
onClick(item)} + > +
+ {item.icon} +
+ + {item.title} + +
+ ); +} diff --git a/frontend/components/pop/hardcoded/production/index.ts b/frontend/components/pop/hardcoded/production/index.ts index 9feabaf4..c3eb862c 100644 --- a/frontend/components/pop/hardcoded/production/index.ts +++ b/frontend/components/pop/hardcoded/production/index.ts @@ -1,3 +1,4 @@ +export { ProductionMain } from "./ProductionMain"; export { WorkOrderList } from "./WorkOrderList"; export { ProcessWork } from "./ProcessWork"; export { ProcessTimer } from "./ProcessTimer";