Skip to content

Commit

Permalink
feat: lab screen for cv demo (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
junglesub authored Dec 2, 2024
1 parent 4ea7c89 commit dbc0cff
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 5 deletions.
4 changes: 2 additions & 2 deletions src/main/front/src/components/MainDisplay.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Box, CssBaseline } from "@mui/material";
import MainDrawer from "../components/MainDrawer";

function MainDisplay({ children }) {
function MainDisplay({ children, noCount = false }) {
return (
<Box
sx={{
Expand All @@ -14,7 +14,7 @@ function MainDisplay({ children }) {
}}
>
<CssBaseline />
<MainDrawer />
<MainDrawer noCount={noCount} />
<Box
component="main"
sx={{ flexGrow: 1, px: 1.5, pb: 6, maxWidth: 700, mx: "auto" }}
Expand Down
4 changes: 2 additions & 2 deletions src/main/front/src/components/MainDrawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ const Drawer = styled(MuiDrawer, {
],
}));

export default function MainDrawer() {
export default function MainDrawer({ noCount = false }) {
const navigate = useNavigate();
// eslint-disable-next-line unused-imports/no-unused-vars
const [open, setOpen] = React.useState(false);
const location = useLocation();
const [feedNumber] = useFeedCount();
const [feedNumber] = noCount ? [0] : useFeedCount();

const MENUS = [
{
Expand Down
86 changes: 86 additions & 0 deletions src/main/front/src/components/lab/menu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { useState } from "react";
import {
Tabs,
Tab,
Typography,
Box,
List,
ListItem,
ListItemText,
} from "@mui/material";

const Menu = () => {
const [selectedTab, setSelectedTab] = useState(0);

const menu = [
{
category: "커피류",
items: [
"에스프레소",
"아메리카노",
"카페 라떼",
"카푸치노",
"카페 모카",
"카라멜 마키아토",
],
},
{
category: "논커피 음료",
items: [
"핫 초콜릿",
"그린티 라떼",
"고구마 라떼",
"아이스티 (레몬, 복숭아 등)",
],
},
{
category: "기타 음료",
items: [
"에이드 (레몬, 자몽, 블루베리 등)",
"스무디 (딸기, 망고, 블루베리 등)",
],
},
];

const handleChange = (event, newValue) => {
setSelectedTab(newValue);
};

return (
<Box sx={{ width: "100%", maxWidth: 600, mx: "auto", mt: 4 }}>
<Tabs
value={selectedTab}
onChange={handleChange}
variant="fullWidth"
indicatorColor="primary"
textColor="primary"
>
{menu.map((section, index) => (
<Tab label={section.category} key={index} />
))}
</Tabs>
<Box sx={{ mt: 2 }}>
{menu.map((section, index) => (
<div role="tabpanel" hidden={selectedTab !== index} key={index}>
{selectedTab === index && (
<Box>
<Typography variant="h6" sx={{ mb: 2 }}>
{section.category}
</Typography>
<List>
{section.items.map((item, idx) => (
<ListItem key={idx} disablePadding>
<ListItemText primary={item} />
</ListItem>
))}
</List>
</Box>
)}
</div>
))}
</Box>
</Box>
);
};

export default Menu;
5 changes: 5 additions & 0 deletions src/main/front/src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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([
{
Expand All @@ -36,6 +37,10 @@ const router = createBrowserRouter([
path: "/kafeed/:messageId",
element: <KafeedDetail />,
},
{
path: "/lab",
element: <LabScreen />,
},
...ADMINMENU.map((menu) => ({
path: `/admin/${menu.id}`,
element: <LoginProtected comp={menu.comp} />,
Expand Down
107 changes: 107 additions & 0 deletions src/main/front/src/pages/LabScreen.jsx
Original file line number Diff line number Diff line change
@@ -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 <LoginPage />;
return (
<MainDisplay>
<Card sx={{ my: 2 }}>
<CardContent>
<Typography variant="h6" component="div" align="center">
에인트 대기 현황
</Typography>
<Typography
variant="h4"
component="div"
align="center"
sx={{ mb: 1 }}
>
<b>{doc.current}명 대기중</b>
</Typography>
<Typography variant="h6" component="div" align="center">
<b>평균 대기시간: {convertSecondsToMinuteSecond(doc.avgTime)}</b>
</Typography>
<Typography variant="body2" component="div" align="center">
<b>최근업데이트: {convertEpochToKoreanTime(doc.lastUpdate)}</b>
</Typography>

<Menu />
</CardContent>
</Card>
</MainDisplay>
);
}

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}`;
}
2 changes: 1 addition & 1 deletion src/main/java/app/handong/feed/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 패턴
}

}
28 changes: 28 additions & 0 deletions src/main/java/app/handong/feed/controller/LabSSEController.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
52 changes: 52 additions & 0 deletions src/main/java/app/handong/feed/service/LabSSEService.java
Original file line number Diff line number Diff line change
@@ -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<SseEmitter> 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 <T> 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);
}
}
}

0 comments on commit dbc0cff

Please sign in to comment.