From 241589214f216b0e9ab558915a15714e44e9696e Mon Sep 17 00:00:00 2001 From: dasosann Date: Wed, 11 Mar 2026 17:12:53 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/adminpage/_components/AdminDropdown.tsx | 71 +++++ app/adminpage/_components/AdminHeader.tsx | 107 ++++++++ app/adminpage/_components/AdminListItem.tsx | 59 +++++ app/adminpage/_components/AdminNotAllowed.tsx | 16 ++ app/adminpage/_components/AdminWarnItem.tsx | 21 ++ .../_components/ManagementComponents.tsx | 142 ++++++++++ app/adminpage/_components/Pagination.tsx | 83 ++++++ .../_components/RequestUserComponent.tsx | 112 ++++++++ .../_components/SearchUserComponent.tsx | 35 +++ app/adminpage/layout.tsx | 29 +++ app/adminpage/myPage/event/discount/page.tsx | 160 ++++++++++++ .../myPage/event/free-match/page.tsx | 155 +++++++++++ app/adminpage/myPage/event/history/page.tsx | 60 +++++ app/adminpage/myPage/event/list/page.tsx | 77 ++++++ app/adminpage/myPage/event/page.tsx | 66 +++++ .../myPage/event/registercomplete/page.tsx | 57 ++++ app/adminpage/myPage/notice/complete/page.tsx | 57 ++++ app/adminpage/myPage/notice/history/page.tsx | 59 +++++ app/adminpage/myPage/notice/list/page.tsx | 76 ++++++ app/adminpage/myPage/notice/page.tsx | 55 ++++ .../myPage/notice/reservation/page.tsx | 202 +++++++++++++++ app/adminpage/myPage/page.tsx | 85 ++++++ app/adminpage/myPage/search/page.tsx | 163 ++++++++++++ app/adminpage/page.tsx | 124 +++++++++ app/adminpage/payrequest/page.tsx | 108 ++++++++ app/adminpage/register/page.tsx | 180 +++++++++++++ .../user/[uuid]/PaymentHistory/page.tsx | 100 +++++++ .../user/[uuid]/SendWarnMessage/page.tsx | 196 ++++++++++++++ app/adminpage/user/[uuid]/page.tsx | 140 ++++++++++ .../user/[uuid]/pointManage/page.tsx | 213 +++++++++++++++ .../user/[uuid]/warnhistory/page.tsx | 53 ++++ app/adminpage/webmail-check/page.tsx | 149 +++++++++++ hooks/useAdminAuth.ts | 98 +++++++ hooks/useAdminManagement.ts | 244 ++++++++++++++++++ public/logo/admin_header_logo.svg | 9 + public/logo/admin_page_logo.svg | 9 + public/logo/coin.svg | 9 + public/logo/empty-heart.svg | 9 + public/logo/event-register-heart.svg | 9 + public/logo/female-icon.svg | 3 + public/logo/full-heart.svg | 9 + public/logo/male-icon.svg | 3 + public/logo/minus-button.svg | 18 ++ public/logo/modal-warn.svg | 3 + public/logo/not-allowed.svg | 3 + public/logo/plus-button.svg | 18 ++ public/logo/refresh-button.svg | 3 + public/logo/search-logo.svg | 3 + public/logo/under-triangle.svg | 3 + utils/dateFormatter.ts | 30 +++ 50 files changed, 3693 insertions(+) create mode 100644 app/adminpage/_components/AdminDropdown.tsx create mode 100644 app/adminpage/_components/AdminHeader.tsx create mode 100644 app/adminpage/_components/AdminListItem.tsx create mode 100644 app/adminpage/_components/AdminNotAllowed.tsx create mode 100644 app/adminpage/_components/AdminWarnItem.tsx create mode 100644 app/adminpage/_components/ManagementComponents.tsx create mode 100644 app/adminpage/_components/Pagination.tsx create mode 100644 app/adminpage/_components/RequestUserComponent.tsx create mode 100644 app/adminpage/_components/SearchUserComponent.tsx create mode 100644 app/adminpage/layout.tsx create mode 100644 app/adminpage/myPage/event/discount/page.tsx create mode 100644 app/adminpage/myPage/event/free-match/page.tsx create mode 100644 app/adminpage/myPage/event/history/page.tsx create mode 100644 app/adminpage/myPage/event/list/page.tsx create mode 100644 app/adminpage/myPage/event/page.tsx create mode 100644 app/adminpage/myPage/event/registercomplete/page.tsx create mode 100644 app/adminpage/myPage/notice/complete/page.tsx create mode 100644 app/adminpage/myPage/notice/history/page.tsx create mode 100644 app/adminpage/myPage/notice/list/page.tsx create mode 100644 app/adminpage/myPage/notice/page.tsx create mode 100644 app/adminpage/myPage/notice/reservation/page.tsx create mode 100644 app/adminpage/myPage/page.tsx create mode 100644 app/adminpage/myPage/search/page.tsx create mode 100644 app/adminpage/page.tsx create mode 100644 app/adminpage/payrequest/page.tsx create mode 100644 app/adminpage/register/page.tsx create mode 100644 app/adminpage/user/[uuid]/PaymentHistory/page.tsx create mode 100644 app/adminpage/user/[uuid]/SendWarnMessage/page.tsx create mode 100644 app/adminpage/user/[uuid]/page.tsx create mode 100644 app/adminpage/user/[uuid]/pointManage/page.tsx create mode 100644 app/adminpage/user/[uuid]/warnhistory/page.tsx create mode 100644 app/adminpage/webmail-check/page.tsx create mode 100644 hooks/useAdminAuth.ts create mode 100644 hooks/useAdminManagement.ts create mode 100644 public/logo/admin_header_logo.svg create mode 100644 public/logo/admin_page_logo.svg create mode 100644 public/logo/coin.svg create mode 100644 public/logo/empty-heart.svg create mode 100644 public/logo/event-register-heart.svg create mode 100644 public/logo/female-icon.svg create mode 100644 public/logo/full-heart.svg create mode 100644 public/logo/male-icon.svg create mode 100644 public/logo/minus-button.svg create mode 100644 public/logo/modal-warn.svg create mode 100644 public/logo/not-allowed.svg create mode 100644 public/logo/plus-button.svg create mode 100644 public/logo/refresh-button.svg create mode 100644 public/logo/search-logo.svg create mode 100644 public/logo/under-triangle.svg create mode 100644 utils/dateFormatter.ts diff --git a/app/adminpage/_components/AdminDropdown.tsx b/app/adminpage/_components/AdminDropdown.tsx new file mode 100644 index 0000000..de40b15 --- /dev/null +++ b/app/adminpage/_components/AdminDropdown.tsx @@ -0,0 +1,71 @@ +"use client"; + +import React, { useState, useRef, useEffect } from "react"; +import { ChevronDown } from "lucide-react"; + +interface AdminDropdownProps { + options: string[]; + selectedValue: string; + onSelect: (value: string) => void; + height?: string; + className?: string; +} + +export const AdminDropdown = ({ + options, + selectedValue, + onSelect, + height, + className +}: AdminDropdownProps) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+
setIsOpen(!isOpen)} + className="w-full h-full bg-[#f4f4f4] rounded-lg border border-[#e5e5e5] px-3 flex items-center justify-between cursor-pointer text-[18px] font-semibold text-black" + > + {selectedValue} + +
+ + {isOpen && ( +
+ {options.map((option) => ( +
{ + onSelect(option); + setIsOpen(false); + }} + className="px-3 py-2 hover:bg-[#f4f4f4] cursor-pointer text-[18px] font-medium text-black" + > + {option} +
+ ))} +
+ )} +
+ ); +}; diff --git a/app/adminpage/_components/AdminHeader.tsx b/app/adminpage/_components/AdminHeader.tsx new file mode 100644 index 0000000..e4aaa1a --- /dev/null +++ b/app/adminpage/_components/AdminHeader.tsx @@ -0,0 +1,107 @@ +"use client"; + +import React from "react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { ChevronDown } from "lucide-react"; + +interface AdminHeaderProps { + adminSelect?: string; + setAdminSelect?: (val: string) => void; + university?: string; + role?: string; + nickname?: string; +} + +export const AdminHeader = ({ + adminSelect, + setAdminSelect, + university, + role, + nickname +}: AdminHeaderProps) => { + const router = useRouter(); + + const goToMainButton = () => { + if (setAdminSelect) setAdminSelect("Main"); + router.push("/adminpage/myPage"); + }; + + const goToTeamButton = () => { + if (setAdminSelect) setAdminSelect("팀관리"); + router.push("/adminpage/myPage?tab=팀관리"); + }; + + const goToMemberButton = () => { + if (setAdminSelect) setAdminSelect("가입자관리"); + router.push("/adminpage/myPage?tab=가입자관리"); + }; + + const getRoleLabel = (role?: string) => { + if (!role) return ""; + return role.includes("ADMIN") ? "관리자" : "오퍼레이터"; + }; + + return ( +
+ 코매칭 로고 router.push("/adminpage")} + /> + + + +
+
+
{university}
+
{getRoleLabel(role)} {nickname}님
+
+ +
+
+ ); +}; + +export const AdminRegisterHeader = () => { + const router = useRouter(); + return ( +
+ 코매칭 로고 router.push("/adminpage")} + /> +
+ ); +}; diff --git a/app/adminpage/_components/AdminListItem.tsx b/app/adminpage/_components/AdminListItem.tsx new file mode 100644 index 0000000..973353d --- /dev/null +++ b/app/adminpage/_components/AdminListItem.tsx @@ -0,0 +1,59 @@ +"use client"; + +import React from "react"; + +interface AdminListItemProps { + title: string; + subTitle?: string; + statusText: string; + date: string; + startTime: string; + endTime: string; + onCancel?: () => void; + cancelButtonText?: string; +} + +export const AdminListItem = ({ + title, + subTitle, + statusText, + date, + startTime, + endTime, + onCancel, + cancelButtonText +}: AdminListItemProps) => { + return ( +
+
+ {statusText} + {title} {subTitle && {subTitle}} +
+
+
+
+ 시작일: + {date} +
+
+ 시작 시각: + {startTime} +
+
+ 종료 시각: + {endTime} +
+
+ + {onCancel && ( + + )} +
+
+ ); +}; diff --git a/app/adminpage/_components/AdminNotAllowed.tsx b/app/adminpage/_components/AdminNotAllowed.tsx new file mode 100644 index 0000000..4f83dfc --- /dev/null +++ b/app/adminpage/_components/AdminNotAllowed.tsx @@ -0,0 +1,16 @@ +"use client"; + +import React from "react"; +import Image from "next/image"; + +export default function AdminNotAllowed() { + return ( +
+ Error +
+ 미승인 오퍼레이터입니다.
+ 관리자의 승인을 대기해 주세요. +
+
+ ); +} diff --git a/app/adminpage/_components/AdminWarnItem.tsx b/app/adminpage/_components/AdminWarnItem.tsx new file mode 100644 index 0000000..bb736da --- /dev/null +++ b/app/adminpage/_components/AdminWarnItem.tsx @@ -0,0 +1,21 @@ +"use client"; + +import React from "react"; + +interface AdminWarnItemProps { + reason: string; + time: string; +} + +export const AdminWarnItem = ({ reason, time }: AdminWarnItemProps) => { + return ( +
+
+ {reason} +
+
+ {time} +
+
+ ); +}; diff --git a/app/adminpage/_components/ManagementComponents.tsx b/app/adminpage/_components/ManagementComponents.tsx new file mode 100644 index 0000000..f29e72d --- /dev/null +++ b/app/adminpage/_components/ManagementComponents.tsx @@ -0,0 +1,142 @@ +"use client"; + +import React from "react"; +import { useRouter } from "next/navigation"; +import { useToggle1000Button } from "@/hooks/useAdminManagement"; + +// --- Types --- +interface ManagementProps { + adminData: { + nickname: string; + role: string; + university: string; + schoolEmail: string; + }; +} + +// --- AdminMyPageMain --- +export const AdminMyPageMain = ({ adminData }: ManagementProps) => { + return ( +
+
+ 내 정보 + 모든 기능을 이용할 수 있습니다 +
+
+ 이름 : {adminData.nickname} +
+
+ 권한 : {adminData.role} +
+
+ 소속 : {adminData.university} +
+
+ 웹메일 : {adminData.schoolEmail} +
+
+
+
+ ); +}; + +// --- MasterManageComponent --- +export const MasterManageComponent = () => { + const router = useRouter(); + const toggle1000 = useToggle1000Button(); + + const handle1000Button = async () => { + try { + const data = await toggle1000.mutateAsync(); + if (data.data === "활성화") { + alert("1000원 버튼 활성화 되었습니다."); + } else if (data.data === "비활성화") { + alert("1000원 버튼 비활성화 되었습니다."); + } + } catch (error) { + console.error("천원 버튼 요청 실패", error); + alert("천원 버튼 요청에 실패했습니다."); + } + }; + + const menuItems = [ + { title: "가입자 결제 요청 관리", sub: "유저 결제 요청 관리", path: "/adminpage/payrequest" }, + { title: "가입자 검색 및 관리", sub: "결제내역 및 포인트 사용내역 열람, 포인트 조정, 블랙리스트 추가", path: "/adminpage/myPage/search" }, + { title: "가입자 성비 분석", sub: "가입자의 성비 분석", path: "/adminpage/myPage/gender" }, + { title: "공지사항 등록", sub: "전체알림 공지", path: "/adminpage/myPage/notice" }, + { title: "블랙리스트 확인 및 해제", sub: "블랙리스트 조회와 해제", path: "/adminpage/myPage/blacklist" }, + { title: "이벤트 등록", sub: "관리자의 이벤트 등록", path: "/adminpage/myPage/event" }, + { title: "문의 및 신고목록", sub: "가입자로부터 온 문의와 신고 열람", path: "/adminpage/myPage/Q&A" }, + ]; + + return ( +
+ {menuItems.map((item, idx) => ( +
router.push(item.path)} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] flex flex-col justify-center text-left p-6 gap-2 cursor-pointer min-w-[317px]" + > +
{item.title}
+
{item.sub}
+
+ ))} +
+
1000원 맞추기 버튼
+
코매칭 마지막 날 진행할 유저포인트 천원 버튼 활성화
+
+
+ ); +}; + +// --- OperatorManageComponent --- +export const OperatorManageComponent = () => { + const router = useRouter(); + + const menuItems = [ + { title: "가입자 결제 요청 관리", sub: "유저 결제 요청 관리", path: "/adminpage/payrequest" }, + { title: "가입자 검색 및 관리", sub: "결제내역 및 포인트 사용내역 열람, 포인트 조정, 블랙리스트 추가", path: "/adminpage/myPage/search" }, + { title: "가입자 성비 분석", sub: "가입자의 성비 분석", path: null }, + { title: "문의 및 신고목록", sub: "가입자로부터 온 문의와 신고 열람", path: null }, + { title: "블랙리스트 확인 및 해제", sub: "블랙리스트 조회와 해제", path: null }, + ]; + + return ( +
+ {menuItems.map((item, idx) => ( +
item.path && router.push(item.path)} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] flex flex-col justify-center text-left p-6 gap-2 cursor-pointer min-w-[317px]" + > +
{item.title}
+
{item.sub}
+
+ ))} +
+ ); +}; + +// --- AdminTeamManage --- +export const AdminTeamManage = () => { + return ( +
+
+
+ 오퍼레이터 승인 요청 +
+ 3 +
+
+ 오퍼레이터 승인 요청 +
+
+ 오퍼레이터 관리 + 오퍼레이터 관리 +
+
+ ); +}; diff --git a/app/adminpage/_components/Pagination.tsx b/app/adminpage/_components/Pagination.tsx new file mode 100644 index 0000000..1d0c9ae --- /dev/null +++ b/app/adminpage/_components/Pagination.tsx @@ -0,0 +1,83 @@ +"use client"; + +import React from "react"; + +interface PaginationProps { + totalPage: number; + currentPage: number; + onPageChange: (page: number) => void; +} + +export const Pagination = ({ totalPage, currentPage, onPageChange }: PaginationProps) => { + const pagePerGroup = 10; + const currentGroup = Math.floor((currentPage - 1) / pagePerGroup); + const startPage = currentGroup * pagePerGroup + 1; + const endPage = Math.min(startPage + pagePerGroup - 1, totalPage); + + const handlePrevGroup = () => { + onPageChange(Math.max(1, startPage - 1)); + }; + + const handleNextGroup = () => { + onPageChange(Math.min(totalPage, endPage + 1)); + }; + + const handleNextPage = () => { + if (currentPage < totalPage) onPageChange(currentPage + 1); + }; + + const handlePrevPage = () => { + if (currentPage > 1) onPageChange(currentPage - 1); + }; + + const pageNumbers = []; + for (let i = startPage; i <= endPage; i++) { + pageNumbers.push(i); + } + + return ( +
+ + + + {pageNumbers.map((page) => ( + + ))} + + + +
+ ); +}; diff --git a/app/adminpage/_components/RequestUserComponent.tsx b/app/adminpage/_components/RequestUserComponent.tsx new file mode 100644 index 0000000..22e6968 --- /dev/null +++ b/app/adminpage/_components/RequestUserComponent.tsx @@ -0,0 +1,112 @@ +"use client"; + +import React from "react"; +import Image from "next/image"; +import { useApproveCharge, useRejectCharge } from "@/hooks/useAdminManagement"; + +interface RequestUserProps { + contact: string; + orderId: string; + point: number; + username: string; + price: number; + requestAt: string; + productName: string; + onUpdate: () => void; + realName: string; +} + +export const RequestUserComponent = ({ + orderId, + username, + price, + requestAt, + productName, + onUpdate, + realName +}: RequestUserProps) => { + const approveMutation = useApproveCharge(); + const rejectMutation = useRejectCharge(); + + const formatDateTime = (isoString: string) => { + if (!isoString) return "알 수 없음"; + try { + const date = new Date(isoString); + if (isNaN(date.getTime())) return "알 수 없음"; + + // KST 시간대 적용 (UTC+9) + const kstDate = new Date(date.getTime() + 9 * 60 * 60 * 1000); + + const year = kstDate.getUTCFullYear(); + const month = String(kstDate.getUTCMonth() + 1).padStart(2, '0'); + const day = String(kstDate.getUTCDate()).padStart(2, '0'); + const hours = String(kstDate.getUTCHours()).padStart(2, '0'); + const minutes = String(kstDate.getUTCMinutes()).padStart(2, '0'); + + return `${year}-${month}-${day} ${hours}시 ${minutes}분`; + } catch (error) { + return "알 수 없음"; + } + }; + + const handleApprove = async () => { + try { + await approveMutation.mutateAsync(orderId); + alert("충전 요청이 수락되었습니다."); + onUpdate(); + } catch (error) { + alert("수락 처리 중 오류가 발생했습니다."); + } + }; + + const handleReject = async () => { + try { + await rejectMutation.mutateAsync(orderId); + alert("충전 요청이 거절되었습니다."); + onUpdate(); + } catch (error) { + alert("거절 처리 중 오류가 발생했습니다."); + } + }; + + return ( +
+
+
닉네임 : {username}
+
입금자명 : {realName}
+
요청시각 : {formatDateTime(requestAt)}
+
주문번호 : {orderId}
+
+ +
+
+
+ Coin + {productName} +
+
+ 가격 : + {price}원 +
+
+ +
+ + +
+
+
+ ); +}; diff --git a/app/adminpage/_components/SearchUserComponent.tsx b/app/adminpage/_components/SearchUserComponent.tsx new file mode 100644 index 0000000..306fe3f --- /dev/null +++ b/app/adminpage/_components/SearchUserComponent.tsx @@ -0,0 +1,35 @@ +"use client"; + +import React from "react"; +import { useRouter } from "next/navigation"; + +interface SearchUserProps { + nickname: string; + email: string; + uuid: string; +} + +export const SearchUserComponent = ({ nickname, email, uuid }: SearchUserProps) => { + const router = useRouter(); + + return ( +
+
+ Nickname : + {nickname} +
+
+
+ E-mail : + {email} +
+ +
+
+ ); +}; diff --git a/app/adminpage/layout.tsx b/app/adminpage/layout.tsx new file mode 100644 index 0000000..06689d9 --- /dev/null +++ b/app/adminpage/layout.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useAdminInfo } from "@/hooks/useAdminAuth"; +import { useRouter, usePathname } from "next/navigation"; +import { useEffect } from "react"; + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + const { data: adminInfo, isLoading, isError } = useAdminInfo(); + const router = useRouter(); + const pathname = usePathname(); + + useEffect(() => { + // If we are not on the login or register page and not loading, check for auth + if (!isLoading && !adminInfo && pathname !== "/adminpage" && pathname !== "/adminpage/register") { + router.push("/adminpage"); + } + }, [adminInfo, isLoading, pathname, router]); + + // Optionally show a loading spinner while checking auth + if (isLoading && pathname !== "/adminpage" && pathname !== "/adminpage/register") { + return ( +
+
+
+ ); + } + + return <>{children}; +} diff --git a/app/adminpage/myPage/event/discount/page.tsx b/app/adminpage/myPage/event/discount/page.tsx new file mode 100644 index 0000000..55261c5 --- /dev/null +++ b/app/adminpage/myPage/event/discount/page.tsx @@ -0,0 +1,160 @@ +"use client"; + +import React, { useState } from "react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { AdminHeader } from "../../../_components/AdminHeader"; +import { AdminDropdown } from "../../../_components/AdminDropdown"; +import { useAdminInfo } from "@/hooks/useAdminAuth"; + +const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0')); +const minutes = Array.from({ length: 6 }, (_, i) => String(i * 10).padStart(2, '0')); +const percentages = Array.from({ length: 4 }, (_, i) => String((i + 1) * 10)); + +export default function EventDiscountPage() { + const router = useRouter(); + const [adminSelect, setAdminSelect] = useState("가입자관리"); + const [selectedDate, setSelectedDate] = useState("오늘"); + const [startTime, setStartTime] = useState("선택"); + const [startMinutes, setStartMinutes] = useState("선택"); + const [endTime, setEndTime] = useState("선택"); + const [endMinutes, setEndMinutes] = useState("선택"); + const [selectedDiscount, setSelectedDiscount] = useState("선택"); + const [showModal, setShowModal] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + const { data: adminResponse } = useAdminInfo(); + const adminData = adminResponse?.data; + + const getDurationText = () => { + const sH = parseInt(startTime); + const sM = parseInt(startMinutes); + const eH = parseInt(endTime); + const eM = parseInt(endMinutes); + + if (isNaN(sH) || isNaN(sM) || isNaN(eH) || isNaN(eM)) return "0시간"; + + const diff = (eH * 60 + eM) - (sH * 60 + sM); + if (diff <= 0) return "0시간"; + + const h = Math.floor(diff / 60); + const m = diff % 60; + return m > 0 ? `${h}시간 ${m}분` : `${h}시간`; + }; + + const handleConfirm = () => { + const sH = parseInt(startTime); + const sM = parseInt(startMinutes); + const eH = parseInt(endTime); + const eM = parseInt(endMinutes); + + if (isNaN(sH) || isNaN(sM) || isNaN(eH) || isNaN(eM)) { + alert("시간을 올바르게 선택해주세요."); + return; + } + if (selectedDiscount === "선택") { + alert("할인율을 선택해주세요."); + return; + } + + const startTotal = sH * 60 + sM; + const endTotal = eH * 60 + eM; + + if (startTotal >= endTotal) { + setErrorMessage(<>이벤트 시작 시간이 종료 시간보다
같거나 늦을 수 없습니다.); + setShowModal(true); + return; + } + + // API Call logic + router.push("/adminpage/myPage/event/registercomplete"); + }; + + return ( +
+ + +
+
+
포인트 충전 할인 이벤트 예약
+
+ +
+
+ {["오늘", "내일", "모레"].map((date) => ( + + ))} +
+ +
+
이벤트 시간설정(최대 2시간)
+ +
+ + + + 분 부터 +
+ +
+ + + + 분 까지 +
+ +
+
+ 교내 가입자 전원에게 {getDurationText()}동안 최대 3번 구매 가능한 +
+
+ +
+ %의 포인트 충전 할인을 제공합니다. +
+
+
+ + +
+
+
+ + {showModal && ( +
+
+
+ warning +
+ {errorMessage} +
+
+
setShowModal(false)} + > + 확인 +
+
+
+ )} +
+ ); +} diff --git a/app/adminpage/myPage/event/free-match/page.tsx b/app/adminpage/myPage/event/free-match/page.tsx new file mode 100644 index 0000000..fb6fb98 --- /dev/null +++ b/app/adminpage/myPage/event/free-match/page.tsx @@ -0,0 +1,155 @@ +"use client"; + +import React, { useState } from "react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { AdminHeader } from "../../../_components/AdminHeader"; +import { AdminDropdown } from "../../../_components/AdminDropdown"; +import { useAdminInfo } from "@/hooks/useAdminAuth"; + +const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0')); +const minutes = Array.from({ length: 6 }, (_, i) => String(i * 10).padStart(2, '0')); + +export default function EventFreeMatchPage() { + const router = useRouter(); + const [adminSelect, setAdminSelect] = useState("가입자관리"); + const [selectedDate, setSelectedDate] = useState("오늘"); + const [startTime, setStartTime] = useState("선택"); + const [startMinutes, setStartMinutes] = useState("선택"); + const [endTime, setEndTime] = useState("선택"); + const [endMinutes, setEndMinutes] = useState("선택"); + const [showModal, setShowModal] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + const { data: adminResponse } = useAdminInfo(); + const adminData = adminResponse?.data; + + const [remainingEvents] = useState(3); + + const handleConfirm = () => { + const sH = parseInt(startTime); + const sM = parseInt(startMinutes); + const eH = parseInt(endTime); + const eM = parseInt(endMinutes); + + if (isNaN(sH) || isNaN(sM) || isNaN(eH) || isNaN(eM)) { + alert("시간을 올바르게 선택해주세요."); + return; + } + + const startTotal = sH * 60 + sM; + const endTotal = eH * 60 + eM; + + if (selectedDate === "오늘") { + const now = new Date(); + if (startTotal < now.getHours() * 60 + now.getMinutes()) { + setErrorMessage(<>이벤트 시작 시각은 현재 시각보다
이전으로 설정할 수 없습니다.); + setShowModal(true); + return; + } + } + + if (startTotal >= endTotal) { + setErrorMessage(<>이벤트 시작 시간이 종료 시간보다
같거나 늦을 수 없습니다.); + setShowModal(true); + return; + } + + // API Call logic here (using fetch or axios) + // For now, redirect to completion page + router.push("/adminpage/myPage/event/registercomplete"); + }; + + return ( +
+ + +
+
+
매칭 기회 제공 이벤트 예약
+
현재 잔여 이벤트 횟수는 {remainingEvents}회입니다.
+
+ {[...Array(4)].map((_, i) => ( + heart + ))} +
+
+ +
+
+ {["오늘", "내일", "모레"].map((date) => ( + + ))} +
+ +
+
이벤트 시간설정
+ +
+ + + + 분 부터 +
+ +
+ + + + 분 까지 +
+
+ +
+
+ 교내 가입자 전원에게 매칭 1회의 기회를 제공합니다. +
+ +
+
+
+ + {showModal && ( +
+
+
+ warning +
+ {errorMessage} +
+
+
setShowModal(false)} + > + 확인 +
+
+
+ )} +
+ ); +} diff --git a/app/adminpage/myPage/event/history/page.tsx b/app/adminpage/myPage/event/history/page.tsx new file mode 100644 index 0000000..56f75bb --- /dev/null +++ b/app/adminpage/myPage/event/history/page.tsx @@ -0,0 +1,60 @@ +"use client"; + +import React, { useState } from "react"; +import { AdminHeader } from "../../../_components/AdminHeader"; +import { AdminListItem } from "../../../_components/AdminListItem"; +import { useAdminInfo } from "@/hooks/useAdminAuth"; +import { useEventList } from "@/hooks/useAdminManagement"; +import { formatDateTime } from "@/utils/dateFormatter"; + +export default function EventHistoryPage() { + const [adminSelect, setAdminSelect] = useState("가입자관리"); + const { data: adminResponse } = useAdminInfo(); + const { data: eventResponse } = useEventList("HISTORY"); + + const adminData = adminResponse?.data; + const eventList = eventResponse?.data || []; + + return ( +
+ + +
+
+
이벤트 히스토리
+
진행한 이벤트의 히스토리
+ +
+ {eventList.length > 0 ? ( + eventList.map((item: any) => { + const start = formatDateTime(item.start); + const end = formatDateTime(item.end); + return ( + + ); + }) + ) : ( +
+ 이벤트 히스토리가 없습니다. +
+ )} +
+
+
+
+ ); +} diff --git a/app/adminpage/myPage/event/list/page.tsx b/app/adminpage/myPage/event/list/page.tsx new file mode 100644 index 0000000..45f4378 --- /dev/null +++ b/app/adminpage/myPage/event/list/page.tsx @@ -0,0 +1,77 @@ +"use client"; + +import React, { useState } from "react"; +import { AdminHeader } from "../../../_components/AdminHeader"; +import { AdminListItem } from "../../../_components/AdminListItem"; +import { useAdminInfo } from "@/hooks/useAdminAuth"; +import { useEventList, useDeleteEvent } from "@/hooks/useAdminManagement"; +import { formatDateTime } from "@/utils/dateFormatter"; + +export default function EventListPage() { + const [adminSelect, setAdminSelect] = useState("가입자관리"); + const { data: adminResponse } = useAdminInfo(); + const { data: eventResponse, refetch } = useEventList("RESERVATION"); + const deleteEventMutation = useDeleteEvent(); + + const adminData = adminResponse?.data; + const eventList = eventResponse?.data || []; + + const handleCancel = async (id: number) => { + if (confirm("이 이벤트를 정말로 취소하시겠어요?")) { + try { + await deleteEventMutation.mutateAsync(id); + alert("이벤트가 취소되었습니다."); + refetch(); + } catch (error) { + alert("이벤트 취소 중 오류가 발생했습니다."); + } + } + }; + + return ( +
+ + +
+
+
+ 이벤트 예약목록 및 취소 +
+
두 이벤트 예약 리스트 통합 예약 내역 및 취소
+ +
+ {eventList.length > 0 ? ( + eventList.map((item: any) => { + const start = formatDateTime(item.start); + const end = formatDateTime(item.end); + return ( + handleCancel(item.id)} + cancelButtonText="이벤트 취소" + /> + ); + }) + ) : ( +
+ 예약된 이벤트가 없습니다. +
+ )} +
+
+
+
+ ); +} diff --git a/app/adminpage/myPage/event/page.tsx b/app/adminpage/myPage/event/page.tsx new file mode 100644 index 0000000..ae33a74 --- /dev/null +++ b/app/adminpage/myPage/event/page.tsx @@ -0,0 +1,66 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { AdminHeader } from "../../_components/AdminHeader"; +import { useAdminInfo } from "@/hooks/useAdminAuth"; + +export default function AdminEventPage() { + const [adminSelect, setAdminSelect] = useState("가입자관리"); + const router = useRouter(); + const { data: adminResponse } = useAdminInfo(); + const adminData = adminResponse?.data; + + // Mock heart logic if needed in future + const [remainingEvents] = useState(3); + + return ( +
+ + +
+
+
router.push("/adminpage/myPage/event/free-match")} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] flex-1 p-6 pt-[26px] flex flex-col gap-2 cursor-pointer hover:shadow-lg transition-shadow" + > +
매칭 기회 제공 이벤트
+
이벤트 1회당 이성뽑기 1회 상한 존재
+
+ +
router.push("/adminpage/myPage/event/discount")} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] flex-1 p-6 pt-[26px] flex flex-col gap-2 cursor-pointer hover:shadow-lg transition-shadow" + > +
포인트 충전 할인 이벤트
+
40%의 할인 상한 존재, 최대 2시간 상한 존재
+
+
+ +
+
router.push("/adminpage/myPage/event/list")} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] flex-1 p-6 pt-[26px] flex flex-col gap-2 cursor-pointer hover:shadow-lg transition-shadow" + > +
이벤트 예약목록 및 취소
+
두 이벤트 예약 리스트 통합 예약 내역 및 취소
+
+ +
router.push("/adminpage/myPage/event/history")} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] flex-1 p-6 pt-[26px] flex flex-col gap-2 cursor-pointer hover:shadow-lg transition-shadow" + > +
이벤트 히스토리
+
지금까지 진행한 과거 이벤트의 히스토리
+
+
+
+
+ ); +} diff --git a/app/adminpage/myPage/event/registercomplete/page.tsx b/app/adminpage/myPage/event/registercomplete/page.tsx new file mode 100644 index 0000000..7bc825d --- /dev/null +++ b/app/adminpage/myPage/event/registercomplete/page.tsx @@ -0,0 +1,57 @@ +"use client"; + +import React, { useState } from "react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { AdminHeader } from "../../../_components/AdminHeader"; +import { useAdminInfo } from "@/hooks/useAdminAuth"; + +export default function EventRegisterCompletePage() { + const router = useRouter(); + const [adminSelect, setAdminSelect] = useState("가입자관리"); + const { data: adminResponse } = useAdminInfo(); + const adminData = adminResponse?.data; + + return ( +
+ + +
+
+
이벤트 등록 완료 안내
+ heart +
+ 해당 이벤트 예약 내역은 좌측 하단 이벤트 예약목록에서 열람하거나 취소할 수 있습니다.
+ 이벤트 사유를 공지하고 싶다면 우측 하단의 공지사항 등록을 이용하십시오. +
+
+ +
+
router.push("/adminpage/myPage/event/list")} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_rgba(196,196,196,0.3)] flex-1 p-6 pt-[26px] flex flex-col gap-2 cursor-pointer hover:shadow-lg transition-shadow" + > +
+ 이벤트 예약목록 및 취소 +
+
두 이벤트 예약 리스트 통합 예약 내역 및 취소
+
+ +
router.push("/adminpage/myPage/notice/reservation")} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_rgba(196,196,196,0.3)] flex-1 p-6 pt-[26px] flex flex-col gap-2 cursor-pointer hover:shadow-lg transition-shadow" + > +
공지사항 등록
+
이벤트 사유를 공지하고 싶으신가요?
+
+
+
+
+ ); +} diff --git a/app/adminpage/myPage/notice/complete/page.tsx b/app/adminpage/myPage/notice/complete/page.tsx new file mode 100644 index 0000000..be01446 --- /dev/null +++ b/app/adminpage/myPage/notice/complete/page.tsx @@ -0,0 +1,57 @@ +"use client"; + +import React, { useState } from "react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { AdminHeader } from "../../../_components/AdminHeader"; +import { useAdminInfo } from "@/hooks/useAdminAuth"; + +export default function NoticeRegisterCompletePage() { + const router = useRouter(); + const [adminSelect, setAdminSelect] = useState("가입자관리"); + const { data: adminResponse } = useAdminInfo(); + const adminData = adminResponse?.data; + + return ( +
+ + +
+
+
공지사항 예약 완료 안내
+ heart +
+ 해당 공지사항 예약 내역은 좌측 하단 공지사항 예약목록에서 열람하거나 취소할 수 있습니다.
+ 이벤트 예약을 잊으셨다면 우측 하단의 이벤트 예약을 이용하십시오. +
+
+ +
+
router.push("/adminpage/myPage/notice/list")} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_rgba(196,196,196,0.3)] flex-1 p-6 pt-[26px] flex flex-col gap-2 cursor-pointer hover:shadow-lg transition-shadow" + > +
+ 공지사항 예약목록 및 취소 +
+
전체 공지사항 예약 내역 및 취소
+
+ +
router.push("/adminpage/myPage/event/page")} // or event main + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_rgba(196,196,196,0.3)] flex-1 p-6 pt-[26px] flex flex-col gap-2 cursor-pointer hover:shadow-lg transition-shadow" + > +
이벤트 예약
+
이벤트를 아직 예약하지 않으셨나요?
+
+
+
+
+ ); +} diff --git a/app/adminpage/myPage/notice/history/page.tsx b/app/adminpage/myPage/notice/history/page.tsx new file mode 100644 index 0000000..70b5c2b --- /dev/null +++ b/app/adminpage/myPage/notice/history/page.tsx @@ -0,0 +1,59 @@ +"use client"; + +import React, { useState } from "react"; +import { AdminHeader } from "../../../_components/AdminHeader"; +import { AdminListItem } from "../../../_components/AdminListItem"; +import { useAdminInfo } from "@/hooks/useAdminAuth"; +import { useNoticeList } from "@/hooks/useAdminManagement"; +import { formatDateTime } from "@/utils/dateFormatter"; + +export default function NoticeHistoryPage() { + const [adminSelect, setAdminSelect] = useState("가입자관리"); + const { data: adminResponse } = useAdminInfo(); + const { data: noticeResponse } = useNoticeList("HISTORY"); + + const adminData = adminResponse?.data; + const noticeList = noticeResponse?.data || []; + + return ( +
+ + +
+
+
공지사항 히스토리
+
진행한 공지사항의 히스토리
+ +
+ {noticeList.length > 0 ? ( + noticeList.map((item: any) => { + const posted = formatDateTime(item.postedAt); + const closed = formatDateTime(item.closedAt); + return ( + + ); + }) + ) : ( +
+ 공지사항 히스토리가 없습니다. +
+ )} +
+
+
+
+ ); +} diff --git a/app/adminpage/myPage/notice/list/page.tsx b/app/adminpage/myPage/notice/list/page.tsx new file mode 100644 index 0000000..ad16b5e --- /dev/null +++ b/app/adminpage/myPage/notice/list/page.tsx @@ -0,0 +1,76 @@ +"use client"; + +import React, { useState } from "react"; +import { AdminHeader } from "../../../_components/AdminHeader"; +import { AdminListItem } from "../../../_components/AdminListItem"; +import { useAdminInfo } from "@/hooks/useAdminAuth"; +import { useNoticeList, useDeleteNotice } from "@/hooks/useAdminManagement"; +import { formatDateTime } from "@/utils/dateFormatter"; + +export default function NoticeListPage() { + const [adminSelect, setAdminSelect] = useState("가입자관리"); + const { data: adminResponse } = useAdminInfo(); + const { data: noticeResponse, refetch } = useNoticeList("RESERVATION"); + const deleteNoticeMutation = useDeleteNotice(); + + const adminData = adminResponse?.data; + const noticeList = noticeResponse?.data || []; + + const handleCancel = async (id: number) => { + if (confirm("이 공지를 정말로 취소하시겠어요?")) { + try { + await deleteNoticeMutation.mutateAsync(id); + alert("공지가 삭제되었습니다."); + refetch(); + } catch (error) { + alert("공지 삭제 중 오류가 발생했습니다."); + } + } + }; + + return ( +
+ + +
+
+
+ 공지사항 예약목록 및 취소 +
+
전체 공지사항 예약 내역 및 취소
+ +
+ {noticeList.length > 0 ? ( + noticeList.map((item: any) => { + const posted = formatDateTime(item.postedAt); + const closed = formatDateTime(item.closedAt); + return ( + handleCancel(item.id)} + cancelButtonText="공지 취소" + /> + ); + }) + ) : ( +
+ 예약된 공지사항이 없습니다. +
+ )} +
+
+
+
+ ); +} diff --git a/app/adminpage/myPage/notice/page.tsx b/app/adminpage/myPage/notice/page.tsx new file mode 100644 index 0000000..5c8d8b3 --- /dev/null +++ b/app/adminpage/myPage/notice/page.tsx @@ -0,0 +1,55 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { AdminHeader } from "../../_components/AdminHeader"; +import { useAdminInfo } from "@/hooks/useAdminAuth"; + +export default function AdminNoticeMainPage() { + const [adminSelect, setAdminSelect] = useState("가입자관리"); + const router = useRouter(); + const { data: adminResponse } = useAdminInfo(); + const adminData = adminResponse?.data; + + return ( +
+ + +
+
router.push("/adminpage/myPage/notice/reservation")} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] w-full p-6 pt-[26px] flex flex-col gap-2 cursor-pointer hover:shadow-lg transition-shadow" + > +
공지사항 예약
+
공지사항은 예약제
+
+ +
+
router.push("/adminpage/myPage/notice/list")} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] flex-1 p-6 pt-[26px] flex flex-col gap-2 cursor-pointer hover:shadow-lg transition-shadow" + > +
+ 공지사항 예약목록 및 취소 +
+
전체 공지사항 예약 내역 및 취소
+
+ +
router.push("/adminpage/myPage/notice/history")} + className="bg-white rounded-[24px] border border-white/30 shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] flex-1 p-6 pt-[26px] flex flex-col gap-2 cursor-pointer hover:shadow-lg transition-shadow" + > +
공지사항 히스토리
+
지금까지 진행한 과거 공지사항 히스토리
+
+
+
+
+ ); +} diff --git a/app/adminpage/myPage/notice/reservation/page.tsx b/app/adminpage/myPage/notice/reservation/page.tsx new file mode 100644 index 0000000..e92685e --- /dev/null +++ b/app/adminpage/myPage/notice/reservation/page.tsx @@ -0,0 +1,202 @@ +"use client"; + +import React, { useState } from "react"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { AdminHeader } from "../../../_components/AdminHeader"; +import { AdminDropdown } from "../../../_components/AdminDropdown"; +import { useAdminInfo } from "@/hooks/useAdminAuth"; +import { useRegisterNotice } from "@/hooks/useAdminManagement"; + +const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0')); +const minutes = Array.from({ length: 12 }, (_, i) => String(i * 5).padStart(2, '0')); // 5 min interval for better precision? Old used 10 but let's stick to 10 if that's standard. +// Actually standard was minutes = Array.from({ length: 6 }, (_, i) => String(i * 10).padStart(2, '0')); +const stdMinutes = Array.from({ length: 6 }, (_, i) => String(i * 10).padStart(2, '0')); + +export default function NoticeReservationPage() { + const router = useRouter(); + const [adminSelect, setAdminSelect] = useState("가입자관리"); + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + const [selectedDate, setSelectedDate] = useState("오늘"); + const [startTime, setStartTime] = useState("선택"); + const [startMinutes, setStartMinutes] = useState("선택"); + const [endTime, setEndTime] = useState("선택"); + const [endMinutes, setEndMinutes] = useState("선택"); + const [showModal, setShowModal] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + const { data: adminResponse } = useAdminInfo(); + const adminData = adminResponse?.data; + const registerNoticeMutation = useRegisterNotice(); + + const handleConfirm = async () => { + if (!title.trim() || !content.trim()) { + setErrorMessage(<>공지사항 제목과 내용을
모두 입력해주세요.); + setShowModal(true); + return; + } + + const sH = parseInt(startTime); + const sM = parseInt(startMinutes); + const eH = parseInt(endTime); + const eM = parseInt(endMinutes); + + if (isNaN(sH) || isNaN(sM) || isNaN(eH) || isNaN(eM)) { + alert("시간을 올바르게 선택해주세요."); + return; + } + + const startTotal = sH * 60 + sM; + const endTotal = eH * 60 + eM; + + if (selectedDate === "오늘") { + const now = new Date(); + const currentTotal = now.getHours() * 60 + now.getMinutes(); + if (startTotal < currentTotal + 10) { + setErrorMessage(<>공지사항 시작 시각은 현재 시각보다
최소 10분 이후로 설정해야 합니다.); + setShowModal(true); + return; + } + } + + if (startTotal >= endTotal) { + setErrorMessage(<>시작 시간이 종료 시간보다
같거나 늦을 수 없습니다.); + setShowModal(true); + return; + } + + const today = new Date(); + let selectedDateObject = new Date(today); + if (selectedDate === "내일") selectedDateObject.setDate(today.getDate() + 1); + else if (selectedDate === "모레") selectedDateObject.setDate(today.getDate() + 2); + + const formatTimePart = (h: number, m: number) => { + const d = new Date(selectedDateObject); + d.setHours(h, m, 0, 0); + // Adjust to KST (UTC+9) + const kst = new Date(d.getTime() + 9 * 60 * 60 * 1000); + return kst.toISOString().slice(0, 19); + }; + + const payload = { + title, + content, + postedAt: formatTimePart(sH, sM), + closedAt: formatTimePart(eH, eM), + }; + + try { + await registerNoticeMutation.mutateAsync(payload); + router.push("/adminpage/myPage/notice/complete"); + } catch (error) { + setErrorMessage(<>공지사항 등록 중 오류가 발생했습니다.
다시 시도해주세요.); + setShowModal(true); + } + }; + + return ( +
+ + +
+
+
+
+
공지사항 제목 등록
+
아래에 공지사항 제목을 입력해주세요.
+ setTitle(e.target.value)} + placeholder="제목을 입력하세요" + className="w-full border border-[#979797] rounded-md p-3 text-2xl font-medium shadow-inner placeholder:text-[#b3b3b3] outline-none focus:ring-2 focus:ring-[#ff775e]" + /> +
+ +
+
내용 등록
+
아래에 공지사항 내용을 입력하세요.
+