Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<GlobalStyles />

<Routes>
<Route path="/section-1" element={<Home />} />
<Route path="/" element={<Navigate to={lastSessionPath} replace />} />
<Route path="/detail/:id" element={<Detail />} />
<Route path="/*" element={<Home />} />
<Route path="/:sessionId" element={<Home />} />
</Routes>
</>
);
Expand Down
17 changes: 17 additions & 0 deletions src/apis/deviceHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { DeviceHistoryPoint } from "../types/deviceHistory";

export const fetchDeviceHistory = async (
serialNumber: string,
): Promise<DeviceHistoryPoint[]> => {
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 : [];
};
24 changes: 20 additions & 4 deletions src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -29,9 +29,22 @@ function Sidebar({ onNavItemClick = () => {} }: SidebarProps) {
const navigate = useNavigate();
const location = useLocation();

const [navItems, setNavItems] = useState<SidebarNavItem[]>([
{ id: "section-1", label: "Section 1", path: "/section-1" },
]);
const [navItems, setNavItems] = useState<SidebarNavItem[]>(() => {
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) => {
Expand All @@ -50,6 +63,9 @@ function Sidebar({ onNavItemClick = () => {} }: SidebarProps) {
} else {
navigate(`/${itemId}`);
}
if (item?.path) {
localStorage.setItem("lastSessionPath", item.path);
}
onNavItemClick(itemId);
},
[navItems, navigate, onNavItemClick],
Expand Down
89 changes: 64 additions & 25 deletions src/hooks/useDeviceData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Device[]>(() => {
return JSON.parse(localStorage.getItem("devices") || "[]");
const storedIds: string[] = JSON.parse(
localStorage.getItem(deviceIdsStorageKey) || "[]",
);
const savedNames: Record<string, string> = 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}`);
Expand All @@ -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 {
Expand All @@ -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<string, string> = 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,
}),
);
};

Expand Down
16 changes: 16 additions & 0 deletions src/hooks/useDeviceHistory.ts
Original file line number Diff line number Diff line change
@@ -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<DeviceHistoryPoint[], Error>({
queryKey: ["deviceHistory", serialNumber],
enabled,
queryFn: () => {
if (!serialNumber) return Promise.resolve([]);
return fetchDeviceHistory(serialNumber);
},
});
}
47 changes: 19 additions & 28 deletions src/pages/detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,8 +28,19 @@ ChartJS.register(
function Detail() {
const { id } = useParams<{ id: string }>();
const chartBodyRef = React.useRef<HTMLDivElement | null>(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,
Expand All @@ -52,7 +65,7 @@ function Detail() {
scales: {
x: {
grid: {
color: "#393939", // 더 연하게
color: "#393939",
},
ticks: {
color: "#8B9096",
Expand All @@ -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,
},
],
Expand All @@ -132,7 +121,9 @@ function Detail() {
))}
</YAxisContainer>
<ChartWrapper style={{ width: `${chartWidth}px` }}>
<Line data={data} options={options} />
{!isLoading && !isError && history.length > 0 && (
<Line data={data} options={options} />
)}
</ChartWrapper>
</ChartBody>
</ChartContainer>
Expand Down
31 changes: 24 additions & 7 deletions src/pages/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -22,9 +37,7 @@ function Home() {
<Sidebar />
<MainContent>
<Header>
<DeviceText>
연결된 기기
</DeviceText>
<DeviceText>연결된 기기</DeviceText>
<DeviceDeleteButton
onClick={() => toggleDeleteMode(devices.map((device) => device.id))}
isDeleteMode={isDeleteMode}
Expand Down Expand Up @@ -55,7 +68,11 @@ function Home() {
/>
)}
{isAddMode && (
<DeviceRegisterModal onClose={() => setIsAddMode(false)} addDevice={addDevice} deviceCount={devices.length} />
<DeviceRegisterModal
onClose={() => setIsAddMode(false)}
addDevice={addDevice}
deviceCount={devices.length}
/>
)}
{selectedItems.length > 0 && (
<DeleteButton
Expand Down
6 changes: 6 additions & 0 deletions src/types/deviceHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface DeviceHistoryPoint {
time: string;
deviceId: number;
temperature: number;
risk: number;
}
Loading
Loading