diff --git a/src/App.tsx b/src/App.tsx index 6dd9def..c9d47c6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,22 @@ -import { Routes, Route } from "react-router-dom"; +import { Routes, Route, Navigate } from "react-router-dom"; import GlobalStyles from "./styles/globalStyles"; import Home from "./pages/home/home"; import Detail from "./pages/detail.tsx"; function App() { + const lastSessionPath = + typeof window !== "undefined" + ? localStorage.getItem("lastSessionPath") || "/section-1" + : "/section-1"; + return ( <> - } /> + } /> } /> - } /> + } /> ); diff --git a/src/apis/deviceHistory.ts b/src/apis/deviceHistory.ts new file mode 100644 index 0000000..2e0708b --- /dev/null +++ b/src/apis/deviceHistory.ts @@ -0,0 +1,17 @@ +import type { DeviceHistoryPoint } from "../types/deviceHistory"; + +export const fetchDeviceHistory = async ( + serialNumber: string, +): Promise => { + const baseUrl = import.meta.env.VITE_API_BASE_URL; + const response = await fetch( + `${baseUrl}/histories/all/${encodeURIComponent(serialNumber)}`, + ); + + if (!response.ok) { + throw new Error(String(response.status)); + } + + const data: DeviceHistoryPoint[] = await response.json(); + return Array.isArray(data) ? data : []; +}; diff --git a/src/components/sidebar/sidebar.tsx b/src/components/sidebar/sidebar.tsx index 6b5aa72..1aefac5 100644 --- a/src/components/sidebar/sidebar.tsx +++ b/src/components/sidebar/sidebar.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback, useState, useEffect } from "react"; import styled from "@emotion/styled"; import { useNavigate, useLocation } from "react-router-dom"; import { SidebarHeader } from "./header/sidebar-header"; @@ -29,9 +29,22 @@ function Sidebar({ onNavItemClick = () => {} }: SidebarProps) { const navigate = useNavigate(); const location = useLocation(); - const [navItems, setNavItems] = useState([ - { id: "section-1", label: "Section 1", path: "/section-1" }, - ]); + const [navItems, setNavItems] = useState(() => { + const saved = localStorage.getItem("sessions"); + if (saved) { + try { + return JSON.parse(saved) as SidebarNavItem[]; + } catch { + // 파싱 실패 시 기본값으로 초기화 + } + } + return [{ id: "section-1", label: "Section 1", path: "/section-1" }]; + }); + + // 세션 목록 변경 시 localStorage에 저장 + useEffect(() => { + localStorage.setItem("sessions", JSON.stringify(navItems)); + }, [navItems]); const isMainItemActive = useCallback( (itemId: string) => { @@ -50,6 +63,9 @@ function Sidebar({ onNavItemClick = () => {} }: SidebarProps) { } else { navigate(`/${itemId}`); } + if (item?.path) { + localStorage.setItem("lastSessionPath", item.path); + } onNavItemClick(itemId); }, [navItems, navigate, onNavItemClick], diff --git a/src/hooks/useDeviceData.ts b/src/hooks/useDeviceData.ts index d441980..8a1c191 100644 --- a/src/hooks/useDeviceData.ts +++ b/src/hooks/useDeviceData.ts @@ -2,20 +2,34 @@ import { useState, useMemo, useEffect } from "react"; import type { Device } from "../types/device"; import useDeviceStomp from "./useDeviceStomp"; -export default function useDeviceData() { +export default function useDeviceData(sessionId?: string) { const severUrl = import.meta.env.VITE_API_BASE_URL; - const { isConnected, deviceData, subscribeDevice, unsubscribeDevice } = useDeviceStomp(`${severUrl}/ws-stomp`); + const { isConnected, deviceData, subscribeDevice, unsubscribeDevice } = + useDeviceStomp(`${severUrl}/ws-stomp`); + + const effectiveSessionId = sessionId ?? "default"; + const deviceIdsStorageKey = `deviceIds_${effectiveSessionId}`; + const deviceNamesStorageKey = `deviceNames_${effectiveSessionId}`; const [devices, setDevices] = useState(() => { - return JSON.parse(localStorage.getItem("devices") || "[]"); + const storedIds: string[] = JSON.parse( + localStorage.getItem(deviceIdsStorageKey) || "[]", + ); + const savedNames: Record = JSON.parse( + localStorage.getItem(deviceNamesStorageKey) || "{}", + ); + + return storedIds.map((id, index) => ({ + id, + name: savedNames[id] || `기기 ${index + 1}`, + temperature: 0, + warning: false, + hasShownWarning: false, + })); }); - - useEffect(() => { - localStorage.setItem("devices", JSON.stringify(devices)); - }, [devices]); useEffect(() => { - if(!isConnected) return; + if (!isConnected) return; devices.forEach((device) => { subscribeDevice(device.id); console.log(`소켓 구독 ${device.id}`); @@ -24,7 +38,10 @@ export default function useDeviceData() { const liveDevices = useMemo(() => { return devices.map((device) => { - const data = deviceData[device.id] as { temperature: number; risk: number }; + const data = deviceData[device.id] as { + temperature: number; + risk: number; + }; const currentTemp = data ? data.temperature : 0; return { @@ -38,43 +55,65 @@ export default function useDeviceData() { const warningDevice = liveDevices.find((device) => device.warning); - const addDevice = (id: string) => { - const savedNames = JSON.parse(localStorage.getItem("deviceNames") || "{}"); + const addDevice = (id: string) => { + const savedNames: Record = JSON.parse( + localStorage.getItem(deviceNamesStorageKey) || "{}", + ); setDevices((prev) => { if (prev.some((device) => device.id === id)) { return prev; } - const deviceName = savedNames[id] || `기기 ${devices.length + 1}`; + const deviceName = savedNames[id] || `기기 ${prev.length + 1}`; - const newDevice: Device = { - id: id, - name: deviceName, - temperature: 0, - warning: false, - hasShownWarning: false, - }; + const newDevice: Device = { + id: id, + name: deviceName, + temperature: 0, + warning: false, + hasShownWarning: false, + }; + + const newDevices = [...prev, newDevice]; + const newIds = newDevices.map((device) => device.id); + localStorage.setItem(deviceIdsStorageKey, JSON.stringify(newIds)); - return [...prev, newDevice]; + return newDevices; }); }; const deleteDevice = (id: string) => { - setDevices((prev) => prev.filter((device) => device.id !== id)); + setDevices((prev) => { + const filtered = prev.filter((device) => device.id !== id); + const ids = filtered.map((device) => device.id); + localStorage.setItem(deviceIdsStorageKey, JSON.stringify(ids)); + return filtered; + }); unsubscribeDevice(id); console.log(`소켓 구독 해제 ${id}`); }; const checkWarning = (id: string) => { - setDevices((prev) => prev.map((device) => (device.id === id ? { ...device, hasShownWarning: true } : device))); + setDevices((prev) => + prev.map((device) => + device.id === id ? { ...device, hasShownWarning: true } : device, + ), + ); }; const updateDeviceName = (id: string, newName: string) => { - setDevices((prev) => prev.map((device) => (device.id === id ? { ...device, name: newName } : device))); + setDevices((prev) => + prev.map((device) => + device.id === id ? { ...device, name: newName } : device, + ), + ); localStorage.setItem( - "deviceNames", - JSON.stringify({ ...JSON.parse(localStorage.getItem("deviceNames") || "{}"), [id]: newName }), + deviceNamesStorageKey, + JSON.stringify({ + ...JSON.parse(localStorage.getItem(deviceNamesStorageKey) || "{}"), + [id]: newName, + }), ); }; diff --git a/src/hooks/useDeviceHistory.ts b/src/hooks/useDeviceHistory.ts new file mode 100644 index 0000000..358c1f6 --- /dev/null +++ b/src/hooks/useDeviceHistory.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import { fetchDeviceHistory } from "../apis/deviceHistory"; +import type { DeviceHistoryPoint } from "../types/deviceHistory"; + +export default function useDeviceHistory(serialNumber?: string) { + const enabled = Boolean(serialNumber); + + return useQuery({ + queryKey: ["deviceHistory", serialNumber], + enabled, + queryFn: () => { + if (!serialNumber) return Promise.resolve([]); + return fetchDeviceHistory(serialNumber); + }, + }); +} diff --git a/src/pages/detail.tsx b/src/pages/detail.tsx index 8147bdd..13c64c0 100644 --- a/src/pages/detail.tsx +++ b/src/pages/detail.tsx @@ -13,6 +13,8 @@ import { Legend, } from "chart.js"; import type { ChartOptions } from "chart.js"; +import useDeviceHistory from "../hooks/useDeviceHistory"; +import { mapHistoryToChartData } from "../utils/deviceHistoryUtils"; ChartJS.register( CategoryScale, @@ -26,8 +28,19 @@ ChartJS.register( function Detail() { const { id } = useParams<{ id: string }>(); const chartBodyRef = React.useRef(null); + const serialNumber = id ?? ""; const displayId = id?.split("-").pop(); + const { + data: history = [], + isLoading, + isError, + } = useDeviceHistory(serialNumber); + + const { labels, riskData } = React.useMemo(() => { + return mapHistoryToChartData(history); + }, [history]); + const options: ChartOptions<"line"> = { responsive: true, maintainAspectRatio: false, @@ -52,7 +65,7 @@ function Detail() { scales: { x: { grid: { - color: "#393939", // 더 연하게 + color: "#393939", }, ticks: { color: "#8B9096", @@ -78,34 +91,10 @@ function Detail() { const yAxisLabels = [100, 75, 50, 25, 0]; const data = { - labels: [ - "11:57", - "11:58", - "11:59", - "12:00", - "12:01", - "12:02", - "12:03", - "12:04", - "12:05", - "12:06", - "11:57", - "11:58", - "11:59", - "12:00", - "12:01", - "12:02", - "12:03", - "12:04", - "12:05", - "12:06", - ], + labels, datasets: [ { - data: [ - 6, 15, 12, 28, 30, 45, 60, 55, 70, 80, 6, 15, 12, 28, 30, 45, 60, 55, - 70, 80, - ], + data: riskData, fill: false, }, ], @@ -132,7 +121,9 @@ function Detail() { ))} - + {!isLoading && !isError && history.length > 0 && ( + + )} diff --git a/src/pages/home/home.tsx b/src/pages/home/home.tsx index b29a2a1..2cb773e 100644 --- a/src/pages/home/home.tsx +++ b/src/pages/home/home.tsx @@ -9,11 +9,26 @@ import useDeviceAddMode from "../../hooks/useDeviceAddMode"; import DeviceRegisterModal from "../../components/modal/DeviceRegisterModal.tsx"; import WarningModal from "../../components/modal/WarningModal.tsx"; import useDeviceData from "../../hooks/useDeviceData.ts"; +import { useParams } from "react-router-dom"; function Home() { - const { devices, checkWarning, deleteDevice, addDevice, updateDeviceName, warningDevices } = useDeviceData(); - const { isDeleteMode, selectedItems, toggleDeleteMode, toggleItemSelection, setIsDeleteMode, setSelectedItems } = - useDeleteMode(); + const { sessionId } = useParams<{ sessionId: string }>(); + const { + devices, + checkWarning, + deleteDevice, + addDevice, + updateDeviceName, + warningDevices, + } = useDeviceData(sessionId); + const { + isDeleteMode, + selectedItems, + toggleDeleteMode, + toggleItemSelection, + setIsDeleteMode, + setSelectedItems, + } = useDeleteMode(); const { isAddMode, toggleAddMode, setIsAddMode } = useDeviceAddMode(); const warningModalDevice = warningDevices.find((device) => device.showModal); @@ -22,9 +37,7 @@ function Home() {
- - 연결된 기기 - + 연결된 기기 toggleDeleteMode(devices.map((device) => device.id))} isDeleteMode={isDeleteMode} @@ -55,7 +68,11 @@ function Home() { /> )} {isAddMode && ( - setIsAddMode(false)} addDevice={addDevice} deviceCount={devices.length} /> + setIsAddMode(false)} + addDevice={addDevice} + deviceCount={devices.length} + /> )} {selectedItems.length > 0 && ( value.toString().padStart(2, "0"); + +export const mapHistoryToChartData = (history: DeviceHistoryPoint[]) => { + const labels: string[] = []; + const riskData: number[] = []; + const temperatureData: number[] = []; + + history.forEach((point) => { + const date = new Date(point.time); + const label = `${padZero(date.getHours())}:${padZero(date.getMinutes())}`; + + labels.push(label); + riskData.push(point.risk); + temperatureData.push(point.temperature); + }); + + return { labels, riskData, temperatureData }; +};