From 96654fd15a9e46bf0a062da68cdc96d8e6f3e08e Mon Sep 17 00:00:00 2001 From: seyun31 <2ne1jenna@naver.com> Date: Fri, 12 Sep 2025 02:32:16 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20toast=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 25 ++++++++++++++++++++++++- package.json | 3 ++- src/App.tsx | 14 ++++++++++++++ src/components/EventTable.tsx | 26 ++++++++++++++------------ src/page/login.tsx | 2 +- src/utils/alert.ts | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 src/utils/alert.ts diff --git a/package-lock.json b/package-lock.json index 8889e34..0aa3c38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-icons": "^5.5.0", - "react-router-dom": "^7.8.2" + "react-router-dom": "^7.8.2", + "react-toastify": "^11.0.5" }, "devDependencies": { "@eslint/js": "^9.33.0", @@ -2253,6 +2254,15 @@ "node": ">=18" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3684,6 +3694,19 @@ "react-dom": ">=18" } }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/package.json b/package.json index 505b2db..882a942 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-icons": "^5.5.0", - "react-router-dom": "^7.8.2" + "react-router-dom": "^7.8.2", + "react-toastify": "^11.0.5" }, "devDependencies": { "@eslint/js": "^9.33.0", diff --git a/src/App.tsx b/src/App.tsx index 5480fff..8157e02 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { Routes, Route } from 'react-router-dom'; +import { ToastContainer, Bounce } from 'react-toastify'; import Home from './page/home'; import Login from './page/login'; import Event from './page/event'; @@ -13,6 +14,19 @@ const App: React.FC = () => { } /> } /> + > ); }; diff --git a/src/components/EventTable.tsx b/src/components/EventTable.tsx index da811dd..8735607 100644 --- a/src/components/EventTable.tsx +++ b/src/components/EventTable.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import QRScannerComponent from './QRScanner'; +import { showError, showSuccess, showWarning } from '../utils/alert'; import { getActivityById, updateActivity } from '../api/activity/put-activities'; import { deleteActivity } from '../api/activity/delete-acitivites'; import { GetActivities } from '../api/activity/get-activities'; @@ -26,7 +27,7 @@ const EventTable: React.FC = ({ rows, setRows }) => { const handleQRClose = () => { - // URL 파라미터에서 id 제거 + // URL 파라미터에서 ID 제거 const url = new URL(window.location.href); url.searchParams.delete('id'); window.history.pushState({}, '', url); @@ -68,19 +69,20 @@ const EventTable: React.FC = ({ rows, setRows }) => { setShowEditModal(true); } else { console.log('활동 정보를 찾을 수 없습니다.'); - alert('활동 정보를 불러올 수 없습니다.'); + showError('활동 정보를 불러올 수 없습니다.'); } } catch (error) { console.error('수정 모달 열기 오류:', error); - alert('오류가 발생했습니다.'); + showError('오류가 발생했습니다.'); } }; const handleDelete = async (activityId: number) => { - if (confirm('정말로 이 활동을 삭제하시겠습니까?')) { + const confirmed = await showWarning('정말로 이 활동을 삭제하시겠습니까?'); + if (confirmed) { const success = await deleteActivity(activityId); if (success) { - alert('활동이 삭제되었습니다.'); + showSuccess('활동이 삭제되었습니다.'); // 삭제 후 자동으로 정보 업데이트 const updatedActivities = await GetActivities(); const formattedRows = updatedActivities.map(activity => ({ @@ -90,7 +92,7 @@ const EventTable: React.FC = ({ rows, setRows }) => { })); setRows(formattedRows); } else { - alert('삭제에 실패했습니다.'); + showError('삭제에 실패했습니다.'); } } }; @@ -111,13 +113,13 @@ const EventTable: React.FC = ({ rows, setRows }) => { e.preventDefault(); if (!eventName.trim()) { - alert('행사 이름을 입력하세요.'); + showError('행사 이름을 입력하세요.'); return; } const pointValue = parseInt(pointAmount, 10); if (isNaN(pointValue)) { - alert('올바른 포인트 값을 입력하세요.'); + showError('올바른 포인트 값을 입력하세요.'); return; } @@ -125,7 +127,7 @@ const EventTable: React.FC = ({ rows, setRows }) => { // 수정 모드 const success = await updateActivity(editingActivityId, eventName.trim(), pointValue); if (success) { - alert('행사가 수정되었습니다.'); + showSuccess('행사가 수정되었습니다.'); handleCloseModal(); // 수정 후 자동으로 정보 업데이트 const updatedActivities = await GetActivities(); @@ -136,13 +138,13 @@ const EventTable: React.FC = ({ rows, setRows }) => { })); setRows(formattedRows); } else { - alert('행사 수정에 실패했습니다.'); + showError('행사 수정에 실패했습니다.'); } } else { // 생성 모드 const success = await PostMemberActivities(eventName.trim(), pointValue); if (success) { - alert('행사가 생성되었습니다.'); + showSuccess('행사가 생성되었습니다.'); handleCloseModal(); // 생성 후 자동으로 정보 업데이트 const updatedActivities = await GetActivities(); @@ -153,7 +155,7 @@ const EventTable: React.FC = ({ rows, setRows }) => { })); setRows(formattedRows); } else { - alert('행사 생성에 실패했습니다.'); + showError('행사 생성에 실패했습니다.'); } } }; diff --git a/src/page/login.tsx b/src/page/login.tsx index 4153ce1..7b7de5c 100644 --- a/src/page/login.tsx +++ b/src/page/login.tsx @@ -15,7 +15,7 @@ const Login: React.FC = () => { placeholder="Enter ID"> */} - 관리자 인증 diff --git a/src/utils/alert.ts b/src/utils/alert.ts new file mode 100644 index 0000000..8f12c26 --- /dev/null +++ b/src/utils/alert.ts @@ -0,0 +1,32 @@ +import { toast } from "react-toastify"; + +export const showError = (message: string) => { + toast.error(message, { + position: "top-center", + autoClose: 2500, + icon: false, + }); +}; + +export const showSuccess = (message: string) => { + toast.success(message, { + position: "top-center", + autoClose: 2500, + icon: false, + }); +}; + +export const showWarning = (message: string): Promise => { + return new Promise((resolve) => { + const result = window.confirm(message); + resolve(result); + }); +}; + +export const showConfirm = (message: string): Promise => { + return new Promise((resolve) => { + const result = window.confirm(message); + resolve(result); + }); +}; + From 08a5b0260487be594f47bba5e4f7db874720b5d0 Mon Sep 17 00:00:00 2001 From: seyun31 <2ne1jenna@naver.com> Date: Fri, 12 Sep 2025 02:50:58 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20QR=20=EC=8A=A4=EC=BA=94?= =?UTF-8?q?=EC=97=90=EC=84=9C=20alert=20=EB=B6=80=EB=B6=84=20toast=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/activity/post-memebr-activities.ts | 10 +++++---- src/components/QRScanner.tsx | 26 +++++++++------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/api/activity/post-memebr-activities.ts b/src/api/activity/post-memebr-activities.ts index 677329a..b82aab5 100644 --- a/src/api/activity/post-memebr-activities.ts +++ b/src/api/activity/post-memebr-activities.ts @@ -1,4 +1,5 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +import { showError } from '../../utils/alert'; export interface MemberActivityRequest { activityId: number; @@ -26,18 +27,19 @@ export async function PostMemberActivities(activityId: number, memberId: number) if (!res.ok) { switch (res.status) { case 400: - alert("잘못된 요청입니다. 입력 데이터를 확인해주세요."); + showError("잘못된 요청입니다. 관리자에게 문의해주세요."); break; case 404: - alert("해당 활동 또는 회원을 찾을 수 없습니다."); + showError("해당 활동 또는 회원을 찾을 수 없습니다. 관리자에게 문의해주세요."); break; case 409: - alert("이미 참여한 활동입니다."); + showError("이미 참여한 활동입니다."); break; default: - alert("요청 처리 중 오류가 발생했습니다."); + showError("요청 처리 중 오류가 발생했습니다. 관리자에게 문의해주세요."); break; } + return false; } return true; diff --git a/src/components/QRScanner.tsx b/src/components/QRScanner.tsx index 7af9ae6..a1c7cb5 100644 --- a/src/components/QRScanner.tsx +++ b/src/components/QRScanner.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import QrScanner from 'qr-scanner'; +import { showSuccess, showError } from '../utils/alert'; import { GetQRCode } from '../api/activity/get-qrcode'; import { PostMemberActivities } from '../api/activity/post-memebr-activities'; @@ -10,7 +11,6 @@ interface QRScannerProps { 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); @@ -40,13 +40,13 @@ const QRScannerComponent: React.FC = ({ onClose }) => { lastProcessedQR.current = uuid; lastScanTime.current = currentTime; setIsProcessing(true); - setError(''); + // setError(''); try { // 로컬 저장소에서 활동 ID 가져오기 const storedActivityId = localStorage.getItem('currentActivityId'); if (!storedActivityId) { - setError('활동 ID를 찾을 수 없습니다.'); + showError('활동 ID를 찾을 수 없습니다.'); setIsProcessing(false); isThrottled.current = false; return; @@ -55,20 +55,16 @@ const QRScannerComponent: React.FC = ({ onClose }) => { // QR 코드 UUID로 멤버 정보 조회 const qrResult = await GetQRCode(uuid); if (!qrResult.success || !qrResult.data) { - alert(qrResult.error || 'QR 코드를 통한 멤버 정보 조회에 실패했습니다.'); + showError('QR 코드를 새로고침 후 다시 시도하세요.'); + setIsProcessing(false); 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}님이 참석했습니다.`); + showSuccess(`${qrResult.data.name}님이 참석했습니다.`); // 성공 후 다시 스캔 가능하도록 설정 setTimeout(() => { setIsProcessing(false); @@ -83,7 +79,7 @@ const QRScannerComponent: React.FC = ({ onClose }) => { } catch (error) { console.error('QR 코드 처리 중 오류 발생:', error); - alert('QR 코드 처리 중 오류가 발생했습니다. 다시 시도해주세요.'); + showError('QR 코드 처리 중 오류가 발생했습니다. 다시 시도해주세요.'); setIsProcessing(false); isThrottled.current = false; } @@ -112,7 +108,7 @@ const QRScannerComponent: React.FC = ({ onClose }) => { qrScanner.start().catch((err) => { console.error('QR Scanner Error:', err); if (isActive) { - setError('카메라에 접근할 수 없습니다. 카메라 권한을 확인해주세요.'); + showError('카메라에 접근할 수 없습니다. 카메라 권한을 확인해주세요.'); } }); @@ -137,9 +133,7 @@ const QRScannerComponent: React.FC = ({ onClose }) => { - {error ? ( - {error} - ) : ( + { = ({ onClose }) => { {isProcessing ? '처리 중...' : 'QR 코드를 카메라에 비춰주세요! 📷✨'} - )} + } Date: Fri, 12 Sep 2025 03:31:05 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EC=B9=B4=EB=A9=94=EB=9D=BC=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth/logout.ts | 4 +- src/api/auth/members.ts | 20 ++-- src/components/Header.tsx | 8 +- src/components/QRScanner.tsx | 220 +++++++++++++++++++++++++---------- src/utils/alert.ts | 32 ----- src/utils/alert.tsx | 72 ++++++++++++ 6 files changed, 250 insertions(+), 106 deletions(-) delete mode 100644 src/utils/alert.ts create mode 100644 src/utils/alert.tsx diff --git a/src/api/auth/logout.ts b/src/api/auth/logout.ts index 0fdbca6..909df8b 100644 --- a/src/api/auth/logout.ts +++ b/src/api/auth/logout.ts @@ -35,7 +35,7 @@ export async function logout(): Promise { sessionStorage.clear(); // 환영 메시지 플래그도 함께 삭제됨 localStorage.removeItem('currentActivityId'); // 기존에 사용된 로컬스토리지 항목만 삭제 - console.log("로그아웃 완료 - 모든 쿠키와 세션 정보가 삭제되었습니다."); + // console.log("로그아웃 완료 - 모든 쿠키와 세션 정보가 삭제되었습니다."); return true; } catch (err) { console.error("로그아웃 처리 실패:", err); @@ -47,7 +47,7 @@ export async function logout(): Promise { export function logoutWithSpecificCookie(cookieName: string): boolean { try { deleteCookie(cookieName); - console.log(`쿠키 '${cookieName}'이(가) 삭제되었습니다.`); + // console.log(`쿠키 '${cookieName}'이(가) 삭제되었습니다.`); return true; } catch (err) { console.error("쿠키 삭제 실패:", err); diff --git a/src/api/auth/members.ts b/src/api/auth/members.ts index 846c06b..3093540 100644 --- a/src/api/auth/members.ts +++ b/src/api/auth/members.ts @@ -1,4 +1,5 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +import { showError, showSuccess } from '../../utils/alert'; export async function Members(): Promise { try { @@ -18,18 +19,23 @@ export async function Members(): Promise { const data = await res.json(); if (data.role === "ADMIN") { - // 세션 스토리지에서 환영 메시지 표시 여부 확인 - const hasShownWelcome = sessionStorage.getItem('hasShownWelcome'); + // 로그인 페이지에서는 환영 메시지를 표시하지 않음 + const isLoginPage = window.location.pathname === '/login'; - if (!hasShownWelcome) { - alert(`안녕하세요 ${data.name} 관리자님!`); - // 환영 메시지를 표시했다는 것을 세션 스토리지에 저장 - sessionStorage.setItem('hasShownWelcome', 'true'); + if (!isLoginPage) { + // 세션 스토리지에서 환영 메시지 표시 여부 확인 + const hasShownWelcome = sessionStorage.getItem('hasShownWelcome'); + + if (!hasShownWelcome) { + showSuccess(`안녕하세요 ${data.name} 관리자님!`); + // 환영 메시지를 표시했다는 것을 세션 스토리지에 저장 + sessionStorage.setItem('hasShownWelcome', 'true'); + } } return true; } else { - alert("로그인 실패! 관리자만 접근 가능합니다."); + showError("로그인 실패! 관리자만 접근 가능합니다."); return false; } } catch (err) { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index cd250da..89438c0 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -2,6 +2,7 @@ import { IoLogOutOutline } from "react-icons/io5"; import AegisLogo from '../assets/logos/aegis.svg'; import { useNavigate } from "react-router-dom"; import { logout } from '../api/auth/logout'; +import { showSuccess, showError, showCustomConfirm } from "../utils/alert"; const Header: React.FC = () => { const navigate = useNavigate(); @@ -11,13 +12,14 @@ const Header: React.FC = () => { } const handleLogout = async () => { - if (confirm('로그아웃 하시겠습니까?')) { + const confirmed = await showCustomConfirm('로그아웃 하시겠습니까?'); + if (confirmed) { const success = await logout(); if (success) { - alert('로그아웃 되었습니다.'); + showSuccess('로그아웃 되었습니다.'); window.location.href = '/login'; } else { - alert('로그아웃 중 오류가 발생했습니다.'); + showError('로그아웃 중 오류가 발생했습니다.'); } } } diff --git a/src/components/QRScanner.tsx b/src/components/QRScanner.tsx index a1c7cb5..7829dfb 100644 --- a/src/components/QRScanner.tsx +++ b/src/components/QRScanner.tsx @@ -21,69 +21,106 @@ const QRScannerComponent: React.FC = ({ onClose }) => { onClose(); }, [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(''); - + + const handleRefreshCamera = useCallback(async () => { try { - // 로컬 저장소에서 활동 ID 가져오기 - const storedActivityId = localStorage.getItem('currentActivityId'); - if (!storedActivityId) { - showError('활동 ID를 찾을 수 없습니다.'); - setIsProcessing(false); - isThrottled.current = false; + // 기존 스캐너 정리 + if (scanner) { + await scanner.stop(); + await scanner.start(); + showSuccess("카메라가 새로고침 되었습니다!"); return; } + + // 잠시 대기 후 카메라 재시작 + setTimeout(() => { + if (!videoRef.current) return; - // QR 코드 UUID로 멤버 정보 조회 - const qrResult = await GetQRCode(uuid); - if (!qrResult.success || !qrResult.data) { - showError('QR 코드를 새로고침 후 다시 시도하세요.'); - setIsProcessing(false); - isThrottled.current = false; - return; - } + const qrScanner = new QrScanner( + videoRef.current, + async (result) => { + const uuid = result.data; + 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); + + try { + // 로컬 저장소에서 활동 ID 가져오기 + const storedActivityId = localStorage.getItem('currentActivityId'); + if (!storedActivityId) { + showError('활동 ID를 찾을 수 없습니다.'); + setIsProcessing(false); + isThrottled.current = false; + return; + } - // 멤버 활동 참여 등록 - const success = await PostMemberActivities(parseInt(storedActivityId), qrResult.data.memberId); - if (success) { - showSuccess(`${qrResult.data.name}님이 참석했습니다.`); - // 성공 후 다시 스캔 가능하도록 설정 - setTimeout(() => { - setIsProcessing(false); - isThrottled.current = false; - lastProcessedQR.current = ''; - lastScanTime.current = 0; - }, 1000); - } else { - setIsProcessing(false); - isThrottled.current = false; - } + // QR 코드 UUID로 멤버 정보 조회 + const qrResult = await GetQRCode(uuid); + if (!qrResult.success || !qrResult.data) { + showError('QR 코드를 새로고침 후 다시 시도하세요.'); + setIsProcessing(false); + isThrottled.current = false; + return; + } + + // 멤버 활동 참여 등록 + const success = await PostMemberActivities(parseInt(storedActivityId), qrResult.data.memberId); + if (success) { + showSuccess(`${qrResult.data.name}님이 참석했습니다.`); + // 성공 후 다시 스캔 가능하도록 설정 + setTimeout(() => { + setIsProcessing(false); + isThrottled.current = false; + lastProcessedQR.current = ''; + lastScanTime.current = 0; + }, 1000); + } else { + setIsProcessing(false); + isThrottled.current = false; + } + + } catch (error) { + console.error('QR 코드 처리 중 오류 발생:', error); + showError('QR 코드 처리 중 오류가 발생했습니다. 다시 시도해주세요.'); + setIsProcessing(false); + isThrottled.current = false; + } + }, + { + highlightScanRegion: true, + highlightCodeOutline: true, + preferredCamera: 'environment', + } + ); + setScanner(qrScanner); + + qrScanner.start().catch((err) => { + console.error('QR Scanner Refresh Error:', err); + showError('카메라를 새로고침할 수 없습니다. 카메라 권한을 확인해주세요.'); + }); + }, 200); + } catch (error) { - console.error('QR 코드 처리 중 오류 발생:', error); - showError('QR 코드 처리 중 오류가 발생했습니다. 다시 시도해주세요.'); - setIsProcessing(false); - isThrottled.current = false; + console.error('Camera refresh error:', error); + showError('카메라 새로고침 중 오류가 발생했습니다.'); } - }, [scanner, onClose]); + }, [scanner]); useEffect(() => { if (!videoRef.current) return; @@ -91,9 +128,69 @@ const QRScannerComponent: React.FC = ({ onClose }) => { let isActive = true; const qrScanner = new QrScanner( videoRef.current, - (result) => { - if (isActive) { - handleQRResult(result.data); + async (result) => { + if (!isActive) return; + + const uuid = result.data; + 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); + + try { + // 로컬 저장소에서 활동 ID 가져오기 + const storedActivityId = localStorage.getItem('currentActivityId'); + if (!storedActivityId) { + showError('활동 ID를 찾을 수 없습니다.'); + setIsProcessing(false); + isThrottled.current = false; + return; + } + + // QR 코드 UUID로 멤버 정보 조회 + const qrResult = await GetQRCode(uuid); + if (!qrResult.success || !qrResult.data) { + showError('QR 코드를 새로고침 후 다시 시도하세요.'); + setIsProcessing(false); + isThrottled.current = false; + return; + } + + // 멤버 활동 참여 등록 + const success = await PostMemberActivities(parseInt(storedActivityId), qrResult.data.memberId); + if (success) { + showSuccess(`${qrResult.data.name}님이 참석했습니다.`); + // 성공 후 다시 스캔 가능하도록 설정 + setTimeout(() => { + setIsProcessing(false); + isThrottled.current = false; + lastProcessedQR.current = ''; + lastScanTime.current = 0; + }, 1000); + } else { + setIsProcessing(false); + isThrottled.current = false; + } + + } catch (error) { + console.error('QR 코드 처리 중 오류 발생:', error); + showError('QR 코드 처리 중 오류가 발생했습니다. 다시 시도해주세요.'); + setIsProcessing(false); + isThrottled.current = false; } }, { @@ -116,7 +213,6 @@ const QRScannerComponent: React.FC = ({ onClose }) => { isActive = false; qrScanner.stop(); qrScanner.destroy(); - setScanner(null); }; }, []); @@ -146,10 +242,10 @@ const QRScannerComponent: React.FC = ({ onClose }) => { } - 뒤로가기 + 🔄 카메라 새로고침 diff --git a/src/utils/alert.ts b/src/utils/alert.ts deleted file mode 100644 index 8f12c26..0000000 --- a/src/utils/alert.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { toast } from "react-toastify"; - -export const showError = (message: string) => { - toast.error(message, { - position: "top-center", - autoClose: 2500, - icon: false, - }); -}; - -export const showSuccess = (message: string) => { - toast.success(message, { - position: "top-center", - autoClose: 2500, - icon: false, - }); -}; - -export const showWarning = (message: string): Promise => { - return new Promise((resolve) => { - const result = window.confirm(message); - resolve(result); - }); -}; - -export const showConfirm = (message: string): Promise => { - return new Promise((resolve) => { - const result = window.confirm(message); - resolve(result); - }); -}; - diff --git a/src/utils/alert.tsx b/src/utils/alert.tsx new file mode 100644 index 0000000..78eec42 --- /dev/null +++ b/src/utils/alert.tsx @@ -0,0 +1,72 @@ +import { toast } from "react-toastify"; + +export const showError = (message: string) => { + toast.error(message, { + position: "top-center", + autoClose: 2500, + icon: false, + }); +}; + +export const showSuccess = (message: string) => { + toast.success(message, { + position: "top-center", + autoClose: 2500, + icon: false, + }); +}; + +export const showWarning = (message: string): Promise => { + return new Promise((resolve) => { + const result = window.confirm(message); + resolve(result); + }); +}; + +export const showConfirm = (message: string): Promise => { + return new Promise((resolve) => { + const result = window.confirm(message); + resolve(result); + }); +}; + +export const showCustomConfirm = (message: string): Promise => { + return new Promise((resolve) => { + toast( + ({ closeToast }) => ( + + {message} + + { + closeToast?.(); + resolve(true); + }} + > + 확인 + + { + closeToast?.(); + resolve(false); + }} + > + 취소 + + + + ), + { + position: "top-center", + autoClose: false, + closeOnClick: false, + closeButton: false, + draggable: false, + className: "flex justify-center items-center text-center", + } + ); + }); +}; +