Skip to content

Commit 8a3d8bf

Browse files
authored
Merge pull request #42 from CSID-DGU/feat/#31
#31 [FEAT] 동시팝업창 구현
2 parents 9830e39 + e60f68d commit 8a3d8bf

File tree

8 files changed

+187
-10
lines changed

8 files changed

+187
-10
lines changed

client/package-lock.json

Lines changed: 55 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"@fortawesome/fontawesome-svg-core": "^6.5.2",
77
"@fortawesome/free-solid-svg-icons": "^6.5.2",
88
"@fortawesome/react-fontawesome": "^0.2.2",
9+
"@stomp/stompjs": "^7.0.0",
910
"@testing-library/jest-dom": "^5.17.0",
1011
"@testing-library/react": "^13.4.0",
1112
"@testing-library/user-event": "^13.5.0",
@@ -25,6 +26,7 @@
2526
"react-router-dom": "^6.23.1",
2627
"react-scripts": "5.0.1",
2728
"recharts": "^2.13.3",
29+
"sockjs-client": "^1.6.1",
2830
"styled-components": "^6.1.11",
2931
"typescript": "^4.9.5",
3032
"web-vitals": "^2.1.4"
@@ -52,5 +54,8 @@
5254
"last 1 firefox version",
5355
"last 1 safari version"
5456
]
57+
},
58+
"devDependencies": {
59+
"@types/sockjs-client": "^1.5.4"
5560
}
5661
}

client/src/pages/Device.tsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@ const Device: React.FC = () => {
1616
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
1717
const [isLayoutModalOpen, setIsLayoutModalOpen] = useState(false);
1818
const [isCommandModalOpen, setIsCommandModalOpen] = useState(false);
19-
const [currentDeviceName, setCurrentDeviceName] = useState<string>(""); // 현재 기기 이름
20-
const [command, setCommand] = useState<boolean>(true); // ON이 기본값
19+
const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false); // 성공 모달 상태
20+
const [currentDeviceName, setCurrentDeviceName] = useState<string>("");
21+
const [command, setCommand] = useState<boolean>(true);
2122

22-
// 이미지 URL
2323
const layoutImage =
2424
"https://github.com/CSID-DGU/2024-1-CECD1-1921-3/blob/develop/data/IoTImg/%EB%B0%B0%EC%B9%98%EB%8F%84-%EC%8B%A0%EA%B3%B5%ED%95%99%EA%B4%805145%ED%98%B8.png?raw=true";
2525

2626
const [building, setBuilding] = useState<string>("신공학관");
2727
const [room, setRoom] = useState<string>("5145호");
2828

29+
// IoT 기기 데이터 가져오기
2930
useEffect(() => {
3031
fetch(
3132
`https://www.dgu1921.p-e.kr/devices/filter?buildingName=${building}&location=${room}`
@@ -55,7 +56,7 @@ const Device: React.FC = () => {
5556
const sendCommand = () => {
5657
const payload = {
5758
sensorId: "000100010000000093",
58-
command: command, // ON/OFF에 따라 true/false 전송
59+
command: command,
5960
};
6061

6162
fetch("https://www.dgu1921.p-e.kr/command", {
@@ -69,10 +70,15 @@ const Device: React.FC = () => {
6970
.then((data) => {
7071
console.log("Command response:", data);
7172
closeCommandModal();
73+
setIsSuccessModalOpen(true); // 성공 모달 열기
7274
})
7375
.catch((error) => console.error("Error sending command:", error));
7476
};
7577

78+
const closeSuccessModal = () => {
79+
setIsSuccessModalOpen(false);
80+
};
81+
7682
return (
7783
<div className="dashboard-container">
7884
<Sidebar onToggle={setIsSidebarOpen} />
@@ -173,7 +179,7 @@ const Device: React.FC = () => {
173179
>
174180
<p>
175181
현재 '<strong className="device-name">{currentDeviceName}</strong>
176-
' 는 ON 상태입니다. 제어 명령을 전송하시겠습니까?
182+
' 는 {command ? "ON" : "OFF"} 상태입니다. 제어 명령을 전송하시겠습니까?
177183
</p>
178184
<div className="command-toggle">
179185
<label>
@@ -196,14 +202,29 @@ const Device: React.FC = () => {
196202
</label>
197203
</div>
198204
<button className="control-button" onClick={sendCommand}>
199-
제어 명령 전송
205+
전송
200206
</button>
201207
<button className="modal-close" onClick={closeCommandModal}>
202208
X
203209
</button>
204210
</div>
205211
</div>
206212
)}
213+
214+
{isSuccessModalOpen && (
215+
<div className="modal-overlay" onClick={closeSuccessModal}>
216+
<div
217+
className="modal-content command-modal"
218+
onClick={(e) => e.stopPropagation()}
219+
>
220+
<p>명령이 전송되었습니다.</p>
221+
<br></br><br></br>
222+
<button className="control-button" onClick={closeSuccessModal}>
223+
확인
224+
</button>
225+
</div>
226+
</div>
227+
)}
207228
</div>
208229
</div>
209230
);

client/src/pages/Modal.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React, { useEffect, useState } from "react";
2+
import { Client } from "@stomp/stompjs";
3+
import SockJS from "sockjs-client";
4+
5+
const WebSocketModal = () => {
6+
const [isCommandModalOpen, setIsCommandModalOpen] = useState(false);
7+
const [currentDeviceName, setCurrentDeviceName] = useState("");
8+
const [command, setCommand] = useState<boolean | null>(null);
9+
10+
useEffect(() => {
11+
const stompClient = new Client({
12+
webSocketFactory: () => new SockJS("http://localhost:8080/ws"),
13+
onConnect: () => {
14+
stompClient.subscribe("/topic/commands", (message) => {
15+
const data = JSON.parse(message.body);
16+
setCurrentDeviceName(data.deviceName);
17+
setCommand(data.command);
18+
setIsCommandModalOpen(true); // 알림 표시
19+
});
20+
},
21+
});
22+
23+
stompClient.activate();
24+
25+
return () => {
26+
stompClient.deactivate();
27+
};
28+
}, []);
29+
30+
const closeCommandModal = () => {
31+
setIsCommandModalOpen(false);
32+
};
33+
34+
return (
35+
<>
36+
{isCommandModalOpen && (
37+
<div className="modal-overlay" onClick={closeCommandModal}>
38+
<div
39+
className="modal-content command-modal"
40+
onClick={(e) => e.stopPropagation()}
41+
>
42+
<p>
43+
현재 '<strong className="device-name">{currentDeviceName}</strong>'는{" "}
44+
{command ? "ON" : "OFF"} 상태입니다. 제어 명령을 전송하시겠습니까?
45+
</p>
46+
<button className="control-button" onClick={closeCommandModal}>
47+
닫기
48+
</button>
49+
</div>
50+
</div>
51+
)}
52+
</>
53+
);
54+
};
55+
56+
export default WebSocketModal;

server/build.gradle

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ dependencies {
3737
implementation("software.amazon.awssdk:s3:2.21.0")
3838
//actuator
3939
implementation 'org.springframework.boot:spring-boot-starter-actuator'
40-
// Spring Security
40+
//Spring Security
4141
implementation 'org.springframework.boot:spring-boot-starter-security'
4242
testImplementation 'org.springframework.security:spring-security-test'
4343
//JWT Token
@@ -48,10 +48,13 @@ dependencies {
4848
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
4949
//Jsoup
5050
implementation 'org.jsoup:jsoup:1.18.1'
51-
// JSON 라이브러리
51+
//JSON
5252
implementation 'org.json:json:20231013'
53-
// Apache HttpClient 라이브러리
53+
//Apache HttpClient
5454
implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1'
55+
//Web Socket
56+
implementation 'org.springframework.boot:spring-boot-starter-websocket'
57+
implementation 'org.springframework.boot:spring-boot-starter-web'
5558
}
5659

5760
tasks.named('test') {

server/src/main/java/org/cecd/server/controller/CommandController.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import org.cecd.server.dto.CommandRequest;
55
import org.cecd.server.service.CommandService;
66
import org.springframework.http.ResponseEntity;
7+
import org.springframework.messaging.handler.annotation.MessageMapping;
8+
import org.springframework.messaging.handler.annotation.SendTo;
79
import org.springframework.web.bind.annotation.*;
810

911
@RestController
@@ -23,4 +25,11 @@ public ResponseEntity<String> sendControlCommand(@RequestBody CommandRequest com
2325
public ResponseEntity<CommandRequest> echoControlCommand(@RequestBody CommandRequest commandRequest) {
2426
return ResponseEntity.ok(commandRequest);
2527
}
28+
29+
@MessageMapping("/socket/control") // 클라이언트가 이 경로로 메시지를 송신
30+
@SendTo("/topic/commands") // 이 경로를 구독 중인 모든 클라이언트에게 메시지 전달
31+
public CommandRequest sendCommand (CommandRequest commandRequest) {
32+
System.out.println("Received command: " + commandRequest); // 로그 추가
33+
return commandRequest; // command는 클라이언트로 전달할 메시지
34+
}
2635
}

server/src/main/java/org/cecd/server/external/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws
4545
.authorizeHttpRequests(authorize -> authorize // 권한 설정
4646
.requestMatchers("/jwt-login/info").authenticated()
4747
.requestMatchers("/jwt-login/admin/**").hasAuthority(MemberRole.ADMIN.name())
48-
.anyRequest().permitAll()
48+
.requestMatchers("/ws/**").permitAll() // WebSocket 엔드포인트 허용
49+
.anyRequest().permitAll() // 추후 수정 예정
4950
)
5051
.exceptionHandling(exceptionHandling ->
5152
exceptionHandling
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.cecd.server.external;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
5+
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
6+
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
7+
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
8+
9+
@Configuration
10+
@EnableWebSocketMessageBroker
11+
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
12+
13+
@Override
14+
public void configureMessageBroker(MessageBrokerRegistry config) {
15+
config.enableSimpleBroker("/topic");
16+
config.setApplicationDestinationPrefixes("/app");
17+
}
18+
19+
@Override
20+
public void registerStompEndpoints(StompEndpointRegistry registry) {
21+
registry.addEndpoint("/ws")
22+
.setAllowedOriginPatterns("http://localhost:3000","https://www.dgu1921.p-e.kr")
23+
.withSockJS();
24+
}
25+
}
26+
27+

0 commit comments

Comments
 (0)