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
55 changes: 55 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@stomp/stompjs": "^7.0.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
Expand All @@ -25,6 +26,7 @@
"react-router-dom": "^6.23.1",
"react-scripts": "5.0.1",
"recharts": "^2.13.3",
"sockjs-client": "^1.6.1",
"styled-components": "^6.1.11",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
Expand Down Expand Up @@ -52,5 +54,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/sockjs-client": "^1.5.4"
}
}
33 changes: 27 additions & 6 deletions client/src/pages/Device.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ const Device: React.FC = () => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isLayoutModalOpen, setIsLayoutModalOpen] = useState(false);
const [isCommandModalOpen, setIsCommandModalOpen] = useState(false);
const [currentDeviceName, setCurrentDeviceName] = useState<string>(""); // 현재 기기 이름
const [command, setCommand] = useState<boolean>(true); // ON이 기본값
const [isSuccessModalOpen, setIsSuccessModalOpen] = useState(false); // 성공 모달 상태
const [currentDeviceName, setCurrentDeviceName] = useState<string>("");
const [command, setCommand] = useState<boolean>(true);

// 이미지 URL
const layoutImage =
"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";

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

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

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

const closeSuccessModal = () => {
setIsSuccessModalOpen(false);
};

return (
<div className="dashboard-container">
<Sidebar onToggle={setIsSidebarOpen} />
Expand Down Expand Up @@ -173,7 +179,7 @@ const Device: React.FC = () => {
>
<p>
현재 '<strong className="device-name">{currentDeviceName}</strong>
' 는 ON 상태입니다. 제어 명령을 전송하시겠습니까?
' 는 {command ? "ON" : "OFF"} 상태입니다. 제어 명령을 전송하시겠습니까?
</p>
<div className="command-toggle">
<label>
Expand All @@ -196,14 +202,29 @@ const Device: React.FC = () => {
</label>
</div>
<button className="control-button" onClick={sendCommand}>
제어 명령 전송
전송
</button>
<button className="modal-close" onClick={closeCommandModal}>
X
</button>
</div>
</div>
)}

{isSuccessModalOpen && (
<div className="modal-overlay" onClick={closeSuccessModal}>
<div
className="modal-content command-modal"
onClick={(e) => e.stopPropagation()}
>
<p>명령이 전송되었습니다.</p>
<br></br><br></br>
<button className="control-button" onClick={closeSuccessModal}>
확인
</button>
</div>
</div>
)}
</div>
</div>
);
Expand Down
56 changes: 56 additions & 0 deletions client/src/pages/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useEffect, useState } from "react";
import { Client } from "@stomp/stompjs";
import SockJS from "sockjs-client";

const WebSocketModal = () => {
const [isCommandModalOpen, setIsCommandModalOpen] = useState(false);
const [currentDeviceName, setCurrentDeviceName] = useState("");
const [command, setCommand] = useState<boolean | null>(null);

useEffect(() => {
const stompClient = new Client({
webSocketFactory: () => new SockJS("http://localhost:8080/ws"),
onConnect: () => {
stompClient.subscribe("/topic/commands", (message) => {
const data = JSON.parse(message.body);
setCurrentDeviceName(data.deviceName);
setCommand(data.command);
setIsCommandModalOpen(true); // 알림 표시
});
},
});

stompClient.activate();

return () => {
stompClient.deactivate();
};
}, []);

const closeCommandModal = () => {
setIsCommandModalOpen(false);
};

return (
<>
{isCommandModalOpen && (
<div className="modal-overlay" onClick={closeCommandModal}>
<div
className="modal-content command-modal"
onClick={(e) => e.stopPropagation()}
>
<p>
현재 '<strong className="device-name">{currentDeviceName}</strong>'는{" "}
{command ? "ON" : "OFF"} 상태입니다. 제어 명령을 전송하시겠습니까?
</p>
<button className="control-button" onClick={closeCommandModal}>
닫기
</button>
</div>
</div>
)}
</>
);
};

export default WebSocketModal;
9 changes: 6 additions & 3 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ dependencies {
implementation("software.amazon.awssdk:s3:2.21.0")
//actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Spring Security
//Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
//JWT Token
Expand All @@ -48,10 +48,13 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
//Jsoup
implementation 'org.jsoup:jsoup:1.18.1'
// JSON 라이브러리
//JSON
implementation 'org.json:json:20231013'
// Apache HttpClient 라이브러리
//Apache HttpClient
implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.1'
//Web Socket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-web'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import org.cecd.server.dto.CommandRequest;
import org.cecd.server.service.CommandService;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.web.bind.annotation.*;

@RestController
Expand All @@ -23,4 +25,11 @@ public ResponseEntity<String> sendControlCommand(@RequestBody CommandRequest com
public ResponseEntity<CommandRequest> echoControlCommand(@RequestBody CommandRequest commandRequest) {
return ResponseEntity.ok(commandRequest);
}

@MessageMapping("/socket/control") // 클라이언트가 이 경로로 메시지를 송신
@SendTo("/topic/commands") // 이 경로를 구독 중인 모든 클라이언트에게 메시지 전달
public CommandRequest sendCommand (CommandRequest commandRequest) {
System.out.println("Received command: " + commandRequest); // 로그 추가
return commandRequest; // command는 클라이언트로 전달할 메시지
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws
.authorizeHttpRequests(authorize -> authorize // 권한 설정
.requestMatchers("/jwt-login/info").authenticated()
.requestMatchers("/jwt-login/admin/**").hasAuthority(MemberRole.ADMIN.name())
.anyRequest().permitAll()
.requestMatchers("/ws/**").permitAll() // WebSocket 엔드포인트 허용
.anyRequest().permitAll() // 추후 수정 예정
)
.exceptionHandling(exceptionHandling ->
exceptionHandling
Expand Down
27 changes: 27 additions & 0 deletions server/src/main/java/org/cecd/server/external/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.cecd.server.external;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("http://localhost:3000","https://www.dgu1921.p-e.kr")
.withSockJS();
}
}