-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
283 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
src/main/java/app/handong/feed/controller/LabSSEController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |