diff --git a/src/api/activity/delete-acitivites.ts b/src/api/activity/delete-acitivites.ts new file mode 100644 index 0000000..f0b45da --- /dev/null +++ b/src/api/activity/delete-acitivites.ts @@ -0,0 +1,23 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +export async function deleteActivity(activityId: number): Promise { + try { + const res = await fetch(`${API_BASE_URL}/admin/activities/${activityId}`, { + method: 'DELETE', + credentials: 'include', + headers: { + accept: '*/*', + }, + }); + + if (!res.ok) { + console.log("활동 삭제 실패:", res.status); + return false; + } + + return true; + } catch (err) { + console.error("활동 삭제 실패:", err); + return false; + } +} \ No newline at end of file diff --git a/src/api/activity/get-activities.ts b/src/api/activity/get-activities.ts new file mode 100644 index 0000000..20862e6 --- /dev/null +++ b/src/api/activity/get-activities.ts @@ -0,0 +1,30 @@ +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(); + 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..a173be7 --- /dev/null +++ b/src/api/activity/get-qrcode.ts @@ -0,0 +1,30 @@ +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<{ success: boolean; data: MemberInfo | null; error?: string }> { + 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 { success: false, data: null, error: `QR 코드 인식에 실패했습니다. (오류 코드: ${res.status})` }; + } + + const data: MemberInfo = await res.json(); + return { success: true, data }; + } catch (err) { + console.error("QR 코드 멤버 정보 조회 실패:", err); + return { success: false, data: null, error: "네트워크 오류가 발생했습니다." }; + } +} \ No newline at end of file diff --git a/src/api/activity/post-activities.ts b/src/api/activity/post-activities.ts new file mode 100644 index 0000000..f04f23d --- /dev/null +++ b/src/api/activity/post-activities.ts @@ -0,0 +1,35 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +export interface MemberActivityRequest { + name: string; + pointAmount: number; +} + +export async function PostMemberActivities(name: string, pointAmount: number): Promise { + try { + const requestBody: MemberActivityRequest = { + name, + pointAmount, + }; + + const res = await fetch(`${API_BASE_URL}/admin/activities`, { + 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; + } + return true; + } catch (err) { + console.error("활동 등록 실패:", err); + return false; + } +} \ 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..af5a8d9 --- /dev/null +++ b/src/api/activity/post-memebr-activities.ts @@ -0,0 +1,36 @@ +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/activity-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; + } + + return true; + } catch (err) { + console.error("멤버 활동 참여 등록 실패:", err); + return false; + } +} \ No newline at end of file diff --git a/src/api/activity/put-activities.ts b/src/api/activity/put-activities.ts new file mode 100644 index 0000000..332cca2 --- /dev/null +++ b/src/api/activity/put-activities.ts @@ -0,0 +1,64 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +export interface ActivityInfo { + activityId: number; + name: string; + pointAmount: number; +} + +// 조회 +export async function getActivityById(activityId: number): Promise { + try { + const res = await fetch(`${API_BASE_URL}/admin/activities/${activityId}`, { + method: 'GET', + credentials: 'include', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + console.log("응답이 실패했습니다:", res.status); + return null; + } + + const data: ActivityInfo = await res.json(); + console.log("받아온 활동 정보:", data); + + return data; + } catch (err) { + console.error("활동 정보 조회 실패:", err); + return null; + } +} + +// 수정 +export async function updateActivity(activityId: number, name: string, pointAmount: number): Promise { + try { + const requestBody = { + name, + pointAmount, + }; + + const res = await fetch(`${API_BASE_URL}/admin/activities/${activityId}`, { + method: 'PUT', + credentials: 'include', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }); + + if (!res.ok) { + console.log("활동 수정 실패:", res.status); + return false; + } + + return true; + } catch (err) { + console.error("활동 수정 실패:", err); + return false; + } +} \ No newline at end of file diff --git a/src/api/auth/members.ts b/src/api/auth/members.ts new file mode 100644 index 0000000..ac40c3d --- /dev/null +++ b/src/api/auth/members.ts @@ -0,0 +1,31 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +export async function Members(): Promise { + try { + const res = await fetch(`${API_BASE_URL}/members`, { + method: 'GET', + credentials: 'include', + headers: { + accept: 'application/json', + }, + }); + + 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/components/EventTable.tsx b/src/components/EventTable.tsx index f64972a..da811dd 100644 --- a/src/components/EventTable.tsx +++ b/src/components/EventTable.tsx @@ -1,5 +1,9 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import QRScannerComponent from './QRScanner'; +import { getActivityById, updateActivity } from '../api/activity/put-activities'; +import { deleteActivity } from '../api/activity/delete-acitivites'; +import { GetActivities } from '../api/activity/get-activities'; +import { PostMemberActivities } from '../api/activity/post-activities'; interface EventRow { id: number; @@ -9,39 +13,162 @@ interface EventRow { interface EventTableProps { rows: EventRow[]; - // onEdit: (id: number) => void; - // onDelete: (id: number) => void; - // onGoQR: (id: number) => void; + setRows: (rows: EventRow[]) => void; } -const EventTable: React.FC = ({ rows }) => { +const EventTable: React.FC = ({ rows, setRows }) => { const [showQRScanner, setShowQRScanner] = useState(false); - const [selectedEventId, setSelectedEventId] = useState(null); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [eventName, setEventName] = useState(''); + const [pointAmount, setPointAmount] = useState(''); + const [editingActivityId, setEditingActivityId] = 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); }; + const handleEdit = async (activityId: number) => { + + // 현재 테이블에서 해당 활동 찾기 + const currentRow = rows.find(row => row.id === activityId); + if (currentRow) { + setEditingActivityId(activityId); + setEventName(currentRow.name); + setPointAmount(currentRow.amount.toString()); + setShowEditModal(true); + return; + } + + // API 호출 + try { + const activity = await getActivityById(activityId); + if (activity) { + setEditingActivityId(activityId); + setEventName(activity.name); + setPointAmount(activity.pointAmount.toString()); + setShowEditModal(true); + } else { + console.log('활동 정보를 찾을 수 없습니다.'); + alert('활동 정보를 불러올 수 없습니다.'); + } + } catch (error) { + console.error('수정 모달 열기 오류:', error); + alert('오류가 발생했습니다.'); + } + }; + + const handleDelete = async (activityId: number) => { + if (confirm('정말로 이 활동을 삭제하시겠습니까?')) { + const success = await deleteActivity(activityId); + if (success) { + alert('활동이 삭제되었습니다.'); + // 삭제 후 자동으로 정보 업데이트 + const updatedActivities = await GetActivities(); + const formattedRows = updatedActivities.map(activity => ({ + id: activity.activityId, + name: activity.name, + amount: activity.pointAmount + })); + setRows(formattedRows); + } else { + alert('삭제에 실패했습니다.'); + } + } + }; + + const handleCreateEvent = () => { + setShowCreateModal(true); + }; + + const handleCloseModal = () => { + setShowCreateModal(false); + setShowEditModal(false); + setEventName(''); + setPointAmount(''); + setEditingActivityId(null); + }; + + const handleSubmitEvent = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!eventName.trim()) { + alert('행사 이름을 입력하세요.'); + return; + } + + const pointValue = parseInt(pointAmount, 10); + if (isNaN(pointValue)) { + alert('올바른 포인트 값을 입력하세요.'); + return; + } + + if (editingActivityId) { + // 수정 모드 + const success = await updateActivity(editingActivityId, eventName.trim(), pointValue); + if (success) { + alert('행사가 수정되었습니다.'); + handleCloseModal(); + // 수정 후 자동으로 정보 업데이트 + const updatedActivities = await GetActivities(); + const formattedRows = updatedActivities.map(activity => ({ + id: activity.activityId, + name: activity.name, + amount: activity.pointAmount + })); + setRows(formattedRows); + } else { + alert('행사 수정에 실패했습니다.'); + } + } else { + // 생성 모드 + const success = await PostMemberActivities(eventName.trim(), pointValue); + if (success) { + alert('행사가 생성되었습니다.'); + handleCloseModal(); + // 생성 후 자동으로 정보 업데이트 + const updatedActivities = await GetActivities(); + const formattedRows = updatedActivities.map(activity => ({ + id: activity.activityId, + name: activity.name, + amount: activity.pointAmount + })); + setRows(formattedRows); + } else { + alert('행사 생성에 실패했습니다.'); + } + } + }; + return( <> {/* 버튼 section */}
- +
@@ -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/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/components/QRScanner.tsx b/src/components/QRScanner.tsx index 6b6daf3..b654cc4 100644 --- a/src/components/QRScanner.tsx +++ b/src/components/QRScanner.tsx @@ -1,28 +1,110 @@ -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 handleClose = useCallback(() => { + // 뒤로가기 버튼 클릭 시 카메라를 종료하지 않고 모달만 닫기 + 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(''); + + 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}님이 참석했습니다.`); + // 성공 후 다시 스캔 가능하도록 설정 + setTimeout(() => { + setIsProcessing(false); + isThrottled.current = false; + lastProcessedQR.current = ''; + lastScanTime.current = 0; + }, 1000); + } else { + alert('활동 참여 등록에 실패했습니다. 다시 시도해주세요.'); + setIsProcessing(false); + isThrottled.current = false; + } + + } catch (error) { + console.error('QR 코드 처리 중 오류 발생:', error); + alert('QR 코드 처리 중 오류가 발생했습니다. 다시 시도해주세요.'); + setIsProcessing(false); + isThrottled.current = false; + } + }, [scanner, 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 +112,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}
) : (