diff --git a/frontend/app/(main)/COMPANY_10/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_10/monitoring/equipment/page.tsx index ccb5aaf6..14adae20 100644 --- a/frontend/app/(main)/COMPANY_10/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_10/monitoring/equipment/page.tsx @@ -12,16 +12,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { - RefreshCw, - Clock, - Loader2, - Inbox, - Wrench, - Zap, - Pause, - Power, -} from "lucide-react"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import { RefreshCw, Clock, Loader2, Inbox, Wrench, Zap, Pause, Power, Settings2 } from "lucide-react"; /* ───── 상태 정의 ───── */ @@ -134,11 +128,16 @@ interface WorkInstruction { /* ───── 컴포넌트 ───── */ export default function EquipmentMonitoringPage() { + const { settings } = useMonitoringSettings("equipment"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const df = settings.displayFields; + const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [filterStatus, setFilterStatus] = useState("all"); const autoRefreshRef = useRef(autoRefresh); @@ -182,13 +181,13 @@ export default function EquipmentMonitoringPage() { fetchData(); }, [fetchData]); - /* ── 자동 갱신 (30초) ── */ + /* ── 자동 갱신 ── */ useEffect(() => { const interval = setInterval(() => { if (autoRefreshRef.current) fetchData(); - }, 30000); + }, settings.refreshInterval * 1000); return () => clearInterval(interval); - }, [fetchData]); + }, [fetchData, settings.refreshInterval]); /* ── 요약 통계 ── */ const stats = useMemo(() => { @@ -293,11 +292,11 @@ export default function EquipmentMonitoringPage() { /* ── 필터 pill ── */ const filterPills: { label: string; value: OperationStatus | "all"; color: string }[] = [ - { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-300 hover:bg-blue-500/30" }, - { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30" }, - { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-300 hover:bg-amber-500/30" }, - { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-300 hover:bg-red-500/30" }, - { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-300 hover:bg-gray-500/30" }, + { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-700 hover:bg-blue-500/30" }, + { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-700 hover:bg-emerald-500/30" }, + { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-700 hover:bg-amber-500/30" }, + { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-700 hover:bg-red-500/30" }, + { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-700 hover:bg-gray-500/30" }, ]; /* ── 포맷 ── */ @@ -309,20 +308,26 @@ export default function EquipmentMonitoringPage() { /* ────────────── 렌더 ────────────── */ return ( -
+
{/* ── 헤더 ── */}
-

설비운영모니터링

+

설비운영모니터링

{/* 현재 시간 */} -
+
{formatDate(currentTime)} - {formatTime(currentTime)} + {formatTime(currentTime)}
{/* 자동갱신 토글 */} @@ -330,51 +335,56 @@ export default function EquipmentMonitoringPage() { variant="outline" size="sm" className={cn( - "border-gray-700 text-xs gap-1.5", + "gap-1.5 text-xs", autoRefresh - ? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20" - : "bg-gray-800 text-gray-400 hover:bg-gray-700" + ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20" + : "bg-muted text-muted-foreground hover:bg-accent", )} onClick={() => setAutoRefresh((v) => !v)} > - + 자동갱신 {autoRefresh ? "ON" : "OFF"} {/* 새로고침 */} + + + {/* 설정 */}
{/* ── 요약 카드 5개 ── */} -
+
{summaryCards.map((card) => ( @@ -390,40 +400,35 @@ export default function EquipmentMonitoringPage() { className={cn( "rounded-full px-4 py-1.5 text-sm font-medium transition-all", filterStatus === pill.value - ? cn(pill.color, "ring-1 ring-white/20") - : "bg-gray-800/60 text-gray-500 hover:text-gray-300 hover:bg-gray-800" + ? cn(pill.color, "ring-1 ring-foreground/10") + : "bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground", )} > {pill.label} ))} - - {filteredEquipments.length}대 표시 - + {filteredEquipments.length}대 표시
{/* ── 로딩 ── */} {loading && equipments.length === 0 && (
- - 설비 데이터를 불러오는 중... + + 설비 데이터를 불러오는 중...
)} {/* ── 데이터 없음 ── */} {!loading && equipments.length === 0 && ( -
- +
+

등록된 설비가 없습니다.

)} {/* ── 설비 카드 그리드 ── */} {filteredEquipments.length > 0 && ( -
+
{filteredEquipments.map((eq) => { const status = resolveStatus(eq.operation_status); const cfg = STATUS_MAP[status]; @@ -434,129 +439,139 @@ export default function EquipmentMonitoringPage() {
{/* 좌측 색상 바 */} -
+
{/* 상단: 설비명 + 상태 배지 */}
-

- {eq.equipment_name || "이름 없음"} -

-

- {eq.equipment_type || "-"} · {eq.installation_location || "-"} + {df.equipmentName && ( +

+ {eq.equipment_name || "이름 없음"} +

+ )} +

+ {df.equipmentType && (eq.equipment_type || "-")} + {df.equipmentType && df.equipmentLocation && " · "} + {df.equipmentLocation && (eq.installation_location || "-")}

- - {cfg.icon} - {cfg.label} - -
- - {/* 구분선 */} -
- - {/* 정보 그리드 */} -
-
- 금일 가동시간 -

-

-
-
- 생산수량 -

-

-
-
- 작업자 -

- {eqWIs.length > 0 && eqWIs[0].worker_name - ? eqWIs[0].worker_name - : "-"} -

-
-
- 설비코드 -

{eq.equipment_code || "-"}

-
-
- - {/* 구분선 */} -
- - {/* 가동률 프로그레스 */} -
-
- 가동률 - - {utilization !== null ? `${utilization}%` : "-"} - -
-
- {utilization !== null && ( -
- )} -
-
- - {/* 구분선 */} -
- - {/* 현재 작업지시 */} -
-

현재 작업지시

- {eqWIs.length > 0 ? ( -
- {eqWIs.slice(0, 2).map((wi) => ( -
- - {wi.instruction_number || "-"} - - - {wi.item_name || "-"} - -
- ))} - {eqWIs.length > 2 && ( -

+{eqWIs.length - 2}건 더

- )} -
- ) : ( -

배정된 작업 없음

+ {df.operationStatus && ( + + {cfg.icon} + {cfg.label} + )}
{/* 구분선 */} -
+
- {/* 센서 데이터 (PLC 미연동) */} -
-
- 온도 - - -
-
- 압력 - - -
-
- RPM - - + {/* 정보 그리드 */} +
+ {df.dailyOperationTime && ( +
+ 금일 가동시간 +

-

+
+ )} + {df.dailyProductionQty && ( +
+ 생산수량 +

-

+
+ )} + {df.worker && ( +
+ 작업자 +

+ {eqWIs.length > 0 && eqWIs[0].worker_name ? eqWIs[0].worker_name : "-"} +

+
+ )} +
+ 설비코드 +

{eq.equipment_code || "-"}

+ + {/* 구분선 */} +
+ + {/* 가동률 프로그레스 */} + {df.utilizationBar && ( +
+
+ 가동률 + + {utilization !== null ? `${utilization}%` : "-"} + +
+
+ {utilization !== null && ( +
+ )} +
+
+ )} + + {/* 구분선 */} +
+ + {/* 현재 작업지시 */} + {df.currentWorkInstruction && ( +
+

현재 작업지시

+ {eqWIs.length > 0 ? ( +
+ {eqWIs.slice(0, 2).map((wi) => ( +
+ + {wi.instruction_number || "-"} + + {wi.item_name || "-"} +
+ ))} + {eqWIs.length > 2 &&

+{eqWIs.length - 2}건 더

} +
+ ) : ( +

배정된 작업 없음

+ )} +
+ )} + + {/* 구분선 */} +
+ + {/* 센서 데이터 (PLC 미연동) */} + {df.sensorData && ( +
+
+ 온도 + - +
+
+ 압력 + - +
+
+ RPM + - +
+
+ )}
); })} @@ -565,8 +580,8 @@ export default function EquipmentMonitoringPage() { {/* 필터 결과 없음 */} {!loading && equipments.length > 0 && filteredEquipments.length === 0 && ( -
- +
+

해당 상태의 설비가 없습니다.

)} diff --git a/frontend/app/(main)/COMPANY_10/monitoring/production/page.tsx b/frontend/app/(main)/COMPANY_10/monitoring/production/page.tsx index fb8aa24b..80d49a93 100644 --- a/frontend/app/(main)/COMPANY_10/monitoring/production/page.tsx +++ b/frontend/app/(main)/COMPANY_10/monitoring/production/page.tsx @@ -5,6 +5,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import type { ProductionDisplayFields } from "@/types/monitoringSettings"; import { RefreshCw, Clock, @@ -16,6 +20,7 @@ import { TrendingUp, Play, Pause, + Settings2, } from "lucide-react"; // ─── 타입 정의 ───────────────────────────────────────────── @@ -71,10 +76,7 @@ function formatTime(date: Date): string { } // 작업지시별 공정현황으로 진행상태 계산 -function computeProgress( - wiId: string, - processMap: Map -): "대기" | "진행중" | "완료" { +function computeProgress(wiId: string, processMap: Map): "대기" | "진행중" | "완료" { const steps = processMap.get(wiId); if (!steps || steps.length === 0) return "대기"; const completedCount = steps.filter((s) => s.status === "completed").length; @@ -85,11 +87,15 @@ function computeProgress( // ─── 메인 컴포넌트 ──────────────────────────────────────── export default function ProductionMonitoringPage() { + const { settings } = useMonitoringSettings("production"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const [workInstructions, setWorkInstructions] = useState([]); const [processMap, setProcessMap] = useState>(new Map()); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [activeTab, setActiveTab] = useState("전체"); // ─── 실시간 시계 ───────────────────────────────────────── @@ -105,8 +111,7 @@ export default function ProductionMonitoringPage() { // 작업지시 목록 조회 const wiRes = await apiClient.get("/work-instruction/list"); - const wiRaw: WorkInstruction[] = - wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; + const wiRaw: WorkInstruction[] = wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; // 작업지시번호 기준 중복 제거 (디테일 JOIN으로 중복 발생 가능) const seen = new Set(); const wiData = wiRaw.filter((wi) => { @@ -120,7 +125,9 @@ export default function ProductionMonitoringPage() { // 공정현황 조회 (실패해도 작업지시는 표시) try { const procRes = await apiClient.post("/table-management/tables/work_order_process/data", { - page: 1, size: 1000, autoFilter: true, + page: 1, + size: 1000, + autoFilter: true, }); const rows: ProcessStep[] = procRes.data?.data?.data || procRes.data?.data?.rows || procRes.data?.data || []; @@ -162,12 +169,12 @@ export default function ProductionMonitoringPage() { fetchData(); }, [fetchData]); - // ─── 자동갱신 (30초) ───────────────────────────────────── + // ─── 자동갱신 ──────────────────────────────────────────── useEffect(() => { if (!autoRefresh) return; - const timer = setInterval(fetchData, 30000); + const timer = setInterval(fetchData, settings.refreshInterval * 1000); return () => clearInterval(timer); - }, [autoRefresh, fetchData]); + }, [autoRefresh, fetchData, settings.refreshInterval]); // ─── 통계 계산 ─────────────────────────────────────────── const stats = useMemo(() => { @@ -202,23 +209,17 @@ export default function ProductionMonitoringPage() { // ─── 렌더링 ────────────────────────────────────────────── return ( -
+
{/* 헤더 */} -
-

생산모니터링

+
+

생산모니터링

-
- +
+ {formatTime(currentTime)}
- +
{/* 요약 카드 */} -
+
} + icon={} label="대기중" value={stats.waiting} colorClass="text-amber-500 bg-amber-500/10 border-amber-500/20" /> } + icon={} label="진행중" value={stats.inProgress} colorClass="text-blue-500 bg-blue-500/10 border-blue-500/20" /> } + icon={} label="완료" value={stats.completed} colorClass="text-emerald-500 bg-emerald-500/10 border-emerald-500/20" /> } + icon={} label="달성율" value={`${stats.achievementRate}%`} colorClass="text-purple-500 bg-purple-500/10 border-purple-500/20" @@ -262,7 +272,7 @@ export default function ProductionMonitoringPage() {
{/* 탭 필터 */} -
+
{(["전체", "대기", "진행중", "완료"] as FilterTab[]).map((tab) => ( +
{/* ── 본문 ── */} -
+
{/* 요약 카드 */}
{summaryCards.map((card) => ( -
-

- {card.label} -

+
+

{card.label}

{card.value} - {card.sub && ( - - {card.sub} - - )} + {card.sub && {card.sub}}

))} @@ -322,10 +286,10 @@ export default function QualityMonitoringPage() { key={tab.key} onClick={() => setActiveTab(tab.key)} className={cn( - "px-4 py-1.5 rounded-full text-sm font-medium transition-colors", + "rounded-full px-4 py-1.5 text-sm font-medium transition-colors", activeTab === tab.key ? "bg-emerald-600 text-white shadow" - : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 border", + : "border bg-white text-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700", )} > {tab.label} @@ -334,170 +298,120 @@ export default function QualityMonitoringPage() {
{/* 테이블 영역 */} -
+
{/* 입고/출하 준비중 */} - {(activeTab === "incoming" || activeTab === "shipping") ? ( + {activeTab === "incoming" || activeTab === "shipping" ? (
- +

준비중

-

- {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 - 아직 지원되지 않습니다. +

+ {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 아직 지원되지 않습니다.

) : loading && filteredRows.length === 0 ? (
- +

데이터를 불러오는 중...

) : filteredRows.length === 0 ? (
- +

금일 검사 데이터가 없습니다

) : (
- + No - 검사번호 - - 검사유형 - - 품목명 - 규격 - - 검사수량 - - - 합격수량 - - - 불합격수량 - - - 불량율 - - - 검사결과 - - - 판정 - - - 검사자 - - 검사일시 - 비고 + {tc.inspectionNo && 검사번호} + {tc.inspectionType && 검사유형} + {tc.itemName && 품목명} + {tc.spec && 규격} + {tc.inspectionQty && 검사수량} + {tc.passFailQty && 합격수량} + {tc.passFailQty && 불합격수량} + {tc.defectRate && 불량율} + {tc.resultBar && 검사결과} + {tc.judgment && 판정} + {tc.inspector && 검사자} + {tc.inspectedAt && 검사일시} + {tc.inspectionCriteria && 검사기준} {filteredRows.map((row) => { - const goodPct = - row.inspectionQty > 0 - ? (row.goodQty / row.inspectionQty) * 100 - : 0; - const defectPct = - row.inspectionQty > 0 - ? (row.defectQty / row.inspectionQty) * 100 - : 0; + const goodPct = row.inspectionQty > 0 ? (row.goodQty / row.inspectionQty) * 100 : 0; + const defectPct = row.inspectionQty > 0 ? (row.defectQty / row.inspectionQty) * 100 : 0; return ( - - - {row.no} - - - {row.inspectionNo} - - - - {row.inspectionType} - - - - {row.itemName} - - - {row.spec} - - - {fmt(row.inspectionQty)} - - - {fmt(row.goodQty)} - - - {fmt(row.defectQty)} - - - {pct(row.defectRate)} - - {/* 검사결과 프로그레스바 */} - -
-
-
-
+ + {row.no} + {tc.inspectionNo && {row.inspectionNo}} + {tc.inspectionType && ( + + + {row.inspectionType} + + + )} + {tc.itemName && {row.itemName}} + {tc.spec && {row.spec}} + {tc.inspectionQty && ( + {fmt(row.inspectionQty)} + )} + {tc.passFailQty && ( + {fmt(row.goodQty)} + )} + {tc.passFailQty && ( + {fmt(row.defectQty)} + )} + {tc.defectRate && ( + + {pct(row.defectRate)} + + )} + {tc.resultBar && ( + +
+
+
+
+
+ + {pct(goodPct)} +
- - {pct(goodPct)} - -
- - {/* 판정 배지 */} - - - {row.result} - - - - {row.inspectorName} - - - {row.inspectedAt !== "-" - ? new Date(row.inspectedAt).toLocaleString( - "ko-KR", - { + + )} + {tc.judgment && ( + + + {row.result} + + + )} + {tc.inspector && {row.inspectorName}} + {tc.inspectedAt && ( + + {row.inspectedAt !== "-" + ? new Date(row.inspectedAt).toLocaleString("ko-KR", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, - }, - ) - : "-"} - - - {row.remark || "-"} - + }) + : "-"} + + )} + {tc.inspectionCriteria && -} ); })} diff --git a/frontend/app/(main)/COMPANY_16/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_16/monitoring/equipment/page.tsx index ccb5aaf6..14adae20 100644 --- a/frontend/app/(main)/COMPANY_16/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_16/monitoring/equipment/page.tsx @@ -12,16 +12,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { - RefreshCw, - Clock, - Loader2, - Inbox, - Wrench, - Zap, - Pause, - Power, -} from "lucide-react"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import { RefreshCw, Clock, Loader2, Inbox, Wrench, Zap, Pause, Power, Settings2 } from "lucide-react"; /* ───── 상태 정의 ───── */ @@ -134,11 +128,16 @@ interface WorkInstruction { /* ───── 컴포넌트 ───── */ export default function EquipmentMonitoringPage() { + const { settings } = useMonitoringSettings("equipment"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const df = settings.displayFields; + const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [filterStatus, setFilterStatus] = useState("all"); const autoRefreshRef = useRef(autoRefresh); @@ -182,13 +181,13 @@ export default function EquipmentMonitoringPage() { fetchData(); }, [fetchData]); - /* ── 자동 갱신 (30초) ── */ + /* ── 자동 갱신 ── */ useEffect(() => { const interval = setInterval(() => { if (autoRefreshRef.current) fetchData(); - }, 30000); + }, settings.refreshInterval * 1000); return () => clearInterval(interval); - }, [fetchData]); + }, [fetchData, settings.refreshInterval]); /* ── 요약 통계 ── */ const stats = useMemo(() => { @@ -293,11 +292,11 @@ export default function EquipmentMonitoringPage() { /* ── 필터 pill ── */ const filterPills: { label: string; value: OperationStatus | "all"; color: string }[] = [ - { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-300 hover:bg-blue-500/30" }, - { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30" }, - { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-300 hover:bg-amber-500/30" }, - { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-300 hover:bg-red-500/30" }, - { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-300 hover:bg-gray-500/30" }, + { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-700 hover:bg-blue-500/30" }, + { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-700 hover:bg-emerald-500/30" }, + { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-700 hover:bg-amber-500/30" }, + { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-700 hover:bg-red-500/30" }, + { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-700 hover:bg-gray-500/30" }, ]; /* ── 포맷 ── */ @@ -309,20 +308,26 @@ export default function EquipmentMonitoringPage() { /* ────────────── 렌더 ────────────── */ return ( -
+
{/* ── 헤더 ── */}
-

설비운영모니터링

+

설비운영모니터링

{/* 현재 시간 */} -
+
{formatDate(currentTime)} - {formatTime(currentTime)} + {formatTime(currentTime)}
{/* 자동갱신 토글 */} @@ -330,51 +335,56 @@ export default function EquipmentMonitoringPage() { variant="outline" size="sm" className={cn( - "border-gray-700 text-xs gap-1.5", + "gap-1.5 text-xs", autoRefresh - ? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20" - : "bg-gray-800 text-gray-400 hover:bg-gray-700" + ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20" + : "bg-muted text-muted-foreground hover:bg-accent", )} onClick={() => setAutoRefresh((v) => !v)} > - + 자동갱신 {autoRefresh ? "ON" : "OFF"} {/* 새로고침 */} + + + {/* 설정 */}
{/* ── 요약 카드 5개 ── */} -
+
{summaryCards.map((card) => ( @@ -390,40 +400,35 @@ export default function EquipmentMonitoringPage() { className={cn( "rounded-full px-4 py-1.5 text-sm font-medium transition-all", filterStatus === pill.value - ? cn(pill.color, "ring-1 ring-white/20") - : "bg-gray-800/60 text-gray-500 hover:text-gray-300 hover:bg-gray-800" + ? cn(pill.color, "ring-1 ring-foreground/10") + : "bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground", )} > {pill.label} ))} - - {filteredEquipments.length}대 표시 - + {filteredEquipments.length}대 표시
{/* ── 로딩 ── */} {loading && equipments.length === 0 && (
- - 설비 데이터를 불러오는 중... + + 설비 데이터를 불러오는 중...
)} {/* ── 데이터 없음 ── */} {!loading && equipments.length === 0 && ( -
- +
+

등록된 설비가 없습니다.

)} {/* ── 설비 카드 그리드 ── */} {filteredEquipments.length > 0 && ( -
+
{filteredEquipments.map((eq) => { const status = resolveStatus(eq.operation_status); const cfg = STATUS_MAP[status]; @@ -434,129 +439,139 @@ export default function EquipmentMonitoringPage() {
{/* 좌측 색상 바 */} -
+
{/* 상단: 설비명 + 상태 배지 */}
-

- {eq.equipment_name || "이름 없음"} -

-

- {eq.equipment_type || "-"} · {eq.installation_location || "-"} + {df.equipmentName && ( +

+ {eq.equipment_name || "이름 없음"} +

+ )} +

+ {df.equipmentType && (eq.equipment_type || "-")} + {df.equipmentType && df.equipmentLocation && " · "} + {df.equipmentLocation && (eq.installation_location || "-")}

- - {cfg.icon} - {cfg.label} - -
- - {/* 구분선 */} -
- - {/* 정보 그리드 */} -
-
- 금일 가동시간 -

-

-
-
- 생산수량 -

-

-
-
- 작업자 -

- {eqWIs.length > 0 && eqWIs[0].worker_name - ? eqWIs[0].worker_name - : "-"} -

-
-
- 설비코드 -

{eq.equipment_code || "-"}

-
-
- - {/* 구분선 */} -
- - {/* 가동률 프로그레스 */} -
-
- 가동률 - - {utilization !== null ? `${utilization}%` : "-"} - -
-
- {utilization !== null && ( -
- )} -
-
- - {/* 구분선 */} -
- - {/* 현재 작업지시 */} -
-

현재 작업지시

- {eqWIs.length > 0 ? ( -
- {eqWIs.slice(0, 2).map((wi) => ( -
- - {wi.instruction_number || "-"} - - - {wi.item_name || "-"} - -
- ))} - {eqWIs.length > 2 && ( -

+{eqWIs.length - 2}건 더

- )} -
- ) : ( -

배정된 작업 없음

+ {df.operationStatus && ( + + {cfg.icon} + {cfg.label} + )}
{/* 구분선 */} -
+
- {/* 센서 데이터 (PLC 미연동) */} -
-
- 온도 - - -
-
- 압력 - - -
-
- RPM - - + {/* 정보 그리드 */} +
+ {df.dailyOperationTime && ( +
+ 금일 가동시간 +

-

+
+ )} + {df.dailyProductionQty && ( +
+ 생산수량 +

-

+
+ )} + {df.worker && ( +
+ 작업자 +

+ {eqWIs.length > 0 && eqWIs[0].worker_name ? eqWIs[0].worker_name : "-"} +

+
+ )} +
+ 설비코드 +

{eq.equipment_code || "-"}

+ + {/* 구분선 */} +
+ + {/* 가동률 프로그레스 */} + {df.utilizationBar && ( +
+
+ 가동률 + + {utilization !== null ? `${utilization}%` : "-"} + +
+
+ {utilization !== null && ( +
+ )} +
+
+ )} + + {/* 구분선 */} +
+ + {/* 현재 작업지시 */} + {df.currentWorkInstruction && ( +
+

현재 작업지시

+ {eqWIs.length > 0 ? ( +
+ {eqWIs.slice(0, 2).map((wi) => ( +
+ + {wi.instruction_number || "-"} + + {wi.item_name || "-"} +
+ ))} + {eqWIs.length > 2 &&

+{eqWIs.length - 2}건 더

} +
+ ) : ( +

배정된 작업 없음

+ )} +
+ )} + + {/* 구분선 */} +
+ + {/* 센서 데이터 (PLC 미연동) */} + {df.sensorData && ( +
+
+ 온도 + - +
+
+ 압력 + - +
+
+ RPM + - +
+
+ )}
); })} @@ -565,8 +580,8 @@ export default function EquipmentMonitoringPage() { {/* 필터 결과 없음 */} {!loading && equipments.length > 0 && filteredEquipments.length === 0 && ( -
- +
+

해당 상태의 설비가 없습니다.

)} diff --git a/frontend/app/(main)/COMPANY_16/monitoring/production/page.tsx b/frontend/app/(main)/COMPANY_16/monitoring/production/page.tsx index fb8aa24b..80d49a93 100644 --- a/frontend/app/(main)/COMPANY_16/monitoring/production/page.tsx +++ b/frontend/app/(main)/COMPANY_16/monitoring/production/page.tsx @@ -5,6 +5,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import type { ProductionDisplayFields } from "@/types/monitoringSettings"; import { RefreshCw, Clock, @@ -16,6 +20,7 @@ import { TrendingUp, Play, Pause, + Settings2, } from "lucide-react"; // ─── 타입 정의 ───────────────────────────────────────────── @@ -71,10 +76,7 @@ function formatTime(date: Date): string { } // 작업지시별 공정현황으로 진행상태 계산 -function computeProgress( - wiId: string, - processMap: Map -): "대기" | "진행중" | "완료" { +function computeProgress(wiId: string, processMap: Map): "대기" | "진행중" | "완료" { const steps = processMap.get(wiId); if (!steps || steps.length === 0) return "대기"; const completedCount = steps.filter((s) => s.status === "completed").length; @@ -85,11 +87,15 @@ function computeProgress( // ─── 메인 컴포넌트 ──────────────────────────────────────── export default function ProductionMonitoringPage() { + const { settings } = useMonitoringSettings("production"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const [workInstructions, setWorkInstructions] = useState([]); const [processMap, setProcessMap] = useState>(new Map()); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [activeTab, setActiveTab] = useState("전체"); // ─── 실시간 시계 ───────────────────────────────────────── @@ -105,8 +111,7 @@ export default function ProductionMonitoringPage() { // 작업지시 목록 조회 const wiRes = await apiClient.get("/work-instruction/list"); - const wiRaw: WorkInstruction[] = - wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; + const wiRaw: WorkInstruction[] = wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; // 작업지시번호 기준 중복 제거 (디테일 JOIN으로 중복 발생 가능) const seen = new Set(); const wiData = wiRaw.filter((wi) => { @@ -120,7 +125,9 @@ export default function ProductionMonitoringPage() { // 공정현황 조회 (실패해도 작업지시는 표시) try { const procRes = await apiClient.post("/table-management/tables/work_order_process/data", { - page: 1, size: 1000, autoFilter: true, + page: 1, + size: 1000, + autoFilter: true, }); const rows: ProcessStep[] = procRes.data?.data?.data || procRes.data?.data?.rows || procRes.data?.data || []; @@ -162,12 +169,12 @@ export default function ProductionMonitoringPage() { fetchData(); }, [fetchData]); - // ─── 자동갱신 (30초) ───────────────────────────────────── + // ─── 자동갱신 ──────────────────────────────────────────── useEffect(() => { if (!autoRefresh) return; - const timer = setInterval(fetchData, 30000); + const timer = setInterval(fetchData, settings.refreshInterval * 1000); return () => clearInterval(timer); - }, [autoRefresh, fetchData]); + }, [autoRefresh, fetchData, settings.refreshInterval]); // ─── 통계 계산 ─────────────────────────────────────────── const stats = useMemo(() => { @@ -202,23 +209,17 @@ export default function ProductionMonitoringPage() { // ─── 렌더링 ────────────────────────────────────────────── return ( -
+
{/* 헤더 */} -
-

생산모니터링

+
+

생산모니터링

-
- +
+ {formatTime(currentTime)}
- +
{/* 요약 카드 */} -
+
} + icon={} label="대기중" value={stats.waiting} colorClass="text-amber-500 bg-amber-500/10 border-amber-500/20" /> } + icon={} label="진행중" value={stats.inProgress} colorClass="text-blue-500 bg-blue-500/10 border-blue-500/20" /> } + icon={} label="완료" value={stats.completed} colorClass="text-emerald-500 bg-emerald-500/10 border-emerald-500/20" /> } + icon={} label="달성율" value={`${stats.achievementRate}%`} colorClass="text-purple-500 bg-purple-500/10 border-purple-500/20" @@ -262,7 +272,7 @@ export default function ProductionMonitoringPage() {
{/* 탭 필터 */} -
+
{(["전체", "대기", "진행중", "완료"] as FilterTab[]).map((tab) => ( +
{/* ── 본문 ── */} -
+
{/* 요약 카드 */}
{summaryCards.map((card) => ( -
-

- {card.label} -

+
+

{card.label}

{card.value} - {card.sub && ( - - {card.sub} - - )} + {card.sub && {card.sub}}

))} @@ -322,10 +286,10 @@ export default function QualityMonitoringPage() { key={tab.key} onClick={() => setActiveTab(tab.key)} className={cn( - "px-4 py-1.5 rounded-full text-sm font-medium transition-colors", + "rounded-full px-4 py-1.5 text-sm font-medium transition-colors", activeTab === tab.key ? "bg-emerald-600 text-white shadow" - : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 border", + : "border bg-white text-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700", )} > {tab.label} @@ -334,170 +298,120 @@ export default function QualityMonitoringPage() {
{/* 테이블 영역 */} -
+
{/* 입고/출하 준비중 */} - {(activeTab === "incoming" || activeTab === "shipping") ? ( + {activeTab === "incoming" || activeTab === "shipping" ? (
- +

준비중

-

- {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 - 아직 지원되지 않습니다. +

+ {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 아직 지원되지 않습니다.

) : loading && filteredRows.length === 0 ? (
- +

데이터를 불러오는 중...

) : filteredRows.length === 0 ? (
- +

금일 검사 데이터가 없습니다

) : (
- + No - 검사번호 - - 검사유형 - - 품목명 - 규격 - - 검사수량 - - - 합격수량 - - - 불합격수량 - - - 불량율 - - - 검사결과 - - - 판정 - - - 검사자 - - 검사일시 - 비고 + {tc.inspectionNo && 검사번호} + {tc.inspectionType && 검사유형} + {tc.itemName && 품목명} + {tc.spec && 규격} + {tc.inspectionQty && 검사수량} + {tc.passFailQty && 합격수량} + {tc.passFailQty && 불합격수량} + {tc.defectRate && 불량율} + {tc.resultBar && 검사결과} + {tc.judgment && 판정} + {tc.inspector && 검사자} + {tc.inspectedAt && 검사일시} + {tc.inspectionCriteria && 검사기준} {filteredRows.map((row) => { - const goodPct = - row.inspectionQty > 0 - ? (row.goodQty / row.inspectionQty) * 100 - : 0; - const defectPct = - row.inspectionQty > 0 - ? (row.defectQty / row.inspectionQty) * 100 - : 0; + const goodPct = row.inspectionQty > 0 ? (row.goodQty / row.inspectionQty) * 100 : 0; + const defectPct = row.inspectionQty > 0 ? (row.defectQty / row.inspectionQty) * 100 : 0; return ( - - - {row.no} - - - {row.inspectionNo} - - - - {row.inspectionType} - - - - {row.itemName} - - - {row.spec} - - - {fmt(row.inspectionQty)} - - - {fmt(row.goodQty)} - - - {fmt(row.defectQty)} - - - {pct(row.defectRate)} - - {/* 검사결과 프로그레스바 */} - -
-
-
-
+ + {row.no} + {tc.inspectionNo && {row.inspectionNo}} + {tc.inspectionType && ( + + + {row.inspectionType} + + + )} + {tc.itemName && {row.itemName}} + {tc.spec && {row.spec}} + {tc.inspectionQty && ( + {fmt(row.inspectionQty)} + )} + {tc.passFailQty && ( + {fmt(row.goodQty)} + )} + {tc.passFailQty && ( + {fmt(row.defectQty)} + )} + {tc.defectRate && ( + + {pct(row.defectRate)} + + )} + {tc.resultBar && ( + +
+
+
+
+
+ + {pct(goodPct)} +
- - {pct(goodPct)} - -
- - {/* 판정 배지 */} - - - {row.result} - - - - {row.inspectorName} - - - {row.inspectedAt !== "-" - ? new Date(row.inspectedAt).toLocaleString( - "ko-KR", - { + + )} + {tc.judgment && ( + + + {row.result} + + + )} + {tc.inspector && {row.inspectorName}} + {tc.inspectedAt && ( + + {row.inspectedAt !== "-" + ? new Date(row.inspectedAt).toLocaleString("ko-KR", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, - }, - ) - : "-"} - - - {row.remark || "-"} - + }) + : "-"} + + )} + {tc.inspectionCriteria && -} ); })} diff --git a/frontend/app/(main)/COMPANY_29/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_29/monitoring/equipment/page.tsx index ccb5aaf6..14adae20 100644 --- a/frontend/app/(main)/COMPANY_29/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_29/monitoring/equipment/page.tsx @@ -12,16 +12,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { - RefreshCw, - Clock, - Loader2, - Inbox, - Wrench, - Zap, - Pause, - Power, -} from "lucide-react"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import { RefreshCw, Clock, Loader2, Inbox, Wrench, Zap, Pause, Power, Settings2 } from "lucide-react"; /* ───── 상태 정의 ───── */ @@ -134,11 +128,16 @@ interface WorkInstruction { /* ───── 컴포넌트 ───── */ export default function EquipmentMonitoringPage() { + const { settings } = useMonitoringSettings("equipment"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const df = settings.displayFields; + const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [filterStatus, setFilterStatus] = useState("all"); const autoRefreshRef = useRef(autoRefresh); @@ -182,13 +181,13 @@ export default function EquipmentMonitoringPage() { fetchData(); }, [fetchData]); - /* ── 자동 갱신 (30초) ── */ + /* ── 자동 갱신 ── */ useEffect(() => { const interval = setInterval(() => { if (autoRefreshRef.current) fetchData(); - }, 30000); + }, settings.refreshInterval * 1000); return () => clearInterval(interval); - }, [fetchData]); + }, [fetchData, settings.refreshInterval]); /* ── 요약 통계 ── */ const stats = useMemo(() => { @@ -293,11 +292,11 @@ export default function EquipmentMonitoringPage() { /* ── 필터 pill ── */ const filterPills: { label: string; value: OperationStatus | "all"; color: string }[] = [ - { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-300 hover:bg-blue-500/30" }, - { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30" }, - { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-300 hover:bg-amber-500/30" }, - { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-300 hover:bg-red-500/30" }, - { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-300 hover:bg-gray-500/30" }, + { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-700 hover:bg-blue-500/30" }, + { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-700 hover:bg-emerald-500/30" }, + { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-700 hover:bg-amber-500/30" }, + { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-700 hover:bg-red-500/30" }, + { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-700 hover:bg-gray-500/30" }, ]; /* ── 포맷 ── */ @@ -309,20 +308,26 @@ export default function EquipmentMonitoringPage() { /* ────────────── 렌더 ────────────── */ return ( -
+
{/* ── 헤더 ── */}
-

설비운영모니터링

+

설비운영모니터링

{/* 현재 시간 */} -
+
{formatDate(currentTime)} - {formatTime(currentTime)} + {formatTime(currentTime)}
{/* 자동갱신 토글 */} @@ -330,51 +335,56 @@ export default function EquipmentMonitoringPage() { variant="outline" size="sm" className={cn( - "border-gray-700 text-xs gap-1.5", + "gap-1.5 text-xs", autoRefresh - ? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20" - : "bg-gray-800 text-gray-400 hover:bg-gray-700" + ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20" + : "bg-muted text-muted-foreground hover:bg-accent", )} onClick={() => setAutoRefresh((v) => !v)} > - + 자동갱신 {autoRefresh ? "ON" : "OFF"} {/* 새로고침 */} + + + {/* 설정 */}
{/* ── 요약 카드 5개 ── */} -
+
{summaryCards.map((card) => ( @@ -390,40 +400,35 @@ export default function EquipmentMonitoringPage() { className={cn( "rounded-full px-4 py-1.5 text-sm font-medium transition-all", filterStatus === pill.value - ? cn(pill.color, "ring-1 ring-white/20") - : "bg-gray-800/60 text-gray-500 hover:text-gray-300 hover:bg-gray-800" + ? cn(pill.color, "ring-1 ring-foreground/10") + : "bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground", )} > {pill.label} ))} - - {filteredEquipments.length}대 표시 - + {filteredEquipments.length}대 표시
{/* ── 로딩 ── */} {loading && equipments.length === 0 && (
- - 설비 데이터를 불러오는 중... + + 설비 데이터를 불러오는 중...
)} {/* ── 데이터 없음 ── */} {!loading && equipments.length === 0 && ( -
- +
+

등록된 설비가 없습니다.

)} {/* ── 설비 카드 그리드 ── */} {filteredEquipments.length > 0 && ( -
+
{filteredEquipments.map((eq) => { const status = resolveStatus(eq.operation_status); const cfg = STATUS_MAP[status]; @@ -434,129 +439,139 @@ export default function EquipmentMonitoringPage() {
{/* 좌측 색상 바 */} -
+
{/* 상단: 설비명 + 상태 배지 */}
-

- {eq.equipment_name || "이름 없음"} -

-

- {eq.equipment_type || "-"} · {eq.installation_location || "-"} + {df.equipmentName && ( +

+ {eq.equipment_name || "이름 없음"} +

+ )} +

+ {df.equipmentType && (eq.equipment_type || "-")} + {df.equipmentType && df.equipmentLocation && " · "} + {df.equipmentLocation && (eq.installation_location || "-")}

- - {cfg.icon} - {cfg.label} - -
- - {/* 구분선 */} -
- - {/* 정보 그리드 */} -
-
- 금일 가동시간 -

-

-
-
- 생산수량 -

-

-
-
- 작업자 -

- {eqWIs.length > 0 && eqWIs[0].worker_name - ? eqWIs[0].worker_name - : "-"} -

-
-
- 설비코드 -

{eq.equipment_code || "-"}

-
-
- - {/* 구분선 */} -
- - {/* 가동률 프로그레스 */} -
-
- 가동률 - - {utilization !== null ? `${utilization}%` : "-"} - -
-
- {utilization !== null && ( -
- )} -
-
- - {/* 구분선 */} -
- - {/* 현재 작업지시 */} -
-

현재 작업지시

- {eqWIs.length > 0 ? ( -
- {eqWIs.slice(0, 2).map((wi) => ( -
- - {wi.instruction_number || "-"} - - - {wi.item_name || "-"} - -
- ))} - {eqWIs.length > 2 && ( -

+{eqWIs.length - 2}건 더

- )} -
- ) : ( -

배정된 작업 없음

+ {df.operationStatus && ( + + {cfg.icon} + {cfg.label} + )}
{/* 구분선 */} -
+
- {/* 센서 데이터 (PLC 미연동) */} -
-
- 온도 - - -
-
- 압력 - - -
-
- RPM - - + {/* 정보 그리드 */} +
+ {df.dailyOperationTime && ( +
+ 금일 가동시간 +

-

+
+ )} + {df.dailyProductionQty && ( +
+ 생산수량 +

-

+
+ )} + {df.worker && ( +
+ 작업자 +

+ {eqWIs.length > 0 && eqWIs[0].worker_name ? eqWIs[0].worker_name : "-"} +

+
+ )} +
+ 설비코드 +

{eq.equipment_code || "-"}

+ + {/* 구분선 */} +
+ + {/* 가동률 프로그레스 */} + {df.utilizationBar && ( +
+
+ 가동률 + + {utilization !== null ? `${utilization}%` : "-"} + +
+
+ {utilization !== null && ( +
+ )} +
+
+ )} + + {/* 구분선 */} +
+ + {/* 현재 작업지시 */} + {df.currentWorkInstruction && ( +
+

현재 작업지시

+ {eqWIs.length > 0 ? ( +
+ {eqWIs.slice(0, 2).map((wi) => ( +
+ + {wi.instruction_number || "-"} + + {wi.item_name || "-"} +
+ ))} + {eqWIs.length > 2 &&

+{eqWIs.length - 2}건 더

} +
+ ) : ( +

배정된 작업 없음

+ )} +
+ )} + + {/* 구분선 */} +
+ + {/* 센서 데이터 (PLC 미연동) */} + {df.sensorData && ( +
+
+ 온도 + - +
+
+ 압력 + - +
+
+ RPM + - +
+
+ )}
); })} @@ -565,8 +580,8 @@ export default function EquipmentMonitoringPage() { {/* 필터 결과 없음 */} {!loading && equipments.length > 0 && filteredEquipments.length === 0 && ( -
- +
+

해당 상태의 설비가 없습니다.

)} diff --git a/frontend/app/(main)/COMPANY_29/monitoring/production/page.tsx b/frontend/app/(main)/COMPANY_29/monitoring/production/page.tsx index fb8aa24b..80d49a93 100644 --- a/frontend/app/(main)/COMPANY_29/monitoring/production/page.tsx +++ b/frontend/app/(main)/COMPANY_29/monitoring/production/page.tsx @@ -5,6 +5,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import type { ProductionDisplayFields } from "@/types/monitoringSettings"; import { RefreshCw, Clock, @@ -16,6 +20,7 @@ import { TrendingUp, Play, Pause, + Settings2, } from "lucide-react"; // ─── 타입 정의 ───────────────────────────────────────────── @@ -71,10 +76,7 @@ function formatTime(date: Date): string { } // 작업지시별 공정현황으로 진행상태 계산 -function computeProgress( - wiId: string, - processMap: Map -): "대기" | "진행중" | "완료" { +function computeProgress(wiId: string, processMap: Map): "대기" | "진행중" | "완료" { const steps = processMap.get(wiId); if (!steps || steps.length === 0) return "대기"; const completedCount = steps.filter((s) => s.status === "completed").length; @@ -85,11 +87,15 @@ function computeProgress( // ─── 메인 컴포넌트 ──────────────────────────────────────── export default function ProductionMonitoringPage() { + const { settings } = useMonitoringSettings("production"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const [workInstructions, setWorkInstructions] = useState([]); const [processMap, setProcessMap] = useState>(new Map()); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [activeTab, setActiveTab] = useState("전체"); // ─── 실시간 시계 ───────────────────────────────────────── @@ -105,8 +111,7 @@ export default function ProductionMonitoringPage() { // 작업지시 목록 조회 const wiRes = await apiClient.get("/work-instruction/list"); - const wiRaw: WorkInstruction[] = - wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; + const wiRaw: WorkInstruction[] = wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; // 작업지시번호 기준 중복 제거 (디테일 JOIN으로 중복 발생 가능) const seen = new Set(); const wiData = wiRaw.filter((wi) => { @@ -120,7 +125,9 @@ export default function ProductionMonitoringPage() { // 공정현황 조회 (실패해도 작업지시는 표시) try { const procRes = await apiClient.post("/table-management/tables/work_order_process/data", { - page: 1, size: 1000, autoFilter: true, + page: 1, + size: 1000, + autoFilter: true, }); const rows: ProcessStep[] = procRes.data?.data?.data || procRes.data?.data?.rows || procRes.data?.data || []; @@ -162,12 +169,12 @@ export default function ProductionMonitoringPage() { fetchData(); }, [fetchData]); - // ─── 자동갱신 (30초) ───────────────────────────────────── + // ─── 자동갱신 ──────────────────────────────────────────── useEffect(() => { if (!autoRefresh) return; - const timer = setInterval(fetchData, 30000); + const timer = setInterval(fetchData, settings.refreshInterval * 1000); return () => clearInterval(timer); - }, [autoRefresh, fetchData]); + }, [autoRefresh, fetchData, settings.refreshInterval]); // ─── 통계 계산 ─────────────────────────────────────────── const stats = useMemo(() => { @@ -202,23 +209,17 @@ export default function ProductionMonitoringPage() { // ─── 렌더링 ────────────────────────────────────────────── return ( -
+
{/* 헤더 */} -
-

생산모니터링

+
+

생산모니터링

-
- +
+ {formatTime(currentTime)}
- +
{/* 요약 카드 */} -
+
} + icon={} label="대기중" value={stats.waiting} colorClass="text-amber-500 bg-amber-500/10 border-amber-500/20" /> } + icon={} label="진행중" value={stats.inProgress} colorClass="text-blue-500 bg-blue-500/10 border-blue-500/20" /> } + icon={} label="완료" value={stats.completed} colorClass="text-emerald-500 bg-emerald-500/10 border-emerald-500/20" /> } + icon={} label="달성율" value={`${stats.achievementRate}%`} colorClass="text-purple-500 bg-purple-500/10 border-purple-500/20" @@ -262,7 +272,7 @@ export default function ProductionMonitoringPage() {
{/* 탭 필터 */} -
+
{(["전체", "대기", "진행중", "완료"] as FilterTab[]).map((tab) => ( +
{/* ── 본문 ── */} -
+
{/* 요약 카드 */}
{summaryCards.map((card) => ( -
-

- {card.label} -

+
+

{card.label}

{card.value} - {card.sub && ( - - {card.sub} - - )} + {card.sub && {card.sub}}

))} @@ -322,10 +286,10 @@ export default function QualityMonitoringPage() { key={tab.key} onClick={() => setActiveTab(tab.key)} className={cn( - "px-4 py-1.5 rounded-full text-sm font-medium transition-colors", + "rounded-full px-4 py-1.5 text-sm font-medium transition-colors", activeTab === tab.key ? "bg-emerald-600 text-white shadow" - : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 border", + : "border bg-white text-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700", )} > {tab.label} @@ -334,170 +298,120 @@ export default function QualityMonitoringPage() {
{/* 테이블 영역 */} -
+
{/* 입고/출하 준비중 */} - {(activeTab === "incoming" || activeTab === "shipping") ? ( + {activeTab === "incoming" || activeTab === "shipping" ? (
- +

준비중

-

- {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 - 아직 지원되지 않습니다. +

+ {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 아직 지원되지 않습니다.

) : loading && filteredRows.length === 0 ? (
- +

데이터를 불러오는 중...

) : filteredRows.length === 0 ? (
- +

금일 검사 데이터가 없습니다

) : (
- + No - 검사번호 - - 검사유형 - - 품목명 - 규격 - - 검사수량 - - - 합격수량 - - - 불합격수량 - - - 불량율 - - - 검사결과 - - - 판정 - - - 검사자 - - 검사일시 - 비고 + {tc.inspectionNo && 검사번호} + {tc.inspectionType && 검사유형} + {tc.itemName && 품목명} + {tc.spec && 규격} + {tc.inspectionQty && 검사수량} + {tc.passFailQty && 합격수량} + {tc.passFailQty && 불합격수량} + {tc.defectRate && 불량율} + {tc.resultBar && 검사결과} + {tc.judgment && 판정} + {tc.inspector && 검사자} + {tc.inspectedAt && 검사일시} + {tc.inspectionCriteria && 검사기준} {filteredRows.map((row) => { - const goodPct = - row.inspectionQty > 0 - ? (row.goodQty / row.inspectionQty) * 100 - : 0; - const defectPct = - row.inspectionQty > 0 - ? (row.defectQty / row.inspectionQty) * 100 - : 0; + const goodPct = row.inspectionQty > 0 ? (row.goodQty / row.inspectionQty) * 100 : 0; + const defectPct = row.inspectionQty > 0 ? (row.defectQty / row.inspectionQty) * 100 : 0; return ( - - - {row.no} - - - {row.inspectionNo} - - - - {row.inspectionType} - - - - {row.itemName} - - - {row.spec} - - - {fmt(row.inspectionQty)} - - - {fmt(row.goodQty)} - - - {fmt(row.defectQty)} - - - {pct(row.defectRate)} - - {/* 검사결과 프로그레스바 */} - -
-
-
-
+ + {row.no} + {tc.inspectionNo && {row.inspectionNo}} + {tc.inspectionType && ( + + + {row.inspectionType} + + + )} + {tc.itemName && {row.itemName}} + {tc.spec && {row.spec}} + {tc.inspectionQty && ( + {fmt(row.inspectionQty)} + )} + {tc.passFailQty && ( + {fmt(row.goodQty)} + )} + {tc.passFailQty && ( + {fmt(row.defectQty)} + )} + {tc.defectRate && ( + + {pct(row.defectRate)} + + )} + {tc.resultBar && ( + +
+
+
+
+
+ + {pct(goodPct)} +
- - {pct(goodPct)} - -
- - {/* 판정 배지 */} - - - {row.result} - - - - {row.inspectorName} - - - {row.inspectedAt !== "-" - ? new Date(row.inspectedAt).toLocaleString( - "ko-KR", - { + + )} + {tc.judgment && ( + + + {row.result} + + + )} + {tc.inspector && {row.inspectorName}} + {tc.inspectedAt && ( + + {row.inspectedAt !== "-" + ? new Date(row.inspectedAt).toLocaleString("ko-KR", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, - }, - ) - : "-"} - - - {row.remark || "-"} - + }) + : "-"} + + )} + {tc.inspectionCriteria && -} ); })} diff --git a/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx index ccb5aaf6..14adae20 100644 --- a/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx @@ -12,16 +12,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { - RefreshCw, - Clock, - Loader2, - Inbox, - Wrench, - Zap, - Pause, - Power, -} from "lucide-react"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import { RefreshCw, Clock, Loader2, Inbox, Wrench, Zap, Pause, Power, Settings2 } from "lucide-react"; /* ───── 상태 정의 ───── */ @@ -134,11 +128,16 @@ interface WorkInstruction { /* ───── 컴포넌트 ───── */ export default function EquipmentMonitoringPage() { + const { settings } = useMonitoringSettings("equipment"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const df = settings.displayFields; + const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [filterStatus, setFilterStatus] = useState("all"); const autoRefreshRef = useRef(autoRefresh); @@ -182,13 +181,13 @@ export default function EquipmentMonitoringPage() { fetchData(); }, [fetchData]); - /* ── 자동 갱신 (30초) ── */ + /* ── 자동 갱신 ── */ useEffect(() => { const interval = setInterval(() => { if (autoRefreshRef.current) fetchData(); - }, 30000); + }, settings.refreshInterval * 1000); return () => clearInterval(interval); - }, [fetchData]); + }, [fetchData, settings.refreshInterval]); /* ── 요약 통계 ── */ const stats = useMemo(() => { @@ -293,11 +292,11 @@ export default function EquipmentMonitoringPage() { /* ── 필터 pill ── */ const filterPills: { label: string; value: OperationStatus | "all"; color: string }[] = [ - { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-300 hover:bg-blue-500/30" }, - { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30" }, - { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-300 hover:bg-amber-500/30" }, - { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-300 hover:bg-red-500/30" }, - { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-300 hover:bg-gray-500/30" }, + { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-700 hover:bg-blue-500/30" }, + { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-700 hover:bg-emerald-500/30" }, + { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-700 hover:bg-amber-500/30" }, + { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-700 hover:bg-red-500/30" }, + { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-700 hover:bg-gray-500/30" }, ]; /* ── 포맷 ── */ @@ -309,20 +308,26 @@ export default function EquipmentMonitoringPage() { /* ────────────── 렌더 ────────────── */ return ( -
+
{/* ── 헤더 ── */}
-

설비운영모니터링

+

설비운영모니터링

{/* 현재 시간 */} -
+
{formatDate(currentTime)} - {formatTime(currentTime)} + {formatTime(currentTime)}
{/* 자동갱신 토글 */} @@ -330,51 +335,56 @@ export default function EquipmentMonitoringPage() { variant="outline" size="sm" className={cn( - "border-gray-700 text-xs gap-1.5", + "gap-1.5 text-xs", autoRefresh - ? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20" - : "bg-gray-800 text-gray-400 hover:bg-gray-700" + ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20" + : "bg-muted text-muted-foreground hover:bg-accent", )} onClick={() => setAutoRefresh((v) => !v)} > - + 자동갱신 {autoRefresh ? "ON" : "OFF"} {/* 새로고침 */} + + + {/* 설정 */}
{/* ── 요약 카드 5개 ── */} -
+
{summaryCards.map((card) => ( @@ -390,40 +400,35 @@ export default function EquipmentMonitoringPage() { className={cn( "rounded-full px-4 py-1.5 text-sm font-medium transition-all", filterStatus === pill.value - ? cn(pill.color, "ring-1 ring-white/20") - : "bg-gray-800/60 text-gray-500 hover:text-gray-300 hover:bg-gray-800" + ? cn(pill.color, "ring-1 ring-foreground/10") + : "bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground", )} > {pill.label} ))} - - {filteredEquipments.length}대 표시 - + {filteredEquipments.length}대 표시
{/* ── 로딩 ── */} {loading && equipments.length === 0 && (
- - 설비 데이터를 불러오는 중... + + 설비 데이터를 불러오는 중...
)} {/* ── 데이터 없음 ── */} {!loading && equipments.length === 0 && ( -
- +
+

등록된 설비가 없습니다.

)} {/* ── 설비 카드 그리드 ── */} {filteredEquipments.length > 0 && ( -
+
{filteredEquipments.map((eq) => { const status = resolveStatus(eq.operation_status); const cfg = STATUS_MAP[status]; @@ -434,129 +439,139 @@ export default function EquipmentMonitoringPage() {
{/* 좌측 색상 바 */} -
+
{/* 상단: 설비명 + 상태 배지 */}
-

- {eq.equipment_name || "이름 없음"} -

-

- {eq.equipment_type || "-"} · {eq.installation_location || "-"} + {df.equipmentName && ( +

+ {eq.equipment_name || "이름 없음"} +

+ )} +

+ {df.equipmentType && (eq.equipment_type || "-")} + {df.equipmentType && df.equipmentLocation && " · "} + {df.equipmentLocation && (eq.installation_location || "-")}

- - {cfg.icon} - {cfg.label} - -
- - {/* 구분선 */} -
- - {/* 정보 그리드 */} -
-
- 금일 가동시간 -

-

-
-
- 생산수량 -

-

-
-
- 작업자 -

- {eqWIs.length > 0 && eqWIs[0].worker_name - ? eqWIs[0].worker_name - : "-"} -

-
-
- 설비코드 -

{eq.equipment_code || "-"}

-
-
- - {/* 구분선 */} -
- - {/* 가동률 프로그레스 */} -
-
- 가동률 - - {utilization !== null ? `${utilization}%` : "-"} - -
-
- {utilization !== null && ( -
- )} -
-
- - {/* 구분선 */} -
- - {/* 현재 작업지시 */} -
-

현재 작업지시

- {eqWIs.length > 0 ? ( -
- {eqWIs.slice(0, 2).map((wi) => ( -
- - {wi.instruction_number || "-"} - - - {wi.item_name || "-"} - -
- ))} - {eqWIs.length > 2 && ( -

+{eqWIs.length - 2}건 더

- )} -
- ) : ( -

배정된 작업 없음

+ {df.operationStatus && ( + + {cfg.icon} + {cfg.label} + )}
{/* 구분선 */} -
+
- {/* 센서 데이터 (PLC 미연동) */} -
-
- 온도 - - -
-
- 압력 - - -
-
- RPM - - + {/* 정보 그리드 */} +
+ {df.dailyOperationTime && ( +
+ 금일 가동시간 +

-

+
+ )} + {df.dailyProductionQty && ( +
+ 생산수량 +

-

+
+ )} + {df.worker && ( +
+ 작업자 +

+ {eqWIs.length > 0 && eqWIs[0].worker_name ? eqWIs[0].worker_name : "-"} +

+
+ )} +
+ 설비코드 +

{eq.equipment_code || "-"}

+ + {/* 구분선 */} +
+ + {/* 가동률 프로그레스 */} + {df.utilizationBar && ( +
+
+ 가동률 + + {utilization !== null ? `${utilization}%` : "-"} + +
+
+ {utilization !== null && ( +
+ )} +
+
+ )} + + {/* 구분선 */} +
+ + {/* 현재 작업지시 */} + {df.currentWorkInstruction && ( +
+

현재 작업지시

+ {eqWIs.length > 0 ? ( +
+ {eqWIs.slice(0, 2).map((wi) => ( +
+ + {wi.instruction_number || "-"} + + {wi.item_name || "-"} +
+ ))} + {eqWIs.length > 2 &&

+{eqWIs.length - 2}건 더

} +
+ ) : ( +

배정된 작업 없음

+ )} +
+ )} + + {/* 구분선 */} +
+ + {/* 센서 데이터 (PLC 미연동) */} + {df.sensorData && ( +
+
+ 온도 + - +
+
+ 압력 + - +
+
+ RPM + - +
+
+ )}
); })} @@ -565,8 +580,8 @@ export default function EquipmentMonitoringPage() { {/* 필터 결과 없음 */} {!loading && equipments.length > 0 && filteredEquipments.length === 0 && ( -
- +
+

해당 상태의 설비가 없습니다.

)} diff --git a/frontend/app/(main)/COMPANY_30/monitoring/production/page.tsx b/frontend/app/(main)/COMPANY_30/monitoring/production/page.tsx index fb8aa24b..80d49a93 100644 --- a/frontend/app/(main)/COMPANY_30/monitoring/production/page.tsx +++ b/frontend/app/(main)/COMPANY_30/monitoring/production/page.tsx @@ -5,6 +5,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import type { ProductionDisplayFields } from "@/types/monitoringSettings"; import { RefreshCw, Clock, @@ -16,6 +20,7 @@ import { TrendingUp, Play, Pause, + Settings2, } from "lucide-react"; // ─── 타입 정의 ───────────────────────────────────────────── @@ -71,10 +76,7 @@ function formatTime(date: Date): string { } // 작업지시별 공정현황으로 진행상태 계산 -function computeProgress( - wiId: string, - processMap: Map -): "대기" | "진행중" | "완료" { +function computeProgress(wiId: string, processMap: Map): "대기" | "진행중" | "완료" { const steps = processMap.get(wiId); if (!steps || steps.length === 0) return "대기"; const completedCount = steps.filter((s) => s.status === "completed").length; @@ -85,11 +87,15 @@ function computeProgress( // ─── 메인 컴포넌트 ──────────────────────────────────────── export default function ProductionMonitoringPage() { + const { settings } = useMonitoringSettings("production"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const [workInstructions, setWorkInstructions] = useState([]); const [processMap, setProcessMap] = useState>(new Map()); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [activeTab, setActiveTab] = useState("전체"); // ─── 실시간 시계 ───────────────────────────────────────── @@ -105,8 +111,7 @@ export default function ProductionMonitoringPage() { // 작업지시 목록 조회 const wiRes = await apiClient.get("/work-instruction/list"); - const wiRaw: WorkInstruction[] = - wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; + const wiRaw: WorkInstruction[] = wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; // 작업지시번호 기준 중복 제거 (디테일 JOIN으로 중복 발생 가능) const seen = new Set(); const wiData = wiRaw.filter((wi) => { @@ -120,7 +125,9 @@ export default function ProductionMonitoringPage() { // 공정현황 조회 (실패해도 작업지시는 표시) try { const procRes = await apiClient.post("/table-management/tables/work_order_process/data", { - page: 1, size: 1000, autoFilter: true, + page: 1, + size: 1000, + autoFilter: true, }); const rows: ProcessStep[] = procRes.data?.data?.data || procRes.data?.data?.rows || procRes.data?.data || []; @@ -162,12 +169,12 @@ export default function ProductionMonitoringPage() { fetchData(); }, [fetchData]); - // ─── 자동갱신 (30초) ───────────────────────────────────── + // ─── 자동갱신 ──────────────────────────────────────────── useEffect(() => { if (!autoRefresh) return; - const timer = setInterval(fetchData, 30000); + const timer = setInterval(fetchData, settings.refreshInterval * 1000); return () => clearInterval(timer); - }, [autoRefresh, fetchData]); + }, [autoRefresh, fetchData, settings.refreshInterval]); // ─── 통계 계산 ─────────────────────────────────────────── const stats = useMemo(() => { @@ -202,23 +209,17 @@ export default function ProductionMonitoringPage() { // ─── 렌더링 ────────────────────────────────────────────── return ( -
+
{/* 헤더 */} -
-

생산모니터링

+
+

생산모니터링

-
- +
+ {formatTime(currentTime)}
- +
{/* 요약 카드 */} -
+
} + icon={} label="대기중" value={stats.waiting} colorClass="text-amber-500 bg-amber-500/10 border-amber-500/20" /> } + icon={} label="진행중" value={stats.inProgress} colorClass="text-blue-500 bg-blue-500/10 border-blue-500/20" /> } + icon={} label="완료" value={stats.completed} colorClass="text-emerald-500 bg-emerald-500/10 border-emerald-500/20" /> } + icon={} label="달성율" value={`${stats.achievementRate}%`} colorClass="text-purple-500 bg-purple-500/10 border-purple-500/20" @@ -262,7 +272,7 @@ export default function ProductionMonitoringPage() {
{/* 탭 필터 */} -
+
{(["전체", "대기", "진행중", "완료"] as FilterTab[]).map((tab) => ( +
{/* ── 본문 ── */} -
+
{/* 요약 카드 */}
{summaryCards.map((card) => ( -
-

- {card.label} -

+
+

{card.label}

{card.value} - {card.sub && ( - - {card.sub} - - )} + {card.sub && {card.sub}}

))} @@ -322,10 +286,10 @@ export default function QualityMonitoringPage() { key={tab.key} onClick={() => setActiveTab(tab.key)} className={cn( - "px-4 py-1.5 rounded-full text-sm font-medium transition-colors", + "rounded-full px-4 py-1.5 text-sm font-medium transition-colors", activeTab === tab.key ? "bg-emerald-600 text-white shadow" - : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 border", + : "border bg-white text-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700", )} > {tab.label} @@ -334,170 +298,120 @@ export default function QualityMonitoringPage() {
{/* 테이블 영역 */} -
+
{/* 입고/출하 준비중 */} - {(activeTab === "incoming" || activeTab === "shipping") ? ( + {activeTab === "incoming" || activeTab === "shipping" ? (
- +

준비중

-

- {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 - 아직 지원되지 않습니다. +

+ {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 아직 지원되지 않습니다.

) : loading && filteredRows.length === 0 ? (
- +

데이터를 불러오는 중...

) : filteredRows.length === 0 ? (
- +

금일 검사 데이터가 없습니다

) : (
- + No - 검사번호 - - 검사유형 - - 품목명 - 규격 - - 검사수량 - - - 합격수량 - - - 불합격수량 - - - 불량율 - - - 검사결과 - - - 판정 - - - 검사자 - - 검사일시 - 비고 + {tc.inspectionNo && 검사번호} + {tc.inspectionType && 검사유형} + {tc.itemName && 품목명} + {tc.spec && 규격} + {tc.inspectionQty && 검사수량} + {tc.passFailQty && 합격수량} + {tc.passFailQty && 불합격수량} + {tc.defectRate && 불량율} + {tc.resultBar && 검사결과} + {tc.judgment && 판정} + {tc.inspector && 검사자} + {tc.inspectedAt && 검사일시} + {tc.inspectionCriteria && 검사기준} {filteredRows.map((row) => { - const goodPct = - row.inspectionQty > 0 - ? (row.goodQty / row.inspectionQty) * 100 - : 0; - const defectPct = - row.inspectionQty > 0 - ? (row.defectQty / row.inspectionQty) * 100 - : 0; + const goodPct = row.inspectionQty > 0 ? (row.goodQty / row.inspectionQty) * 100 : 0; + const defectPct = row.inspectionQty > 0 ? (row.defectQty / row.inspectionQty) * 100 : 0; return ( - - - {row.no} - - - {row.inspectionNo} - - - - {row.inspectionType} - - - - {row.itemName} - - - {row.spec} - - - {fmt(row.inspectionQty)} - - - {fmt(row.goodQty)} - - - {fmt(row.defectQty)} - - - {pct(row.defectRate)} - - {/* 검사결과 프로그레스바 */} - -
-
-
-
+ + {row.no} + {tc.inspectionNo && {row.inspectionNo}} + {tc.inspectionType && ( + + + {row.inspectionType} + + + )} + {tc.itemName && {row.itemName}} + {tc.spec && {row.spec}} + {tc.inspectionQty && ( + {fmt(row.inspectionQty)} + )} + {tc.passFailQty && ( + {fmt(row.goodQty)} + )} + {tc.passFailQty && ( + {fmt(row.defectQty)} + )} + {tc.defectRate && ( + + {pct(row.defectRate)} + + )} + {tc.resultBar && ( + +
+
+
+
+
+ + {pct(goodPct)} +
- - {pct(goodPct)} - -
- - {/* 판정 배지 */} - - - {row.result} - - - - {row.inspectorName} - - - {row.inspectedAt !== "-" - ? new Date(row.inspectedAt).toLocaleString( - "ko-KR", - { + + )} + {tc.judgment && ( + + + {row.result} + + + )} + {tc.inspector && {row.inspectorName}} + {tc.inspectedAt && ( + + {row.inspectedAt !== "-" + ? new Date(row.inspectedAt).toLocaleString("ko-KR", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, - }, - ) - : "-"} - - - {row.remark || "-"} - + }) + : "-"} + + )} + {tc.inspectionCriteria && -} ); })} diff --git a/frontend/app/(main)/COMPANY_7/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_7/monitoring/equipment/page.tsx index ccb5aaf6..14adae20 100644 --- a/frontend/app/(main)/COMPANY_7/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_7/monitoring/equipment/page.tsx @@ -12,16 +12,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { - RefreshCw, - Clock, - Loader2, - Inbox, - Wrench, - Zap, - Pause, - Power, -} from "lucide-react"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import { RefreshCw, Clock, Loader2, Inbox, Wrench, Zap, Pause, Power, Settings2 } from "lucide-react"; /* ───── 상태 정의 ───── */ @@ -134,11 +128,16 @@ interface WorkInstruction { /* ───── 컴포넌트 ───── */ export default function EquipmentMonitoringPage() { + const { settings } = useMonitoringSettings("equipment"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const df = settings.displayFields; + const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [filterStatus, setFilterStatus] = useState("all"); const autoRefreshRef = useRef(autoRefresh); @@ -182,13 +181,13 @@ export default function EquipmentMonitoringPage() { fetchData(); }, [fetchData]); - /* ── 자동 갱신 (30초) ── */ + /* ── 자동 갱신 ── */ useEffect(() => { const interval = setInterval(() => { if (autoRefreshRef.current) fetchData(); - }, 30000); + }, settings.refreshInterval * 1000); return () => clearInterval(interval); - }, [fetchData]); + }, [fetchData, settings.refreshInterval]); /* ── 요약 통계 ── */ const stats = useMemo(() => { @@ -293,11 +292,11 @@ export default function EquipmentMonitoringPage() { /* ── 필터 pill ── */ const filterPills: { label: string; value: OperationStatus | "all"; color: string }[] = [ - { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-300 hover:bg-blue-500/30" }, - { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30" }, - { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-300 hover:bg-amber-500/30" }, - { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-300 hover:bg-red-500/30" }, - { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-300 hover:bg-gray-500/30" }, + { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-700 hover:bg-blue-500/30" }, + { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-700 hover:bg-emerald-500/30" }, + { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-700 hover:bg-amber-500/30" }, + { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-700 hover:bg-red-500/30" }, + { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-700 hover:bg-gray-500/30" }, ]; /* ── 포맷 ── */ @@ -309,20 +308,26 @@ export default function EquipmentMonitoringPage() { /* ────────────── 렌더 ────────────── */ return ( -
+
{/* ── 헤더 ── */}
-

설비운영모니터링

+

설비운영모니터링

{/* 현재 시간 */} -
+
{formatDate(currentTime)} - {formatTime(currentTime)} + {formatTime(currentTime)}
{/* 자동갱신 토글 */} @@ -330,51 +335,56 @@ export default function EquipmentMonitoringPage() { variant="outline" size="sm" className={cn( - "border-gray-700 text-xs gap-1.5", + "gap-1.5 text-xs", autoRefresh - ? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20" - : "bg-gray-800 text-gray-400 hover:bg-gray-700" + ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20" + : "bg-muted text-muted-foreground hover:bg-accent", )} onClick={() => setAutoRefresh((v) => !v)} > - + 자동갱신 {autoRefresh ? "ON" : "OFF"} {/* 새로고침 */} + + + {/* 설정 */}
{/* ── 요약 카드 5개 ── */} -
+
{summaryCards.map((card) => ( @@ -390,40 +400,35 @@ export default function EquipmentMonitoringPage() { className={cn( "rounded-full px-4 py-1.5 text-sm font-medium transition-all", filterStatus === pill.value - ? cn(pill.color, "ring-1 ring-white/20") - : "bg-gray-800/60 text-gray-500 hover:text-gray-300 hover:bg-gray-800" + ? cn(pill.color, "ring-1 ring-foreground/10") + : "bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground", )} > {pill.label} ))} - - {filteredEquipments.length}대 표시 - + {filteredEquipments.length}대 표시
{/* ── 로딩 ── */} {loading && equipments.length === 0 && (
- - 설비 데이터를 불러오는 중... + + 설비 데이터를 불러오는 중...
)} {/* ── 데이터 없음 ── */} {!loading && equipments.length === 0 && ( -
- +
+

등록된 설비가 없습니다.

)} {/* ── 설비 카드 그리드 ── */} {filteredEquipments.length > 0 && ( -
+
{filteredEquipments.map((eq) => { const status = resolveStatus(eq.operation_status); const cfg = STATUS_MAP[status]; @@ -434,129 +439,139 @@ export default function EquipmentMonitoringPage() {
{/* 좌측 색상 바 */} -
+
{/* 상단: 설비명 + 상태 배지 */}
-

- {eq.equipment_name || "이름 없음"} -

-

- {eq.equipment_type || "-"} · {eq.installation_location || "-"} + {df.equipmentName && ( +

+ {eq.equipment_name || "이름 없음"} +

+ )} +

+ {df.equipmentType && (eq.equipment_type || "-")} + {df.equipmentType && df.equipmentLocation && " · "} + {df.equipmentLocation && (eq.installation_location || "-")}

- - {cfg.icon} - {cfg.label} - -
- - {/* 구분선 */} -
- - {/* 정보 그리드 */} -
-
- 금일 가동시간 -

-

-
-
- 생산수량 -

-

-
-
- 작업자 -

- {eqWIs.length > 0 && eqWIs[0].worker_name - ? eqWIs[0].worker_name - : "-"} -

-
-
- 설비코드 -

{eq.equipment_code || "-"}

-
-
- - {/* 구분선 */} -
- - {/* 가동률 프로그레스 */} -
-
- 가동률 - - {utilization !== null ? `${utilization}%` : "-"} - -
-
- {utilization !== null && ( -
- )} -
-
- - {/* 구분선 */} -
- - {/* 현재 작업지시 */} -
-

현재 작업지시

- {eqWIs.length > 0 ? ( -
- {eqWIs.slice(0, 2).map((wi) => ( -
- - {wi.instruction_number || "-"} - - - {wi.item_name || "-"} - -
- ))} - {eqWIs.length > 2 && ( -

+{eqWIs.length - 2}건 더

- )} -
- ) : ( -

배정된 작업 없음

+ {df.operationStatus && ( + + {cfg.icon} + {cfg.label} + )}
{/* 구분선 */} -
+
- {/* 센서 데이터 (PLC 미연동) */} -
-
- 온도 - - -
-
- 압력 - - -
-
- RPM - - + {/* 정보 그리드 */} +
+ {df.dailyOperationTime && ( +
+ 금일 가동시간 +

-

+
+ )} + {df.dailyProductionQty && ( +
+ 생산수량 +

-

+
+ )} + {df.worker && ( +
+ 작업자 +

+ {eqWIs.length > 0 && eqWIs[0].worker_name ? eqWIs[0].worker_name : "-"} +

+
+ )} +
+ 설비코드 +

{eq.equipment_code || "-"}

+ + {/* 구분선 */} +
+ + {/* 가동률 프로그레스 */} + {df.utilizationBar && ( +
+
+ 가동률 + + {utilization !== null ? `${utilization}%` : "-"} + +
+
+ {utilization !== null && ( +
+ )} +
+
+ )} + + {/* 구분선 */} +
+ + {/* 현재 작업지시 */} + {df.currentWorkInstruction && ( +
+

현재 작업지시

+ {eqWIs.length > 0 ? ( +
+ {eqWIs.slice(0, 2).map((wi) => ( +
+ + {wi.instruction_number || "-"} + + {wi.item_name || "-"} +
+ ))} + {eqWIs.length > 2 &&

+{eqWIs.length - 2}건 더

} +
+ ) : ( +

배정된 작업 없음

+ )} +
+ )} + + {/* 구분선 */} +
+ + {/* 센서 데이터 (PLC 미연동) */} + {df.sensorData && ( +
+
+ 온도 + - +
+
+ 압력 + - +
+
+ RPM + - +
+
+ )}
); })} @@ -565,8 +580,8 @@ export default function EquipmentMonitoringPage() { {/* 필터 결과 없음 */} {!loading && equipments.length > 0 && filteredEquipments.length === 0 && ( -
- +
+

해당 상태의 설비가 없습니다.

)} diff --git a/frontend/app/(main)/COMPANY_7/monitoring/production/page.tsx b/frontend/app/(main)/COMPANY_7/monitoring/production/page.tsx index fb8aa24b..80d49a93 100644 --- a/frontend/app/(main)/COMPANY_7/monitoring/production/page.tsx +++ b/frontend/app/(main)/COMPANY_7/monitoring/production/page.tsx @@ -5,6 +5,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import type { ProductionDisplayFields } from "@/types/monitoringSettings"; import { RefreshCw, Clock, @@ -16,6 +20,7 @@ import { TrendingUp, Play, Pause, + Settings2, } from "lucide-react"; // ─── 타입 정의 ───────────────────────────────────────────── @@ -71,10 +76,7 @@ function formatTime(date: Date): string { } // 작업지시별 공정현황으로 진행상태 계산 -function computeProgress( - wiId: string, - processMap: Map -): "대기" | "진행중" | "완료" { +function computeProgress(wiId: string, processMap: Map): "대기" | "진행중" | "완료" { const steps = processMap.get(wiId); if (!steps || steps.length === 0) return "대기"; const completedCount = steps.filter((s) => s.status === "completed").length; @@ -85,11 +87,15 @@ function computeProgress( // ─── 메인 컴포넌트 ──────────────────────────────────────── export default function ProductionMonitoringPage() { + const { settings } = useMonitoringSettings("production"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const [workInstructions, setWorkInstructions] = useState([]); const [processMap, setProcessMap] = useState>(new Map()); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [activeTab, setActiveTab] = useState("전체"); // ─── 실시간 시계 ───────────────────────────────────────── @@ -105,8 +111,7 @@ export default function ProductionMonitoringPage() { // 작업지시 목록 조회 const wiRes = await apiClient.get("/work-instruction/list"); - const wiRaw: WorkInstruction[] = - wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; + const wiRaw: WorkInstruction[] = wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; // 작업지시번호 기준 중복 제거 (디테일 JOIN으로 중복 발생 가능) const seen = new Set(); const wiData = wiRaw.filter((wi) => { @@ -120,7 +125,9 @@ export default function ProductionMonitoringPage() { // 공정현황 조회 (실패해도 작업지시는 표시) try { const procRes = await apiClient.post("/table-management/tables/work_order_process/data", { - page: 1, size: 1000, autoFilter: true, + page: 1, + size: 1000, + autoFilter: true, }); const rows: ProcessStep[] = procRes.data?.data?.data || procRes.data?.data?.rows || procRes.data?.data || []; @@ -162,12 +169,12 @@ export default function ProductionMonitoringPage() { fetchData(); }, [fetchData]); - // ─── 자동갱신 (30초) ───────────────────────────────────── + // ─── 자동갱신 ──────────────────────────────────────────── useEffect(() => { if (!autoRefresh) return; - const timer = setInterval(fetchData, 30000); + const timer = setInterval(fetchData, settings.refreshInterval * 1000); return () => clearInterval(timer); - }, [autoRefresh, fetchData]); + }, [autoRefresh, fetchData, settings.refreshInterval]); // ─── 통계 계산 ─────────────────────────────────────────── const stats = useMemo(() => { @@ -202,23 +209,17 @@ export default function ProductionMonitoringPage() { // ─── 렌더링 ────────────────────────────────────────────── return ( -
+
{/* 헤더 */} -
-

생산모니터링

+
+

생산모니터링

-
- +
+ {formatTime(currentTime)}
- +
{/* 요약 카드 */} -
+
} + icon={} label="대기중" value={stats.waiting} colorClass="text-amber-500 bg-amber-500/10 border-amber-500/20" /> } + icon={} label="진행중" value={stats.inProgress} colorClass="text-blue-500 bg-blue-500/10 border-blue-500/20" /> } + icon={} label="완료" value={stats.completed} colorClass="text-emerald-500 bg-emerald-500/10 border-emerald-500/20" /> } + icon={} label="달성율" value={`${stats.achievementRate}%`} colorClass="text-purple-500 bg-purple-500/10 border-purple-500/20" @@ -262,7 +272,7 @@ export default function ProductionMonitoringPage() {
{/* 탭 필터 */} -
+
{(["전체", "대기", "진행중", "완료"] as FilterTab[]).map((tab) => ( +
{/* ── 본문 ── */} -
+
{/* 요약 카드 */}
{summaryCards.map((card) => ( -
-

- {card.label} -

+
+

{card.label}

{card.value} - {card.sub && ( - - {card.sub} - - )} + {card.sub && {card.sub}}

))} @@ -322,10 +286,10 @@ export default function QualityMonitoringPage() { key={tab.key} onClick={() => setActiveTab(tab.key)} className={cn( - "px-4 py-1.5 rounded-full text-sm font-medium transition-colors", + "rounded-full px-4 py-1.5 text-sm font-medium transition-colors", activeTab === tab.key ? "bg-emerald-600 text-white shadow" - : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 border", + : "border bg-white text-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700", )} > {tab.label} @@ -334,170 +298,120 @@ export default function QualityMonitoringPage() {
{/* 테이블 영역 */} -
+
{/* 입고/출하 준비중 */} - {(activeTab === "incoming" || activeTab === "shipping") ? ( + {activeTab === "incoming" || activeTab === "shipping" ? (
- +

준비중

-

- {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 - 아직 지원되지 않습니다. +

+ {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 아직 지원되지 않습니다.

) : loading && filteredRows.length === 0 ? (
- +

데이터를 불러오는 중...

) : filteredRows.length === 0 ? (
- +

금일 검사 데이터가 없습니다

) : (
- + No - 검사번호 - - 검사유형 - - 품목명 - 규격 - - 검사수량 - - - 합격수량 - - - 불합격수량 - - - 불량율 - - - 검사결과 - - - 판정 - - - 검사자 - - 검사일시 - 비고 + {tc.inspectionNo && 검사번호} + {tc.inspectionType && 검사유형} + {tc.itemName && 품목명} + {tc.spec && 규격} + {tc.inspectionQty && 검사수량} + {tc.passFailQty && 합격수량} + {tc.passFailQty && 불합격수량} + {tc.defectRate && 불량율} + {tc.resultBar && 검사결과} + {tc.judgment && 판정} + {tc.inspector && 검사자} + {tc.inspectedAt && 검사일시} + {tc.inspectionCriteria && 검사기준} {filteredRows.map((row) => { - const goodPct = - row.inspectionQty > 0 - ? (row.goodQty / row.inspectionQty) * 100 - : 0; - const defectPct = - row.inspectionQty > 0 - ? (row.defectQty / row.inspectionQty) * 100 - : 0; + const goodPct = row.inspectionQty > 0 ? (row.goodQty / row.inspectionQty) * 100 : 0; + const defectPct = row.inspectionQty > 0 ? (row.defectQty / row.inspectionQty) * 100 : 0; return ( - - - {row.no} - - - {row.inspectionNo} - - - - {row.inspectionType} - - - - {row.itemName} - - - {row.spec} - - - {fmt(row.inspectionQty)} - - - {fmt(row.goodQty)} - - - {fmt(row.defectQty)} - - - {pct(row.defectRate)} - - {/* 검사결과 프로그레스바 */} - -
-
-
-
+ + {row.no} + {tc.inspectionNo && {row.inspectionNo}} + {tc.inspectionType && ( + + + {row.inspectionType} + + + )} + {tc.itemName && {row.itemName}} + {tc.spec && {row.spec}} + {tc.inspectionQty && ( + {fmt(row.inspectionQty)} + )} + {tc.passFailQty && ( + {fmt(row.goodQty)} + )} + {tc.passFailQty && ( + {fmt(row.defectQty)} + )} + {tc.defectRate && ( + + {pct(row.defectRate)} + + )} + {tc.resultBar && ( + +
+
+
+
+
+ + {pct(goodPct)} +
- - {pct(goodPct)} - -
- - {/* 판정 배지 */} - - - {row.result} - - - - {row.inspectorName} - - - {row.inspectedAt !== "-" - ? new Date(row.inspectedAt).toLocaleString( - "ko-KR", - { + + )} + {tc.judgment && ( + + + {row.result} + + + )} + {tc.inspector && {row.inspectorName}} + {tc.inspectedAt && ( + + {row.inspectedAt !== "-" + ? new Date(row.inspectedAt).toLocaleString("ko-KR", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, - }, - ) - : "-"} - - - {row.remark || "-"} - + }) + : "-"} + + )} + {tc.inspectionCriteria && -} ); })} diff --git a/frontend/app/(main)/COMPANY_8/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_8/monitoring/equipment/page.tsx index ccb5aaf6..14adae20 100644 --- a/frontend/app/(main)/COMPANY_8/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_8/monitoring/equipment/page.tsx @@ -12,16 +12,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { - RefreshCw, - Clock, - Loader2, - Inbox, - Wrench, - Zap, - Pause, - Power, -} from "lucide-react"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import { RefreshCw, Clock, Loader2, Inbox, Wrench, Zap, Pause, Power, Settings2 } from "lucide-react"; /* ───── 상태 정의 ───── */ @@ -134,11 +128,16 @@ interface WorkInstruction { /* ───── 컴포넌트 ───── */ export default function EquipmentMonitoringPage() { + const { settings } = useMonitoringSettings("equipment"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const df = settings.displayFields; + const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [filterStatus, setFilterStatus] = useState("all"); const autoRefreshRef = useRef(autoRefresh); @@ -182,13 +181,13 @@ export default function EquipmentMonitoringPage() { fetchData(); }, [fetchData]); - /* ── 자동 갱신 (30초) ── */ + /* ── 자동 갱신 ── */ useEffect(() => { const interval = setInterval(() => { if (autoRefreshRef.current) fetchData(); - }, 30000); + }, settings.refreshInterval * 1000); return () => clearInterval(interval); - }, [fetchData]); + }, [fetchData, settings.refreshInterval]); /* ── 요약 통계 ── */ const stats = useMemo(() => { @@ -293,11 +292,11 @@ export default function EquipmentMonitoringPage() { /* ── 필터 pill ── */ const filterPills: { label: string; value: OperationStatus | "all"; color: string }[] = [ - { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-300 hover:bg-blue-500/30" }, - { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30" }, - { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-300 hover:bg-amber-500/30" }, - { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-300 hover:bg-red-500/30" }, - { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-300 hover:bg-gray-500/30" }, + { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-700 hover:bg-blue-500/30" }, + { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-700 hover:bg-emerald-500/30" }, + { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-700 hover:bg-amber-500/30" }, + { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-700 hover:bg-red-500/30" }, + { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-700 hover:bg-gray-500/30" }, ]; /* ── 포맷 ── */ @@ -309,20 +308,26 @@ export default function EquipmentMonitoringPage() { /* ────────────── 렌더 ────────────── */ return ( -
+
{/* ── 헤더 ── */}
-

설비운영모니터링

+

설비운영모니터링

{/* 현재 시간 */} -
+
{formatDate(currentTime)} - {formatTime(currentTime)} + {formatTime(currentTime)}
{/* 자동갱신 토글 */} @@ -330,51 +335,56 @@ export default function EquipmentMonitoringPage() { variant="outline" size="sm" className={cn( - "border-gray-700 text-xs gap-1.5", + "gap-1.5 text-xs", autoRefresh - ? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20" - : "bg-gray-800 text-gray-400 hover:bg-gray-700" + ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20" + : "bg-muted text-muted-foreground hover:bg-accent", )} onClick={() => setAutoRefresh((v) => !v)} > - + 자동갱신 {autoRefresh ? "ON" : "OFF"} {/* 새로고침 */} + + + {/* 설정 */}
{/* ── 요약 카드 5개 ── */} -
+
{summaryCards.map((card) => ( @@ -390,40 +400,35 @@ export default function EquipmentMonitoringPage() { className={cn( "rounded-full px-4 py-1.5 text-sm font-medium transition-all", filterStatus === pill.value - ? cn(pill.color, "ring-1 ring-white/20") - : "bg-gray-800/60 text-gray-500 hover:text-gray-300 hover:bg-gray-800" + ? cn(pill.color, "ring-1 ring-foreground/10") + : "bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground", )} > {pill.label} ))} - - {filteredEquipments.length}대 표시 - + {filteredEquipments.length}대 표시
{/* ── 로딩 ── */} {loading && equipments.length === 0 && (
- - 설비 데이터를 불러오는 중... + + 설비 데이터를 불러오는 중...
)} {/* ── 데이터 없음 ── */} {!loading && equipments.length === 0 && ( -
- +
+

등록된 설비가 없습니다.

)} {/* ── 설비 카드 그리드 ── */} {filteredEquipments.length > 0 && ( -
+
{filteredEquipments.map((eq) => { const status = resolveStatus(eq.operation_status); const cfg = STATUS_MAP[status]; @@ -434,129 +439,139 @@ export default function EquipmentMonitoringPage() {
{/* 좌측 색상 바 */} -
+
{/* 상단: 설비명 + 상태 배지 */}
-

- {eq.equipment_name || "이름 없음"} -

-

- {eq.equipment_type || "-"} · {eq.installation_location || "-"} + {df.equipmentName && ( +

+ {eq.equipment_name || "이름 없음"} +

+ )} +

+ {df.equipmentType && (eq.equipment_type || "-")} + {df.equipmentType && df.equipmentLocation && " · "} + {df.equipmentLocation && (eq.installation_location || "-")}

- - {cfg.icon} - {cfg.label} - -
- - {/* 구분선 */} -
- - {/* 정보 그리드 */} -
-
- 금일 가동시간 -

-

-
-
- 생산수량 -

-

-
-
- 작업자 -

- {eqWIs.length > 0 && eqWIs[0].worker_name - ? eqWIs[0].worker_name - : "-"} -

-
-
- 설비코드 -

{eq.equipment_code || "-"}

-
-
- - {/* 구분선 */} -
- - {/* 가동률 프로그레스 */} -
-
- 가동률 - - {utilization !== null ? `${utilization}%` : "-"} - -
-
- {utilization !== null && ( -
- )} -
-
- - {/* 구분선 */} -
- - {/* 현재 작업지시 */} -
-

현재 작업지시

- {eqWIs.length > 0 ? ( -
- {eqWIs.slice(0, 2).map((wi) => ( -
- - {wi.instruction_number || "-"} - - - {wi.item_name || "-"} - -
- ))} - {eqWIs.length > 2 && ( -

+{eqWIs.length - 2}건 더

- )} -
- ) : ( -

배정된 작업 없음

+ {df.operationStatus && ( + + {cfg.icon} + {cfg.label} + )}
{/* 구분선 */} -
+
- {/* 센서 데이터 (PLC 미연동) */} -
-
- 온도 - - -
-
- 압력 - - -
-
- RPM - - + {/* 정보 그리드 */} +
+ {df.dailyOperationTime && ( +
+ 금일 가동시간 +

-

+
+ )} + {df.dailyProductionQty && ( +
+ 생산수량 +

-

+
+ )} + {df.worker && ( +
+ 작업자 +

+ {eqWIs.length > 0 && eqWIs[0].worker_name ? eqWIs[0].worker_name : "-"} +

+
+ )} +
+ 설비코드 +

{eq.equipment_code || "-"}

+ + {/* 구분선 */} +
+ + {/* 가동률 프로그레스 */} + {df.utilizationBar && ( +
+
+ 가동률 + + {utilization !== null ? `${utilization}%` : "-"} + +
+
+ {utilization !== null && ( +
+ )} +
+
+ )} + + {/* 구분선 */} +
+ + {/* 현재 작업지시 */} + {df.currentWorkInstruction && ( +
+

현재 작업지시

+ {eqWIs.length > 0 ? ( +
+ {eqWIs.slice(0, 2).map((wi) => ( +
+ + {wi.instruction_number || "-"} + + {wi.item_name || "-"} +
+ ))} + {eqWIs.length > 2 &&

+{eqWIs.length - 2}건 더

} +
+ ) : ( +

배정된 작업 없음

+ )} +
+ )} + + {/* 구분선 */} +
+ + {/* 센서 데이터 (PLC 미연동) */} + {df.sensorData && ( +
+
+ 온도 + - +
+
+ 압력 + - +
+
+ RPM + - +
+
+ )}
); })} @@ -565,8 +580,8 @@ export default function EquipmentMonitoringPage() { {/* 필터 결과 없음 */} {!loading && equipments.length > 0 && filteredEquipments.length === 0 && ( -
- +
+

해당 상태의 설비가 없습니다.

)} diff --git a/frontend/app/(main)/COMPANY_8/monitoring/production/page.tsx b/frontend/app/(main)/COMPANY_8/monitoring/production/page.tsx index fb8aa24b..80d49a93 100644 --- a/frontend/app/(main)/COMPANY_8/monitoring/production/page.tsx +++ b/frontend/app/(main)/COMPANY_8/monitoring/production/page.tsx @@ -5,6 +5,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import type { ProductionDisplayFields } from "@/types/monitoringSettings"; import { RefreshCw, Clock, @@ -16,6 +20,7 @@ import { TrendingUp, Play, Pause, + Settings2, } from "lucide-react"; // ─── 타입 정의 ───────────────────────────────────────────── @@ -71,10 +76,7 @@ function formatTime(date: Date): string { } // 작업지시별 공정현황으로 진행상태 계산 -function computeProgress( - wiId: string, - processMap: Map -): "대기" | "진행중" | "완료" { +function computeProgress(wiId: string, processMap: Map): "대기" | "진행중" | "완료" { const steps = processMap.get(wiId); if (!steps || steps.length === 0) return "대기"; const completedCount = steps.filter((s) => s.status === "completed").length; @@ -85,11 +87,15 @@ function computeProgress( // ─── 메인 컴포넌트 ──────────────────────────────────────── export default function ProductionMonitoringPage() { + const { settings } = useMonitoringSettings("production"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const [workInstructions, setWorkInstructions] = useState([]); const [processMap, setProcessMap] = useState>(new Map()); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [activeTab, setActiveTab] = useState("전체"); // ─── 실시간 시계 ───────────────────────────────────────── @@ -105,8 +111,7 @@ export default function ProductionMonitoringPage() { // 작업지시 목록 조회 const wiRes = await apiClient.get("/work-instruction/list"); - const wiRaw: WorkInstruction[] = - wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; + const wiRaw: WorkInstruction[] = wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; // 작업지시번호 기준 중복 제거 (디테일 JOIN으로 중복 발생 가능) const seen = new Set(); const wiData = wiRaw.filter((wi) => { @@ -120,7 +125,9 @@ export default function ProductionMonitoringPage() { // 공정현황 조회 (실패해도 작업지시는 표시) try { const procRes = await apiClient.post("/table-management/tables/work_order_process/data", { - page: 1, size: 1000, autoFilter: true, + page: 1, + size: 1000, + autoFilter: true, }); const rows: ProcessStep[] = procRes.data?.data?.data || procRes.data?.data?.rows || procRes.data?.data || []; @@ -162,12 +169,12 @@ export default function ProductionMonitoringPage() { fetchData(); }, [fetchData]); - // ─── 자동갱신 (30초) ───────────────────────────────────── + // ─── 자동갱신 ──────────────────────────────────────────── useEffect(() => { if (!autoRefresh) return; - const timer = setInterval(fetchData, 30000); + const timer = setInterval(fetchData, settings.refreshInterval * 1000); return () => clearInterval(timer); - }, [autoRefresh, fetchData]); + }, [autoRefresh, fetchData, settings.refreshInterval]); // ─── 통계 계산 ─────────────────────────────────────────── const stats = useMemo(() => { @@ -202,23 +209,17 @@ export default function ProductionMonitoringPage() { // ─── 렌더링 ────────────────────────────────────────────── return ( -
+
{/* 헤더 */} -
-

생산모니터링

+
+

생산모니터링

-
- +
+ {formatTime(currentTime)}
- +
{/* 요약 카드 */} -
+
} + icon={} label="대기중" value={stats.waiting} colorClass="text-amber-500 bg-amber-500/10 border-amber-500/20" /> } + icon={} label="진행중" value={stats.inProgress} colorClass="text-blue-500 bg-blue-500/10 border-blue-500/20" /> } + icon={} label="완료" value={stats.completed} colorClass="text-emerald-500 bg-emerald-500/10 border-emerald-500/20" /> } + icon={} label="달성율" value={`${stats.achievementRate}%`} colorClass="text-purple-500 bg-purple-500/10 border-purple-500/20" @@ -262,7 +272,7 @@ export default function ProductionMonitoringPage() {
{/* 탭 필터 */} -
+
{(["전체", "대기", "진행중", "완료"] as FilterTab[]).map((tab) => ( +
{/* ── 본문 ── */} -
+
{/* 요약 카드 */}
{summaryCards.map((card) => ( -
-

- {card.label} -

+
+

{card.label}

{card.value} - {card.sub && ( - - {card.sub} - - )} + {card.sub && {card.sub}}

))} @@ -322,10 +286,10 @@ export default function QualityMonitoringPage() { key={tab.key} onClick={() => setActiveTab(tab.key)} className={cn( - "px-4 py-1.5 rounded-full text-sm font-medium transition-colors", + "rounded-full px-4 py-1.5 text-sm font-medium transition-colors", activeTab === tab.key ? "bg-emerald-600 text-white shadow" - : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 border", + : "border bg-white text-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700", )} > {tab.label} @@ -334,170 +298,120 @@ export default function QualityMonitoringPage() {
{/* 테이블 영역 */} -
+
{/* 입고/출하 준비중 */} - {(activeTab === "incoming" || activeTab === "shipping") ? ( + {activeTab === "incoming" || activeTab === "shipping" ? (
- +

준비중

-

- {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 - 아직 지원되지 않습니다. +

+ {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 아직 지원되지 않습니다.

) : loading && filteredRows.length === 0 ? (
- +

데이터를 불러오는 중...

) : filteredRows.length === 0 ? (
- +

금일 검사 데이터가 없습니다

) : (
- + No - 검사번호 - - 검사유형 - - 품목명 - 규격 - - 검사수량 - - - 합격수량 - - - 불합격수량 - - - 불량율 - - - 검사결과 - - - 판정 - - - 검사자 - - 검사일시 - 비고 + {tc.inspectionNo && 검사번호} + {tc.inspectionType && 검사유형} + {tc.itemName && 품목명} + {tc.spec && 규격} + {tc.inspectionQty && 검사수량} + {tc.passFailQty && 합격수량} + {tc.passFailQty && 불합격수량} + {tc.defectRate && 불량율} + {tc.resultBar && 검사결과} + {tc.judgment && 판정} + {tc.inspector && 검사자} + {tc.inspectedAt && 검사일시} + {tc.inspectionCriteria && 검사기준} {filteredRows.map((row) => { - const goodPct = - row.inspectionQty > 0 - ? (row.goodQty / row.inspectionQty) * 100 - : 0; - const defectPct = - row.inspectionQty > 0 - ? (row.defectQty / row.inspectionQty) * 100 - : 0; + const goodPct = row.inspectionQty > 0 ? (row.goodQty / row.inspectionQty) * 100 : 0; + const defectPct = row.inspectionQty > 0 ? (row.defectQty / row.inspectionQty) * 100 : 0; return ( - - - {row.no} - - - {row.inspectionNo} - - - - {row.inspectionType} - - - - {row.itemName} - - - {row.spec} - - - {fmt(row.inspectionQty)} - - - {fmt(row.goodQty)} - - - {fmt(row.defectQty)} - - - {pct(row.defectRate)} - - {/* 검사결과 프로그레스바 */} - -
-
-
-
+ + {row.no} + {tc.inspectionNo && {row.inspectionNo}} + {tc.inspectionType && ( + + + {row.inspectionType} + + + )} + {tc.itemName && {row.itemName}} + {tc.spec && {row.spec}} + {tc.inspectionQty && ( + {fmt(row.inspectionQty)} + )} + {tc.passFailQty && ( + {fmt(row.goodQty)} + )} + {tc.passFailQty && ( + {fmt(row.defectQty)} + )} + {tc.defectRate && ( + + {pct(row.defectRate)} + + )} + {tc.resultBar && ( + +
+
+
+
+
+ + {pct(goodPct)} +
- - {pct(goodPct)} - -
- - {/* 판정 배지 */} - - - {row.result} - - - - {row.inspectorName} - - - {row.inspectedAt !== "-" - ? new Date(row.inspectedAt).toLocaleString( - "ko-KR", - { + + )} + {tc.judgment && ( + + + {row.result} + + + )} + {tc.inspector && {row.inspectorName}} + {tc.inspectedAt && ( + + {row.inspectedAt !== "-" + ? new Date(row.inspectedAt).toLocaleString("ko-KR", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, - }, - ) - : "-"} - - - {row.remark || "-"} - + }) + : "-"} + + )} + {tc.inspectionCriteria && -} ); })} diff --git a/frontend/app/(main)/COMPANY_9/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_9/monitoring/equipment/page.tsx index ccb5aaf6..14adae20 100644 --- a/frontend/app/(main)/COMPANY_9/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_9/monitoring/equipment/page.tsx @@ -12,16 +12,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { - RefreshCw, - Clock, - Loader2, - Inbox, - Wrench, - Zap, - Pause, - Power, -} from "lucide-react"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import { RefreshCw, Clock, Loader2, Inbox, Wrench, Zap, Pause, Power, Settings2 } from "lucide-react"; /* ───── 상태 정의 ───── */ @@ -134,11 +128,16 @@ interface WorkInstruction { /* ───── 컴포넌트 ───── */ export default function EquipmentMonitoringPage() { + const { settings } = useMonitoringSettings("equipment"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const df = settings.displayFields; + const [equipments, setEquipments] = useState([]); const [workInstructions, setWorkInstructions] = useState([]); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [filterStatus, setFilterStatus] = useState("all"); const autoRefreshRef = useRef(autoRefresh); @@ -182,13 +181,13 @@ export default function EquipmentMonitoringPage() { fetchData(); }, [fetchData]); - /* ── 자동 갱신 (30초) ── */ + /* ── 자동 갱신 ── */ useEffect(() => { const interval = setInterval(() => { if (autoRefreshRef.current) fetchData(); - }, 30000); + }, settings.refreshInterval * 1000); return () => clearInterval(interval); - }, [fetchData]); + }, [fetchData, settings.refreshInterval]); /* ── 요약 통계 ── */ const stats = useMemo(() => { @@ -293,11 +292,11 @@ export default function EquipmentMonitoringPage() { /* ── 필터 pill ── */ const filterPills: { label: string; value: OperationStatus | "all"; color: string }[] = [ - { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-300 hover:bg-blue-500/30" }, - { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30" }, - { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-300 hover:bg-amber-500/30" }, - { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-300 hover:bg-red-500/30" }, - { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-300 hover:bg-gray-500/30" }, + { label: "전체", value: "all", color: "bg-blue-500/20 text-blue-700 hover:bg-blue-500/30" }, + { label: "가동중", value: "running", color: "bg-emerald-500/20 text-emerald-700 hover:bg-emerald-500/30" }, + { label: "대기", value: "idle", color: "bg-amber-500/20 text-amber-700 hover:bg-amber-500/30" }, + { label: "점검/수리", value: "maintenance", color: "bg-red-500/20 text-red-700 hover:bg-red-500/30" }, + { label: "비가동", value: "off", color: "bg-gray-500/20 text-gray-700 hover:bg-gray-500/30" }, ]; /* ── 포맷 ── */ @@ -309,20 +308,26 @@ export default function EquipmentMonitoringPage() { /* ────────────── 렌더 ────────────── */ return ( -
+
{/* ── 헤더 ── */}
-

설비운영모니터링

+

설비운영모니터링

{/* 현재 시간 */} -
+
{formatDate(currentTime)} - {formatTime(currentTime)} + {formatTime(currentTime)}
{/* 자동갱신 토글 */} @@ -330,51 +335,56 @@ export default function EquipmentMonitoringPage() { variant="outline" size="sm" className={cn( - "border-gray-700 text-xs gap-1.5", + "gap-1.5 text-xs", autoRefresh - ? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20" - : "bg-gray-800 text-gray-400 hover:bg-gray-700" + ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500/20" + : "bg-muted text-muted-foreground hover:bg-accent", )} onClick={() => setAutoRefresh((v) => !v)} > - + 자동갱신 {autoRefresh ? "ON" : "OFF"} {/* 새로고침 */} + + + {/* 설정 */}
{/* ── 요약 카드 5개 ── */} -
+
{summaryCards.map((card) => ( @@ -390,40 +400,35 @@ export default function EquipmentMonitoringPage() { className={cn( "rounded-full px-4 py-1.5 text-sm font-medium transition-all", filterStatus === pill.value - ? cn(pill.color, "ring-1 ring-white/20") - : "bg-gray-800/60 text-gray-500 hover:text-gray-300 hover:bg-gray-800" + ? cn(pill.color, "ring-1 ring-foreground/10") + : "bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground", )} > {pill.label} ))} - - {filteredEquipments.length}대 표시 - + {filteredEquipments.length}대 표시
{/* ── 로딩 ── */} {loading && equipments.length === 0 && (
- - 설비 데이터를 불러오는 중... + + 설비 데이터를 불러오는 중...
)} {/* ── 데이터 없음 ── */} {!loading && equipments.length === 0 && ( -
- +
+

등록된 설비가 없습니다.

)} {/* ── 설비 카드 그리드 ── */} {filteredEquipments.length > 0 && ( -
+
{filteredEquipments.map((eq) => { const status = resolveStatus(eq.operation_status); const cfg = STATUS_MAP[status]; @@ -434,129 +439,139 @@ export default function EquipmentMonitoringPage() {
{/* 좌측 색상 바 */} -
+
{/* 상단: 설비명 + 상태 배지 */}
-

- {eq.equipment_name || "이름 없음"} -

-

- {eq.equipment_type || "-"} · {eq.installation_location || "-"} + {df.equipmentName && ( +

+ {eq.equipment_name || "이름 없음"} +

+ )} +

+ {df.equipmentType && (eq.equipment_type || "-")} + {df.equipmentType && df.equipmentLocation && " · "} + {df.equipmentLocation && (eq.installation_location || "-")}

- - {cfg.icon} - {cfg.label} - -
- - {/* 구분선 */} -
- - {/* 정보 그리드 */} -
-
- 금일 가동시간 -

-

-
-
- 생산수량 -

-

-
-
- 작업자 -

- {eqWIs.length > 0 && eqWIs[0].worker_name - ? eqWIs[0].worker_name - : "-"} -

-
-
- 설비코드 -

{eq.equipment_code || "-"}

-
-
- - {/* 구분선 */} -
- - {/* 가동률 프로그레스 */} -
-
- 가동률 - - {utilization !== null ? `${utilization}%` : "-"} - -
-
- {utilization !== null && ( -
- )} -
-
- - {/* 구분선 */} -
- - {/* 현재 작업지시 */} -
-

현재 작업지시

- {eqWIs.length > 0 ? ( -
- {eqWIs.slice(0, 2).map((wi) => ( -
- - {wi.instruction_number || "-"} - - - {wi.item_name || "-"} - -
- ))} - {eqWIs.length > 2 && ( -

+{eqWIs.length - 2}건 더

- )} -
- ) : ( -

배정된 작업 없음

+ {df.operationStatus && ( + + {cfg.icon} + {cfg.label} + )}
{/* 구분선 */} -
+
- {/* 센서 데이터 (PLC 미연동) */} -
-
- 온도 - - -
-
- 압력 - - -
-
- RPM - - + {/* 정보 그리드 */} +
+ {df.dailyOperationTime && ( +
+ 금일 가동시간 +

-

+
+ )} + {df.dailyProductionQty && ( +
+ 생산수량 +

-

+
+ )} + {df.worker && ( +
+ 작업자 +

+ {eqWIs.length > 0 && eqWIs[0].worker_name ? eqWIs[0].worker_name : "-"} +

+
+ )} +
+ 설비코드 +

{eq.equipment_code || "-"}

+ + {/* 구분선 */} +
+ + {/* 가동률 프로그레스 */} + {df.utilizationBar && ( +
+
+ 가동률 + + {utilization !== null ? `${utilization}%` : "-"} + +
+
+ {utilization !== null && ( +
+ )} +
+
+ )} + + {/* 구분선 */} +
+ + {/* 현재 작업지시 */} + {df.currentWorkInstruction && ( +
+

현재 작업지시

+ {eqWIs.length > 0 ? ( +
+ {eqWIs.slice(0, 2).map((wi) => ( +
+ + {wi.instruction_number || "-"} + + {wi.item_name || "-"} +
+ ))} + {eqWIs.length > 2 &&

+{eqWIs.length - 2}건 더

} +
+ ) : ( +

배정된 작업 없음

+ )} +
+ )} + + {/* 구분선 */} +
+ + {/* 센서 데이터 (PLC 미연동) */} + {df.sensorData && ( +
+
+ 온도 + - +
+
+ 압력 + - +
+
+ RPM + - +
+
+ )}
); })} @@ -565,8 +580,8 @@ export default function EquipmentMonitoringPage() { {/* 필터 결과 없음 */} {!loading && equipments.length > 0 && filteredEquipments.length === 0 && ( -
- +
+

해당 상태의 설비가 없습니다.

)} diff --git a/frontend/app/(main)/COMPANY_9/monitoring/production/page.tsx b/frontend/app/(main)/COMPANY_9/monitoring/production/page.tsx index fb8aa24b..80d49a93 100644 --- a/frontend/app/(main)/COMPANY_9/monitoring/production/page.tsx +++ b/frontend/app/(main)/COMPANY_9/monitoring/production/page.tsx @@ -5,6 +5,10 @@ import { apiClient } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; +import { useMonitoringSettings } from "@/hooks/useMonitoringSettings"; +import { getMonitoringTheme } from "@/lib/monitoringTheme"; +import { useTabStore } from "@/stores/tabStore"; +import type { ProductionDisplayFields } from "@/types/monitoringSettings"; import { RefreshCw, Clock, @@ -16,6 +20,7 @@ import { TrendingUp, Play, Pause, + Settings2, } from "lucide-react"; // ─── 타입 정의 ───────────────────────────────────────────── @@ -71,10 +76,7 @@ function formatTime(date: Date): string { } // 작업지시별 공정현황으로 진행상태 계산 -function computeProgress( - wiId: string, - processMap: Map -): "대기" | "진행중" | "완료" { +function computeProgress(wiId: string, processMap: Map): "대기" | "진행중" | "완료" { const steps = processMap.get(wiId); if (!steps || steps.length === 0) return "대기"; const completedCount = steps.filter((s) => s.status === "completed").length; @@ -85,11 +87,15 @@ function computeProgress( // ─── 메인 컴포넌트 ──────────────────────────────────────── export default function ProductionMonitoringPage() { + const { settings } = useMonitoringSettings("production"); + const theme = getMonitoringTheme(settings.theme); + const openTab = useTabStore((s) => s.openTab); + const [workInstructions, setWorkInstructions] = useState([]); const [processMap, setProcessMap] = useState>(new Map()); const [loading, setLoading] = useState(true); const [currentTime, setCurrentTime] = useState(new Date()); - const [autoRefresh, setAutoRefresh] = useState(true); + const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [activeTab, setActiveTab] = useState("전체"); // ─── 실시간 시계 ───────────────────────────────────────── @@ -105,8 +111,7 @@ export default function ProductionMonitoringPage() { // 작업지시 목록 조회 const wiRes = await apiClient.get("/work-instruction/list"); - const wiRaw: WorkInstruction[] = - wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; + const wiRaw: WorkInstruction[] = wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; // 작업지시번호 기준 중복 제거 (디테일 JOIN으로 중복 발생 가능) const seen = new Set(); const wiData = wiRaw.filter((wi) => { @@ -120,7 +125,9 @@ export default function ProductionMonitoringPage() { // 공정현황 조회 (실패해도 작업지시는 표시) try { const procRes = await apiClient.post("/table-management/tables/work_order_process/data", { - page: 1, size: 1000, autoFilter: true, + page: 1, + size: 1000, + autoFilter: true, }); const rows: ProcessStep[] = procRes.data?.data?.data || procRes.data?.data?.rows || procRes.data?.data || []; @@ -162,12 +169,12 @@ export default function ProductionMonitoringPage() { fetchData(); }, [fetchData]); - // ─── 자동갱신 (30초) ───────────────────────────────────── + // ─── 자동갱신 ──────────────────────────────────────────── useEffect(() => { if (!autoRefresh) return; - const timer = setInterval(fetchData, 30000); + const timer = setInterval(fetchData, settings.refreshInterval * 1000); return () => clearInterval(timer); - }, [autoRefresh, fetchData]); + }, [autoRefresh, fetchData, settings.refreshInterval]); // ─── 통계 계산 ─────────────────────────────────────────── const stats = useMemo(() => { @@ -202,23 +209,17 @@ export default function ProductionMonitoringPage() { // ─── 렌더링 ────────────────────────────────────────────── return ( -
+
{/* 헤더 */} -
-

생산모니터링

+
+

생산모니터링

-
- +
+ {formatTime(currentTime)}
- +
{/* 요약 카드 */} -
+
} + icon={} label="대기중" value={stats.waiting} colorClass="text-amber-500 bg-amber-500/10 border-amber-500/20" /> } + icon={} label="진행중" value={stats.inProgress} colorClass="text-blue-500 bg-blue-500/10 border-blue-500/20" /> } + icon={} label="완료" value={stats.completed} colorClass="text-emerald-500 bg-emerald-500/10 border-emerald-500/20" /> } + icon={} label="달성율" value={`${stats.achievementRate}%`} colorClass="text-purple-500 bg-purple-500/10 border-purple-500/20" @@ -262,7 +272,7 @@ export default function ProductionMonitoringPage() {
{/* 탭 필터 */} -
+
{(["전체", "대기", "진행중", "완료"] as FilterTab[]).map((tab) => ( +
{/* ── 본문 ── */} -
+
{/* 요약 카드 */}
{summaryCards.map((card) => ( -
-

- {card.label} -

+
+

{card.label}

{card.value} - {card.sub && ( - - {card.sub} - - )} + {card.sub && {card.sub}}

))} @@ -322,10 +286,10 @@ export default function QualityMonitoringPage() { key={tab.key} onClick={() => setActiveTab(tab.key)} className={cn( - "px-4 py-1.5 rounded-full text-sm font-medium transition-colors", + "rounded-full px-4 py-1.5 text-sm font-medium transition-colors", activeTab === tab.key ? "bg-emerald-600 text-white shadow" - : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 border", + : "border bg-white text-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700", )} > {tab.label} @@ -334,170 +298,120 @@ export default function QualityMonitoringPage() {
{/* 테이블 영역 */} -
+
{/* 입고/출하 준비중 */} - {(activeTab === "incoming" || activeTab === "shipping") ? ( + {activeTab === "incoming" || activeTab === "shipping" ? (
- +

준비중

-

- {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 - 아직 지원되지 않습니다. +

+ {activeTab === "incoming" ? "입고검사" : "출하검사"} 데이터는 아직 지원되지 않습니다.

) : loading && filteredRows.length === 0 ? (
- +

데이터를 불러오는 중...

) : filteredRows.length === 0 ? (
- +

금일 검사 데이터가 없습니다

) : (
- + No - 검사번호 - - 검사유형 - - 품목명 - 규격 - - 검사수량 - - - 합격수량 - - - 불합격수량 - - - 불량율 - - - 검사결과 - - - 판정 - - - 검사자 - - 검사일시 - 비고 + {tc.inspectionNo && 검사번호} + {tc.inspectionType && 검사유형} + {tc.itemName && 품목명} + {tc.spec && 규격} + {tc.inspectionQty && 검사수량} + {tc.passFailQty && 합격수량} + {tc.passFailQty && 불합격수량} + {tc.defectRate && 불량율} + {tc.resultBar && 검사결과} + {tc.judgment && 판정} + {tc.inspector && 검사자} + {tc.inspectedAt && 검사일시} + {tc.inspectionCriteria && 검사기준} {filteredRows.map((row) => { - const goodPct = - row.inspectionQty > 0 - ? (row.goodQty / row.inspectionQty) * 100 - : 0; - const defectPct = - row.inspectionQty > 0 - ? (row.defectQty / row.inspectionQty) * 100 - : 0; + const goodPct = row.inspectionQty > 0 ? (row.goodQty / row.inspectionQty) * 100 : 0; + const defectPct = row.inspectionQty > 0 ? (row.defectQty / row.inspectionQty) * 100 : 0; return ( - - - {row.no} - - - {row.inspectionNo} - - - - {row.inspectionType} - - - - {row.itemName} - - - {row.spec} - - - {fmt(row.inspectionQty)} - - - {fmt(row.goodQty)} - - - {fmt(row.defectQty)} - - - {pct(row.defectRate)} - - {/* 검사결과 프로그레스바 */} - -
-
-
-
+ + {row.no} + {tc.inspectionNo && {row.inspectionNo}} + {tc.inspectionType && ( + + + {row.inspectionType} + + + )} + {tc.itemName && {row.itemName}} + {tc.spec && {row.spec}} + {tc.inspectionQty && ( + {fmt(row.inspectionQty)} + )} + {tc.passFailQty && ( + {fmt(row.goodQty)} + )} + {tc.passFailQty && ( + {fmt(row.defectQty)} + )} + {tc.defectRate && ( + + {pct(row.defectRate)} + + )} + {tc.resultBar && ( + +
+
+
+
+
+ + {pct(goodPct)} +
- - {pct(goodPct)} - -
- - {/* 판정 배지 */} - - - {row.result} - - - - {row.inspectorName} - - - {row.inspectedAt !== "-" - ? new Date(row.inspectedAt).toLocaleString( - "ko-KR", - { + + )} + {tc.judgment && ( + + + {row.result} + + + )} + {tc.inspector && {row.inspectorName}} + {tc.inspectedAt && ( + + {row.inspectedAt !== "-" + ? new Date(row.inspectedAt).toLocaleString("ko-KR", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, - }, - ) - : "-"} - - - {row.remark || "-"} - + }) + : "-"} + + )} + {tc.inspectionCriteria && -} ); })} diff --git a/frontend/components/layout/AdminPageRenderer.tsx b/frontend/components/layout/AdminPageRenderer.tsx index 0fce1309..6dbd56b6 100644 --- a/frontend/components/layout/AdminPageRenderer.tsx +++ b/frontend/components/layout/AdminPageRenderer.tsx @@ -148,9 +148,11 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_16/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_16/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_16/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_16/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_16/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_16/monitoring/settings": dynamic(() => import("@/app/(main)/COMPANY_16/monitoring/settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_7/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_7/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_7/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_7/monitoring/settings": dynamic(() => import("@/app/(main)/COMPANY_7/monitoring/settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/purchase/order": dynamic(() => import("@/app/(main)/COMPANY_7/purchase/order/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/purchase/purchase-item": dynamic(() => import("@/app/(main)/COMPANY_7/purchase/purchase-item/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_7/purchase/supplier": dynamic(() => import("@/app/(main)/COMPANY_7/purchase/supplier/page"), { ssr: false, loading: LoadingFallback }), @@ -200,6 +202,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_8/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_8/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_8/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_8/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_8/monitoring/settings": dynamic(() => import("@/app/(main)/COMPANY_8/monitoring/settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_8/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_8/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_8/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_8/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), @@ -243,6 +246,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_10/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_10/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_10/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_10/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_10/monitoring/settings": dynamic(() => import("@/app/(main)/COMPANY_10/monitoring/settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_10/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_10/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_10/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_10/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), @@ -286,6 +290,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_29/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_29/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_29/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_29/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_29/monitoring/settings": dynamic(() => import("@/app/(main)/COMPANY_29/monitoring/settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_29/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_29/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), @@ -329,6 +334,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_9/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_9/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_9/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_9/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_9/monitoring/settings": dynamic(() => import("@/app/(main)/COMPANY_9/monitoring/settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_9/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_9/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_9/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_9/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), @@ -373,6 +379,7 @@ const ADMIN_PAGE_REGISTRY: Record> = { "/COMPANY_30/monitoring/production": dynamic(() => import("@/app/(main)/COMPANY_30/monitoring/production/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/monitoring/equipment": dynamic(() => import("@/app/(main)/COMPANY_30/monitoring/equipment/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/monitoring/quality": dynamic(() => import("@/app/(main)/COMPANY_30/monitoring/quality/page"), { ssr: false, loading: LoadingFallback }), + "/COMPANY_30/monitoring/settings": dynamic(() => import("@/app/(main)/COMPANY_30/monitoring/settings/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/logistics/material-status": dynamic(() => import("@/app/(main)/COMPANY_30/logistics/material-status/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/logistics/outbound": dynamic(() => import("@/app/(main)/COMPANY_30/logistics/outbound/page"), { ssr: false, loading: LoadingFallback }), "/COMPANY_30/logistics/inbound-outbound": dynamic(() => import("@/app/(main)/COMPANY_30/logistics/inbound-outbound/page"), { ssr: false, loading: LoadingFallback }), diff --git a/frontend/components/monitoring/MonitoringSettingsPage.tsx b/frontend/components/monitoring/MonitoringSettingsPage.tsx new file mode 100644 index 00000000..a41a47b5 --- /dev/null +++ b/frontend/components/monitoring/MonitoringSettingsPage.tsx @@ -0,0 +1,594 @@ +"use client"; + +import React, { useState } from "react"; +import { useMonitoringSettingsAll } from "@/hooks/useMonitoringSettings"; +import type { + MonitoringTheme, + ProductionLayout, + RefreshInterval, + AllMonitoringSettings, +} from "@/types/monitoringSettings"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Switch } from "@/components/ui/switch"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import { Settings2, Save, RotateCcw, Factory, Wrench, ClipboardCheck } from "lucide-react"; + +// ─── 탭 타입 ───────────────────────────────────────────────── + +type MonitorTab = "production" | "equipment" | "quality"; + +const TABS: { key: MonitorTab; label: string; icon: React.ReactNode; desc: string }[] = [ + { key: "production", label: "생산모니터링", icon: , desc: "작업지시 진행현황" }, + { key: "equipment", label: "설비운영모니터링", icon: , desc: "설비 가동 현황" }, + { key: "quality", label: "품질점검현황", icon: , desc: "검사 현황 모니터링" }, +]; + +// ─── 필드 라벨 매핑 ────────────────────────────────────────── + +const PRODUCTION_FIELD_LABELS: Record = { + workInstructionNo: "작업지시번호", + itemName: "품목명", + spec: "규격", + customerName: "거래처", + worker: "작업자/작업조", + dueDate: "납기일", + equipment: "사용설비", + processProgress: "공정 진행현황", + progressBar: "진행률 바", + priority: "우선순위 표시", + salesOrderNo: "수주번호", + quantityInfo: "지시수량/완료수량", +}; + +const EQUIPMENT_FIELD_LABELS: Record = { + equipmentName: "설비명", + equipmentType: "설비유형", + equipmentLocation: "설비위치", + operationStatus: "가동상태", + utilizationBar: "가동률 바", + dailyOperationTime: "금일 가동시간", + dailyProductionQty: "금일 생산수량", + worker: "작업자", + currentWorkInstruction: "현재 작업지시", + sensorData: "센서 데이터 (온도/압력/RPM)", + cumulativeOperationTime: "누적 가동시간", + nextInspectionDate: "다음 점검 예정일", +}; + +const QUALITY_COLUMN_LABELS: Record = { + inspectionNo: "검사번호", + inspectionType: "검사유형", + itemName: "품목명", + spec: "규격", + inspectionQty: "검사수량", + passFailQty: "합격/불합격 수량", + defectRate: "불량률", + resultBar: "검사결과 바", + judgment: "판정", + inspector: "검사자", + inspectedAt: "검사일시", + inspectionCriteria: "검사기준", +}; + +const QUALITY_INSPECTION_LABELS: Record = { + incoming: "입고검사", + process: "공정검사", + shipping: "출하검사", +}; + +// ─── 메인 컴포넌트 ─────────────────────────────────────────── + +export default function MonitoringSettingsPage() { + const { settings, setSettings, saveAll, resetAll, isLoaded } = useMonitoringSettingsAll(); + const [activeTab, setActiveTab] = useState("production"); + const [saved, setSaved] = useState(false); + + const handleSave = () => { + saveAll(); + setSaved(true); + setTimeout(() => setSaved(false), 2000); + }; + + const handleReset = () => { + if (!window.confirm("설정을 초기화하시겠습니까?")) return; + resetAll(); + }; + + if (!isLoaded) { + return
설정을 불러오는 중...
; + } + + return ( +
+
+ {/* 헤더 */} +
+

+ + 모니터링 설정 +

+

+ 각 모니터링 화면의 디자인, 레이아웃, 표시 항목을 설정할 수 있습니다. +

+
+ + {/* 모니터링 선택 탭 */} +
+ {TABS.map((tab) => ( + + ))} +
+ + {/* 설정 내용 */} + {activeTab === "production" && } + {activeTab === "equipment" && } + {activeTab === "quality" && } + + {/* 하단 저장 바 */} +
+ + +
+
+
+ ); +} + +// ─── 테마 선택기 ───────────────────────────────────────────── + +function ThemeSelector({ value, onChange }: { value: MonitoringTheme; onChange: (theme: MonitoringTheme) => void }) { + const themes: { key: MonitoringTheme; label: string; preview: string; bg: string }[] = [ + { key: "dark", label: "다크 모드", preview: "bg-gray-900", bg: "bg-gray-900" }, + { key: "blue", label: "딥 블루", preview: "bg-slate-800", bg: "bg-slate-800" }, + { key: "light", label: "라이트 모드", preview: "bg-gray-100 border border-gray-200", bg: "bg-gray-100" }, + ]; + + return ( +
+ {themes.map((t) => ( + + ))} +
+ ); +} + +// ─── 레이아웃 선택기 (생산만) ──────────────────────────────── + +function LayoutSelector({ + value, + onChange, +}: { + value: ProductionLayout; + onChange: (layout: ProductionLayout) => void; +}) { + const layouts: { key: ProductionLayout; label: string }[] = [ + { key: "grid", label: "그리드형" }, + { key: "list", label: "리스트형" }, + { key: "split", label: "분할형" }, + ]; + + return ( +
+ {layouts.map((l) => ( + + ))} +
+ ); +} + +// ─── 갱신 설정 섹션 ────────────────────────────────────────── + +function RefreshSettings({ + refreshInterval, + autoRefresh, + soundOrAlarm, + soundOrAlarmLabel, + onIntervalChange, + onAutoRefreshChange, + onSoundOrAlarmChange, +}: { + refreshInterval: RefreshInterval; + autoRefresh: boolean; + soundOrAlarm: boolean; + soundOrAlarmLabel: string; + onIntervalChange: (v: RefreshInterval) => void; + onAutoRefreshChange: (v: boolean) => void; + onSoundOrAlarmChange: (v: boolean) => void; +}) { + return ( +
+
+ + +
+
+ +
+ + {autoRefresh ? "사용" : "사용안함"} +
+
+
+ +
+ + {soundOrAlarm ? "사용" : "사용안함"} +
+
+
+ ); +} + +// ─── 체크박스 그리드 ───────────────────────────────────────── + +function FieldCheckboxGrid({ + fields, + labels, + onChange, +}: { + fields: Record; + labels: Record; + onChange: (key: string, checked: boolean) => void; +}) { + const allChecked = Object.values(fields).every(Boolean); + const noneChecked = Object.values(fields).every((v) => !v); + + return ( +
+
+ + +
+
+ {Object.entries(fields).map(([key, checked]) => ( + + ))} +
+
+ ); +} + +// ─── 생산모니터링 설정 ─────────────────────────────────────── + +function ProductionSettings({ + settings, + setSettings, +}: { + settings: AllMonitoringSettings; + setSettings: React.Dispatch>; +}) { + const prod = settings.production; + + const update = (partial: Partial) => { + setSettings((prev) => ({ + ...prev, + production: { ...prev.production, ...partial }, + })); + }; + + return ( +
+ + + 테마 설정 + 모니터링 화면의 배경 테마를 선택합니다 + + + update({ theme })} /> + + + + + + 레이아웃 + 카드 배치 방식을 선택합니다 + + + update({ layout })} /> + + + + + + 갱신 설정 + + + update({ refreshInterval })} + onAutoRefreshChange={(autoRefresh) => update({ autoRefresh })} + onSoundOrAlarmChange={(soundEnabled) => update({ soundEnabled })} + /> + + + + + + 표시 항목 + 작업 카드에 표시할 정보를 선택합니다 + + + + update({ + displayFields: { ...prod.displayFields, [key]: checked }, + }) + } + /> + + +
+ ); +} + +// ─── 설비모니터링 설정 ─────────────────────────────────────── + +function EquipmentSettings({ + settings, + setSettings, +}: { + settings: AllMonitoringSettings; + setSettings: React.Dispatch>; +}) { + const equip = settings.equipment; + + const update = (partial: Partial) => { + setSettings((prev) => ({ + ...prev, + equipment: { ...prev.equipment, ...partial }, + })); + }; + + return ( +
+ + + 테마 설정 + 모니터링 화면의 배경 테마를 선택합니다 + + + update({ theme })} /> + + + + + + 갱신 설정 + + + update({ refreshInterval })} + onAutoRefreshChange={(autoRefresh) => update({ autoRefresh })} + onSoundOrAlarmChange={(alarmEnabled) => update({ alarmEnabled })} + /> + + + + + + 표시 항목 + 설비 카드에 표시할 정보를 선택합니다 + + + + update({ + displayFields: { ...equip.displayFields, [key]: checked }, + }) + } + /> + + +
+ ); +} + +// ─── 품질모니터링 설정 ─────────────────────────────────────── + +function QualitySettings({ + settings, + setSettings, +}: { + settings: AllMonitoringSettings; + setSettings: React.Dispatch>; +}) { + const qual = settings.quality; + + const update = (partial: Partial) => { + setSettings((prev) => ({ + ...prev, + quality: { ...prev.quality, ...partial }, + })); + }; + + return ( +
+ + + 테마 설정 + 모니터링 화면의 배경 테마를 선택합니다 + + + update({ theme })} /> + + + + + + 갱신 설정 + + + update({ refreshInterval })} + onAutoRefreshChange={(autoRefresh) => update({ autoRefresh })} + onSoundOrAlarmChange={(alarmEnabled) => update({ alarmEnabled })} + /> + + + + + + 검사유형 표시 + 모니터링에 표시할 검사유형을 선택합니다 + + +
+ {Object.entries(qual.inspectionTypes).map(([key, checked]) => ( + + ))} +
+
+
+ + + + 테이블 컬럼 + 검사 테이블에 표시할 컬럼을 선택합니다 + + + + update({ + tableColumns: { ...qual.tableColumns, [key]: checked }, + }) + } + /> + + +
+ ); +} diff --git a/frontend/hooks/useMonitoringSettings.ts b/frontend/hooks/useMonitoringSettings.ts new file mode 100644 index 00000000..05c9e1c8 --- /dev/null +++ b/frontend/hooks/useMonitoringSettings.ts @@ -0,0 +1,134 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import type { AllMonitoringSettings, MonitoringSettingsMap } from "@/types/monitoringSettings"; +import { + DEFAULT_ALL_SETTINGS, + DEFAULT_PRODUCTION_SETTINGS, + DEFAULT_EQUIPMENT_SETTINGS, + DEFAULT_QUALITY_SETTINGS, +} from "@/lib/monitoringSettingsDefaults"; +import useAuth from "@/hooks/useAuth"; + +const STORAGE_KEY_PREFIX = "monitoring_settings_"; + +function getStorageKey(companyCode: string) { + return `${STORAGE_KEY_PREFIX}${companyCode}`; +} + +function loadFromStorage(companyCode: string): AllMonitoringSettings { + try { + const raw = localStorage.getItem(getStorageKey(companyCode)); + if (!raw) return structuredClone(DEFAULT_ALL_SETTINGS); + const parsed = JSON.parse(raw); + // 기존 저장값과 기본값 병합 (새 필드 추가 대응) + return { + production: { + ...DEFAULT_PRODUCTION_SETTINGS, + ...parsed.production, + displayFields: { ...DEFAULT_PRODUCTION_SETTINGS.displayFields, ...parsed.production?.displayFields }, + }, + equipment: { + ...DEFAULT_EQUIPMENT_SETTINGS, + ...parsed.equipment, + displayFields: { ...DEFAULT_EQUIPMENT_SETTINGS.displayFields, ...parsed.equipment?.displayFields }, + }, + quality: { + ...DEFAULT_QUALITY_SETTINGS, + ...parsed.quality, + inspectionTypes: { ...DEFAULT_QUALITY_SETTINGS.inspectionTypes, ...parsed.quality?.inspectionTypes }, + tableColumns: { ...DEFAULT_QUALITY_SETTINGS.tableColumns, ...parsed.quality?.tableColumns }, + }, + }; + } catch { + return structuredClone(DEFAULT_ALL_SETTINGS); + } +} + +function saveToStorage(companyCode: string, settings: AllMonitoringSettings) { + try { + localStorage.setItem(getStorageKey(companyCode), JSON.stringify(settings)); + } catch { + // localStorage 용량 초과 등 무시 + } +} + +// ─── 개별 모니터링 페이지용 훅 ────────────────────────────── + +export function useMonitoringSettings( + monitorType: T, +): { + settings: MonitoringSettingsMap[T]; + isLoaded: boolean; +} { + const { companyCode } = useAuth(); + const defaults = { + production: DEFAULT_PRODUCTION_SETTINGS, + equipment: DEFAULT_EQUIPMENT_SETTINGS, + quality: DEFAULT_QUALITY_SETTINGS, + }[monitorType] as MonitoringSettingsMap[T]; + + const [settings, setSettings] = useState(defaults); + const [isLoaded, setIsLoaded] = useState(false); + + useEffect(() => { + if (!companyCode) return; + const all = loadFromStorage(companyCode); + setSettings(all[monitorType] as MonitoringSettingsMap[T]); + setIsLoaded(true); + }, [companyCode, monitorType]); + + // 다른 탭에서 설정 변경 시 동기화 + useEffect(() => { + if (!companyCode) return; + const handler = (e: StorageEvent) => { + if (e.key === getStorageKey(companyCode) && e.newValue) { + try { + const parsed = JSON.parse(e.newValue); + setSettings(parsed[monitorType]); + } catch { + // 파싱 실패 무시 + } + } + }; + window.addEventListener("storage", handler); + return () => window.removeEventListener("storage", handler); + }, [companyCode, monitorType]); + + return { settings, isLoaded }; +} + +// ─── 설정 페이지용 훅 ─────────────────────────────────────── + +export function useMonitoringSettingsAll(): { + settings: AllMonitoringSettings; + setSettings: React.Dispatch>; + saveAll: () => void; + resetAll: () => void; + isLoaded: boolean; +} { + const { companyCode } = useAuth(); + const [settings, setSettings] = useState(structuredClone(DEFAULT_ALL_SETTINGS)); + const [isLoaded, setIsLoaded] = useState(false); + + useEffect(() => { + if (!companyCode) return; + setSettings(loadFromStorage(companyCode)); + setIsLoaded(true); + }, [companyCode]); + + const saveAll = useCallback(() => { + if (!companyCode) return; + saveToStorage(companyCode, settings); + }, [companyCode, settings]); + + const resetAll = useCallback(() => { + const defaults = structuredClone(DEFAULT_ALL_SETTINGS); + setSettings(defaults); + if (companyCode) { + saveToStorage(companyCode, defaults); + } + }, [companyCode]); + + return { settings, setSettings, saveAll, resetAll, isLoaded }; +} diff --git a/frontend/lib/monitoringSettingsDefaults.ts b/frontend/lib/monitoringSettingsDefaults.ts new file mode 100644 index 00000000..007fd5d3 --- /dev/null +++ b/frontend/lib/monitoringSettingsDefaults.ts @@ -0,0 +1,101 @@ +import type { + AllMonitoringSettings, + ProductionMonitoringSettings, + ProductionDisplayFields, + EquipmentMonitoringSettings, + EquipmentDisplayFields, + QualityMonitoringSettings, + QualityInspectionTypes, + QualityTableColumns, +} from "@/types/monitoringSettings"; + +// ─── 생산모니터링 기본값 ───────────────────────────────────── + +export const DEFAULT_PRODUCTION_FIELDS: ProductionDisplayFields = { + workInstructionNo: true, + itemName: true, + spec: true, + customerName: true, + worker: true, + dueDate: true, + equipment: true, + processProgress: true, + progressBar: true, + priority: true, + salesOrderNo: false, + quantityInfo: false, +}; + +export const DEFAULT_PRODUCTION_SETTINGS: ProductionMonitoringSettings = { + theme: "dark", + layout: "grid", + refreshInterval: 30, + autoRefresh: true, + soundEnabled: false, + displayFields: { ...DEFAULT_PRODUCTION_FIELDS }, +}; + +// ─── 설비모니터링 기본값 ───────────────────────────────────── + +export const DEFAULT_EQUIPMENT_FIELDS: EquipmentDisplayFields = { + equipmentName: true, + equipmentType: true, + equipmentLocation: true, + operationStatus: true, + utilizationBar: true, + dailyOperationTime: true, + dailyProductionQty: true, + worker: true, + currentWorkInstruction: true, + sensorData: true, + cumulativeOperationTime: false, + nextInspectionDate: false, +}; + +export const DEFAULT_EQUIPMENT_SETTINGS: EquipmentMonitoringSettings = { + theme: "dark", + refreshInterval: 30, + autoRefresh: true, + alarmEnabled: true, + displayFields: { ...DEFAULT_EQUIPMENT_FIELDS }, +}; + +// ─── 품질모니터링 기본값 ───────────────────────────────────── + +export const DEFAULT_QUALITY_INSPECTION_TYPES: QualityInspectionTypes = { + incoming: true, + process: true, + shipping: true, +}; + +export const DEFAULT_QUALITY_COLUMNS: QualityTableColumns = { + inspectionNo: true, + inspectionType: true, + itemName: true, + spec: true, + inspectionQty: true, + passFailQty: true, + defectRate: true, + resultBar: true, + judgment: true, + inspector: true, + inspectedAt: true, + inspectionCriteria: false, +}; + +export const DEFAULT_QUALITY_SETTINGS: QualityMonitoringSettings = { + theme: "dark", + refreshInterval: 30, + autoRefresh: true, + alarmEnabled: true, + inspectionTypes: { ...DEFAULT_QUALITY_INSPECTION_TYPES }, + tableColumns: { ...DEFAULT_QUALITY_COLUMNS }, +}; + +// ─── 전체 기본값 ───────────────────────────────────────────── + +export const DEFAULT_ALL_SETTINGS: AllMonitoringSettings = { + production: { ...DEFAULT_PRODUCTION_SETTINGS }, + equipment: { ...DEFAULT_EQUIPMENT_SETTINGS }, + quality: { ...DEFAULT_QUALITY_SETTINGS }, +}; diff --git a/frontend/lib/monitoringTheme.ts b/frontend/lib/monitoringTheme.ts new file mode 100644 index 00000000..303d8256 --- /dev/null +++ b/frontend/lib/monitoringTheme.ts @@ -0,0 +1,97 @@ +import type { MonitoringTheme } from "@/types/monitoringSettings"; + +interface ThemeClasses { + root: string; + card: string; + cardBorder: string; + header: string; + headerText: string; + text: string; + mutedText: string; + divider: string; + tableHeader: string; + tableRow: string; + tableRowHover: string; + /** CSS 변수 인라인 오버라이드 (--color-* Tailwind 테마 변수 직접 지정) */ + cssVars?: React.CSSProperties; +} + +// --color-* 변수를 직접 오버라이드하여 bg-background, bg-card 등이 즉시 반영되도록 함 +const DARK_CSS_VARS: React.CSSProperties = { + "--color-background": "hsl(222 47% 6%)", + "--color-foreground": "hsl(210 20% 95%)", + "--color-card": "hsl(220 40% 9%)", + "--color-card-foreground": "hsl(210 20% 95%)", + "--color-popover": "hsl(220 40% 9%)", + "--color-popover-foreground": "hsl(210 20% 95%)", + "--color-primary": "hsl(217 91% 65%)", + "--color-primary-foreground": "hsl(0 0% 100%)", + "--color-secondary": "hsl(220 25% 14%)", + "--color-secondary-foreground": "hsl(210 20% 90%)", + "--color-muted": "hsl(220 20% 13%)", + "--color-muted-foreground": "hsl(215 15% 58%)", + "--color-accent": "hsl(220 25% 16%)", + "--color-accent-foreground": "hsl(210 20% 90%)", + "--color-border": "hsl(220 20% 18%)", + "--color-input": "hsl(220 20% 18%)", + "--color-ring": "hsl(217 91% 65%)", + "--color-destructive": "hsl(0 72% 51%)", + "--color-destructive-foreground": "hsl(0 0% 100%)", +} as React.CSSProperties; + +const BLUE_CSS_VARS: React.CSSProperties = { + "--color-background": "hsl(215 50% 8%)", + "--color-foreground": "hsl(210 20% 95%)", + "--color-card": "hsl(215 45% 12%)", + "--color-card-foreground": "hsl(210 20% 95%)", + "--color-popover": "hsl(215 45% 12%)", + "--color-popover-foreground": "hsl(210 20% 95%)", + "--color-primary": "hsl(217 91% 65%)", + "--color-primary-foreground": "hsl(0 0% 100%)", + "--color-secondary": "hsl(215 30% 14%)", + "--color-secondary-foreground": "hsl(210 20% 90%)", + "--color-muted": "hsl(215 35% 16%)", + "--color-muted-foreground": "hsl(215 15% 58%)", + "--color-accent": "hsl(215 35% 18%)", + "--color-accent-foreground": "hsl(210 20% 90%)", + "--color-border": "hsl(215 30% 20%)", + "--color-input": "hsl(215 30% 20%)", + "--color-ring": "hsl(217 91% 65%)", + "--color-destructive": "hsl(0 72% 51%)", + "--color-destructive-foreground": "hsl(0 0% 100%)", +} as React.CSSProperties; + +const SHARED_CLASSES = { + card: "bg-card", + cardBorder: "border-border", + header: "bg-card", + headerText: "text-foreground", + text: "text-foreground", + mutedText: "text-muted-foreground", + divider: "border-border", + tableHeader: "bg-muted text-muted-foreground", + tableRow: "bg-card text-card-foreground", + tableRowHover: "hover:bg-muted/50", +}; + +const THEME_MAP: Record = { + dark: { + root: "bg-background text-foreground", + ...SHARED_CLASSES, + cssVars: DARK_CSS_VARS, + }, + blue: { + root: "bg-background text-foreground", + ...SHARED_CLASSES, + cssVars: BLUE_CSS_VARS, + }, + light: { + root: "bg-background text-foreground", + ...SHARED_CLASSES, + // cssVars 없음 → 시스템 기본 라이트 변수 사용 + }, +}; + +export function getMonitoringTheme(theme: MonitoringTheme): ThemeClasses { + return THEME_MAP[theme] ?? THEME_MAP.dark; +} diff --git a/frontend/types/monitoringSettings.ts b/frontend/types/monitoringSettings.ts new file mode 100644 index 00000000..bd86151f --- /dev/null +++ b/frontend/types/monitoringSettings.ts @@ -0,0 +1,104 @@ +// ─── 모니터링 설정 타입 정의 ───────────────────────────────── + +export type MonitoringTheme = "dark" | "blue" | "light"; +export type ProductionLayout = "grid" | "list" | "split"; +export type RefreshInterval = 10 | 30 | 60 | 300; + +// ─── 생산모니터링 ──────────────────────────────────────────── + +export interface ProductionDisplayFields { + workInstructionNo: boolean; // 작업지시번호 + itemName: boolean; // 품목명 + spec: boolean; // 규격 + customerName: boolean; // 거래처 + worker: boolean; // 작업자/작업조 + dueDate: boolean; // 납기일 + equipment: boolean; // 사용설비 + processProgress: boolean; // 공정 진행현황 + progressBar: boolean; // 진행률 바 + priority: boolean; // 우선순위 표시 + salesOrderNo: boolean; // 수주번호 + quantityInfo: boolean; // 지시수량/완료수량 +} + +export interface ProductionMonitoringSettings { + theme: MonitoringTheme; + layout: ProductionLayout; + refreshInterval: RefreshInterval; + autoRefresh: boolean; + soundEnabled: boolean; + displayFields: ProductionDisplayFields; +} + +// ─── 설비모니터링 ──────────────────────────────────────────── + +export interface EquipmentDisplayFields { + equipmentName: boolean; // 설비명 + equipmentType: boolean; // 설비유형 + equipmentLocation: boolean; // 설비위치 + operationStatus: boolean; // 가동상태 + utilizationBar: boolean; // 가동률 바 + dailyOperationTime: boolean; // 금일 가동시간 + dailyProductionQty: boolean; // 금일 생산수량 + worker: boolean; // 작업자 + currentWorkInstruction: boolean; // 현재 작업지시 + sensorData: boolean; // 센서 데이터 (온도/압력/RPM) + cumulativeOperationTime: boolean; // 누적 가동시간 + nextInspectionDate: boolean; // 다음 점검 예정일 +} + +export interface EquipmentMonitoringSettings { + theme: MonitoringTheme; + refreshInterval: RefreshInterval; + autoRefresh: boolean; + alarmEnabled: boolean; + displayFields: EquipmentDisplayFields; +} + +// ─── 품질점검현황 ──────────────────────────────────────────── + +export interface QualityInspectionTypes { + incoming: boolean; // 입고검사 + process: boolean; // 공정검사 + shipping: boolean; // 출하검사 +} + +export interface QualityTableColumns { + inspectionNo: boolean; // 검사번호 + inspectionType: boolean; // 검사유형 + itemName: boolean; // 품목명 + spec: boolean; // 규격 + inspectionQty: boolean; // 검사수량 + passFailQty: boolean; // 합격/불합격 수량 + defectRate: boolean; // 불량률 + resultBar: boolean; // 검사결과 바 + judgment: boolean; // 판정 + inspector: boolean; // 검사자 + inspectedAt: boolean; // 검사일시 + inspectionCriteria: boolean; // 검사기준 +} + +export interface QualityMonitoringSettings { + theme: MonitoringTheme; + refreshInterval: RefreshInterval; + autoRefresh: boolean; + alarmEnabled: boolean; + inspectionTypes: QualityInspectionTypes; + tableColumns: QualityTableColumns; +} + +// ─── 전체 설정 ─────────────────────────────────────────────── + +export interface AllMonitoringSettings { + production: ProductionMonitoringSettings; + equipment: EquipmentMonitoringSettings; + quality: QualityMonitoringSettings; +} + +// ─── 타입 매핑 ─────────────────────────────────────────────── + +export type MonitoringSettingsMap = { + production: ProductionMonitoringSettings; + equipment: EquipmentMonitoringSettings; + quality: QualityMonitoringSettings; +};