From dbc0cff022c6d1aa9d9fd95aff758dfa2f0f4152 Mon Sep 17 00:00:00 2001 From: Jungsub Ryoo Date: Tue, 3 Dec 2024 02:11:05 +0900 Subject: [PATCH] feat: lab screen for cv demo (#55) --- src/main/front/src/components/MainDisplay.jsx | 4 +- src/main/front/src/components/MainDrawer.jsx | 4 +- src/main/front/src/components/lab/menu.jsx | 86 ++++++++++++++ src/main/front/src/main.jsx | 5 + src/main/front/src/pages/LabScreen.jsx | 107 ++++++++++++++++++ .../app/handong/feed/config/WebMvcConfig.java | 2 +- .../feed/controller/LabSSEController.java | 28 +++++ .../handong/feed/service/LabSSEService.java | 52 +++++++++ 8 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 src/main/front/src/components/lab/menu.jsx create mode 100644 src/main/front/src/pages/LabScreen.jsx create mode 100644 src/main/java/app/handong/feed/controller/LabSSEController.java create mode 100644 src/main/java/app/handong/feed/service/LabSSEService.java diff --git a/src/main/front/src/components/MainDisplay.jsx b/src/main/front/src/components/MainDisplay.jsx index e330c7b..c1cc11f 100644 --- a/src/main/front/src/components/MainDisplay.jsx +++ b/src/main/front/src/components/MainDisplay.jsx @@ -1,7 +1,7 @@ import { Box, CssBaseline } from "@mui/material"; import MainDrawer from "../components/MainDrawer"; -function MainDisplay({ children }) { +function MainDisplay({ children, noCount = false }) { return ( - + { + const [selectedTab, setSelectedTab] = useState(0); + + const menu = [ + { + category: "커피류", + items: [ + "에스프레소", + "아메리카노", + "카페 라떼", + "카푸치노", + "카페 모카", + "카라멜 마키아토", + ], + }, + { + category: "논커피 음료", + items: [ + "핫 초콜릿", + "그린티 라떼", + "고구마 라떼", + "아이스티 (레몬, 복숭아 등)", + ], + }, + { + category: "기타 음료", + items: [ + "에이드 (레몬, 자몽, 블루베리 등)", + "스무디 (딸기, 망고, 블루베리 등)", + ], + }, + ]; + + const handleChange = (event, newValue) => { + setSelectedTab(newValue); + }; + + return ( + + + {menu.map((section, index) => ( + + ))} + + + {menu.map((section, index) => ( + + ))} + + + ); +}; + +export default Menu; diff --git a/src/main/front/src/main.jsx b/src/main/front/src/main.jsx index ce4eb26..856fbe7 100644 --- a/src/main/front/src/main.jsx +++ b/src/main/front/src/main.jsx @@ -14,6 +14,7 @@ import FavFeed from "./pages/FavFeed"; import PWAInstallModal from "./components/modals/PWAInstallModal"; import { ADMINMENU } from "./pages/admin"; import KafeedDetail from "./pages/KafeedDetail"; +import LabScreen from "./pages/LabScreen"; const router = createBrowserRouter([ { @@ -36,6 +37,10 @@ const router = createBrowserRouter([ path: "/kafeed/:messageId", element: , }, + { + path: "/lab", + element: , + }, ...ADMINMENU.map((menu) => ({ path: `/admin/${menu.id}`, element: , diff --git a/src/main/front/src/pages/LabScreen.jsx b/src/main/front/src/pages/LabScreen.jsx new file mode 100644 index 0000000..451d84c --- /dev/null +++ b/src/main/front/src/pages/LabScreen.jsx @@ -0,0 +1,107 @@ +import { Card, CardContent, Typography } from "@mui/material"; +import MainDisplay from "../components/MainDisplay"; +import { useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { useRecoilValue } from "recoil"; +import { authJwtAtom } from "../recoil/authAtom"; +import { fetchBe } from "../tools/api"; +import LoginPage from "../components/LoginPage"; +import Menu from "../components/lab/menu"; +import { serverRootUrl } from "../constants"; + +function LabScreen() { + const { messageId } = useParams(); + const [doc, setDoc] = useState({ + current: 0, + avgTime: 0, + lastUpdate: 0, + }); + + const jwtValue = useRecoilValue(authJwtAtom); + + useEffect(() => { + if (!jwtValue) return; + }, [jwtValue, messageId]); + + useEffect(() => { + const eventSource = new EventSource(`${serverRootUrl}/lab/sub`); + + eventSource.onopen = async () => { + console.log("sse opened!"); + }; + + eventSource.onmessage = (event) => { + console.log("Message from server:", event.data); + try { + const json = JSON.parse(event.data); + console.log(json); + setDoc({ + ...json, + lastUpdate: Math.floor(Date.now() / 1000), // Current epoch time in seconds + }); + } catch {} + }; + + eventSource.onerror = () => { + console.error("Error occurred. Closing connection."); + eventSource.close(); + }; + + return () => { + eventSource.close(); + }; + }, []); + + if (!jwtValue) return ; + return ( + + + + + 에인트 대기 현황 + + + {doc.current}명 대기중 + + + 평균 대기시간: {convertSecondsToMinuteSecond(doc.avgTime)} + + + 최근업데이트: {convertEpochToKoreanTime(doc.lastUpdate)} + + + + + + + ); +} + +export default LabScreen; + +function convertSecondsToMinuteSecond(seconds) { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}분 ${remainingSeconds}초`; +} +function convertEpochToKoreanTime(epochSeconds) { + const date = new Date(epochSeconds * 1000); + const year = date.getFullYear(); + const month = date.getMonth() + 1; // Months are zero-indexed + const day = date.getDate(); + const hours = date.getHours(); + const minutes = date.getMinutes().toString().padStart(2, "0"); // Ensure leading zero + const seconds = date.getSeconds().toString().padStart(2, "0"); // Ensure leading zero + + const isAM = hours < 12; + const formattedHour = hours % 12 === 0 ? 12 : hours % 12; // Convert to 12-hour format + + return `${year}년 ${month}월 ${day}일 ${ + isAM ? "오전" : "오후" + } ${formattedHour}:${minutes}:${seconds}`; +} diff --git a/src/main/java/app/handong/feed/config/WebMvcConfig.java b/src/main/java/app/handong/feed/config/WebMvcConfig.java index 3f65478..8909942 100644 --- a/src/main/java/app/handong/feed/config/WebMvcConfig.java +++ b/src/main/java/app/handong/feed/config/WebMvcConfig.java @@ -13,7 +13,7 @@ public class WebMvcConfig implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new DefaultInterceptor()) .addPathPatterns("/api/**") //인터셉터가 실행되야 하는 url 패턴 설정 - .excludePathPatterns("/resources/**", "/api/tbuser/login/google"); //인터셉터가 실행되지 않아야 하는 url 패턴 + .excludePathPatterns("/resources/**", "/api/tbuser/login/google", "/api/lab/**"); //인터셉터가 실행되지 않아야 하는 url 패턴 } } \ No newline at end of file diff --git a/src/main/java/app/handong/feed/controller/LabSSEController.java b/src/main/java/app/handong/feed/controller/LabSSEController.java new file mode 100644 index 0000000..52ea36a --- /dev/null +++ b/src/main/java/app/handong/feed/controller/LabSSEController.java @@ -0,0 +1,28 @@ +package app.handong.feed.controller; + +import java.io.IOException; + +import app.handong.feed.service.LabSSEService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@RequestMapping("/api/lab") +@RestController +@RequiredArgsConstructor +public class LabSSEController { + private final LabSSEService notificationService; + + @GetMapping(value = "/sub", produces = "text/event-stream;charset=UTF-8") + public SseEmitter subscribe() { + return notificationService.subscribe(); + } + + @PostMapping("/broadcast") + public void broadcast(@RequestBody String message) { + notificationService.broadcast(message, "Broadcast message"); + } +} diff --git a/src/main/java/app/handong/feed/service/LabSSEService.java b/src/main/java/app/handong/feed/service/LabSSEService.java new file mode 100644 index 0000000..28fadbf --- /dev/null +++ b/src/main/java/app/handong/feed/service/LabSSEService.java @@ -0,0 +1,52 @@ +package app.handong.feed.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; + +@Service +@RequiredArgsConstructor +public class LabSSEService { + + private static final Long DEFAULT_TIMEOUT = 600L * 1000 * 60; + + // Store all active emitters + private final CopyOnWriteArrayList emitters = new CopyOnWriteArrayList<>(); + + public SseEmitter subscribe() { + // Create a new SseEmitter + SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); + + // Add the emitter to the list + emitters.add(emitter); + + // Remove the emitter on completion, timeout, or error + emitter.onCompletion(() -> emitters.remove(emitter)); + emitter.onTimeout(() -> emitters.remove(emitter)); + emitter.onError((ex) -> emitters.remove(emitter)); + + // Send an initial event to confirm the connection + sendToClient(emitter, "Connection established", "SSE connected"); + + return emitter; + } + + public void broadcast(Object data, String comment) { + // Iterate through all active emitters and send the event + emitters.forEach(emitter -> sendToClient(emitter, data, comment)); + } + + private void sendToClient(SseEmitter emitter, T data, String comment) { + try { + emitter.send(SseEmitter.event() + .data(data) + .comment(comment)); + } catch (IOException e) { + // Remove the emitter if sending fails + emitters.remove(emitter); + } + } +}