From 2b45d582b68205f11e9ae2979793550272d40777 Mon Sep 17 00:00:00 2001 From: pwc2002 Date: Sun, 7 Sep 2025 16:33:40 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=BD=94=EC=96=B4=20=EB=A9=A4=EB=B2=84?= =?UTF-8?q?=20=EC=A7=80=EC=9B=90=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EB=8F=99=EC=9D=98=20=EC=B2=B4=ED=81=AC=EB=B0=95=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9D=BC=EC=A0=95=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/core-recruit/page.jsx | 81 +++++++-- src/app/core-recruit/submit/page.jsx | 14 +- src/app/coreadmin/layout.js | 20 +++ src/app/coreadmin/page.jsx | 194 ++++++++++++++++++++++ src/components/admin/UserDetailsModal.jsx | 39 ++++- src/components/ui/common/MenuHeader.jsx | 6 + 6 files changed, 331 insertions(+), 23 deletions(-) create mode 100644 src/app/coreadmin/layout.js create mode 100644 src/app/coreadmin/page.jsx diff --git a/src/app/core-recruit/page.jsx b/src/app/core-recruit/page.jsx index e791ee5..3856ff4 100644 --- a/src/app/core-recruit/page.jsx +++ b/src/app/core-recruit/page.jsx @@ -28,6 +28,7 @@ export default function CoreRecruit() { const [uploading, setUploading] = useState(false); const [submitting, setSubmitting] = useState(false); const [touched, setTouched] = useState(false); + const [agreed, setAgreed] = useState(false); const handleValueChange = (key) => (value) => { setForm((prev) => ({ ...prev, [key]: value })); @@ -116,9 +117,22 @@ export default function CoreRecruit() { return (
-

+

{/* 구글 로고 - 커스텀 그라데이션 적용 */} - + @@ -222,7 +236,16 @@ export default function CoreRecruit() { - GDGoC INHA Core Member 지원 + + GDGoC INHA Core Member 지원 +

@@ -464,17 +487,47 @@ export default function CoreRecruit() {
-
마지막으로 일정을 확인해주시기 바랍니다!
-
-
코어멤버 모집 안내
-
✅ 서류 지원 기간 : 2025년 09월 8일 (월) - 2025년 09월 12일 (금) 23:59:59
-
✅ 서류 결과 발표 : ~ 2025년 09월 14일 (일)
-
✅ 면접 진행 기간 : 2025년 09월 15일 (월) - 2025년 09월 19일 (금)
-
✅ 최종 결과 발표 : ~ 2025년 09월 21일 (일)
-
※ 면접은 지원자와 면접관의 일정에 따라 조정되며, 인하대학교 내부 장소에서 진행됩니다.
-
※ 면접은 대면을 원칙으로 하며, 부득이한 경우에만 비대면으로 진행됩니다.
-
❗️운영진으로 활동 시, 매주 화요일 19:00~21:00 정기 운영진 회의에 반드시 참석해야 합니다.
+
+ 마지막으로 공지 및 일정을 확인해주시기 바랍니다!
+
+
+
모집 일정
+
✅ 서류 지원 기간 : 2025년 09월 8일 (월) ~ 2025년 09월 21일 (일) 11:59:59
+
✅ 서류 결과 발표 : ~ 2025년 09월 21일 (일)
+
✅ 면접 진행 기간 : 2025년 09월 22일 (월) ~ 2025년 09월 26일 (금)
+
※ 지원자 및 면접관의 일정에 따라 마감 전 조기 면접 진행이 가능할 수 있습니다.
+
✅ 최종 결과 발표 : ~ 2025년 09월 28일 (일)
+
❗️ 첫 온보딩 : 2025년 09월 30일 (화)
+
+
+
+
면접 안내
+
• 원칙적으로 대면 면접을 진행하며, 부득이한 경우에 한해 비대면으로 조정될 수 있습니다.
+
• 면접은 인하대학교 내부 장소에서 진행됩니다.
+
+
+
+
활동 안내
+
• 운영진으로 활동 시, 매주 화요일 19:00~21:00 정기 운영진 회의에 필수참석해야합니다.
+
• 단, 2시간 전체 참석이 아닌 최소 1시간 이상 필참을 원칙으로 합니다.
+
+
+
+
+ + 공지사항 및 일정을 확인하였으며, 이에 동의합니다. +
@@ -485,7 +538,7 @@ export default function CoreRecruit() {
diff --git a/src/app/core-recruit/submit/page.jsx b/src/app/core-recruit/submit/page.jsx index d0f2a1f..3722027 100644 --- a/src/app/core-recruit/submit/page.jsx +++ b/src/app/core-recruit/submit/page.jsx @@ -25,14 +25,12 @@ export default function CoreRecruitSubmit() {
코어 멤버 지원서 제출이 완료되었습니다.
-
아래 일정과 안내를 한번더 확인해주세요!
-
✅ 서류 지원 기간 : 2025년 09월 8일 (월) - 2025년 09월 12일 (금) 23:59:59
-
✅ 서류 결과 발표 : ~ 2025년 09월 14일 (일)
-
✅ 면접 진행 기간 : 2025년 09월 15일 (월) - 2025년 09월 19일 (금)
-
✅ 최종 결과 발표 : ~ 2025년 09월 21일 (일)
-
※ 면접은 지원자와 면접관의 일정에 따라 조정되며, 인하대학교 내부 장소에서 진행됩니다.
-
※ 면접은 대면을 원칙으로 하며, 부득이한 경우에만 비대면으로 진행됩니다.
-
❗️운영진으로 활동 시, 매주 화요일 19:00~21:00 정기 운영진 회의에 반드시 참석해야 합니다.
+
아래 일정을 한번 더 확인해주세요!
+
✅ 서류 지원 기간 : 2025년 09월 8일 (월) ~ 2025년 09월 21일 (일) 11:59:59
+
✅ 서류 결과 발표 : ~ 2025년 09월 21일 (일)
+
✅ 면접 진행 기간 : 2025년 09월 22일 (월) ~ 2025년 09월 26일 (금)
+
※ 지원자 및 면접관의 일정에 따라 마감 전 조기 면접 진행이 가능할 수 있습니다.
+
✅ 최종 결과 발표 : ~ 2025년 09월 28일 (일)
diff --git a/src/app/coreadmin/layout.js b/src/app/coreadmin/layout.js new file mode 100644 index 0000000..3091e43 --- /dev/null +++ b/src/app/coreadmin/layout.js @@ -0,0 +1,20 @@ +import MenuHeader from '@/components/ui/common/MenuHeader'; +import ApiCodeGuard from '@/components/auth/ApiCodeGuard.jsx'; + +export const metadata = { + title: 'CoreAdmin', + description: 'Core member application management', +}; + +export default function CoreAdminLayout({ children }) { + return ( + + <> + + {children} + + + ); +} + + diff --git a/src/app/coreadmin/page.jsx b/src/app/coreadmin/page.jsx new file mode 100644 index 0000000..96ca2b1 --- /dev/null +++ b/src/app/coreadmin/page.jsx @@ -0,0 +1,194 @@ +"use client"; + +import React, { useCallback, useEffect, useRef } from 'react'; +import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Chip } from '@nextui-org/react'; + +import UserDetailsModal from '@/components/admin/UserDetailsModal'; +import AdminTableTopContent from '@/components/admin/AdminTableTopContent'; +import AdminTableBottomContent from '@/components/admin/AdminTableBottomContent'; +import { useAuthenticatedApi } from '@/hooks/useAuthenticatedApi'; + +const columns = [ + { name: 'NAME', uid: 'name' }, + { name: 'MAJOR / ID', uid: 'major' }, + { name: 'TEAM', uid: 'team' }, + { name: 'EMAIL', uid: 'email' }, + { name: 'PHONE', uid: 'phone' }, +]; + +export default function CoreAdminPage() { + const { apiClient } = useAuthenticatedApi(); + + const [page, setPage] = React.useState(1); + const [modalOpen, setModalOpen] = React.useState(false); + const modalClosing = useRef(false); + + const [selectedUser, setSelectedUser] = React.useState(null); + const [searchValue, setSearchValue] = React.useState(''); + const [query, setQuery] = React.useState(''); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(''); + const [currentUsers, setCurrentUsers] = React.useState([]); + const [totalUsers, setTotalUsers] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + + const rowsPerPage = 10; + + const fetchApplicants = useCallback(async () => { + setLoading(true); + setError(''); + try { + const params = { + page: page - 1, + size: rowsPerPage, + sort: 'createdAt', + dir: 'DESC', + question: query || undefined, + }; + const res = await apiClient.get('/core-recruit/applicants', { params }); + const list = Array.isArray(res?.data?.data) ? res.data.data : []; + const total = res?.data?.meta?.totalElements ?? list.length; + const computedTotalPages = Math.max(1, Math.ceil(total / rowsPerPage)); + + setCurrentUsers(list); + setTotalUsers(total); + setTotalPages(computedTotalPages); + } catch (err) { + setError(String(err?.message || 'failed to load applicants')); + setCurrentUsers([]); + setTotalUsers(0); + } finally { + setLoading(false); + } + }, [apiClient, page, rowsPerPage, query]); + + const renderCell = useCallback((user, columnKey) => { + const value = user[columnKey]; + switch (columnKey) { + case 'name': + return {user?.name ?? ''}; + case 'major': + return ( +
+

{user?.major ?? ''}

+

{user?.studentId ?? ''}

+
+ ); + case 'team': { + const teamLabel = user?.team ?? ''; + const teamColorMap = { + HR: '#EA4335', + BD: '#34A853', + TECH: '#4285F4', + 'PR/DESIGN': '#F9AB00', + }; + const color = teamColorMap[teamLabel] || '#9CA3AF'; + return ( + + {teamLabel} + + ); + } + case 'email': + return {user?.email ?? ''}; + case 'phone': + return {user?.phone ?? ''}; + case 'createdAt': + return {user?.createdAt ? new Date(user.createdAt).toLocaleString() : ''}; + default: + return value; + } + }, []); + + const handleRowClick = async (user) => { + if (modalClosing.current) return; + try { + const id = user?.id; + if (!id) throw new Error('지원자 ID 없음'); + const res = await apiClient.get(`/core-recruit/applicants/${id}`); + const detail = res?.data?.data ?? null; + if (!detail) throw new Error('상세 정보 없음'); + setSelectedUser(detail); + setModalOpen(true); + } catch (e) { + alert('상세 정보를 불러오는 중 오류가 발생했습니다.'); + } + }; + + const handleSearch = () => { + setPage(1); + setQuery((searchValue || '').trim()); + }; + + const handleCloseModal = () => { + modalClosing.current = true; + setModalOpen(false); + setTimeout(() => { + modalClosing.current = false; + }, 300); + }; + + useEffect(() => { + if (searchValue === '' && query !== '') { + setPage(1); + setQuery(''); + } + }, [searchValue, query]); + + useEffect(() => { + fetchApplicants(); + }, [fetchApplicants]); + + return ( +
+ + setPage(newPage)} + /> + + } + topContent={ + + } + > + + {(column) => ( + + {column.name} + + )} + + + {(item) => ( + handleRowClick(item)}> + {(columnKey) => ( + + {renderCell(item, columnKey)} + + )} + + )} + +
+ + {/* 기존 UserDetailsModal 재사용: 핵심 질문/자유문항은 response 배열로 표시됨 */} + +
+ ); +} + + diff --git a/src/components/admin/UserDetailsModal.jsx b/src/components/admin/UserDetailsModal.jsx index 62754e5..f48a7f0 100644 --- a/src/components/admin/UserDetailsModal.jsx +++ b/src/components/admin/UserDetailsModal.jsx @@ -29,7 +29,7 @@ export default function UserDetailsModal({ user, isOpen, onClose, preventClose }
User Details
- {/* User Basic Info (신 응답 스키마에 맞춤) */} + {/* User Basic Info (공통 + 코어 지원 정보 포함) */} {[ @@ -37,6 +37,10 @@ export default function UserDetailsModal({ user, isOpen, onClose, preventClose } ['전공', user?.major], ['학번', user?.studentId], ['회비 송금 여부', typeof user?.isPayed === 'boolean' ? (user.isPayed ? 'Yes' : 'No') : undefined], + ['이메일', user?.email], + ['전화번호', user?.phone], + ['지원팀', user?.team], + ['제출 시각', user?.createdAt], ] .filter(([, val]) => val !== undefined && val !== null && val !== '') .map(([label, userValue], idx) => ( @@ -73,6 +77,39 @@ export default function UserDetailsModal({ user, isOpen, onClose, preventClose } ); })} + + {[ + ['지원 동기', user?.motivation], + ['원하는 업무/프로젝트/비전', user?.wish], + ['장점/역량', user?.strengths], + ['각오', user?.pledge], + ] + .filter(([, val]) => val !== undefined && val !== null && String(val).trim() !== '') + .map(([label, val], idx) => ( +
+
+
{label}
+
{val}
+
+ ))} + + {Array.isArray(user?.fileUrls) && user.fileUrls.length > 0 && ( +
+
+
첨부 파일
+
+ {user.fileUrls.map((url, idx) => ( + + ))} +
+
+ )}
diff --git a/src/components/ui/common/MenuHeader.jsx b/src/components/ui/common/MenuHeader.jsx index 627102a..d46a7d8 100644 --- a/src/components/ui/common/MenuHeader.jsx +++ b/src/components/ui/common/MenuHeader.jsx @@ -15,6 +15,7 @@ export default function MenuHeader() { const menuItems = [ { name: "멤버관리", href: "/admin" }, + { name: "운영자 지원관리", href: "/coreadmin" }, { name: "스터디", href: "#", onClick: () => alert('준비중입니다.') }, { name: "공지사항", href: "#", onClick: () => alert('준비중입니다.') }, { name: "프로젝트", href: "#", onClick: () => alert('준비중입니다.') }, @@ -47,6 +48,11 @@ export default function MenuHeader() { 멤버관리 + + + 운영자 지원관리 + + alert('준비중입니다.')}> 스터디