Skip to content

Commit 2b45d58

Browse files
committed
Feat: 코어 멤버 지원 페이지에 동의 체크박스 추가 및 일정 안내 수정
1 parent d2bfa92 commit 2b45d58

File tree

6 files changed

+331
-23
lines changed

6 files changed

+331
-23
lines changed

src/app/core-recruit/page.jsx

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default function CoreRecruit() {
2828
const [uploading, setUploading] = useState(false);
2929
const [submitting, setSubmitting] = useState(false);
3030
const [touched, setTouched] = useState(false);
31+
const [agreed, setAgreed] = useState(false);
3132

3233
const handleValueChange = (key) => (value) => {
3334
setForm((prev) => ({ ...prev, [key]: value }));
@@ -116,9 +117,22 @@ export default function CoreRecruit() {
116117

117118
return (
118119
<div className='flex flex-col max-w-[900px] mx-auto min-h-[100svh] justify-start text-white py-16 px-6'>
119-
<h1 className='text-4xl font-bold mb-6 flex items-center gap-3'>
120+
<h1
121+
className={`
122+
font-bold mb-6 flex items-center gap-3
123+
text-4xl
124+
tablet:text-3xl
125+
mobile:text-2xl
126+
`}
127+
>
120128
{/* 구글 로고 - 커스텀 그라데이션 적용 */}
121-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 268.1522 273.8827" width="40" height="40" className="inline-block">
129+
<svg
130+
xmlns="http://www.w3.org/2000/svg"
131+
viewBox="0 0 268.1522 273.8827"
132+
width="40"
133+
height="40"
134+
className="inline-block flex-shrink-0"
135+
>
122136
<defs>
123137
<linearGradient id="a">
124138
<stop offset="0" stopColor="#0fbc5c"/>
@@ -222,7 +236,16 @@ export default function CoreRecruit() {
222236
</g>
223237
</g>
224238
</svg>
225-
GDGoC INHA Core Member 지원
239+
<span
240+
className={`
241+
leading-tight
242+
break-keep
243+
whitespace-nowrap
244+
mobile:whitespace-normal
245+
`}
246+
>
247+
GDGoC INHA<wbr /> Core Member 지원
248+
</span>
226249
</h1>
227250

228251
<div className='grid grid-cols-1 gap-14 mt-11'>
@@ -464,17 +487,47 @@ export default function CoreRecruit() {
464487
</div>
465488

466489
<div className='rounded-xl bg-[#111111] p-5 border border-white/10'>
467-
<div className='text-lg font-semibold mb-2'>마지막으로 일정을 확인해주시기 바랍니다!</div>
468-
<div className='space-y-1 text-white/90'>
469-
<div className='text-xl font-bold mb-2'>코어멤버 모집 안내</div>
470-
<div>✅ 서류 지원 기간 : 2025년 09월 8일 (월) - 2025년 09월 12일 (금) 23:59:59</div>
471-
<div>✅ 서류 결과 발표 : ~ 2025년 09월 14일 (일)</div>
472-
<div>✅ 면접 진행 기간 : 2025년 09월 15일 (월) - 2025년 09월 19일 (금)</div>
473-
<div>✅ 최종 결과 발표 : ~ 2025년 09월 21일 (일)</div>
474-
<div className='text-white/70 text-sm mt-3'>※ 면접은 지원자와 면접관의 일정에 따라 조정되며, 인하대학교 내부 장소에서 진행됩니다.</div>
475-
<div className='text-white/70 text-sm'>※ 면접은 대면을 원칙으로 하며, 부득이한 경우에만 비대면으로 진행됩니다.</div>
476-
<div className='text-[#EA4336] text-sm mt-3'>❗️운영진으로 활동 시, 매주 화요일 19:00~21:00 정기 운영진 회의에 반드시 참석해야 합니다.</div>
490+
<div className='text-lg font-semibold mb-4'>
491+
마지막으로 공지 및 일정을 확인해주시기 바랍니다!
477492
</div>
493+
<div className='space-y-6 text-white/90'>
494+
<div>
495+
<div className='text-xl font-bold mb-4'>모집 일정</div>
496+
<div className='mb-2'>✅ 서류 지원 기간 : 2025년 09월 8일 (월) ~ 2025년 09월 21일 (일) 11:59:59</div>
497+
<div className='mb-2'>✅ 서류 결과 발표 : ~ 2025년 09월 21일 (일)</div>
498+
<div className='mb-2'>✅ 면접 진행 기간 : 2025년 09월 22일 (월) ~ 2025년 09월 26일 (금)</div>
499+
<div className='text-white/70 text-sm mb-2'>※ 지원자 및 면접관의 일정에 따라 마감 전 조기 면접 진행이 가능할 수 있습니다.</div>
500+
<div className='mb-2'>✅ 최종 결과 발표 : ~ 2025년 09월 28일 (일)</div>
501+
<div className='mb-2'>❗️ 첫 온보딩 : 2025년 09월 30일 (화)</div>
502+
</div>
503+
<div className="border-t border-white/20 my-4" />
504+
<div>
505+
<div className='text-xl font-bold mb-4'>면접 안내</div>
506+
<div className='mb-2'>• 원칙적으로 대면 면접을 진행하며, 부득이한 경우에 한해 비대면으로 조정될 수 있습니다.</div>
507+
<div className='mb-2'>• 면접은 인하대학교 내부 장소에서 진행됩니다.</div>
508+
</div>
509+
<div className="border-t border-white/20 my-4" />
510+
<div>
511+
<div className='text-xl font-bold mb-4'>활동 안내</div>
512+
<div className='mb-2'>• 운영진으로 활동 시, 매주 화요일 19:00~21:00 정기 운영진 회의에 필수참석해야합니다.</div>
513+
<div className='mb-2'>• 단, 2시간 전체 참석이 아닌 최소 1시간 이상 필참을 원칙으로 합니다.</div>
514+
</div>
515+
</div>
516+
</div>
517+
<div className='flex w-full items-center justify-end'>
518+
<Checkbox
519+
isSelected={agreed}
520+
onValueChange={setAgreed}
521+
radius='none'
522+
color='danger'
523+
className='text-white text-base font-semibold'
524+
classNames={{
525+
wrapper: 'group-data-[selected=true]:after:bg-red-500',
526+
icon: 'bg-red-500',
527+
}}
528+
>
529+
공지사항 및 일정을 확인하였으며, 이에 동의합니다.
530+
</Checkbox>
478531
</div>
479532

480533
<div className='flex justify-end gap-3 mt-6'>
@@ -485,7 +538,7 @@ export default function CoreRecruit() {
485538
<Button
486539
className='bg-red-500 text-white rounded-full w-[183px] h-[57px] text-lg font-semibold'
487540
onPress={handleSubmit}
488-
isDisabled={!isValid || uploading || submitting}
541+
isDisabled={!isValid || !agreed || uploading || submitting}
489542
isLoading={uploading || submitting}
490543
>제출</Button>
491544
</div>

src/app/core-recruit/submit/page.jsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,12 @@ export default function CoreRecruitSubmit() {
2525
<div className="text-3xl mt-[50px] font-semibold mobile:text-2xl">코어 멤버 지원서 제출이 완료되었습니다.</div>
2626
<div className='text-xl mt-[20px] mobile:text-sm items-center justify-center mobile:text-center'>
2727
<div className="flex flex-col items-start mt-5 justify-center text-white gap-2">
28-
<div className="text-xl font-bold mb-2">아래 일정과 안내를 한번더 확인해주세요!</div>
29-
<div>✅ 서류 지원 기간 : 2025년 09월 8일 (월) - 2025년 09월 12일 (금) 23:59:59</div>
30-
<div>✅ 서류 결과 발표 : ~ 2025년 09월 14일 (일)</div>
31-
<div>✅ 면접 진행 기간 : 2025년 09월 15일 (월) - 2025년 09월 19일 (금)</div>
32-
<div>✅ 최종 결과 발표 : ~ 2025년 09월 21일 (일)</div>
33-
<div className="text-white/70 text-sm mt-3">※ 면접은 지원자와 면접관의 일정에 따라 조정되며, 인하대학교 내부 장소에서 진행됩니다.</div>
34-
<div className="text-white/70 text-sm">※ 면접은 대면을 원칙으로 하며, 부득이한 경우에만 비대면으로 진행됩니다.</div>
35-
<div className="text-[#EA4336] text-sm mt-3">❗️운영진으로 활동 시, 매주 화요일 19:00~21:00 정기 운영진 회의에 반드시 참석해야 합니다.</div>
28+
<div className="text-xl font-bold mb-2">아래 일정을 한번 더 확인해주세요!</div>
29+
<div>✅ 서류 지원 기간 : 2025년 09월 8일 (월) ~ 2025년 09월 21일 (일) 11:59:59</div>
30+
<div>✅ 서류 결과 발표 : ~ 2025년 09월 21일 (일)</div>
31+
<div>✅ 면접 진행 기간 : 2025년 09월 22일 (월) ~ 2025년 09월 26일 (금)</div>
32+
<div className="text-white/70 text-sm mb-2">※ 지원자 및 면접관의 일정에 따라 마감 전 조기 면접 진행이 가능할 수 있습니다.</div>
33+
<div>✅ 최종 결과 발표 : ~ 2025년 09월 28일 (일)</div>
3634
</div>
3735
</div>
3836
<div className='flex gap-3 mt-[50px] mb-[50px]'>

src/app/coreadmin/layout.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import MenuHeader from '@/components/ui/common/MenuHeader';
2+
import ApiCodeGuard from '@/components/auth/ApiCodeGuard.jsx';
3+
4+
export const metadata = {
5+
title: 'CoreAdmin',
6+
description: 'Core member application management',
7+
};
8+
9+
export default function CoreAdminLayout({ children }) {
10+
return (
11+
<ApiCodeGuard>
12+
<>
13+
<MenuHeader />
14+
{children}
15+
</>
16+
</ApiCodeGuard>
17+
);
18+
}
19+
20+

src/app/coreadmin/page.jsx

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"use client";
2+
3+
import React, { useCallback, useEffect, useRef } from 'react';
4+
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, Chip } from '@nextui-org/react';
5+
6+
import UserDetailsModal from '@/components/admin/UserDetailsModal';
7+
import AdminTableTopContent from '@/components/admin/AdminTableTopContent';
8+
import AdminTableBottomContent from '@/components/admin/AdminTableBottomContent';
9+
import { useAuthenticatedApi } from '@/hooks/useAuthenticatedApi';
10+
11+
const columns = [
12+
{ name: 'NAME', uid: 'name' },
13+
{ name: 'MAJOR / ID', uid: 'major' },
14+
{ name: 'TEAM', uid: 'team' },
15+
{ name: 'EMAIL', uid: 'email' },
16+
{ name: 'PHONE', uid: 'phone' },
17+
];
18+
19+
export default function CoreAdminPage() {
20+
const { apiClient } = useAuthenticatedApi();
21+
22+
const [page, setPage] = React.useState(1);
23+
const [modalOpen, setModalOpen] = React.useState(false);
24+
const modalClosing = useRef(false);
25+
26+
const [selectedUser, setSelectedUser] = React.useState(null);
27+
const [searchValue, setSearchValue] = React.useState('');
28+
const [query, setQuery] = React.useState('');
29+
const [loading, setLoading] = React.useState(false);
30+
const [error, setError] = React.useState('');
31+
const [currentUsers, setCurrentUsers] = React.useState([]);
32+
const [totalUsers, setTotalUsers] = React.useState(0);
33+
const [totalPages, setTotalPages] = React.useState(0);
34+
35+
const rowsPerPage = 10;
36+
37+
const fetchApplicants = useCallback(async () => {
38+
setLoading(true);
39+
setError('');
40+
try {
41+
const params = {
42+
page: page - 1,
43+
size: rowsPerPage,
44+
sort: 'createdAt',
45+
dir: 'DESC',
46+
question: query || undefined,
47+
};
48+
const res = await apiClient.get('/core-recruit/applicants', { params });
49+
const list = Array.isArray(res?.data?.data) ? res.data.data : [];
50+
const total = res?.data?.meta?.totalElements ?? list.length;
51+
const computedTotalPages = Math.max(1, Math.ceil(total / rowsPerPage));
52+
53+
setCurrentUsers(list);
54+
setTotalUsers(total);
55+
setTotalPages(computedTotalPages);
56+
} catch (err) {
57+
setError(String(err?.message || 'failed to load applicants'));
58+
setCurrentUsers([]);
59+
setTotalUsers(0);
60+
} finally {
61+
setLoading(false);
62+
}
63+
}, [apiClient, page, rowsPerPage, query]);
64+
65+
const renderCell = useCallback((user, columnKey) => {
66+
const value = user[columnKey];
67+
switch (columnKey) {
68+
case 'name':
69+
return <span className='text-white'>{user?.name ?? ''}</span>;
70+
case 'major':
71+
return (
72+
<div className='flex flex-col'>
73+
<p className='text-white text-bold text-sm capitalize'>{user?.major ?? ''}</p>
74+
<p className='text-bold text-sm capitalize text-default-400'>{user?.studentId ?? ''}</p>
75+
</div>
76+
);
77+
case 'team': {
78+
const teamLabel = user?.team ?? '';
79+
const teamColorMap = {
80+
HR: '#EA4335',
81+
BD: '#34A853',
82+
TECH: '#4285F4',
83+
'PR/DESIGN': '#F9AB00',
84+
};
85+
const color = teamColorMap[teamLabel] || '#9CA3AF';
86+
return (
87+
<Chip
88+
size='sm'
89+
variant='bordered'
90+
style={{
91+
borderColor: color,
92+
color,
93+
}}
94+
>
95+
{teamLabel}
96+
</Chip>
97+
);
98+
}
99+
case 'email':
100+
return <span className='text-white'>{user?.email ?? ''}</span>;
101+
case 'phone':
102+
return <span className='text-white'>{user?.phone ?? ''}</span>;
103+
case 'createdAt':
104+
return <span className='text-white'>{user?.createdAt ? new Date(user.createdAt).toLocaleString() : ''}</span>;
105+
default:
106+
return value;
107+
}
108+
}, []);
109+
110+
const handleRowClick = async (user) => {
111+
if (modalClosing.current) return;
112+
try {
113+
const id = user?.id;
114+
if (!id) throw new Error('지원자 ID 없음');
115+
const res = await apiClient.get(`/core-recruit/applicants/${id}`);
116+
const detail = res?.data?.data ?? null;
117+
if (!detail) throw new Error('상세 정보 없음');
118+
setSelectedUser(detail);
119+
setModalOpen(true);
120+
} catch (e) {
121+
alert('상세 정보를 불러오는 중 오류가 발생했습니다.');
122+
}
123+
};
124+
125+
const handleSearch = () => {
126+
setPage(1);
127+
setQuery((searchValue || '').trim());
128+
};
129+
130+
const handleCloseModal = () => {
131+
modalClosing.current = true;
132+
setModalOpen(false);
133+
setTimeout(() => {
134+
modalClosing.current = false;
135+
}, 300);
136+
};
137+
138+
useEffect(() => {
139+
if (searchValue === '' && query !== '') {
140+
setPage(1);
141+
setQuery('');
142+
}
143+
}, [searchValue, query]);
144+
145+
useEffect(() => {
146+
fetchApplicants();
147+
}, [fetchApplicants]);
148+
149+
return (
150+
<div>
151+
<Table
152+
className='dark text-white py-[30px] px-[96px] mobile:px-[10px]'
153+
aria-label='Core applicants table'
154+
bottomContent={
155+
<div className='relative'>
156+
<AdminTableBottomContent
157+
page={page}
158+
totalPages={totalPages}
159+
totalUsers={totalUsers}
160+
onChangePage={(newPage) => setPage(newPage)}
161+
/>
162+
</div>
163+
}
164+
topContent={
165+
<AdminTableTopContent searchValue={searchValue} setSearchValue={setSearchValue} onSearch={handleSearch} />
166+
}
167+
>
168+
<TableHeader columns={columns}>
169+
{(column) => (
170+
<TableColumn key={column.uid} align='start'>
171+
{column.name}
172+
</TableColumn>
173+
)}
174+
</TableHeader>
175+
<TableBody items={currentUsers} isLoading={loading} emptyContent={loading ? '불러오는 중...' : '데이터가 없습니다.'}>
176+
{(item) => (
177+
<TableRow className='hover:bg-[#35353b99] cursor-pointer text-white' key={item.id} onClick={() => handleRowClick(item)}>
178+
{(columnKey) => (
179+
<TableCell>
180+
{renderCell(item, columnKey)}
181+
</TableCell>
182+
)}
183+
</TableRow>
184+
)}
185+
</TableBody>
186+
</Table>
187+
188+
{/* 기존 UserDetailsModal 재사용: 핵심 질문/자유문항은 response 배열로 표시됨 */}
189+
<UserDetailsModal user={selectedUser} isOpen={modalOpen} onClose={handleCloseModal} preventClose />
190+
</div>
191+
);
192+
}
193+
194+

0 commit comments

Comments
 (0)