Files
vexplor_dev/frontend/components/monitoring/MonitoringSettingsPage.tsx
kjs 518990171e feat: Enhance monitoring pages with dynamic settings and themes
- Integrated monitoring settings and theme management into the Equipment, Production, and Quality monitoring pages.
- Updated auto-refresh functionality to utilize user-defined settings for refresh intervals.
- Improved UI elements with dynamic theming for better visual consistency across COMPANY_10, COMPANY_16, and COMPANY_29.
- Added settings button to access monitoring configuration, enhancing user experience in managing monitoring preferences.

These changes aim to provide a more customizable and user-friendly interface for monitoring operations across multiple companies.
2026-04-09 15:12:36 +09:00

595 lines
21 KiB
TypeScript

"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: <Factory className="h-6 w-6" />, desc: "작업지시 진행현황" },
{ key: "equipment", label: "설비운영모니터링", icon: <Wrench className="h-6 w-6" />, desc: "설비 가동 현황" },
{ key: "quality", label: "품질점검현황", icon: <ClipboardCheck className="h-6 w-6" />, desc: "검사 현황 모니터링" },
];
// ─── 필드 라벨 매핑 ──────────────────────────────────────────
const PRODUCTION_FIELD_LABELS: Record<string, string> = {
workInstructionNo: "작업지시번호",
itemName: "품목명",
spec: "규격",
customerName: "거래처",
worker: "작업자/작업조",
dueDate: "납기일",
equipment: "사용설비",
processProgress: "공정 진행현황",
progressBar: "진행률 바",
priority: "우선순위 표시",
salesOrderNo: "수주번호",
quantityInfo: "지시수량/완료수량",
};
const EQUIPMENT_FIELD_LABELS: Record<string, string> = {
equipmentName: "설비명",
equipmentType: "설비유형",
equipmentLocation: "설비위치",
operationStatus: "가동상태",
utilizationBar: "가동률 바",
dailyOperationTime: "금일 가동시간",
dailyProductionQty: "금일 생산수량",
worker: "작업자",
currentWorkInstruction: "현재 작업지시",
sensorData: "센서 데이터 (온도/압력/RPM)",
cumulativeOperationTime: "누적 가동시간",
nextInspectionDate: "다음 점검 예정일",
};
const QUALITY_COLUMN_LABELS: Record<string, string> = {
inspectionNo: "검사번호",
inspectionType: "검사유형",
itemName: "품목명",
spec: "규격",
inspectionQty: "검사수량",
passFailQty: "합격/불합격 수량",
defectRate: "불량률",
resultBar: "검사결과 바",
judgment: "판정",
inspector: "검사자",
inspectedAt: "검사일시",
inspectionCriteria: "검사기준",
};
const QUALITY_INSPECTION_LABELS: Record<string, string> = {
incoming: "입고검사",
process: "공정검사",
shipping: "출하검사",
};
// ─── 메인 컴포넌트 ───────────────────────────────────────────
export default function MonitoringSettingsPage() {
const { settings, setSettings, saveAll, resetAll, isLoaded } = useMonitoringSettingsAll();
const [activeTab, setActiveTab] = useState<MonitorTab>("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 <div className="text-muted-foreground flex h-64 items-center justify-center"> ...</div>;
}
return (
<div className="bg-background flex h-full min-h-0 flex-col overflow-auto">
<div className="mx-auto w-full max-w-[1200px] space-y-6 p-6">
{/* 헤더 */}
<div>
<h1 className="text-foreground flex items-center gap-2 text-xl font-bold">
<Settings2 className="h-5 w-5" />
</h1>
<p className="text-muted-foreground mt-1 text-sm">
, , .
</p>
</div>
{/* 모니터링 선택 탭 */}
<div className="grid grid-cols-3 gap-3">
{TABS.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={cn(
"flex flex-col items-center gap-2 rounded-lg border-2 p-4 text-center transition-all",
activeTab === tab.key ? "border-primary bg-primary/5" : "border-border hover:border-primary/50",
)}
>
<div className="text-primary">{tab.icon}</div>
<div className="text-foreground text-sm font-bold">{tab.label}</div>
<div className="text-muted-foreground text-xs">{tab.desc}</div>
</button>
))}
</div>
{/* 설정 내용 */}
{activeTab === "production" && <ProductionSettings settings={settings} setSettings={setSettings} />}
{activeTab === "equipment" && <EquipmentSettings settings={settings} setSettings={setSettings} />}
{activeTab === "quality" && <QualitySettings settings={settings} setSettings={setSettings} />}
{/* 하단 저장 바 */}
<div className="bg-card border-border sticky bottom-0 flex justify-end gap-3 border-t py-4">
<Button variant="outline" onClick={handleReset}>
<RotateCcw className="mr-1.5 h-4 w-4" />
</Button>
<Button onClick={handleSave}>
<Save className="mr-1.5 h-4 w-4" />
{saved ? "저장 완료!" : "설정 저장"}
</Button>
</div>
</div>
</div>
);
}
// ─── 테마 선택기 ─────────────────────────────────────────────
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 (
<div className="grid grid-cols-3 gap-3">
{themes.map((t) => (
<button
key={t.key}
onClick={() => onChange(t.key)}
className={cn(
"rounded-lg border-2 p-4 text-center transition-all",
value === t.key
? "border-primary ring-primary/15 shadow-sm ring-2"
: "border-border hover:border-primary/50",
)}
>
<div className={cn("mb-2 flex h-16 items-center justify-center rounded-md text-xl", t.preview)}>
{t.key === "dark" ? "🌙" : t.key === "blue" ? "🌊" : "☀️"}
</div>
<div className="text-foreground text-xs font-bold">{t.label}</div>
</button>
))}
</div>
);
}
// ─── 레이아웃 선택기 (생산만) ────────────────────────────────
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 (
<div className="grid grid-cols-3 gap-3">
{layouts.map((l) => (
<button
key={l.key}
onClick={() => onChange(l.key)}
className={cn(
"rounded-lg border-2 p-3 text-center transition-all",
value === l.key
? "border-primary ring-primary/15 shadow-sm ring-2"
: "border-border hover:border-primary/50",
)}
>
<div className="bg-muted mb-2 flex h-12 gap-1 rounded-md p-1.5">
{l.key === "grid" && (
<>
<div className="bg-primary/20 flex-1 rounded" />
<div className="bg-primary/20 flex-1 rounded" />
<div className="bg-primary/20 flex-1 rounded" />
</>
)}
{l.key === "list" && (
<div className="flex w-full flex-col gap-1">
<div className="bg-primary/20 h-3 rounded" />
<div className="bg-primary/20 h-3 rounded" />
<div className="bg-primary/20 h-3 rounded" />
</div>
)}
{l.key === "split" && (
<>
<div className="bg-primary/20 w-[40%] rounded" />
<div className="bg-primary/20 flex-1 rounded" />
</>
)}
</div>
<div className="text-foreground text-xs font-semibold">{l.label}</div>
</button>
))}
</div>
);
}
// ─── 갱신 설정 섹션 ──────────────────────────────────────────
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 (
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label> </Label>
<Select value={String(refreshInterval)} onValueChange={(v) => onIntervalChange(Number(v) as RefreshInterval)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="30">30</SelectItem>
<SelectItem value="60">1</SelectItem>
<SelectItem value="300">5</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label> </Label>
<div className="flex items-center gap-2 pt-1">
<Switch checked={autoRefresh} onCheckedChange={onAutoRefreshChange} />
<span className="text-muted-foreground text-sm">{autoRefresh ? "사용" : "사용안함"}</span>
</div>
</div>
<div className="space-y-2">
<Label>{soundOrAlarmLabel}</Label>
<div className="flex items-center gap-2 pt-1">
<Switch checked={soundOrAlarm} onCheckedChange={onSoundOrAlarmChange} />
<span className="text-muted-foreground text-sm">{soundOrAlarm ? "사용" : "사용안함"}</span>
</div>
</div>
</div>
);
}
// ─── 체크박스 그리드 ─────────────────────────────────────────
function FieldCheckboxGrid({
fields,
labels,
onChange,
}: {
fields: Record<string, boolean>;
labels: Record<string, string>;
onChange: (key: string, checked: boolean) => void;
}) {
const allChecked = Object.values(fields).every(Boolean);
const noneChecked = Object.values(fields).every((v) => !v);
return (
<div className="space-y-3">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => Object.keys(fields).forEach((k) => onChange(k, true))}
disabled={allChecked}
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => Object.keys(fields).forEach((k) => onChange(k, false))}
disabled={noneChecked}
>
</Button>
</div>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4">
{Object.entries(fields).map(([key, checked]) => (
<label
key={key}
className={cn(
"flex cursor-pointer items-center gap-2 rounded-md border px-3 py-2.5 transition-all select-none",
checked ? "bg-primary/5 border-primary/30" : "bg-background border-border hover:border-primary/30",
)}
>
<Checkbox checked={checked} onCheckedChange={(v) => onChange(key, v === true)} />
<span className="text-foreground text-sm font-medium">{labels[key] || key}</span>
</label>
))}
</div>
</div>
);
}
// ─── 생산모니터링 설정 ───────────────────────────────────────
function ProductionSettings({
settings,
setSettings,
}: {
settings: AllMonitoringSettings;
setSettings: React.Dispatch<React.SetStateAction<AllMonitoringSettings>>;
}) {
const prod = settings.production;
const update = (partial: Partial<typeof prod>) => {
setSettings((prev) => ({
...prev,
production: { ...prev.production, ...partial },
}));
};
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<ThemeSelector value={prod.theme} onChange={(theme) => update({ theme })} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<LayoutSelector value={prod.layout} onChange={(layout) => update({ layout })} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent>
<RefreshSettings
refreshInterval={prod.refreshInterval}
autoRefresh={prod.autoRefresh}
soundOrAlarm={prod.soundEnabled}
soundOrAlarmLabel="알림음"
onIntervalChange={(refreshInterval) => update({ refreshInterval })}
onAutoRefreshChange={(autoRefresh) => update({ autoRefresh })}
onSoundOrAlarmChange={(soundEnabled) => update({ soundEnabled })}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<FieldCheckboxGrid
fields={prod.displayFields}
labels={PRODUCTION_FIELD_LABELS}
onChange={(key, checked) =>
update({
displayFields: { ...prod.displayFields, [key]: checked },
})
}
/>
</CardContent>
</Card>
</div>
);
}
// ─── 설비모니터링 설정 ───────────────────────────────────────
function EquipmentSettings({
settings,
setSettings,
}: {
settings: AllMonitoringSettings;
setSettings: React.Dispatch<React.SetStateAction<AllMonitoringSettings>>;
}) {
const equip = settings.equipment;
const update = (partial: Partial<typeof equip>) => {
setSettings((prev) => ({
...prev,
equipment: { ...prev.equipment, ...partial },
}));
};
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<ThemeSelector value={equip.theme} onChange={(theme) => update({ theme })} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent>
<RefreshSettings
refreshInterval={equip.refreshInterval}
autoRefresh={equip.autoRefresh}
soundOrAlarm={equip.alarmEnabled}
soundOrAlarmLabel="이상 알림"
onIntervalChange={(refreshInterval) => update({ refreshInterval })}
onAutoRefreshChange={(autoRefresh) => update({ autoRefresh })}
onSoundOrAlarmChange={(alarmEnabled) => update({ alarmEnabled })}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<FieldCheckboxGrid
fields={equip.displayFields}
labels={EQUIPMENT_FIELD_LABELS}
onChange={(key, checked) =>
update({
displayFields: { ...equip.displayFields, [key]: checked },
})
}
/>
</CardContent>
</Card>
</div>
);
}
// ─── 품질모니터링 설정 ───────────────────────────────────────
function QualitySettings({
settings,
setSettings,
}: {
settings: AllMonitoringSettings;
setSettings: React.Dispatch<React.SetStateAction<AllMonitoringSettings>>;
}) {
const qual = settings.quality;
const update = (partial: Partial<typeof qual>) => {
setSettings((prev) => ({
...prev,
quality: { ...prev.quality, ...partial },
}));
};
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<ThemeSelector value={qual.theme} onChange={(theme) => update({ theme })} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent>
<RefreshSettings
refreshInterval={qual.refreshInterval}
autoRefresh={qual.autoRefresh}
soundOrAlarm={qual.alarmEnabled}
soundOrAlarmLabel="불합격 알림"
onIntervalChange={(refreshInterval) => update({ refreshInterval })}
onAutoRefreshChange={(autoRefresh) => update({ autoRefresh })}
onSoundOrAlarmChange={(alarmEnabled) => update({ alarmEnabled })}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-2">
{Object.entries(qual.inspectionTypes).map(([key, checked]) => (
<label
key={key}
className={cn(
"flex cursor-pointer items-center gap-2 rounded-md border px-3 py-2.5 transition-all select-none",
checked ? "bg-primary/5 border-primary/30" : "bg-background border-border hover:border-primary/30",
)}
>
<Checkbox
checked={checked}
onCheckedChange={(v) =>
update({
inspectionTypes: { ...qual.inspectionTypes, [key]: v === true },
})
}
/>
<span className="text-foreground text-sm font-medium">{QUALITY_INSPECTION_LABELS[key] || key}</span>
</label>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<FieldCheckboxGrid
fields={qual.tableColumns}
labels={QUALITY_COLUMN_LABELS}
onChange={(key, checked) =>
update({
tableColumns: { ...qual.tableColumns, [key]: checked },
})
}
/>
</CardContent>
</Card>
</div>
);
}