Skip to content

Commit b5fb6d6

Browse files
authored
Merge pull request #27 from Team-DBE/feature/#26
#26 세션별 기기 관리 기능 추가 및 히스토리 API 연동
2 parents 4c8ec80 + 34c41e1 commit b5fb6d6

9 files changed

Lines changed: 194 additions & 67 deletions

File tree

src/App.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
import { Routes, Route } from "react-router-dom";
1+
import { Routes, Route, Navigate } from "react-router-dom";
22
import GlobalStyles from "./styles/globalStyles";
33
import Home from "./pages/home/home";
44
import Detail from "./pages/detail.tsx";
55

66
function App() {
7+
const lastSessionPath =
8+
typeof window !== "undefined"
9+
? localStorage.getItem("lastSessionPath") || "/section-1"
10+
: "/section-1";
11+
712
return (
813
<>
914
<GlobalStyles />
1015

1116
<Routes>
12-
<Route path="/section-1" element={<Home />} />
17+
<Route path="/" element={<Navigate to={lastSessionPath} replace />} />
1318
<Route path="/detail/:id" element={<Detail />} />
14-
<Route path="/*" element={<Home />} />
19+
<Route path="/:sessionId" element={<Home />} />
1520
</Routes>
1621
</>
1722
);

src/apis/deviceHistory.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { DeviceHistoryPoint } from "../types/deviceHistory";
2+
3+
export const fetchDeviceHistory = async (
4+
serialNumber: string,
5+
): Promise<DeviceHistoryPoint[]> => {
6+
const baseUrl = import.meta.env.VITE_API_BASE_URL;
7+
const response = await fetch(
8+
`${baseUrl}/histories/all/${encodeURIComponent(serialNumber)}`,
9+
);
10+
11+
if (!response.ok) {
12+
throw new Error(String(response.status));
13+
}
14+
15+
const data: DeviceHistoryPoint[] = await response.json();
16+
return Array.isArray(data) ? data : [];
17+
};

src/components/sidebar/sidebar.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useState } from "react";
1+
import { useCallback, useState, useEffect } from "react";
22
import styled from "@emotion/styled";
33
import { useNavigate, useLocation } from "react-router-dom";
44
import { SidebarHeader } from "./header/sidebar-header";
@@ -29,9 +29,22 @@ function Sidebar({ onNavItemClick = () => {} }: SidebarProps) {
2929
const navigate = useNavigate();
3030
const location = useLocation();
3131

32-
const [navItems, setNavItems] = useState<SidebarNavItem[]>([
33-
{ id: "section-1", label: "Section 1", path: "/section-1" },
34-
]);
32+
const [navItems, setNavItems] = useState<SidebarNavItem[]>(() => {
33+
const saved = localStorage.getItem("sessions");
34+
if (saved) {
35+
try {
36+
return JSON.parse(saved) as SidebarNavItem[];
37+
} catch {
38+
// 파싱 실패 시 기본값으로 초기화
39+
}
40+
}
41+
return [{ id: "section-1", label: "Section 1", path: "/section-1" }];
42+
});
43+
44+
// 세션 목록 변경 시 localStorage에 저장
45+
useEffect(() => {
46+
localStorage.setItem("sessions", JSON.stringify(navItems));
47+
}, [navItems]);
3548

3649
const isMainItemActive = useCallback(
3750
(itemId: string) => {
@@ -50,6 +63,9 @@ function Sidebar({ onNavItemClick = () => {} }: SidebarProps) {
5063
} else {
5164
navigate(`/${itemId}`);
5265
}
66+
if (item?.path) {
67+
localStorage.setItem("lastSessionPath", item.path);
68+
}
5369
onNavItemClick(itemId);
5470
},
5571
[navItems, navigate, onNavItemClick],

src/hooks/useDeviceData.ts

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,34 @@ import { useState, useMemo, useEffect } from "react";
22
import type { Device } from "../types/device";
33
import useDeviceStomp from "./useDeviceStomp";
44

5-
export default function useDeviceData() {
5+
export default function useDeviceData(sessionId?: string) {
66
const severUrl = import.meta.env.VITE_API_BASE_URL;
7-
const { isConnected, deviceData, subscribeDevice, unsubscribeDevice } = useDeviceStomp(`${severUrl}/ws-stomp`);
7+
const { isConnected, deviceData, subscribeDevice, unsubscribeDevice } =
8+
useDeviceStomp(`${severUrl}/ws-stomp`);
9+
10+
const effectiveSessionId = sessionId ?? "default";
11+
const deviceIdsStorageKey = `deviceIds_${effectiveSessionId}`;
12+
const deviceNamesStorageKey = `deviceNames_${effectiveSessionId}`;
813

914
const [devices, setDevices] = useState<Device[]>(() => {
10-
return JSON.parse(localStorage.getItem("devices") || "[]");
15+
const storedIds: string[] = JSON.parse(
16+
localStorage.getItem(deviceIdsStorageKey) || "[]",
17+
);
18+
const savedNames: Record<string, string> = JSON.parse(
19+
localStorage.getItem(deviceNamesStorageKey) || "{}",
20+
);
21+
22+
return storedIds.map((id, index) => ({
23+
id,
24+
name: savedNames[id] || `기기 ${index + 1}`,
25+
temperature: 0,
26+
warning: false,
27+
hasShownWarning: false,
28+
}));
1129
});
12-
13-
useEffect(() => {
14-
localStorage.setItem("devices", JSON.stringify(devices));
15-
}, [devices]);
1630

1731
useEffect(() => {
18-
if(!isConnected) return;
32+
if (!isConnected) return;
1933
devices.forEach((device) => {
2034
subscribeDevice(device.id);
2135
console.log(`소켓 구독 ${device.id}`);
@@ -24,7 +38,10 @@ export default function useDeviceData() {
2438

2539
const liveDevices = useMemo(() => {
2640
return devices.map((device) => {
27-
const data = deviceData[device.id] as { temperature: number; risk: number };
41+
const data = deviceData[device.id] as {
42+
temperature: number;
43+
risk: number;
44+
};
2845
const currentTemp = data ? data.temperature : 0;
2946

3047
return {
@@ -38,43 +55,65 @@ export default function useDeviceData() {
3855

3956
const warningDevice = liveDevices.find((device) => device.warning);
4057

41-
const addDevice = (id: string) => {
42-
const savedNames = JSON.parse(localStorage.getItem("deviceNames") || "{}");
58+
const addDevice = (id: string) => {
59+
const savedNames: Record<string, string> = JSON.parse(
60+
localStorage.getItem(deviceNamesStorageKey) || "{}",
61+
);
4362

4463
setDevices((prev) => {
4564
if (prev.some((device) => device.id === id)) {
4665
return prev;
4766
}
4867

49-
const deviceName = savedNames[id] || `기기 ${devices.length + 1}`;
68+
const deviceName = savedNames[id] || `기기 ${prev.length + 1}`;
5069

51-
const newDevice: Device = {
52-
id: id,
53-
name: deviceName,
54-
temperature: 0,
55-
warning: false,
56-
hasShownWarning: false,
57-
};
70+
const newDevice: Device = {
71+
id: id,
72+
name: deviceName,
73+
temperature: 0,
74+
warning: false,
75+
hasShownWarning: false,
76+
};
77+
78+
const newDevices = [...prev, newDevice];
79+
const newIds = newDevices.map((device) => device.id);
80+
localStorage.setItem(deviceIdsStorageKey, JSON.stringify(newIds));
5881

59-
return [...prev, newDevice];
82+
return newDevices;
6083
});
6184
};
6285

6386
const deleteDevice = (id: string) => {
64-
setDevices((prev) => prev.filter((device) => device.id !== id));
87+
setDevices((prev) => {
88+
const filtered = prev.filter((device) => device.id !== id);
89+
const ids = filtered.map((device) => device.id);
90+
localStorage.setItem(deviceIdsStorageKey, JSON.stringify(ids));
91+
return filtered;
92+
});
6593
unsubscribeDevice(id);
6694
console.log(`소켓 구독 해제 ${id}`);
6795
};
6896

6997
const checkWarning = (id: string) => {
70-
setDevices((prev) => prev.map((device) => (device.id === id ? { ...device, hasShownWarning: true } : device)));
98+
setDevices((prev) =>
99+
prev.map((device) =>
100+
device.id === id ? { ...device, hasShownWarning: true } : device,
101+
),
102+
);
71103
};
72104

73105
const updateDeviceName = (id: string, newName: string) => {
74-
setDevices((prev) => prev.map((device) => (device.id === id ? { ...device, name: newName } : device)));
106+
setDevices((prev) =>
107+
prev.map((device) =>
108+
device.id === id ? { ...device, name: newName } : device,
109+
),
110+
);
75111
localStorage.setItem(
76-
"deviceNames",
77-
JSON.stringify({ ...JSON.parse(localStorage.getItem("deviceNames") || "{}"), [id]: newName }),
112+
deviceNamesStorageKey,
113+
JSON.stringify({
114+
...JSON.parse(localStorage.getItem(deviceNamesStorageKey) || "{}"),
115+
[id]: newName,
116+
}),
78117
);
79118
};
80119

src/hooks/useDeviceHistory.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { fetchDeviceHistory } from "../apis/deviceHistory";
3+
import type { DeviceHistoryPoint } from "../types/deviceHistory";
4+
5+
export default function useDeviceHistory(serialNumber?: string) {
6+
const enabled = Boolean(serialNumber);
7+
8+
return useQuery<DeviceHistoryPoint[], Error>({
9+
queryKey: ["deviceHistory", serialNumber],
10+
enabled,
11+
queryFn: () => {
12+
if (!serialNumber) return Promise.resolve([]);
13+
return fetchDeviceHistory(serialNumber);
14+
},
15+
});
16+
}

src/pages/detail.tsx

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
Legend,
1414
} from "chart.js";
1515
import type { ChartOptions } from "chart.js";
16+
import useDeviceHistory from "../hooks/useDeviceHistory";
17+
import { mapHistoryToChartData } from "../utils/deviceHistoryUtils";
1618

1719
ChartJS.register(
1820
CategoryScale,
@@ -26,8 +28,19 @@ ChartJS.register(
2628
function Detail() {
2729
const { id } = useParams<{ id: string }>();
2830
const chartBodyRef = React.useRef<HTMLDivElement | null>(null);
31+
const serialNumber = id ?? "";
2932
const displayId = id?.split("-").pop();
3033

34+
const {
35+
data: history = [],
36+
isLoading,
37+
isError,
38+
} = useDeviceHistory(serialNumber);
39+
40+
const { labels, riskData } = React.useMemo(() => {
41+
return mapHistoryToChartData(history);
42+
}, [history]);
43+
3144
const options: ChartOptions<"line"> = {
3245
responsive: true,
3346
maintainAspectRatio: false,
@@ -52,7 +65,7 @@ function Detail() {
5265
scales: {
5366
x: {
5467
grid: {
55-
color: "#393939", // 더 연하게
68+
color: "#393939",
5669
},
5770
ticks: {
5871
color: "#8B9096",
@@ -78,34 +91,10 @@ function Detail() {
7891
const yAxisLabels = [100, 75, 50, 25, 0];
7992

8093
const data = {
81-
labels: [
82-
"11:57",
83-
"11:58",
84-
"11:59",
85-
"12:00",
86-
"12:01",
87-
"12:02",
88-
"12:03",
89-
"12:04",
90-
"12:05",
91-
"12:06",
92-
"11:57",
93-
"11:58",
94-
"11:59",
95-
"12:00",
96-
"12:01",
97-
"12:02",
98-
"12:03",
99-
"12:04",
100-
"12:05",
101-
"12:06",
102-
],
94+
labels,
10395
datasets: [
10496
{
105-
data: [
106-
6, 15, 12, 28, 30, 45, 60, 55, 70, 80, 6, 15, 12, 28, 30, 45, 60, 55,
107-
70, 80,
108-
],
97+
data: riskData,
10998
fill: false,
11099
},
111100
],
@@ -132,7 +121,9 @@ function Detail() {
132121
))}
133122
</YAxisContainer>
134123
<ChartWrapper style={{ width: `${chartWidth}px` }}>
135-
<Line data={data} options={options} />
124+
{!isLoading && !isError && history.length > 0 && (
125+
<Line data={data} options={options} />
126+
)}
136127
</ChartWrapper>
137128
</ChartBody>
138129
</ChartContainer>

src/pages/home/home.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,26 @@ import useDeviceAddMode from "../../hooks/useDeviceAddMode";
99
import DeviceRegisterModal from "../../components/modal/DeviceRegisterModal.tsx";
1010
import WarningModal from "../../components/modal/WarningModal.tsx";
1111
import useDeviceData from "../../hooks/useDeviceData.ts";
12+
import { useParams } from "react-router-dom";
1213

1314
function Home() {
14-
const { devices, checkWarning, deleteDevice, addDevice, updateDeviceName, warningDevices } = useDeviceData();
15-
const { isDeleteMode, selectedItems, toggleDeleteMode, toggleItemSelection, setIsDeleteMode, setSelectedItems } =
16-
useDeleteMode();
15+
const { sessionId } = useParams<{ sessionId: string }>();
16+
const {
17+
devices,
18+
checkWarning,
19+
deleteDevice,
20+
addDevice,
21+
updateDeviceName,
22+
warningDevices,
23+
} = useDeviceData(sessionId);
24+
const {
25+
isDeleteMode,
26+
selectedItems,
27+
toggleDeleteMode,
28+
toggleItemSelection,
29+
setIsDeleteMode,
30+
setSelectedItems,
31+
} = useDeleteMode();
1732
const { isAddMode, toggleAddMode, setIsAddMode } = useDeviceAddMode();
1833
const warningModalDevice = warningDevices.find((device) => device.showModal);
1934

@@ -22,9 +37,7 @@ function Home() {
2237
<Sidebar />
2338
<MainContent>
2439
<Header>
25-
<DeviceText>
26-
연결된 기기
27-
</DeviceText>
40+
<DeviceText>연결된 기기</DeviceText>
2841
<DeviceDeleteButton
2942
onClick={() => toggleDeleteMode(devices.map((device) => device.id))}
3043
isDeleteMode={isDeleteMode}
@@ -55,7 +68,11 @@ function Home() {
5568
/>
5669
)}
5770
{isAddMode && (
58-
<DeviceRegisterModal onClose={() => setIsAddMode(false)} addDevice={addDevice} deviceCount={devices.length} />
71+
<DeviceRegisterModal
72+
onClose={() => setIsAddMode(false)}
73+
addDevice={addDevice}
74+
deviceCount={devices.length}
75+
/>
5976
)}
6077
{selectedItems.length > 0 && (
6178
<DeleteButton

src/types/deviceHistory.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface DeviceHistoryPoint {
2+
time: string;
3+
deviceId: number;
4+
temperature: number;
5+
risk: number;
6+
}

0 commit comments

Comments
 (0)