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 };
+};