diff --git a/app/adminpage/_components/AdminDateSelector.tsx b/app/adminpage/_components/AdminDateSelector.tsx
new file mode 100644
index 0000000..b0d0855
--- /dev/null
+++ b/app/adminpage/_components/AdminDateSelector.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import React from "react";
+
+interface AdminDateSelectorProps {
+ selectedDate: string;
+ onSelectDate: (date: string) => void;
+}
+
+export const AdminDateSelector = ({
+ selectedDate,
+ onSelectDate,
+}: AdminDateSelectorProps) => {
+ const dates = ["오늘", "내일", "모레"];
+
+ return (
+
+ {dates.map((date) => (
+
+ ))}
+
+ );
+};
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..76035c6
--- /dev/null
+++ b/app/adminpage/_components/AdminNotAllowed.tsx
@@ -0,0 +1,17 @@
+"use client";
+
+import React from "react";
+import { Ban } from "lucide-react";
+
+export default function AdminNotAllowed() {
+ return (
+
+
+
+ 미승인 오퍼레이터입니다.
+
+ 관리자의 승인을 대기해 주세요.
+
+
+ );
+}
diff --git a/app/adminpage/_components/AdminTimeRow.tsx b/app/adminpage/_components/AdminTimeRow.tsx
new file mode 100644
index 0000000..f085341
--- /dev/null
+++ b/app/adminpage/_components/AdminTimeRow.tsx
@@ -0,0 +1,41 @@
+"use client";
+
+import React from "react";
+import { AdminDropdown } from "./AdminDropdown";
+
+interface AdminTimeRowProps {
+ hours: string[];
+ minutes: string[];
+ selectedHour: string;
+ selectedMinute: string;
+ onHourSelect: (hour: string) => void;
+ onMinuteSelect: (minute: string) => void;
+ suffix: string;
+}
+
+export const AdminTimeRow = ({
+ hours,
+ minutes,
+ selectedHour,
+ selectedMinute,
+ onHourSelect,
+ onMinuteSelect,
+ suffix,
+}: AdminTimeRowProps) => {
+ return (
+
+ );
+};
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/AdminWarningModal.tsx b/app/adminpage/_components/AdminWarningModal.tsx
new file mode 100644
index 0000000..df233ef
--- /dev/null
+++ b/app/adminpage/_components/AdminWarningModal.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import React from "react";
+import { TriangleAlert } from "lucide-react";
+
+interface AdminWarningModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ message: React.ReactNode;
+}
+
+export const AdminWarningModal = ({
+ isOpen,
+ onClose,
+ message,
+}: AdminWarningModalProps) => {
+ if (!isOpen) return null;
+
+ return (
+
+ );
+};
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 (
+
+
+
+ 오퍼레이터 관리
+ 오퍼레이터 관리
+
+
+ );
+};
diff --git a/app/adminpage/_components/Pagination.tsx b/app/adminpage/_components/Pagination.tsx
new file mode 100644
index 0000000..aeb3c9e
--- /dev/null
+++ b/app/adminpage/_components/Pagination.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import {
+ ChevronLeft,
+ ChevronRight,
+ ChevronsLeft,
+ ChevronsRight,
+} from "lucide-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..1da170d
--- /dev/null
+++ b/app/adminpage/_components/RequestUserComponent.tsx
@@ -0,0 +1,120 @@
+"use client";
+
+import React from "react";
+import { Coins } from "lucide-react";
+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}
+
+
+
+
+
+
+ {productName}
+
+
+ 가격 :
+
+ {price}원
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/app/adminpage/_components/ScreenAdminLoginPage.tsx b/app/adminpage/_components/ScreenAdminLoginPage.tsx
new file mode 100644
index 0000000..12c5fe7
--- /dev/null
+++ b/app/adminpage/_components/ScreenAdminLoginPage.tsx
@@ -0,0 +1,141 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client";
+
+import React, { useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useAdminLogin, useAdminInfo } from "@/hooks/useAdminAuth";
+
+export default function ScreenAdminLoginPage() {
+ const [passwordVisible, setPasswordVisible] = useState(false);
+ const [formData, setFormData] = useState({ accountId: "", password: "" });
+ const router = useRouter();
+
+ const loginMutation = useAdminLogin();
+ const { refetch: fetchAdminInfo } = useAdminInfo();
+
+ const togglePasswordVisibility = () => {
+ setPasswordVisible(!passwordVisible);
+ };
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ try {
+ const data = await loginMutation.mutateAsync(formData);
+
+ if (data.status >= 200 && data.status < 300) {
+ await fetchAdminInfo();
+ router.push("/adminpage/myPage");
+ } else {
+ alert("로그인 실패: " + (data.message || "알 수 없는 오류"));
+ }
+ } catch (error: any) {
+ console.error("로그인 중 에러 발생:", error);
+ alert("로그인 중 오류가 발생했습니다.");
+ }
+ };
+
+ return (
+
+
+
+
+
+
Partners Page
+
+
+
+
+
+
+ 가입하기
+
+ |
+
+ ID/비밀번호 찾기
+
+
+
+ 문의하기
+
+
+
+
+ );
+}
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/_components/ScreenAdminMyPage.tsx b/app/adminpage/myPage/_components/ScreenAdminMyPage.tsx
new file mode 100644
index 0000000..3d06fe1
--- /dev/null
+++ b/app/adminpage/myPage/_components/ScreenAdminMyPage.tsx
@@ -0,0 +1,79 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { AdminHeader } from "../../_components/AdminHeader";
+import {
+ AdminMyPageMain,
+ MasterManageComponent,
+ OperatorManageComponent,
+ AdminTeamManage,
+} from "../../_components/ManagementComponents";
+import { useAdminInfo } from "@/hooks/useAdminAuth";
+import { useRouter, useSearchParams } from "next/navigation";
+import AdminNotAllowed from "../../_components/AdminNotAllowed";
+
+export default function ScreenAdminMyPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const tab = searchParams.get("tab");
+ const [adminSelect, setAdminSelect] = useState(tab || "Main");
+ const { data: adminResponse, isLoading } = useAdminInfo();
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ const adminData = adminResponse?.data;
+
+ if (!adminData) return null;
+
+ // Handle specific roles
+ if (adminData.role === "ROLE_SEMI_OPERATOR") {
+ return (
+
+ );
+ }
+
+ if (adminData.role === "ROLE_SEMI_ADMIN") {
+ router.replace("/adminpage/webmail-check");
+ return null;
+ }
+
+ return (
+
+
+
+
+ {adminSelect === "Main" && }
+
+ {adminSelect === "가입자관리" && (
+
+ {adminData.role === "ROLE_OPERATOR" && }
+ {adminData.role === "ROLE_ADMIN" && }
+
+ )}
+
+ {adminSelect === "팀관리" && }
+
+
+ );
+}
diff --git a/app/adminpage/myPage/event/_components/ScreenAdminEventPage.tsx b/app/adminpage/myPage/event/_components/ScreenAdminEventPage.tsx
new file mode 100644
index 0000000..51635a2
--- /dev/null
+++ b/app/adminpage/myPage/event/_components/ScreenAdminEventPage.tsx
@@ -0,0 +1,82 @@
+"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 ScreenAdminEventPage() {
+ 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="flex flex-1 cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg"
+ >
+
+ 매칭 기회 제공 이벤트
+
+
+ 이벤트 1회당 이성뽑기 1회 상한 존재
+
+
+
+
router.push("/adminpage/myPage/event/discount")}
+ className="flex flex-1 cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg"
+ >
+
+ 포인트 충전 할인 이벤트
+
+
+ 40%의 할인 상한 존재, 최대 2시간 상한 존재
+
+
+
+
+
+
router.push("/adminpage/myPage/event/list")}
+ className="flex flex-1 cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg"
+ >
+
+ 이벤트 예약목록 및 취소
+
+
+ 두 이벤트 예약 리스트 통합 예약 내역 및 취소
+
+
+
+
router.push("/adminpage/myPage/event/history")}
+ className="flex flex-1 cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg"
+ >
+
+ 이벤트 히스토리
+
+
+ 지금까지 진행한 과거 이벤트의 히스토리
+
+
+
+
+
+ );
+}
diff --git a/app/adminpage/myPage/event/discount/_components/ScreenEventDiscountPage.tsx b/app/adminpage/myPage/event/discount/_components/ScreenEventDiscountPage.tsx
new file mode 100644
index 0000000..ddfb353
--- /dev/null
+++ b/app/adminpage/myPage/event/discount/_components/ScreenEventDiscountPage.tsx
@@ -0,0 +1,167 @@
+"use client";
+
+import React, { useState } from "react";
+import { TriangleAlert } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { AdminHeader } from "../../../../_components/AdminHeader";
+import { AdminDropdown } from "../../../../_components/AdminDropdown";
+import { AdminDateSelector } from "../../../../_components/AdminDateSelector";
+import { AdminTimeRow } from "../../../../_components/AdminTimeRow";
+import { AdminWarningModal } from "../../../../_components/AdminWarningModal";
+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 ScreenEventDiscountPage() {
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+ 이벤트 시간설정(최대 2시간)
+
+
+
+
+
+
+
+
+ 교내 가입자 전원에게{" "}
+
+ {getDurationText()}동안 최대 3번 구매 가능한
+
+
+
+
+
+ %의 포인트 충전 할인을
+ 제공합니다.
+
+
+
+
+
+
+
+
+
+
setShowModal(false)}
+ message={errorMessage}
+ />
+
+ );
+}
diff --git a/app/adminpage/myPage/event/discount/page.tsx b/app/adminpage/myPage/event/discount/page.tsx
new file mode 100644
index 0000000..cd6c5f0
--- /dev/null
+++ b/app/adminpage/myPage/event/discount/page.tsx
@@ -0,0 +1,5 @@
+import ScreenEventDiscountPage from "./_components/ScreenEventDiscountPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/event/free-match/_components/EventStatusCard.tsx b/app/adminpage/myPage/event/free-match/_components/EventStatusCard.tsx
new file mode 100644
index 0000000..1638be8
--- /dev/null
+++ b/app/adminpage/myPage/event/free-match/_components/EventStatusCard.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import React from "react";
+import { Heart } from "lucide-react";
+
+interface EventStatusCardProps {
+ remainingEvents: number;
+}
+
+export const EventStatusCard = ({ remainingEvents }: EventStatusCardProps) => {
+ return (
+
+
+ 매칭 기회 제공 이벤트 예약
+
+
+ 현재 잔여 이벤트 횟수는 {remainingEvents}회입니다.
+
+
+ {[...Array(4)].map((_, i) => (
+
+ ))}
+
+
+ );
+};
diff --git a/app/adminpage/myPage/event/free-match/_components/ScreenEventFreeMatchPage.tsx b/app/adminpage/myPage/event/free-match/_components/ScreenEventFreeMatchPage.tsx
new file mode 100644
index 0000000..3beab61
--- /dev/null
+++ b/app/adminpage/myPage/event/free-match/_components/ScreenEventFreeMatchPage.tsx
@@ -0,0 +1,144 @@
+"use client";
+
+import React, { useState } from "react";
+import { Heart, TriangleAlert } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { AdminHeader } from "../../../../_components/AdminHeader";
+import { AdminDateSelector } from "../../../../_components/AdminDateSelector";
+import { AdminTimeRow } from "../../../../_components/AdminTimeRow";
+import { AdminWarningModal } from "../../../../_components/AdminWarningModal";
+import { EventStatusCard } from "./EventStatusCard";
+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 ScreenEventFreeMatchPage() {
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 교내 가입자 전원에게{" "}
+ 매칭 1회의 기회를
+ 제공합니다.
+
+
+
+
+
+
+
setShowModal(false)}
+ message={errorMessage}
+ />
+
+ );
+}
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..fa8084f
--- /dev/null
+++ b/app/adminpage/myPage/event/free-match/page.tsx
@@ -0,0 +1,5 @@
+import ScreenEventFreeMatchPage from "./_components/ScreenEventFreeMatchPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/event/history/_components/ScreenEventHistoryPage.tsx b/app/adminpage/myPage/event/history/_components/ScreenEventHistoryPage.tsx
new file mode 100644
index 0000000..c8728f7
--- /dev/null
+++ b/app/adminpage/myPage/event/history/_components/ScreenEventHistoryPage.tsx
@@ -0,0 +1,69 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"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 ScreenEventHistoryPage() {
+ 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/history/page.tsx b/app/adminpage/myPage/event/history/page.tsx
new file mode 100644
index 0000000..29ed4f8
--- /dev/null
+++ b/app/adminpage/myPage/event/history/page.tsx
@@ -0,0 +1,5 @@
+import ScreenEventHistoryPage from "./_components/ScreenEventHistoryPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/event/list/_components/ScreenEventListPage.tsx b/app/adminpage/myPage/event/list/_components/ScreenEventListPage.tsx
new file mode 100644
index 0000000..57db735
--- /dev/null
+++ b/app/adminpage/myPage/event/list/_components/ScreenEventListPage.tsx
@@ -0,0 +1,84 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"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 ScreenEventListPage() {
+ 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/list/page.tsx b/app/adminpage/myPage/event/list/page.tsx
new file mode 100644
index 0000000..2658f63
--- /dev/null
+++ b/app/adminpage/myPage/event/list/page.tsx
@@ -0,0 +1,5 @@
+import ScreenEventListPage from "./_components/ScreenEventListPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/event/page.tsx b/app/adminpage/myPage/event/page.tsx
new file mode 100644
index 0000000..684ee20
--- /dev/null
+++ b/app/adminpage/myPage/event/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminEventPage from "./_components/ScreenAdminEventPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/event/registercomplete/_components/ScreenEventRegisterCompletePage.tsx b/app/adminpage/myPage/event/registercomplete/_components/ScreenEventRegisterCompletePage.tsx
new file mode 100644
index 0000000..9bf577e
--- /dev/null
+++ b/app/adminpage/myPage/event/registercomplete/_components/ScreenEventRegisterCompletePage.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import React, { useState } from "react";
+import { Heart } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { AdminHeader } from "../../../../_components/AdminHeader";
+import { useAdminInfo } from "@/hooks/useAdminAuth";
+
+export default function ScreenEventRegisterCompletePage() {
+ const router = useRouter();
+ const [adminSelect, setAdminSelect] = useState("가입자관리");
+ const { data: adminResponse } = useAdminInfo();
+ const adminData = adminResponse?.data;
+
+ return (
+
+
+
+
+
+
+ 이벤트 등록 완료 안내
+
+
+
+ 해당 이벤트 예약 내역은 좌측 하단 이벤트 예약목록에서 열람하거나{" "}
+ 취소할 수
+ 있습니다.
+
+ 이벤트 사유를 공지하고 싶다면 우측 하단의 공지사항 등록을
+ 이용하십시오.
+
+
+
+
+
router.push("/adminpage/myPage/event/list")}
+ className="flex flex-1 cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg"
+ >
+
+ 이벤트 예약목록 및 취소
+
+
+ 두 이벤트 예약 리스트 통합 예약 내역 및 취소
+
+
+
+
router.push("/adminpage/myPage/notice/reservation")}
+ className="flex flex-1 cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg"
+ >
+
+ 공지사항 등록
+
+
+ 이벤트 사유를 공지하고 싶으신가요?
+
+
+
+
+
+ );
+}
diff --git a/app/adminpage/myPage/event/registercomplete/page.tsx b/app/adminpage/myPage/event/registercomplete/page.tsx
new file mode 100644
index 0000000..7358243
--- /dev/null
+++ b/app/adminpage/myPage/event/registercomplete/page.tsx
@@ -0,0 +1,5 @@
+import ScreenEventRegisterCompletePage from "./_components/ScreenEventRegisterCompletePage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/notice/_components/ScreenAdminNoticeMainPage.tsx b/app/adminpage/myPage/notice/_components/ScreenAdminNoticeMainPage.tsx
new file mode 100644
index 0000000..68238c9
--- /dev/null
+++ b/app/adminpage/myPage/notice/_components/ScreenAdminNoticeMainPage.tsx
@@ -0,0 +1,65 @@
+"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 ScreenAdminNoticeMainPage() {
+ const [adminSelect, setAdminSelect] = useState("가입자관리");
+ const router = useRouter();
+ const { data: adminResponse } = useAdminInfo();
+ const adminData = adminResponse?.data;
+
+ return (
+
+
+
+
+ router.push("/adminpage/myPage/notice/reservation")}
+ className="flex w-full cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg"
+ >
+
+ 공지사항 예약
+
+
+ 공지사항은 예약제
+
+
+
+
+
router.push("/adminpage/myPage/notice/list")}
+ className="flex flex-1 cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg"
+ >
+
+ 공지사항 예약목록 및 취소
+
+
+ 전체 공지사항 예약 내역 및 취소
+
+
+
+
router.push("/adminpage/myPage/notice/history")}
+ className="flex flex-1 cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_1px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg"
+ >
+
+ 공지사항 히스토리
+
+
+ 지금까지 진행한 과거 공지사항 히스토리
+
+
+
+
+
+ );
+}
diff --git a/app/adminpage/myPage/notice/complete/_components/ScreenNoticeRegisterCompletePage.tsx b/app/adminpage/myPage/notice/complete/_components/ScreenNoticeRegisterCompletePage.tsx
new file mode 100644
index 0000000..47a3a19
--- /dev/null
+++ b/app/adminpage/myPage/notice/complete/_components/ScreenNoticeRegisterCompletePage.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import React, { useState } from "react";
+import { Heart } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { AdminHeader } from "../../../../_components/AdminHeader";
+import { useAdminInfo } from "@/hooks/useAdminAuth";
+
+export default function ScreenNoticeRegisterCompletePage() {
+ const router = useRouter();
+ const [adminSelect, setAdminSelect] = useState("가입자관리");
+ const { data: adminResponse } = useAdminInfo();
+ const adminData = adminResponse?.data;
+
+ return (
+
+
+
+
+
+
+ 공지사항 예약 완료 안내
+
+
+
+ 해당 공지사항 예약 내역은 좌측 하단 공지사항 예약목록에서 열람하거나{" "}
+ 취소할 수
+ 있습니다.
+
+ 이벤트 예약을 잊으셨다면 우측 하단의 이벤트 예약을 이용하십시오.
+
+
+
+
+
router.push("/adminpage/myPage/notice/list")}
+ className="flex flex-1 cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg"
+ >
+
+ 공지사항 예약목록 및 취소
+
+
+ 전체 공지사항 예약 내역 및 취소
+
+
+
+
router.push("/adminpage/myPage/event/page")} // or event main
+ className="flex flex-1 cursor-pointer flex-col gap-2 rounded-[24px] border border-white/30 bg-white p-6 pt-[26px] shadow-[1px_1px_20px_rgba(196,196,196,0.3)] transition-shadow hover:shadow-lg"
+ >
+
+ 이벤트 예약
+
+
+ 이벤트를 아직 예약하지 않으셨나요?
+
+
+
+
+
+ );
+}
diff --git a/app/adminpage/myPage/notice/complete/page.tsx b/app/adminpage/myPage/notice/complete/page.tsx
new file mode 100644
index 0000000..711eea9
--- /dev/null
+++ b/app/adminpage/myPage/notice/complete/page.tsx
@@ -0,0 +1,5 @@
+import ScreenNoticeRegisterCompletePage from "./_components/ScreenNoticeRegisterCompletePage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/notice/history/_components/ScreenNoticeHistoryPage.tsx b/app/adminpage/myPage/notice/history/_components/ScreenNoticeHistoryPage.tsx
new file mode 100644
index 0000000..e32d5bf
--- /dev/null
+++ b/app/adminpage/myPage/notice/history/_components/ScreenNoticeHistoryPage.tsx
@@ -0,0 +1,64 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"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 ScreenNoticeHistoryPage() {
+ 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/history/page.tsx b/app/adminpage/myPage/notice/history/page.tsx
new file mode 100644
index 0000000..c15128f
--- /dev/null
+++ b/app/adminpage/myPage/notice/history/page.tsx
@@ -0,0 +1,5 @@
+import ScreenNoticeHistoryPage from "./_components/ScreenNoticeHistoryPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/notice/list/_components/ScreenNoticeListPage.tsx b/app/adminpage/myPage/notice/list/_components/ScreenNoticeListPage.tsx
new file mode 100644
index 0000000..91fd095
--- /dev/null
+++ b/app/adminpage/myPage/notice/list/_components/ScreenNoticeListPage.tsx
@@ -0,0 +1,79 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"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 ScreenNoticeListPage() {
+ 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/list/page.tsx b/app/adminpage/myPage/notice/list/page.tsx
new file mode 100644
index 0000000..7c2372a
--- /dev/null
+++ b/app/adminpage/myPage/notice/list/page.tsx
@@ -0,0 +1,5 @@
+import ScreenNoticeListPage from "./_components/ScreenNoticeListPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/notice/page.tsx b/app/adminpage/myPage/notice/page.tsx
new file mode 100644
index 0000000..c191974
--- /dev/null
+++ b/app/adminpage/myPage/notice/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminNoticeMainPage from "./_components/ScreenAdminNoticeMainPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/notice/reservation/_components/ScreenNoticeReservationPage.tsx b/app/adminpage/myPage/notice/reservation/_components/ScreenNoticeReservationPage.tsx
new file mode 100644
index 0000000..a222892
--- /dev/null
+++ b/app/adminpage/myPage/notice/reservation/_components/ScreenNoticeReservationPage.tsx
@@ -0,0 +1,224 @@
+"use client";
+
+import React, { useState } from "react";
+import { TriangleAlert } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { AdminHeader } from "../../../../_components/AdminHeader";
+import { AdminDropdown } from "../../../../_components/AdminDropdown";
+import { AdminDateSelector } from "../../../../_components/AdminDateSelector";
+import { AdminTimeRow } from "../../../../_components/AdminTimeRow";
+import { AdminWarningModal } from "../../../../_components/AdminWarningModal";
+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 ScreenNoticeReservationPage() {
+ 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();
+ const 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 rounded-md border border-[#979797] p-3 text-2xl font-medium shadow-inner outline-none placeholder:text-[#b3b3b3] focus:ring-2 focus:ring-[#ff775e]"
+ />
+
+
+
+
+ 내용 등록
+
+
+ 아래에 공지사항 내용을 입력하세요.
+
+
+
+
+
+
+
+
+
+
+
+ 교내 가입자 전체에게 팝업 형태로 공지합니다.
+
+
+
+
+
+
+
+
setShowModal(false)}
+ message={errorMessage}
+ />
+
+ );
+}
diff --git a/app/adminpage/myPage/notice/reservation/page.tsx b/app/adminpage/myPage/notice/reservation/page.tsx
new file mode 100644
index 0000000..ac6fa26
--- /dev/null
+++ b/app/adminpage/myPage/notice/reservation/page.tsx
@@ -0,0 +1,5 @@
+import ScreenNoticeReservationPage from "./_components/ScreenNoticeReservationPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/page.tsx b/app/adminpage/myPage/page.tsx
new file mode 100644
index 0000000..55b1812
--- /dev/null
+++ b/app/adminpage/myPage/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminMyPage from "./_components/ScreenAdminMyPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/myPage/search/_components/ScreenAdminSearchPage.tsx b/app/adminpage/myPage/search/_components/ScreenAdminSearchPage.tsx
new file mode 100644
index 0000000..4694072
--- /dev/null
+++ b/app/adminpage/myPage/search/_components/ScreenAdminSearchPage.tsx
@@ -0,0 +1,179 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { Search } from "lucide-react";
+import { AdminHeader } from "../../../_components/AdminHeader";
+import { SearchUserComponent } from "../../../_components/SearchUserComponent";
+import { Pagination } from "../../../_components/Pagination";
+import { useAdminInfo } from "@/hooks/useAdminAuth";
+import { useUserList, useSearchUsers } from "@/hooks/useAdminManagement";
+
+export default function ScreenAdminSearchPage() {
+ const [searchQuery, setSearchQuery] = useState("");
+ const [adminSelect, setAdminSelect] = useState("가입자관리");
+ const [selectedSort, setSelectedSort] = useState("50명씩 정렬");
+ const [currentPage, setCurrentPage] = useState(1);
+ const [userData, setUserData] = useState([]);
+ const [totalPage, setTotalPage] = useState(1);
+
+ const { data: adminResponse } = useAdminInfo();
+ const adminData = adminResponse?.data;
+
+ const pageSize =
+ selectedSort === "50명씩 정렬"
+ ? 50
+ : selectedSort === "10명씩 정렬"
+ ? 10
+ : 5;
+
+ const { data: listResponse, isLoading: isListLoading } = useUserList(
+ currentPage - 1,
+ pageSize,
+ );
+ const searchMutation = useSearchUsers();
+
+ useEffect(() => {
+ if (listResponse && !searchQuery) {
+ setTimeout(() => {
+ setUserData(listResponse.data.content);
+ setTotalPage(listResponse.data.page.totalPages);
+ }, 0);
+ }
+ }, [listResponse, searchQuery]);
+
+ const handleSearchChange = (e: React.ChangeEvent) => {
+ setSearchQuery(e.target.value);
+ };
+
+ const handleSearchClick = async () => {
+ if (searchQuery) {
+ const searchType = searchQuery.includes("@") ? "email" : "username";
+ try {
+ const data = await searchMutation.mutateAsync({
+ searchType,
+ keyword: searchQuery,
+ });
+ setUserData(data.data.content);
+ setTotalPage(1); // Usually search results are single page or differently handled by API
+ setCurrentPage(1);
+ } catch (error) {
+ console.error("Search failed", error);
+ setUserData([]);
+ }
+ } else {
+ // If empty, will revert to paginated list due to useEffect on listResponse
+ setCurrentPage(1);
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ handleSearchClick();
+ }
+ };
+
+ const handleSortChange = (sort: string) => {
+ setSelectedSort(sort);
+ setSearchQuery("");
+ setCurrentPage(1);
+ };
+
+ const isLoading = isListLoading || searchMutation.isPending;
+
+ return (
+
+
+
+
+
+
가입자 목록
+
+ 가나다순 정렬
+
+
+
+
handleSortChange("50명씩 정렬")}
+ />
+ handleSortChange("10명씩 정렬")}
+ />
+ handleSortChange("5명씩 정렬")}
+ />
+
+
+
+
+
+ {isLoading ? (
+
+ ) : userData.length > 0 ? (
+ userData.map((user, idx) => (
+
+ ))
+ ) : (
+
+ 검색 결과가 없습니다.
+
+ )}
+
+
+
+ {!searchQuery && (
+
+ )}
+
+
+ );
+}
+
+const SortButton = ({ label, isSelected, onClick }: any) => (
+
+);
diff --git a/app/adminpage/myPage/search/page.tsx b/app/adminpage/myPage/search/page.tsx
new file mode 100644
index 0000000..c62bf89
--- /dev/null
+++ b/app/adminpage/myPage/search/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminSearchPage from "./_components/ScreenAdminSearchPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/page.tsx b/app/adminpage/page.tsx
new file mode 100644
index 0000000..0e7c6b3
--- /dev/null
+++ b/app/adminpage/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminLoginPage from "./_components/ScreenAdminLoginPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/payrequest/_components/ScreenAdminPayRequestPage.tsx b/app/adminpage/payrequest/_components/ScreenAdminPayRequestPage.tsx
new file mode 100644
index 0000000..b6b954e
--- /dev/null
+++ b/app/adminpage/payrequest/_components/ScreenAdminPayRequestPage.tsx
@@ -0,0 +1,115 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { Search, RefreshCw } from "lucide-react";
+import { AdminHeader } from "../../_components/AdminHeader";
+import { RequestUserComponent } from "../../_components/RequestUserComponent";
+import { useAdminInfo } from "@/hooks/useAdminAuth";
+import { useChargeList } from "@/hooks/useAdminManagement";
+
+export default function ScreenAdminPayRequestPage() {
+ const [searchQuery, setSearchQuery] = useState("");
+ const [adminSelect, setAdminSelect] = useState("가입자관리");
+ const [filteredData, setFilteredData] = useState([]);
+
+ const { data: adminResponse } = useAdminInfo();
+ const adminData = adminResponse?.data;
+
+ const { data: chargeResponse, isLoading, refetch } = useChargeList();
+ const userData = chargeResponse?.data || [];
+
+ useEffect(() => {
+ if (userData) {
+ if (searchQuery) {
+ setTimeout(() => {
+ setFilteredData(
+ userData.filter((item: any) =>
+ item.username.toLowerCase().includes(searchQuery.toLowerCase()),
+ ),
+ );
+ }, 0);
+ } else {
+ setTimeout(() => setFilteredData(userData), 0);
+ }
+ }
+ }, [userData, searchQuery]);
+
+ const handleSearchChange = (e: React.ChangeEvent) => {
+ setSearchQuery(e.target.value);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ // Filter logic is handled by useEffect
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ 충전 요청 목록
+
+
+ 유저로부터 이름, 아이디, 입금 내역 확인해서 충전을 진행합니다.
+
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : filteredData.length > 0 ? (
+ filteredData.map((data, i) => (
+
+ ))
+ ) : (
+
+ 검색 결과가 없습니다.
+
+ )}
+
+
+
+
+ );
+}
diff --git a/app/adminpage/payrequest/page.tsx b/app/adminpage/payrequest/page.tsx
new file mode 100644
index 0000000..436b544
--- /dev/null
+++ b/app/adminpage/payrequest/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminPayRequestPage from "./_components/ScreenAdminPayRequestPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/register/_components/ScreenAdminRegisterPage.tsx b/app/adminpage/register/_components/ScreenAdminRegisterPage.tsx
new file mode 100644
index 0000000..1bff8f1
--- /dev/null
+++ b/app/adminpage/register/_components/ScreenAdminRegisterPage.tsx
@@ -0,0 +1,229 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client";
+
+import React, { useState } from "react";
+import { AdminRegisterHeader } from "../../_components/AdminHeader";
+import { useRouter } from "next/navigation";
+import { useAdminRegister } from "@/hooks/useAdminAuth";
+
+const InputComponent = ({
+ name,
+ title,
+ placeholder,
+ type,
+ options,
+ value,
+ onChange,
+}: any) => {
+ return (
+
+
+ {title}
+
+ {options ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default function ScreenAdminRegisterPage() {
+ const router = useRouter();
+ const registerMutation = useAdminRegister();
+
+ const [formData, setFormData] = useState({
+ accountId: "",
+ password: "",
+ confirmPassword: "",
+ schoolEmail: "",
+ nickname: "",
+ university: "",
+ role: "",
+ });
+
+ const handleChange = (
+ e: React.ChangeEvent,
+ ) => {
+ setFormData({ ...formData, [e.target.name]: e.target.value });
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ if (e) e.preventDefault();
+
+ const requiredFields = [
+ "accountId",
+ "password",
+ "confirmPassword",
+ "schoolEmail",
+ "nickname",
+ "university",
+ "role",
+ ];
+
+ for (const field of requiredFields) {
+ if (
+ !formData[field as keyof typeof formData] ||
+ formData[field as keyof typeof formData].trim() === ""
+ ) {
+ alert(`${field} 입력이 누락되었습니다.`);
+ return;
+ }
+ }
+
+ if (formData.password !== formData.confirmPassword) {
+ alert("비밀번호가 다릅니다. 다시 입력해주세요.");
+ return;
+ }
+
+ let roleToSend = "";
+ if (formData.role === "관리자") {
+ roleToSend = "ROLE_SEMI_ADMIN";
+ } else if (formData.role === "오퍼레이터") {
+ roleToSend = "ROLE_SEMI_OPERATOR";
+ }
+
+ const requestBody = {
+ accountId: formData.accountId,
+ password: formData.password,
+ schoolEmail: formData.schoolEmail,
+ nickname: formData.nickname,
+ university: formData.university,
+ role: roleToSend,
+ };
+
+ try {
+ const data = await registerMutation.mutateAsync(requestBody);
+
+ if (data.status === 200) {
+ if (formData.role === "관리자") {
+ alert("회원가입이 완료되었습니다.");
+ router.push("/adminpage");
+ } else if (formData.role === "오퍼레이터") {
+ alert(
+ "오퍼레이터 가입이 완료되었습니다. 관리자의 승인이 필요합니다.",
+ );
+ router.push("/adminpage");
+ }
+ } else if (data.status === 400 && formData.role === "관리자") {
+ alert("이미 해당 학교에 최고 관리자가 존재합니다.");
+ } else if (data.status === 400 && formData.role === "오퍼레이터") {
+ alert("중복된 계정의 사용자가 존재합니다.");
+ } else {
+ alert(`회원가입 실패: ${data.message || "에러가 발생했습니다."}`);
+ }
+ } catch (error) {
+ console.error("회원 가입 요청 중 에러 발생", error);
+ alert("회원가입 중 오류가 발생했습니다. 다시 시도해 주세요.");
+ }
+ };
+
+ const inputFields = [
+ {
+ name: "accountId",
+ title: "아이디",
+ placeholder: "아이디를 입력해주세요.",
+ type: "text",
+ },
+ {
+ name: "password",
+ title: "비밀번호",
+ placeholder: "비밀번호를 입력해주세요.",
+ type: "password",
+ },
+ {
+ name: "confirmPassword",
+ title: "비밀번호 확인",
+ placeholder: "비밀번호를 다시 한 번 입력해주세요.",
+ type: "password",
+ },
+ {
+ name: "schoolEmail",
+ title: "학교 웹메일",
+ placeholder: "웹메일을 입력해주세요.",
+ type: "email",
+ },
+ null,
+ null,
+ {
+ name: "nickname",
+ title: "이름",
+ placeholder: "실명을 입력해주세요.",
+ type: "text",
+ },
+ {
+ name: "university",
+ title: "소속 대학",
+ placeholder: "선택",
+ options: ["가톨릭대학교", "부천대학교", "동양미래대학교", "성공회대학교"],
+ },
+ {
+ name: "role",
+ title: "신청 권한",
+ placeholder: "선택",
+ options: ["관리자", "오퍼레이터"],
+ },
+ ];
+
+ return (
+
+
+
+
+ 가입하기
+
+ 관리자의 승인을 받은 이후 오퍼레이터 권한을 사용할 수 있습니다
+
+
+
+
+ {inputFields.map((field, index) =>
+ field === null ? (
+
+ ) : (
+
+ ),
+ )}
+
+
+
+ );
+}
diff --git a/app/adminpage/register/page.tsx b/app/adminpage/register/page.tsx
new file mode 100644
index 0000000..57bf269
--- /dev/null
+++ b/app/adminpage/register/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminRegisterPage from "./_components/ScreenAdminRegisterPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/user/[uuid]/PaymentHistory/_components/ScreenAdminPaymentHistoryPage.tsx b/app/adminpage/user/[uuid]/PaymentHistory/_components/ScreenAdminPaymentHistoryPage.tsx
new file mode 100644
index 0000000..ec6b37d
--- /dev/null
+++ b/app/adminpage/user/[uuid]/PaymentHistory/_components/ScreenAdminPaymentHistoryPage.tsx
@@ -0,0 +1,121 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client";
+
+import React, { useState } from "react";
+import { Coins } from "lucide-react";
+import { useParams } from "next/navigation";
+import { AdminHeader } from "../../../../_components/AdminHeader";
+import { useAdminInfo } from "@/hooks/useAdminAuth";
+import { usePaymentHistory } from "@/hooks/useAdminManagement";
+
+const PaymentHistoryComponent = ({
+ cancelReason,
+ orderId,
+ point,
+ price,
+ approvedAt,
+}: any) => {
+ const getStatusStyle = (reason: string) => {
+ switch (reason) {
+ case "결제 성공":
+ return { bg: "bg-[#3fea3a]", text: "text-[#3fea3a]" };
+ case "관리자에 의해 취소됨":
+ return { bg: "bg-[#ea3a3a]", text: "text-[#ea3a3a]" };
+ case "결제 취소":
+ return { bg: "bg-[#ff9800]", text: "text-[#ff9800]" };
+ default:
+ return { bg: "bg-gray-400", text: "text-gray-400" };
+ }
+ };
+
+ const statusStyle = getStatusStyle(cancelReason);
+
+ return (
+
+
+
+
+
+
+ 결제액 :
+
+ {price}
+
+ 원
+
+
+
+ 충전 포인트 :
+
+
+ {point}
+
+ P
+
+
+
+ );
+};
+
+export default function ScreenAdminPaymentHistoryPage() {
+ const { uuid } = useParams();
+ const [adminSelect, setAdminSelect] = useState("가입자관리");
+
+ const { data: adminResponse } = useAdminInfo();
+ const adminData = adminResponse?.data;
+
+ const { data: historyResponse, isLoading } = usePaymentHistory(
+ uuid as string,
+ );
+ const paymentHistory = historyResponse?.data || [];
+
+ return (
+
+
+
+
+
+
+ 결제내역
+
+
+ 최신순 정렬
+
+
+
+ {isLoading ? (
+
+ ) : paymentHistory.length > 0 ? (
+ paymentHistory.map((data: any, i: number) => (
+
+ ))
+ ) : (
+
+ 결제 내역이 없습니다.
+
+ )}
+
+
+
+
+ );
+}
diff --git a/app/adminpage/user/[uuid]/PaymentHistory/page.tsx b/app/adminpage/user/[uuid]/PaymentHistory/page.tsx
new file mode 100644
index 0000000..877ba0d
--- /dev/null
+++ b/app/adminpage/user/[uuid]/PaymentHistory/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminPaymentHistoryPage from "./_components/ScreenAdminPaymentHistoryPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/user/[uuid]/SendWarnMessage/_components/ScreenSendWarnMessagePage.tsx b/app/adminpage/user/[uuid]/SendWarnMessage/_components/ScreenSendWarnMessagePage.tsx
new file mode 100644
index 0000000..64ab8b0
--- /dev/null
+++ b/app/adminpage/user/[uuid]/SendWarnMessage/_components/ScreenSendWarnMessagePage.tsx
@@ -0,0 +1,225 @@
+"use client";
+
+import React, { useState } from "react";
+import { ChevronDown } from "lucide-react";
+import { useParams, useRouter } from "next/navigation";
+import { AdminHeader } from "../../../../_components/AdminHeader";
+import { useAdminInfo } from "@/hooks/useAdminAuth";
+import { useUserDetail, useSendWarnMessage } from "@/hooks/useAdminManagement";
+
+const warningMenu = [
+ "욕설 및 수치심을 주는 발언",
+ "특정인에 대한 비하 및 조롱",
+ "명예훼손, 사생활 노출, 신상 털기",
+ "협박 및 폭력성 발언, 인총자별",
+ "불법성(마약 등) 단어 언급",
+ "부적절한 미팅장소 제시",
+ "금전적 거래",
+ "불순한 의도의 다계정 생성",
+ "타인 명의 계정 이용 및 거래",
+ "스팸 및 광고 활동",
+ "단순 팔로워 늘리기 목적 및 홍보",
+ "포교 활동",
+ "조건 만남 및 성매매",
+ "스토킹",
+ "허위 프로필 및 사기, 관리자 사칭",
+];
+
+export default function ScreenSendWarnMessagePage() {
+ const { uuid } = useParams();
+ const router = useRouter();
+ const [adminSelect, setAdminSelect] = useState("가입자관리");
+ const [selectedReason, setSelectedReason] = useState("");
+ const [customReason, setCustomReason] = useState("");
+ const [showConfirm, setShowConfirm] = useState(false);
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+
+ const { data: adminResponse } = useAdminInfo();
+ const { data: userResponse } = useUserDetail(uuid as string);
+ const sendWarnMutation = useSendWarnMessage();
+
+ const adminData = adminResponse?.data;
+ const userData = userResponse?.data;
+
+ const handleSubmit = () => {
+ if (!selectedReason && !customReason) {
+ alert("경고 사유를 선택하거나 직접 입력해주세요.");
+ return;
+ }
+ setShowConfirm(true);
+ };
+
+ const handleSend = async () => {
+ const finalReason = [selectedReason, customReason]
+ .filter(Boolean)
+ .join(", ");
+ try {
+ await sendWarnMutation.mutateAsync({
+ uuid: uuid as string,
+ message: finalReason,
+ });
+ alert("경고 메시지가 전송되었습니다.");
+ router.push(`/adminpage/user/${uuid}`);
+ } catch (error) {
+ alert("전송 중 오류가 발생했습니다.");
+ }
+ };
+
+ if (!userData) return null;
+
+ return (
+
+
+
+
+
+
+ 가입자 상세정보
+
+
+
+
+ Nickname :
+
+
+ {userData.username}
+
+
+
+
+ E-mail :
+
+
+ {userData.email}
+
+
+
+
+
+ {!showConfirm ? (
+
+
+ 전송할 경고 사유
+
+
+
+
+
setIsDropdownOpen(!isDropdownOpen)}
+ >
+ {selectedReason || "선택"}
+
+
+
+ {isDropdownOpen && (
+
+ {warningMenu.map((reason) => (
+
{
+ setSelectedReason(reason);
+ setIsDropdownOpen(false);
+ }}
+ >
+ {reason}
+
+ ))}
+
+ )}
+
+
+
또는
+
+
setCustomReason(e.target.value)}
+ />
+
+
(으)로
+
+
+
+
+ 해당 가입자에게 경고 메시지를 보냅니다.
+
+
+
+
+ ) : (
+
+
+
+ {userData.username}님.{" "}
+ {[selectedReason, customReason].filter(Boolean).join(", ")}{" "}
+ (으)로
+
+ 1번 경고 드립니다.
+
- 관리자 안내 -
+
+
+
+
+
+
+
+
+
+ 경고 메시지 미리보기
+
+
+ 왼쪽과 같은 형태로 위 가입자에게 경고 메시지가 전송됩니다.
+
+ 경고 메시지는 신중하게 전송해 주십시오.
+
+ 이에 동의하십니까?
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/app/adminpage/user/[uuid]/SendWarnMessage/page.tsx b/app/adminpage/user/[uuid]/SendWarnMessage/page.tsx
new file mode 100644
index 0000000..061d29d
--- /dev/null
+++ b/app/adminpage/user/[uuid]/SendWarnMessage/page.tsx
@@ -0,0 +1,5 @@
+import ScreenSendWarnMessagePage from "./_components/ScreenSendWarnMessagePage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/user/[uuid]/_components/ScreenAdminUserDetailPage.tsx b/app/adminpage/user/[uuid]/_components/ScreenAdminUserDetailPage.tsx
new file mode 100644
index 0000000..56d4e63
--- /dev/null
+++ b/app/adminpage/user/[uuid]/_components/ScreenAdminUserDetailPage.tsx
@@ -0,0 +1,171 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { useParams, useRouter } from "next/navigation";
+import { AdminHeader } from "../../../_components/AdminHeader";
+import { useAdminInfo } from "@/hooks/useAdminAuth";
+import { useUserDetail } from "@/hooks/useAdminManagement";
+
+export default function ScreenAdminUserDetailPage() {
+ const { uuid } = useParams();
+ const router = useRouter();
+ const [adminSelect, setAdminSelect] = useState("가입자관리");
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const { data: adminResponse } = useAdminInfo();
+ const adminData = adminResponse?.data;
+
+ const { data: userResponse, isLoading } = useUserDetail(uuid as string);
+ const userData = userResponse?.data;
+
+ // Mock data for blacklist and gender based on old code structure
+ const isBlacklisted = false;
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ const handleCloseModal = () => {
+ setIsModalOpen(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+ 가입자 상세정보
+
+ {isBlacklisted && (
+
+ (이용제한 가입자)
+
+ )}
+
+
+
+
+
+ Nickname :
+
+
+ {userData?.username}
+
+
+
+
+ E-mail :
+
+
+ {userData?.email}
+
+
+
+
+
+ router.push(`/adminpage/user/${uuid}/warnhistory`)}
+ >
+ 경고 히스토리
+
+
+ router.push(`/adminpage/user/${uuid}/SendWarnMessage`)
+ }
+ >
+ 경고 메시지 전송
+
+ {}} // Handle blacklist toggle
+ >
+ {isBlacklisted ? "블랙리스트 해제" : "블랙리스트 추가"}
+
+
+
+
+
+
+ router.push(`/adminpage/user/${uuid}/PaymentHistory`)
+ }
+ />
+ {}}
+ />
+ router.push(`/adminpage/user/${uuid}/pointManage`)}
+ />
+
+
+ {isModalOpen && (
+
+
+
+ 해당 가입자에게 경고 메시지가
+ 전송 되었습니다.
+
+
+ 확인
+
+
+
+ )}
+
+
+ );
+}
+
+const FunctionButton = ({
+ children,
+ onClick,
+ isBlacklisted,
+ isBlacklist,
+}: any) => (
+
+);
+
+const ActionCard = ({ title, subText, onClick }: any) => (
+
+
+ {title}
+
+
{subText}
+
+);
diff --git a/app/adminpage/user/[uuid]/page.tsx b/app/adminpage/user/[uuid]/page.tsx
new file mode 100644
index 0000000..de846df
--- /dev/null
+++ b/app/adminpage/user/[uuid]/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminUserDetailPage from "./_components/ScreenAdminUserDetailPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/user/[uuid]/pointManage/_components/ScreenAdminPointManagePage.tsx b/app/adminpage/user/[uuid]/pointManage/_components/ScreenAdminPointManagePage.tsx
new file mode 100644
index 0000000..bcd4bab
--- /dev/null
+++ b/app/adminpage/user/[uuid]/pointManage/_components/ScreenAdminPointManagePage.tsx
@@ -0,0 +1,220 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { MinusCircle, PlusCircle } from "lucide-react";
+import { useParams } from "next/navigation";
+import { AdminHeader } from "../../../../_components/AdminHeader";
+import { useAdminInfo } from "@/hooks/useAdminAuth";
+import { useUserDetail, useAdjustPoint } from "@/hooks/useAdminManagement";
+
+export default function ScreenAdminPointManagePage() {
+ const { uuid } = useParams();
+ const [adminSelect, setAdminSelect] = useState("가입자관리");
+ const [pointsInput, setPointsInput] = useState("");
+ const [reason, setReason] = useState("");
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
+ const [showSuccessModal, setShowSuccessModal] = useState(false);
+ const [prevPoint, setPrevPoint] = useState(0);
+
+ const { data: adminResponse } = useAdminInfo();
+ const { data: userResponse, refetch: refetchUser } = useUserDetail(
+ uuid as string,
+ );
+ const adjustPointMutation = useAdjustPoint();
+
+ const adminData = adminResponse?.data;
+ const userData = userResponse?.data;
+
+ const adjustedPoints = pointsInput === "" ? 0 : Number(pointsInput);
+ const totalPoints = (userData?.point || 0) + adjustedPoints;
+ const isActive = adjustedPoints !== 0;
+
+ const handleIncrease = () => {
+ const newVal = Number(pointsInput) + 500;
+ if (newVal <= 30000) setPointsInput(newVal.toString());
+ };
+
+ const handleDecrease = () => {
+ const newVal = Number(pointsInput) - 500;
+ if (newVal >= -30000) setPointsInput(newVal.toString());
+ };
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ if (value === "" || /^[-]?\d*$/.test(value)) {
+ setPointsInput(value);
+ }
+ };
+
+ const handleSubmit = () => {
+ if (reason.length < 5 || adjustedPoints === 0) {
+ alert("조정할 포인트가 0이 아니고 사유가 5자 이상이어야 합니다.");
+ return;
+ }
+ if (adjustedPoints > 30000 || adjustedPoints < -30000) {
+ alert("포인트는 최대 30000까지 조절 가능합니다.");
+ return;
+ }
+ if (totalPoints < 0) {
+ alert("포인트를 음수로 조정할 수 없습니다.");
+ return;
+ }
+ setShowConfirmModal(true);
+ };
+
+ const handleConfirmAdjust = async () => {
+ try {
+ if (userData) setPrevPoint(userData.point);
+ await adjustPointMutation.mutateAsync({
+ uuid: uuid as string,
+ point: adjustedPoints,
+ reason: reason,
+ });
+ setShowConfirmModal(false);
+ setShowSuccessModal(true);
+ await refetchUser();
+ setPointsInput("");
+ setReason("");
+ } catch (error) {
+ alert("포인트 조정에 실패했습니다.");
+ }
+ };
+
+ if (!userData) return null;
+
+ return (
+
+
+
+
+
+
+ 포인트 조정
+
+
+ 오류 및 패널티 관련 포인트 조정 기능. 포인트는 최대 한 번에
+ 30000P까지 조정 가능합니다.
+
+
+
+
+ Nickname :
+
+
+ {userData.username}
+
+
+
+
+ Available Points : {userData.point}P
+
+
+
+
포인트 조정 :
+
+
0 ? "text-[#ff7752]" : "text-[#1a1a1a]") : "text-[#808080]"}`}
+ />
+
+
+
+
+ 조정 후 포인트 : {totalPoints}
+
+
+
+
+
+
+ 포인트 조정 사유
+
+
+ (5자 이상)
+
+
+
+ 모든 조정 사유는 히스토리에 기록되며, 이후 수정 또는 삭제할 수
+ 없습니다.
+
+
+
+ {showConfirmModal && (
+
+
+
+ 정말로 포인트를 조정하시겠습니까?
이 작업은
+
+ 수정 또는 삭제할 수 없습니다.
+
+
+
setShowConfirmModal(false)}
+ >
+ 취소
+
+
+ 확인
+
+
+
+
+ )}
+
+ {showSuccessModal && (
+
+
+
+ 해당 가입자의 포인트를
+ 정상적으로 조정하였습니다.
+
+ {prevPoint}P {"->"} {userData.point}P
+
+
setShowSuccessModal(false)}
+ >
+ 확인
+
+
+
+ )}
+
+
+ );
+}
diff --git a/app/adminpage/user/[uuid]/pointManage/page.tsx b/app/adminpage/user/[uuid]/pointManage/page.tsx
new file mode 100644
index 0000000..12ae390
--- /dev/null
+++ b/app/adminpage/user/[uuid]/pointManage/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminPointManagePage from "./_components/ScreenAdminPointManagePage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/user/[uuid]/warnhistory/_components/ScreenAdminUserWarningHistoryPage.tsx b/app/adminpage/user/[uuid]/warnhistory/_components/ScreenAdminUserWarningHistoryPage.tsx
new file mode 100644
index 0000000..b8a7d11
--- /dev/null
+++ b/app/adminpage/user/[uuid]/warnhistory/_components/ScreenAdminUserWarningHistoryPage.tsx
@@ -0,0 +1,58 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+"use client";
+
+import React, { useState } from "react";
+import { useParams } from "next/navigation";
+import { AdminHeader } from "../../../../_components/AdminHeader";
+import { AdminWarnItem } from "../../../../_components/AdminWarnItem";
+import { useAdminInfo } from "@/hooks/useAdminAuth";
+import { useWarnHistory } from "@/hooks/useAdminManagement";
+
+export default function ScreenAdminUserWarningHistoryPage() {
+ const { uuid } = useParams();
+ const [adminSelect, setAdminSelect] = useState("가입자관리");
+ const { data: adminResponse } = useAdminInfo();
+ const { data: warnResponse } = useWarnHistory(uuid as string);
+
+ const adminData = adminResponse?.data;
+ const warnList = warnResponse?.data || [];
+
+ return (
+
+
+
+
+
+
+ 경고 히스토리
+
+
+ 관리자에 의한 경고 및 누적 히스토리. 경고 처리는 철회할 수 없습니다.
+
+
+
+ {warnList.length > 0 ? (
+ warnList.map((item: any, i: number) => (
+
+ ))
+ ) : (
+
+ 경고 내역이 없습니다.
+
+ )}
+
+
+
+
+ );
+}
diff --git a/app/adminpage/user/[uuid]/warnhistory/page.tsx b/app/adminpage/user/[uuid]/warnhistory/page.tsx
new file mode 100644
index 0000000..041017b
--- /dev/null
+++ b/app/adminpage/user/[uuid]/warnhistory/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminUserWarningHistoryPage from "./_components/ScreenAdminUserWarningHistoryPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/app/adminpage/webmail-check/_components/ScreenAdminWebmailPage.tsx b/app/adminpage/webmail-check/_components/ScreenAdminWebmailPage.tsx
new file mode 100644
index 0000000..b8477f5
--- /dev/null
+++ b/app/adminpage/webmail-check/_components/ScreenAdminWebmailPage.tsx
@@ -0,0 +1,165 @@
+"use client";
+
+import React, { useEffect, useRef, useState } from "react";
+import { useRouter } from "next/navigation";
+import { AdminRegisterHeader } from "../../_components/AdminHeader";
+import { useVerifyWebmail, useResendWebmail } from "@/hooks/useAdminAuth";
+
+const EMAIL_VALID_DURATION = 180;
+
+export default function ScreenAdminWebmailPage() {
+ const router = useRouter();
+ const [values, setValues] = useState(Array(6).fill(""));
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
+ const [timeLeft, setTimeLeft] = useState(EMAIL_VALID_DURATION);
+ const [failCount, setFailCount] = useState(0);
+
+ const verifyMutation = useVerifyWebmail();
+ const resendMutation = useResendWebmail();
+
+ useEffect(() => {
+ const storedTimestamp = localStorage.getItem("emailSendTimestamp");
+ if (!storedTimestamp) {
+ localStorage.setItem("emailSendTimestamp", Date.now().toString());
+ } else {
+ const elapsed = Math.floor(
+ (Date.now() - parseInt(storedTimestamp, 10)) / 1000,
+ );
+ const remaining = EMAIL_VALID_DURATION - elapsed;
+ setTimeout(() => setTimeLeft(remaining > 0 ? remaining : 0), 0);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (timeLeft <= 0) return;
+ const timerId = setInterval(() => {
+ setTimeLeft((prev) => (prev > 0 ? prev - 1 : 0));
+ }, 1000);
+ return () => clearInterval(timerId);
+ }, [timeLeft]);
+
+ const handleChange = (
+ e: React.ChangeEvent,
+ index: number,
+ ) => {
+ const value = e.target.value;
+ if (value && !/^\d$/.test(value)) return;
+
+ const newValues = [...values];
+ newValues[index] = value;
+ setValues(newValues);
+
+ if (value && index < 5) {
+ inputRefs.current[index + 1]?.focus();
+ }
+ };
+
+ const handleKeyDown = (
+ e: React.KeyboardEvent,
+ index: number,
+ ) => {
+ if (e.key === "Backspace" && !values[index] && index > 0) {
+ inputRefs.current[index - 1]?.focus();
+ }
+ };
+
+ const handleResend = async () => {
+ try {
+ await resendMutation.mutateAsync();
+ localStorage.setItem("emailSendTimestamp", Date.now().toString());
+ setTimeLeft(EMAIL_VALID_DURATION);
+ alert("인증코드가 재발송되었습니다.");
+ } catch (error) {
+ alert("재발송에 실패했습니다.");
+ }
+ };
+
+ const handleVerify = async () => {
+ const codeString = values.join("");
+ if (codeString.length !== 6) {
+ alert("6자리 숫자를 모두 입력해주세요.");
+ return;
+ }
+ if (timeLeft <= 0) {
+ alert("시간이 만료되었습니다. 재발송 버튼을 눌러주세요.");
+ return;
+ }
+
+ try {
+ await verifyMutation.mutateAsync(codeString);
+ alert("인증에 성공했습니다.");
+ router.push("/adminpage");
+ } catch (error) {
+ setFailCount((prev) => prev + 1);
+ alert("인증코드가 일치하지 않습니다.");
+ }
+ };
+
+ const minutes = Math.floor(timeLeft / 60);
+ const seconds = timeLeft % 60;
+
+ return (
+
+
+
+
+
+
+ 웹메일 인증하기
+
+
+ 이전 절차에서 입력한 웹메일로 6자리 숫자코드가 전송되었습니다.
+
+ 이메일 코드는 3분 뒤에 만료되며, 최대 10번의 입력이 가능합니다.
+ 이후에는 재발송 버튼을 눌러 인증코드를 다시 받아 주십시오.
+
+
+
+
+
+ {values.map((value, index) => (
+ {
+ inputRefs.current[index] = el;
+ }}
+ type="text"
+ inputMode="numeric"
+ maxLength={1}
+ value={value}
+ onChange={(e) => handleChange(e, index)}
+ onKeyDown={(e) => handleKeyDown(e, index)}
+ className="h-12 w-12 rounded-xl border-none bg-[#e5e5e5] text-center text-3xl font-bold text-black transition-all outline-none focus:ring-2 focus:ring-[#ff775e] md:h-16 md:w-16"
+ />
+ ))}
+
+
+
+
+ {timeLeft > 0 ? (
+ <>
+ 남은 시간: {minutes}:{seconds.toString().padStart(2, "0")}
+ >
+ ) : (
+ "유효시간이 만료되었습니다."
+ )}
+
+
+
+
+
+ 인증이 가능한 횟수 {10 - failCount}회
+
+
+
+
+ );
+}
diff --git a/app/adminpage/webmail-check/page.tsx b/app/adminpage/webmail-check/page.tsx
new file mode 100644
index 0000000..8009451
--- /dev/null
+++ b/app/adminpage/webmail-check/page.tsx
@@ -0,0 +1,5 @@
+import ScreenAdminWebmailPage from "./_components/ScreenAdminWebmailPage";
+
+export default function Page() {
+ return ;
+}
diff --git a/hooks/useAdminAuth.ts b/hooks/useAdminAuth.ts
new file mode 100644
index 0000000..3d5f14b
--- /dev/null
+++ b/hooks/useAdminAuth.ts
@@ -0,0 +1,106 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { api } from "@/lib/axios";
+import { useMutation, useQuery } from "@tanstack/react-query";
+
+export type AdminLoginRequest = {
+ accountId: string;
+ password: string;
+};
+
+export type AdminLoginResponse = {
+ status: number;
+ message?: string;
+ redirectUrl?: string;
+ data?: any;
+};
+
+export type AdminInfoResponse = {
+ status: number;
+ message: string;
+ data: {
+ accountId: string;
+ schoolEmail: string;
+ nickname: string;
+ role: string;
+ university: string;
+ universityAuth: string;
+ };
+};
+
+export type AdminRegisterRequest = {
+ accountId: string;
+ password: string;
+ schoolEmail: string;
+ nickname: string;
+ university: string;
+ role: string;
+};
+
+export type AdminRegisterResponse = {
+ status: number;
+ message: string;
+};
+
+const adminLogin = async (
+ payload: AdminLoginRequest,
+): Promise => {
+ const { data } = await api.post("/admin/login", payload);
+ return data;
+};
+
+const getAdminInfo = async (): Promise => {
+ const { data } = await api.get("/auth/any-admin/info");
+ return data;
+};
+
+const adminRegister = async (
+ payload: AdminRegisterRequest,
+): Promise => {
+ const { data } = await api.post(
+ "/admin/register",
+ payload,
+ );
+ return data;
+};
+
+const verifyWebmail = async (code: string) => {
+ const { data } = await api.post("/auth/admin/webmail/verify", { code });
+ return data;
+};
+
+const resendWebmail = async () => {
+ const { data } = await api.post("/auth/admin/webmail/resend");
+ return data;
+};
+
+export const useAdminLogin = () => {
+ return useMutation({
+ mutationFn: adminLogin,
+ });
+};
+
+export const useAdminInfo = () => {
+ return useQuery({
+ queryKey: ["adminInfo"],
+ queryFn: getAdminInfo,
+ retry: false,
+ });
+};
+
+export const useVerifyWebmail = () => {
+ return useMutation({
+ mutationFn: verifyWebmail,
+ });
+};
+
+export const useResendWebmail = () => {
+ return useMutation({
+ mutationFn: resendWebmail,
+ });
+};
+
+export const useAdminRegister = () => {
+ return useMutation({
+ mutationFn: adminRegister,
+ });
+};
diff --git a/hooks/useAdminManagement.ts b/hooks/useAdminManagement.ts
new file mode 100644
index 0000000..b510bb6
--- /dev/null
+++ b/hooks/useAdminManagement.ts
@@ -0,0 +1,318 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { api } from "@/lib/axios";
+import { useMutation, useQuery } from "@tanstack/react-query";
+
+export type UserListItem = {
+ uuid: string;
+ email: string;
+ username: string;
+ role: string;
+};
+
+export type UserListResponse = {
+ status: number;
+ message: string;
+ data: {
+ content: UserListItem[];
+ page: {
+ size: number;
+ number: number;
+ totalElements: number;
+ totalPages: number;
+ };
+ };
+};
+
+export type AdminActionResponse = {
+ status: number;
+ message: string;
+ data: any;
+};
+
+const getUserList = async (
+ page: number,
+ size: number,
+): Promise => {
+ const { data } = await api.get(`/auth/operator/user-list`, {
+ params: { page, size },
+ });
+ return data;
+};
+
+const searchUsers = async (
+ searchType: "email" | "username",
+ keyword: string,
+): Promise => {
+ const { data } = await api.get(`/auth/operator/user-list`, {
+ params: { searchType, keyword },
+ });
+ return data;
+};
+
+const toggle1000Button = async (): Promise => {
+ const { data } = await api.get("/auth/admin/make1000");
+ return data;
+};
+
+const getUserDetail = async (uuid: string): Promise => {
+ const { data } = await api.get(`/auth/operator/user`, {
+ params: { uuid },
+ });
+ return data;
+};
+
+const getPaymentHistory = async (
+ uuid: string,
+): Promise => {
+ const { data } = await api.get(
+ `/auth/operator/api/history/payment/${uuid}`,
+ );
+ return data;
+};
+
+const adjustPoint = async (payload: {
+ uuid: string;
+ point: number;
+ reason: string;
+}): Promise => {
+ const { data } = await api.patch(
+ `/auth/operator/api/point`,
+ payload,
+ );
+ return data;
+};
+
+const registerNotice = async (payload: {
+ title: string;
+ content: string;
+ postedAt: string;
+ closedAt: string;
+}): Promise => {
+ const { data } = await api.post(
+ "/auth/admin/notice",
+ payload,
+ );
+ return data;
+};
+
+const getNoticeList = async (
+ type: "RESERVATION" | "HISTORY",
+): Promise => {
+ const { data } = await api.get(
+ `/auth/admin/notice/list?type=${type}`,
+ );
+ return data;
+};
+
+const deleteNotice = async (noticeId: number): Promise => {
+ const { data } = await api.delete(
+ `/auth/admin/notice/${noticeId}`,
+ );
+ return data;
+};
+
+const registerDiscountEvent = async (payload: {
+ eventType: "DISCOUNT";
+ start: string;
+ end: string;
+ discountRate: string;
+}): Promise => {
+ const { data } = await api.post(
+ "/auth/admin/event/discount",
+ payload,
+ );
+ return data;
+};
+
+const registerFreeMatchEvent = async (payload: {
+ eventType: "FREE_MATCH";
+ start: string;
+ end: string;
+}): Promise => {
+ const { data } = await api.post(
+ "/auth/admin/event/free-match",
+ payload,
+ );
+ return data;
+};
+
+const getEventList = async (
+ type: "RESERVATION" | "HISTORY",
+): Promise => {
+ const { data } = await api.get(
+ `/auth/admin/event/list?type=${type}`,
+ );
+ return data;
+};
+
+const deleteEvent = async (eventId: number): Promise => {
+ const { data } = await api.delete(
+ `/auth/admin/event/${eventId}`,
+ );
+ return data;
+};
+
+const getWarnHistory = async (uuid: string): Promise => {
+ const { data } = await api.get(
+ `/auth/operator/user/warnhistory?uuid=${uuid}`,
+ );
+ return data;
+};
+
+const sendWarnMessage = async (payload: {
+ uuid: string;
+ message: string;
+}): Promise => {
+ const { data } = await api.post(
+ `/auth/operator/user/warn`,
+ payload,
+ );
+ return data;
+};
+
+const getChargeList = async (): Promise => {
+ const { data } = await api.get(
+ "/auth/operator/tempay/charge-list",
+ );
+ return data;
+};
+
+const approveCharge = async (orderId: string): Promise => {
+ const { data } = await api.post(
+ "/auth/operator/tempay/approval",
+ { orderId },
+ );
+ return data;
+};
+
+const rejectCharge = async (orderId: string): Promise => {
+ const { data } = await api.delete(
+ "/auth/operator/tempay/refund",
+ { data: { orderId } },
+ );
+ return data;
+};
+
+export const useUserList = (page: number, size: number) => {
+ return useQuery({
+ queryKey: ["userList", page, size],
+ queryFn: () => getUserList(page, size),
+ });
+};
+
+export const useChargeList = () => {
+ return useQuery({
+ queryKey: ["chargeList"],
+ queryFn: getChargeList,
+ });
+};
+
+export const useApproveCharge = () => {
+ return useMutation({
+ mutationFn: approveCharge,
+ });
+};
+
+export const useRejectCharge = () => {
+ return useMutation({
+ mutationFn: rejectCharge,
+ });
+};
+
+export const useUserDetail = (uuid: string) => {
+ return useQuery({
+ queryKey: ["userDetail", uuid],
+ queryFn: () => getUserDetail(uuid),
+ enabled: !!uuid,
+ });
+};
+
+export const useSearchUsers = () => {
+ return useMutation({
+ mutationFn: ({
+ searchType,
+ keyword,
+ }: {
+ searchType: "email" | "username";
+ keyword: string;
+ }) => searchUsers(searchType, keyword),
+ });
+};
+
+export const useSendWarnMessage = () => {
+ return useMutation({
+ mutationFn: sendWarnMessage,
+ });
+};
+
+export const useAdjustPoint = () => {
+ return useMutation({
+ mutationFn: adjustPoint,
+ });
+};
+
+export const useRegisterNotice = () => {
+ return useMutation({
+ mutationFn: registerNotice,
+ });
+};
+
+export const useNoticeList = (type: "RESERVATION" | "HISTORY") => {
+ return useQuery({
+ queryKey: ["noticeList", type],
+ queryFn: () => getNoticeList(type),
+ });
+};
+
+export const useDeleteNotice = () => {
+ return useMutation({
+ mutationFn: deleteNotice,
+ });
+};
+
+export const useRegisterDiscountEvent = () => {
+ return useMutation({
+ mutationFn: registerDiscountEvent,
+ });
+};
+
+export const useRegisterFreeMatchEvent = () => {
+ return useMutation({
+ mutationFn: registerFreeMatchEvent,
+ });
+};
+
+export const useEventList = (type: "RESERVATION" | "HISTORY") => {
+ return useQuery({
+ queryKey: ["eventList", type],
+ queryFn: () => getEventList(type),
+ });
+};
+
+export const useDeleteEvent = () => {
+ return useMutation({
+ mutationFn: deleteEvent,
+ });
+};
+
+export const useWarnHistory = (uuid: string) => {
+ return useQuery({
+ queryKey: ["warnHistory", uuid],
+ queryFn: () => getWarnHistory(uuid),
+ enabled: !!uuid,
+ });
+};
+
+export const usePaymentHistory = (uuid: string) => {
+ return useQuery({
+ queryKey: ["paymentHistory", uuid],
+ queryFn: () => getPaymentHistory(uuid),
+ enabled: !!uuid,
+ });
+};
+
+export const useToggle1000Button = () => {
+ return useMutation({
+ mutationFn: toggle1000Button,
+ });
+};
diff --git a/public/logo/admin_header_logo.svg b/public/logo/admin_header_logo.svg
new file mode 100644
index 0000000..0e5797a
--- /dev/null
+++ b/public/logo/admin_header_logo.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/logo/admin_page_logo.svg b/public/logo/admin_page_logo.svg
new file mode 100644
index 0000000..28a67d2
--- /dev/null
+++ b/public/logo/admin_page_logo.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/logo/coin.svg b/public/logo/coin.svg
new file mode 100644
index 0000000..bc500dd
--- /dev/null
+++ b/public/logo/coin.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/logo/empty-heart.svg b/public/logo/empty-heart.svg
new file mode 100644
index 0000000..88437bf
--- /dev/null
+++ b/public/logo/empty-heart.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/logo/event-register-heart.svg b/public/logo/event-register-heart.svg
new file mode 100644
index 0000000..898b9ad
--- /dev/null
+++ b/public/logo/event-register-heart.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/logo/female-icon.svg b/public/logo/female-icon.svg
new file mode 100644
index 0000000..b417328
--- /dev/null
+++ b/public/logo/female-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/logo/full-heart.svg b/public/logo/full-heart.svg
new file mode 100644
index 0000000..66c0f44
--- /dev/null
+++ b/public/logo/full-heart.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/logo/male-icon.svg b/public/logo/male-icon.svg
new file mode 100644
index 0000000..dfa0bf5
--- /dev/null
+++ b/public/logo/male-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/logo/minus-button.svg b/public/logo/minus-button.svg
new file mode 100644
index 0000000..d395a64
--- /dev/null
+++ b/public/logo/minus-button.svg
@@ -0,0 +1,18 @@
+
diff --git a/public/logo/modal-warn.svg b/public/logo/modal-warn.svg
new file mode 100644
index 0000000..dd47619
--- /dev/null
+++ b/public/logo/modal-warn.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/logo/not-allowed.svg b/public/logo/not-allowed.svg
new file mode 100644
index 0000000..193c2f6
--- /dev/null
+++ b/public/logo/not-allowed.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/logo/plus-button.svg b/public/logo/plus-button.svg
new file mode 100644
index 0000000..c473b42
--- /dev/null
+++ b/public/logo/plus-button.svg
@@ -0,0 +1,18 @@
+
diff --git a/public/logo/refresh-button.svg b/public/logo/refresh-button.svg
new file mode 100644
index 0000000..c487a57
--- /dev/null
+++ b/public/logo/refresh-button.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/logo/search-logo.svg b/public/logo/search-logo.svg
new file mode 100644
index 0000000..d126b45
--- /dev/null
+++ b/public/logo/search-logo.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/logo/under-triangle.svg b/public/logo/under-triangle.svg
new file mode 100644
index 0000000..e91a743
--- /dev/null
+++ b/public/logo/under-triangle.svg
@@ -0,0 +1,3 @@
+
diff --git a/utils/dateFormatter.ts b/utils/dateFormatter.ts
new file mode 100644
index 0000000..00091e5
--- /dev/null
+++ b/utils/dateFormatter.ts
@@ -0,0 +1,31 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+export const formatDateTime = (input: any) => {
+ if (!input) return { date: "N/A", time: "N/A" };
+
+ if (Array.isArray(input) && input.length >= 5) {
+ const [year, month, day, hour, minute] = input;
+ return {
+ date: `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`,
+ time: `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`,
+ };
+ }
+
+ if (typeof input === "string") {
+ try {
+ const date = new Date(input);
+ if (isNaN(date.getTime())) return { date: "N/A", time: "N/A" };
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ const hours = String(date.getHours()).padStart(2, "0");
+ const minutes = String(date.getMinutes()).padStart(2, "0");
+ return {
+ date: `${year}-${month}-${day}`,
+ time: `${hours}:${minutes}`,
+ };
+ } catch {
+ return { date: "N/A", time: "N/A" };
+ }
+ }
+ return { date: "N/A", time: "N/A" };
+};