From 629c3461d1bd6551dc7f68ff06c78bf833119614 Mon Sep 17 00:00:00 2001 From: seyun31 <2ne1jenna@naver.com> Date: Tue, 9 Sep 2025 01:15:26 +0900 Subject: [PATCH 1/7] =?UTF-8?q?refactor:=20Header=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B3=A0=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Header.tsx | 10 +++++++++- src/page/login.tsx | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index de2be3c..2cbb02b 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,11 +1,19 @@ import { IoLogOutOutline } from "react-icons/io5"; import AegisLogo from '../assets/logos/aegis.svg'; +import { useNavigate } from "react-router-dom"; const Header: React.FC = () => { + const navigate = useNavigate(); + + const onClick = () => { + navigate("/"); + } + return( <>
-
+
Aegis Logo

Aegis

diff --git a/src/page/login.tsx b/src/page/login.tsx index 6c96b34..1f2d2fd 100644 --- a/src/page/login.tsx +++ b/src/page/login.tsx @@ -7,11 +7,11 @@ const Login: React.FC = () => {

Welcome Aegis Admin

Aegis 관리자만 접근 가능합니다.

- - + placeholder="Enter Password"> */} +
) From dec859a9d463be090a79b652406f0e68135cbe6d Mon Sep 17 00:00:00 2001 From: seyun31 <2ne1jenna@naver.com> Date: Tue, 9 Sep 2025 01:53:03 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/members.ts | 34 ++++++++++++++++++++++++++++++++++ src/page/home.tsx | 38 ++++++++++++++++++++++++++++++++++++++ src/page/login.tsx | 7 ++++++- 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/api/members.ts diff --git a/src/api/members.ts b/src/api/members.ts new file mode 100644 index 0000000..fb0af4d --- /dev/null +++ b/src/api/members.ts @@ -0,0 +1,34 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +export async function Members(): Promise { + try { + console.log("API 요청 시작:", `${API_BASE_URL}/members`); + const res = await fetch(`${API_BASE_URL}/members`, { + method: 'GET', + credentials: 'include', + headers: { + accept: 'application/json', + }, + }); + + // console.log("응답 상태:", res.status, res.statusText); + + if (!res.ok) { + console.log("응답이 실패했습니다:", res.status); + return false; + } + + const data = await res.json(); + + if (data.role === "ADMIN") { + alert(`안녕하세요 ${data.name} 관리자님!`); + return true; + } else { + alert("로그인 실패! 관리자만 접근 가능합니다."); + return false; + } + } catch (err) { + console.error("인증 확인 실패:", err); + return false; + } +} \ No newline at end of file diff --git a/src/page/home.tsx b/src/page/home.tsx index b32a5d6..12f7cf3 100644 --- a/src/page/home.tsx +++ b/src/page/home.tsx @@ -1,6 +1,44 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import Header from "../components/Header"; +import { Members } from '../api/members'; const Home: React.FC = () => { + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(true); + const [isAuthenticated, setIsAuthenticated] = useState(false); + + useEffect(() => { + const checkAuth = async () => { + try { + const authenticated = await Members(); + if (authenticated) { + setIsAuthenticated(true); + } else { + navigate('/login'); + } + } catch (error) { + console.error('인증 확인 실패:', error); + } finally { + setIsLoading(false); + } + }; + + checkAuth(); + }, [navigate]); + + if (isLoading) { + return ( +
+
인증 확인 중...
+
+ ); + } + + if (!isAuthenticated) { + return null; // 리디렉션 중이므로 아무것도 렌더링하지 않음 + } + return( <>
diff --git a/src/page/login.tsx b/src/page/login.tsx index 1f2d2fd..4153ce1 100644 --- a/src/page/login.tsx +++ b/src/page/login.tsx @@ -1,6 +1,10 @@ import AegisLogo from '../assets/logos/aegis.svg'; +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; const Login: React.FC = () => { + const handleLogin = () => { + window.location.href = `${API_BASE_URL}/oauth2/authorization/google`; + }; return(
Aegis Logo @@ -11,7 +15,8 @@ const Login: React.FC = () => { placeholder="Enter ID"> */} - +
) From cc2bb676a6c2032884a1ebc8b167961aaa52f414 Mon Sep 17 00:00:00 2001 From: seyun31 <2ne1jenna@naver.com> Date: Tue, 9 Sep 2025 02:06:54 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=EB=8B=A4=EB=A5=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=EB=8F=84=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=EB=A7=8C=20=EC=A0=91=EA=B7=BC=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20hook=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20context=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/members.ts | 7 ++++++- src/context/AuthContext.tsx | 42 +++++++++++++++++++++++++++++++++++++ src/hooks/useAuthGuard.tsx | 19 +++++++++++++++++ src/main.tsx | 5 ++++- src/page/event.tsx | 14 +++++++++++++ src/page/home.tsx | 27 ++---------------------- 6 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 src/context/AuthContext.tsx create mode 100644 src/hooks/useAuthGuard.tsx diff --git a/src/api/members.ts b/src/api/members.ts index fb0af4d..08eaca1 100644 --- a/src/api/members.ts +++ b/src/api/members.ts @@ -21,7 +21,12 @@ export async function Members(): Promise { const data = await res.json(); if (data.role === "ADMIN") { - alert(`안녕하세요 ${data.name} 관리자님!`); + // 처음 로그인 시에만 환영 메시지 표시 + const Welcome = localStorage.getItem('Welcome'); + if (!Welcome) { + alert(`안녕하세요 ${data.name} 관리자님!`); + localStorage.setItem('Welcome', 'true'); + } return true; } else { alert("로그인 실패! 관리자만 접근 가능합니다."); diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 0000000..8fb50f9 --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,42 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { Members } from '../api/members'; + +interface AuthContextType { + isAuthenticated: boolean | null; // null = loading, true = authenticated, false = not authenticated + checkAuth: () => Promise; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(null); + + const checkAuth = async () => { + try { + const authenticated = await Members(); + setIsAuthenticated(authenticated); + } catch (error) { + console.error('인증 확인 실패:', error); + setIsAuthenticated(false); + } + }; + + useEffect(() => { + checkAuth(); + }, []); + + return ( + + {children} + + ); +}; + +// eslint-disable-next-line react-refresh/only-export-components +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/hooks/useAuthGuard.tsx b/src/hooks/useAuthGuard.tsx new file mode 100644 index 0000000..f53ebcc --- /dev/null +++ b/src/hooks/useAuthGuard.tsx @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; + +export const useAuthGuard = () => { + const navigate = useNavigate(); + const { isAuthenticated } = useAuth(); + + useEffect(() => { + if (isAuthenticated === false) { + navigate('/login'); + } + }, [isAuthenticated, navigate]); + + const isLoading = isAuthenticated === null; + const isAuthenticatedBool = isAuthenticated === true; + + return { isLoading, isAuthenticated: isAuthenticatedBool }; +}; \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 2d7d730..663c86d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,9 +2,12 @@ import { createRoot } from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import './index.css' import App from './App.tsx' +import { AuthProvider } from './context/AuthContext' createRoot(document.getElementById('root')!).render( - + + + ) diff --git a/src/page/event.tsx b/src/page/event.tsx index d668e99..2af6b39 100644 --- a/src/page/event.tsx +++ b/src/page/event.tsx @@ -1,8 +1,22 @@ import EventTable from "../components/EventTable"; import Header from "../components/Header"; import SearchBar from "../components/Search"; +import { useAuthGuard } from '../hooks/useAuthGuard'; const Event: React.FC = () => { + const { isLoading, isAuthenticated } = useAuthGuard(); + + if (isLoading) { + return ( +
+
인증 확인 중...
+
+ ); + } + + if (!isAuthenticated) { + return null; // 리디렉션 중이므로 아무것도 렌더링하지 않음 + } return( <>
diff --git a/src/page/home.tsx b/src/page/home.tsx index 12f7cf3..4ed81d9 100644 --- a/src/page/home.tsx +++ b/src/page/home.tsx @@ -1,31 +1,8 @@ -import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import Header from "../components/Header"; -import { Members } from '../api/members'; +import { useAuthGuard } from '../hooks/useAuthGuard'; const Home: React.FC = () => { - const navigate = useNavigate(); - const [isLoading, setIsLoading] = useState(true); - const [isAuthenticated, setIsAuthenticated] = useState(false); - - useEffect(() => { - const checkAuth = async () => { - try { - const authenticated = await Members(); - if (authenticated) { - setIsAuthenticated(true); - } else { - navigate('/login'); - } - } catch (error) { - console.error('인증 확인 실패:', error); - } finally { - setIsLoading(false); - } - }; - - checkAuth(); - }, [navigate]); + const { isLoading, isAuthenticated } = useAuthGuard(); if (isLoading) { return ( From 4fbcb0bad0005f262827024baa76d5193da060ae Mon Sep 17 00:00:00 2001 From: seyun31 <2ne1jenna@naver.com> Date: Tue, 9 Sep 2025 02:32:29 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=EB=AA=A8=EB=93=A0=20=ED=99=9C?= =?UTF-8?q?=EB=8F=99=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/activity/get-activities.ts | 32 ++++++++++++++++++ src/api/activity/get-qrcode.ts | 31 +++++++++++++++++ src/api/activity/post-memebr-activities.ts | 39 ++++++++++++++++++++++ src/api/{ => auth}/members.ts | 3 -- src/context/AuthContext.tsx | 2 +- src/page/event.tsx | 33 ++++++++++++++---- 6 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 src/api/activity/get-activities.ts create mode 100644 src/api/activity/get-qrcode.ts create mode 100644 src/api/activity/post-memebr-activities.ts rename src/api/{ => auth}/members.ts (87%) diff --git a/src/api/activity/get-activities.ts b/src/api/activity/get-activities.ts new file mode 100644 index 0000000..7a3a010 --- /dev/null +++ b/src/api/activity/get-activities.ts @@ -0,0 +1,32 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +export interface Activity { + activityId: number; + name: string; + pointAmount: number; +} + +export async function GetActivities(): Promise { + try { + const res = await fetch(`${API_BASE_URL}/admin/activities`, { + method: 'GET', + credentials: 'include', + headers: { + accept: 'application/json', + }, + }); + + if (!res.ok) { + console.log("응답이 실패했습니다:", res.status); + return []; + } + + const data: Activity[] = await res.json(); + console.log("받아온 활동 목록:", data); + + return data; + } catch (err) { + console.error("활동 목록 조회 실패:", err); + return []; + } +} \ No newline at end of file diff --git a/src/api/activity/get-qrcode.ts b/src/api/activity/get-qrcode.ts new file mode 100644 index 0000000..a39a811 --- /dev/null +++ b/src/api/activity/get-qrcode.ts @@ -0,0 +1,31 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +export interface MemberInfo { + memberId: number; + name: string; + studentId: string; +} + +export async function GetQRCode(uuid: string): Promise { + try { + const res = await fetch(`${API_BASE_URL}/admin/qrcode/${uuid}`, { + method: 'GET', + credentials: 'include', + headers: { + accept: 'application/json', + }, + }); + + if (!res.ok) { + console.log("응답이 실패했습니다:", res.status); + return null; + } + + const data: MemberInfo = await res.json(); + console.log("받아온 멤버 정보:", data); + return data; + } catch (err) { + console.error("QR 코드 멤버 정보 조회 실패:", err); + return null; + } +} \ No newline at end of file diff --git a/src/api/activity/post-memebr-activities.ts b/src/api/activity/post-memebr-activities.ts new file mode 100644 index 0000000..cb60f8a --- /dev/null +++ b/src/api/activity/post-memebr-activities.ts @@ -0,0 +1,39 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +export interface MemberActivityRequest { + activityId: number; + memberId: number; +} + +export async function PostMemberActivities(activityId: number, memberId: number): Promise { + try { + const requestBody: MemberActivityRequest = { + activityId, + memberId + }; + + const res = await fetch(`${API_BASE_URL}/admin/acitivity-participation`, { + method: 'POST', + credentials: 'include', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }); + + + if (!res.ok) { + console.log("응답이 실패했습니다:", res.status); + return false; + } + + const data = await res.json(); + console.log("응답 데이터:", data); + + return true; + } catch (err) { + console.error("멤버 활동 참여 등록 실패:", err); + return false; + } +} \ No newline at end of file diff --git a/src/api/members.ts b/src/api/auth/members.ts similarity index 87% rename from src/api/members.ts rename to src/api/auth/members.ts index 08eaca1..34e6c65 100644 --- a/src/api/members.ts +++ b/src/api/auth/members.ts @@ -2,7 +2,6 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; export async function Members(): Promise { try { - console.log("API 요청 시작:", `${API_BASE_URL}/members`); const res = await fetch(`${API_BASE_URL}/members`, { method: 'GET', credentials: 'include', @@ -10,8 +9,6 @@ export async function Members(): Promise { accept: 'application/json', }, }); - - // console.log("응답 상태:", res.status, res.statusText); if (!res.ok) { console.log("응답이 실패했습니다:", res.status); diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 8fb50f9..01ff749 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; -import { Members } from '../api/members'; +import { Members } from '../api/auth/members'; interface AuthContextType { isAuthenticated: boolean | null; // null = loading, true = authenticated, false = not authenticated diff --git a/src/page/event.tsx b/src/page/event.tsx index 2af6b39..da3448e 100644 --- a/src/page/event.tsx +++ b/src/page/event.tsx @@ -1,10 +1,29 @@ +import { useEffect, useState } from 'react'; import EventTable from "../components/EventTable"; import Header from "../components/Header"; import SearchBar from "../components/Search"; import { useAuthGuard } from '../hooks/useAuthGuard'; +import { GetActivities } from '../api/activity/get-activities'; +import type { Activity } from '../api/activity/get-activities'; const Event: React.FC = () => { const { isLoading, isAuthenticated } = useAuthGuard(); + const [activities, setActivities] = useState([]); + + useEffect(() => { + const fetchActivities = async () => { + try { + const data = await GetActivities(); + setActivities(data); + } catch (error) { + console.error('활동 목록 조회 중 오류 발생:', error); + } + }; + + if (isAuthenticated) { + fetchActivities(); + } + }, [isAuthenticated]); if (isLoading) { return ( @@ -18,17 +37,19 @@ const Event: React.FC = () => { return null; // 리디렉션 중이므로 아무것도 렌더링하지 않음 } + // 활동 데이터를 EventTable이 기대하는 형식으로 변환 + const tableRows = activities.map(activity => ({ + id: activity.activityId, + name: activity.name, + amount: activity.pointAmount + })); + return( <>
- {/* 임시 데이터 */} - +
From 5a9b0e5253d79ea44cdcff7c8493678dfaa2d5d6 Mon Sep 17 00:00:00 2001 From: seyun31 <2ne1jenna@naver.com> Date: Tue, 9 Sep 2025 04:08:13 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20QR=EC=BD=94=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20=ED=96=89=EC=82=AC=20=EC=B0=B8=EC=97=AC?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/activity/get-qrcode.ts | 8 +- src/api/auth/members.ts | 7 +- src/components/EventTable.tsx | 29 ++++--- src/components/QRScanner.tsx | 144 ++++++++++++++++++++++++++------- src/index.css | 25 ++++++ src/page/event.tsx | 5 +- 6 files changed, 164 insertions(+), 54 deletions(-) diff --git a/src/api/activity/get-qrcode.ts b/src/api/activity/get-qrcode.ts index a39a811..de3bbaf 100644 --- a/src/api/activity/get-qrcode.ts +++ b/src/api/activity/get-qrcode.ts @@ -6,7 +6,7 @@ export interface MemberInfo { studentId: string; } -export async function GetQRCode(uuid: string): Promise { +export async function GetQRCode(uuid: string): Promise<{ success: boolean; data: MemberInfo | null; error?: string }> { try { const res = await fetch(`${API_BASE_URL}/admin/qrcode/${uuid}`, { method: 'GET', @@ -18,14 +18,14 @@ export async function GetQRCode(uuid: string): Promise { if (!res.ok) { console.log("응답이 실패했습니다:", res.status); - return null; + return { success: false, data: null, error: `QR 코드 인식에 실패했습니다. (오류 코드: ${res.status})` }; } const data: MemberInfo = await res.json(); console.log("받아온 멤버 정보:", data); - return data; + return { success: true, data }; } catch (err) { console.error("QR 코드 멤버 정보 조회 실패:", err); - return null; + return { success: false, data: null, error: "네트워크 오류가 발생했습니다." }; } } \ No newline at end of file diff --git a/src/api/auth/members.ts b/src/api/auth/members.ts index 34e6c65..ac40c3d 100644 --- a/src/api/auth/members.ts +++ b/src/api/auth/members.ts @@ -18,12 +18,7 @@ export async function Members(): Promise { const data = await res.json(); if (data.role === "ADMIN") { - // 처음 로그인 시에만 환영 메시지 표시 - const Welcome = localStorage.getItem('Welcome'); - if (!Welcome) { - alert(`안녕하세요 ${data.name} 관리자님!`); - localStorage.setItem('Welcome', 'true'); - } + alert(`안녕하세요 ${data.name} 관리자님!`); return true; } else { alert("로그인 실패! 관리자만 접근 가능합니다."); diff --git a/src/components/EventTable.tsx b/src/components/EventTable.tsx index f64972a..e262801 100644 --- a/src/components/EventTable.tsx +++ b/src/components/EventTable.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import QRScannerComponent from './QRScanner'; interface EventRow { @@ -9,30 +9,30 @@ interface EventRow { interface EventTableProps { rows: EventRow[]; - // onEdit: (id: number) => void; - // onDelete: (id: number) => void; - // onGoQR: (id: number) => void; } const EventTable: React.FC = ({ rows }) => { const [showQRScanner, setShowQRScanner] = useState(false); - const [selectedEventId, setSelectedEventId] = useState(null); - // QR코드 카메라 - const handleQRScan = (result: string) => { - console.log('QR 코드 스캔 결과:', result); - alert(`이벤트 ID ${selectedEventId}의 QR 코드 스캔 완료: ${result}`); - setShowQRScanner(false); - setSelectedEventId(null); - }; const handleQRClose = () => { + // URL 파라미터에서 id 제거 + const url = new URL(window.location.href); + url.searchParams.delete('id'); + window.history.pushState({}, '', url); + setShowQRScanner(false); - setSelectedEventId(null); }; const handleGoQR = (eventId: number) => { - setSelectedEventId(eventId); + // 로컬 저장소에 활동 ID 저장 + localStorage.setItem('currentActivityId', eventId.toString()); + + // URL 파라미터에 이벤트 ID 추가 + const url = new URL(window.location.href); + url.searchParams.set('id', eventId.toString()); + window.history.pushState({}, '', url); + setShowQRScanner(true); }; @@ -106,7 +106,6 @@ const EventTable: React.FC = ({ rows }) => { {/* QR Scanner Modal */} {showQRScanner && ( )} diff --git a/src/components/QRScanner.tsx b/src/components/QRScanner.tsx index 6b6daf3..ca4bf4d 100644 --- a/src/components/QRScanner.tsx +++ b/src/components/QRScanner.tsx @@ -1,28 +1,123 @@ -import React, { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import QrScanner from 'qr-scanner'; +import { GetQRCode } from '../api/activity/get-qrcode'; +import { PostMemberActivities } from '../api/activity/post-memebr-activities'; interface QRScannerProps { - onScan: (result: string) => void; onClose: () => void; } -const QRScannerComponent: React.FC = ({ onScan, onClose }) => { +const QRScannerComponent: React.FC = ({ onClose }) => { const videoRef = useRef(null); const [scanner, setScanner] = useState(null); const [error, setError] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + const lastProcessedQR = useRef(''); + const lastScanTime = useRef(0); + const isThrottled = useRef(false); + + const handleBackButton = useCallback(() => { + // 뒤로가기 버튼 클릭 시 카메라를 종료하지 않고 모달만 닫기 + onClose(); + }, [onClose]); + + const handleClose = useCallback(() => { + // X 버튼이나 에러 발생 시 카메라 완전 종료 + if (scanner) { + scanner.stop(); + scanner.destroy(); + } + onClose(); + }, [scanner, onClose]); + + const handleQRResult = useCallback(async (uuid: string) => { + const currentTime = Date.now(); + + // 이미 처리 중이거나 throttling 중이면 무시 + if (isProcessing || isThrottled.current) { + return; + } + + // 같은 QR 코드거나 1초 이내 요청이면 무시 + if (lastProcessedQR.current === uuid || + (currentTime - lastScanTime.current < 1000)) { + return; + } + + // throttling 활성화 + isThrottled.current = true; + lastProcessedQR.current = uuid; + lastScanTime.current = currentTime; + setIsProcessing(true); + setError(''); + + try { + // 로컬 저장소에서 활동 ID 가져오기 + const storedActivityId = localStorage.getItem('currentActivityId'); + if (!storedActivityId) { + setError('활동 ID를 찾을 수 없습니다.'); + setIsProcessing(false); + isThrottled.current = false; + return; + } + + // QR 코드 UUID로 멤버 정보 조회 + const qrResult = await GetQRCode(uuid); + if (!qrResult.success || !qrResult.data) { + alert(qrResult.error || 'QR 코드를 통한 멤버 정보 조회에 실패했습니다.'); + isThrottled.current = false; + if (scanner) { + scanner.stop(); + scanner.destroy(); + } + onClose(); + return; + } + + // 멤버 활동 참여 등록 + const success = await PostMemberActivities(parseInt(storedActivityId), qrResult.data.memberId); + if (success) { + alert(`${qrResult.data.name}님이 참석했습니다.`); + setIsProcessing(false); + // 성공 후 2초 후에 다시 스캔 가능하도록 설정 + setTimeout(() => { + isThrottled.current = false; + lastProcessedQR.current = ''; + lastScanTime.current = 0; + }, 1000); + } else { + setError('활동 참여 등록에 실패했습니다.'); + setIsProcessing(false); + isThrottled.current = false; + } + + } catch (error) { + console.error('QR 코드 처리 중 오류 발생:', error); + alert('QR 코드 처리 중 오류가 발생했습니다.'); + isThrottled.current = false; + if (scanner) { + scanner.stop(); + scanner.destroy(); + } + onClose(); + } + }, []); useEffect(() => { if (!videoRef.current) return; + let isActive = true; const qrScanner = new QrScanner( videoRef.current, (result) => { - onScan(result.data); - qrScanner.stop(); + if (isActive) { + handleQRResult(result.data); + } }, { highlightScanRegion: true, highlightCodeOutline: true, + preferredCamera: 'environment', } ); @@ -30,56 +125,51 @@ const QRScannerComponent: React.FC = ({ onScan, onClose }) => { qrScanner.start().catch((err) => { console.error('QR Scanner Error:', err); - setError('카메라에 접근할 수 없습니다. 카메라 권한을 확인해주세요.'); + if (isActive) { + setError('카메라에 접근할 수 없습니다. 카메라 권한을 확인해주세요.'); + } }); return () => { + isActive = false; qrScanner.stop(); qrScanner.destroy(); + setScanner(null); }; - }, [onScan]); - - const handleClose = () => { - if (scanner) { - scanner.stop(); - scanner.destroy(); - } - onClose(); - }; + }, []); return (
-
+
-

QR 코드 스캔

+

QR 코드 스캔

{error ? ( -
{error}
+
{error}
) : (
{error ? ( -
{error}
+
{error}
) : (
)}
+
@@ -59,7 +186,7 @@ const EventTable: React.FC = ({ rows }) => { {/* 데이터 내용 */}
- {rows.map((r) => ( + {rows.sort((a, b) => a.id - b.id).map((r) => (
= ({ rows }) => { {/* 수정 */}
+ +
+ +
+
+ )} ) diff --git a/src/index.css b/src/index.css index 96cedbf..0a2e9a5 100644 --- a/src/index.css +++ b/src/index.css @@ -34,6 +34,10 @@ font-size: 32px; } + .font-size-24px { + font-size: 24px; + } + .font-size-20px { font-size: 20px; } diff --git a/src/page/event.tsx b/src/page/event.tsx index c476749..f681b15 100644 --- a/src/page/event.tsx +++ b/src/page/event.tsx @@ -45,12 +45,22 @@ const Event: React.FC = () => { amount: activity.pointAmount })); + // 테이블 행 업데이트 함수 + const updateTableRows = (newRows: Array<{id: number, name: string, amount: number}>) => { + const updatedActivities = newRows.map(row => ({ + activityId: row.id, + name: row.name, + pointAmount: row.amount + })); + setActivities(updatedActivities); + }; + return( <>
- +