Skip to content

Commit 618bb83

Browse files
authored
Merge pull request #185 from Sysone-Final/dev
Dev
2 parents 4006134 + 6027a70 commit 618bb83

39 files changed

Lines changed: 2518 additions & 619 deletions

src/api/alertApi.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import client from "./client";
2+
3+
export interface Alert {
4+
alertId: number;
5+
equipmentId: number;
6+
rackId: number;
7+
serverRoomId: number;
8+
dataCenterId: number;
9+
targetName: string;
10+
targetType: string;
11+
metricType: string;
12+
metricName: string;
13+
level: string;
14+
measuredValue: number;
15+
thresholdValue: number;
16+
triggeredAt: string;
17+
isRead: boolean;
18+
readAt: string | null;
19+
readBy: string | null;
20+
message: string;
21+
additionalInfo: string | null;
22+
createdAt: string;
23+
}
24+
25+
export interface AlertsResponse {
26+
totalPages: number;
27+
pageSize: number;
28+
hasPrevious: boolean;
29+
hasNext: boolean;
30+
currentPage: number;
31+
content: Alert[];
32+
totalElements: number;
33+
}
34+
35+
export interface AlertsParams {
36+
page?: number;
37+
size?: number;
38+
days?: number;
39+
}
40+
41+
export const alertApi = {
42+
// 알림 목록 조회
43+
getAlerts: async (params: AlertsParams = {}): Promise<AlertsResponse> => {
44+
const { page = 0, size = 10, days = 7 } = params;
45+
const response = await client.get<AlertsResponse>("/alerts", {
46+
params: { page, size, days },
47+
});
48+
return response.data;
49+
},
50+
51+
// 알림 읽음 처리
52+
markAsRead: async (alertId: number): Promise<void> => {
53+
await client.patch(`/alerts/${alertId}/read`);
54+
},
55+
56+
// 모든 알림 읽음 처리
57+
markAllAsRead: async (): Promise<void> => {
58+
await client.patch("/alerts/read-all");
59+
},
60+
61+
// 알림 삭제
62+
deleteAlert: async (alertId: number): Promise<void> => {
63+
await client.delete(`/alerts/${alertId}`);
64+
},
65+
66+
// 모든 알림 삭제
67+
deleteAllAlerts: async (): Promise<void> => {
68+
await client.delete("/alerts/delete-all");
69+
},
70+
};

src/api/sseClient.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ export interface SSEConnection {
1616
isConnected: () => boolean;
1717
}
1818

19-
/**
20-
* SSE 연결을 생성하고 관리하는 유틸리티 함수
21-
* EventSource는 헤더를 지원하지 않으므로 fetch로 스트림을 처리
22-
*/
19+
2320
export const createSSEConnection = <T = unknown>(
2421
endpoint: string,
2522
options: SSEOptions<T>
@@ -219,3 +216,12 @@ export const createRackSSE = <T = unknown>(
219216
) => {
220217
return createSSEConnection(`/monitoring/subscribe/rack/${rackId}`, options);
221218
};
219+
220+
/**
221+
* 알림 SSE 연결 생성
222+
*/
223+
export const createAlertSSE = <T = unknown>(
224+
options: Omit<SSEOptions<T>, "onOpen"> & { onOpen?: () => void }
225+
) => {
226+
return createSSEConnection(`/alerts/subscribe`, options);
227+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Search } from 'lucide-react';
2+
3+
interface MemberFiltersProps {
4+
searchTerm: string;
5+
onSearchChange: (value: string) => void;
6+
}
7+
8+
export default function MemberFilters({
9+
searchTerm,
10+
onSearchChange,
11+
}: MemberFiltersProps) {
12+
return (
13+
<div className="mb-6">
14+
{/* 검색창 */}
15+
<div className="relative w-full border border-slate-300/40 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/50 bg-gray-700/50 text-gray-50 placeholder-gray-400">
16+
<Search
17+
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
18+
size={20}
19+
/>
20+
<input
21+
type="text"
22+
placeholder="이름, 사용자ID, 이메일로 검색..."
23+
className="w-full pl-10 pr-4 py-2 bg-transparent"
24+
value={searchTerm}
25+
onChange={(e) => onSearchChange(e.target.value)}
26+
/>
27+
</div>
28+
</div>
29+
);
30+
}

src/domains/humanResource/components/memberTable.config.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ export const memberColumns: ColumnDef<Member>[] = [
2828
cell: ({ row }) => (
2929
<input
3030
type="checkbox"
31-
className="rounded border-gray-600 bg-gray-700 focus:ring-slate-300/40"
31+
className="w-5 h-5 rounded border-gray-600 bg-gray-700 focus:ring-slate-300/40"
3232
checked={row.getIsSelected()}
3333
disabled={!row.getCanSelect()}
3434
onChange={row.getToggleSelectedHandler()}
3535
/>
3636
),
3737
enableSorting: false,
3838
enableHiding: false,
39+
size: 50,
3940
},
4041

4142
// ID
@@ -71,6 +72,7 @@ export const memberColumns: ColumnDef<Member>[] = [
7172
</button>
7273
);
7374
},
75+
size: 140,
7476
},
7577

7678
// 이름
@@ -87,6 +89,7 @@ export const memberColumns: ColumnDef<Member>[] = [
8789
</button>
8890
);
8991
},
92+
size: 120,
9093
},
9194

9295
// 이메일
@@ -96,6 +99,7 @@ export const memberColumns: ColumnDef<Member>[] = [
9699
cell: ({ getValue }) => {
97100
return <span>{getValue<string>()}</span>;
98101
},
102+
size: 200,
99103
},
100104

101105
// 역할
@@ -124,6 +128,7 @@ export const memberColumns: ColumnDef<Member>[] = [
124128
</span>
125129
);
126130
},
131+
size: 110,
127132
},
128133

129134
// 마지막 로그인
@@ -157,14 +162,15 @@ export const memberColumns: ColumnDef<Member>[] = [
157162
</span>
158163
);
159164
},
165+
size: 160,
160166
},
161167

162168
// 관리 액션
163169
{
164170
id: 'actions',
165171
header: '관리',
166172
cell: ({ row, table }) => (
167-
<div className="flex gap-2">
173+
<div className="flex gap-2 justify-start">
168174
<button
169175
className="text-gray-400 hover:text-blue-400"
170176
onClick={() =>
@@ -186,5 +192,6 @@ export const memberColumns: ColumnDef<Member>[] = [
186192
</div>
187193
),
188194
enableSorting: false,
195+
size: 80,
189196
},
190197
];

src/domains/humanResource/pages/HumanResource.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { useState } from 'react';
1+
import { useState, useMemo } from 'react';
22
import {
33
useReactTable,
44
getCoreRowModel,
55
getPaginationRowModel,
66
getSortedRowModel,
7+
getFilteredRowModel,
78
} from '@tanstack/react-table';
89
import type {
910
PaginationState,
@@ -14,6 +15,7 @@ import { Plus, Trash2 } from 'lucide-react';
1415

1516
import { DataTable, DataTablePagination } from '@/shared/table';
1617
import { memberColumns } from '../components/memberTable.config';
18+
import MemberFilters from '../components/MemberFilters';
1719
import AddMemberModal from '../components/AddMemberModal';
1820
import EditMemberModal from '../components/EditMemberModal';
1921
import DeleteMemberModal from '../components/DeleteMemberModal';
@@ -33,6 +35,7 @@ export default function HumanResource() {
3335
});
3436
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
3537
const [sorting, setSorting] = useState<SortingState>([]);
38+
const [searchTerm, setSearchTerm] = useState('');
3639
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
3740
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
3841
const [selectedMember, setSelectedMember] = useState<Member | null>(null);
@@ -45,6 +48,20 @@ export default function HumanResource() {
4548
const deleteMemberMutation = useDeleteMember();
4649
const deleteMultipleMembersMutation = useDeleteMultipleMembers();
4750

51+
// 검색 필터링된 데이터
52+
const filteredData = useMemo(() => {
53+
if (!searchTerm) return memberData;
54+
55+
const lowerSearch = searchTerm.toLowerCase();
56+
return memberData.filter((member) => {
57+
return (
58+
member.name.toLowerCase().includes(lowerSearch) ||
59+
member.userName.toLowerCase().includes(lowerSearch) ||
60+
member.email.toLowerCase().includes(lowerSearch)
61+
);
62+
});
63+
}, [memberData, searchTerm]);
64+
4865
// --- 이벤트 핸들러 ---
4966
const handleAddMember = () => {
5067
setIsAddModalOpen(true);
@@ -100,7 +117,7 @@ export default function HumanResource() {
100117

101118
// --- 테이블 인스턴스 ---
102119
const table = useReactTable({
103-
data: memberData,
120+
data: filteredData,
104121
columns: memberColumns,
105122
state: { pagination, rowSelection, sorting },
106123
onPaginationChange: setPagination,
@@ -109,6 +126,7 @@ export default function HumanResource() {
109126
getCoreRowModel: getCoreRowModel(),
110127
getPaginationRowModel: getPaginationRowModel(),
111128
getSortedRowModel: getSortedRowModel(),
129+
getFilteredRowModel: getFilteredRowModel(),
112130
enableRowSelection: true,
113131
meta: {
114132
onEdit: handleEditMember,
@@ -138,6 +156,12 @@ export default function HumanResource() {
138156

139157
{/* 메인 컨텐츠 */}
140158
<main className="flex-1 overflow-y-auto p-8">
159+
{/* 검색 필터 */}
160+
<MemberFilters
161+
searchTerm={searchTerm}
162+
onSearchChange={setSearchTerm}
163+
/>
164+
141165
{/* 대량 작업 버튼 */}
142166
{selectedCount > 0 && (
143167
<div className="mb-4 flex items-center gap-2">

src/domains/mainDashboard/api/mainDashboardApi.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface Device {
1616
status: string;
1717
rackName: string | null;
1818
rackId: number | null;
19+
equipmentCount: number;
1920
}
2021

2122
// 서버실 정보
@@ -47,6 +48,7 @@ export interface Rack {
4748
gridZ: number;
4849
rotation: number;
4950
status: string;
51+
equipmentCount: number; // 랙 내 장비 개수
5052
equipments: []; // 빈 배열로 초기화 (필요시 추가 API로 장비 정보 로드)
5153
}
5254

@@ -121,6 +123,7 @@ export const getServerRoomRacks = async (
121123
const { devices } = await getServerRoomDevices(serverRoomId);
122124

123125
// deviceType이 "server"인 것만 필터링하여 Rack으로 변환
126+
// API 응답에 이미 equipmentCount가 포함되어 있음
124127
return devices
125128
.filter((device) => device.deviceType === "server")
126129
.map((device) => ({
@@ -133,6 +136,7 @@ export const getServerRoomRacks = async (
133136
gridZ: device.gridZ,
134137
rotation: device.rotation,
135138
status: device.status,
139+
equipmentCount: device.equipmentCount, // API 응답의 equipmentCount 사용
136140
equipments: [], // 빈 배열로 초기화
137141
}));
138142
};

src/domains/mainDashboard/components/CpuUsageChart.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@ export default function CpuUsageChart({
3333
const series: LineChartSeries[] = [
3434
{
3535
name: "최대 사용량",
36-
data: data.map((item) => item.maxUsage.toFixed(2)),
36+
data: data.map((item) => (item.maxUsage ?? 0).toFixed(2)),
3737
showArea: true,
3838
color: "#eab308", // 노란색
3939
},
4040
{
4141
name: "평균 사용량",
42-
data: data.map((item) => item.avgUsage.toFixed(2)),
42+
data: data.map((item) => (item.avgUsage ?? 0).toFixed(2)),
4343
showArea: true,
4444
color: "#22c55e", // 초록색
4545
},
4646
{
4747
name: "최소 사용량",
48-
data: data.map((item) => item.minUsage.toFixed(2)),
48+
data: data.map((item) => (item.minUsage ?? 0).toFixed(2)),
4949
showArea: true,
5050
color: "#0ea5e9", // 하늘색
5151
},
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { PackageOpen } from 'lucide-react';
2+
3+
interface DashboardEmptyFallbackProps {
4+
error: Error;
5+
}
6+
7+
/**
8+
* 메인 대시보드 전용 Empty State Fallback
9+
* 장비가 없는 경우 표시되는 UI
10+
*/
11+
function DashboardEmptyFallback({ error }: DashboardEmptyFallbackProps) {
12+
// 에러 메시지에서 어떤 위치인지 파악
13+
let title = '배치된 장비가 없습니다';
14+
let message = '이 위치에 장비를 배치하면 실시간 모니터링 데이터를 확인할 수 있습니다.';
15+
16+
if (error.message.includes('데이터센터')) {
17+
title = '데이터센터에 배치된 장비가 없습니다';
18+
message = '서버실에 장비를 배치하면 데이터센터의 실시간 모니터링 데이터를 확인할 수 있습니다.';
19+
} else if (error.message.includes('서버실')) {
20+
title = '서버실에 배치된 장비가 없습니다';
21+
message = '랙에 장비를 배치하면 서버실의 실시간 모니터링 데이터를 확인할 수 있습니다.';
22+
} else if (error.message.includes('랙')) {
23+
title = '랙에 배치된 장비가 없습니다';
24+
message = '이 랙에 장비를 배치하면 실시간 모니터링 데이터를 확인할 수 있습니다.';
25+
}
26+
27+
return (
28+
<div className="flex flex-col items-center justify-center p-12 h-full min-h-full">
29+
<PackageOpen className="w-20 h-20 text-gray-600 mb-6" strokeWidth={1.5} />
30+
31+
<h2 className="text-2xl font-semibold text-gray-300 mb-3">
32+
{title}
33+
</h2>
34+
35+
<p className="text-gray-200 text-center max-w-lg leading-relaxed">
36+
{message}
37+
</p>
38+
39+
<div className="mt-8 p-4 bg-neutral-800 rounded-lg border border-neutral-700">
40+
<p className="text-base text-gray-200 text-center font-medium">
41+
장비 배치는 <span className="text-green-600">서버실 뷰</span>에서 할 수 있습니다
42+
</p>
43+
</div>
44+
</div>
45+
);
46+
}
47+
48+
export default DashboardEmptyFallback;

src/domains/mainDashboard/components/DatacenterDashboard.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export default function DatacenterDashboard({
3030
// reconnect,
3131
} = useDatacenterSSE(datacenterId, true);
3232

33+
// 에러 발생 시 throw하여 ErrorBoundary가 처리하도록 함
34+
if (error) {
35+
throw new Error(error);
36+
}
37+
3338
// 로딩 상태
3439
if (!metrics) {
3540
return (

0 commit comments

Comments
 (0)