Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#232 대회 관리 페이지 #233

Merged
merged 11 commits into from
Dec 16, 2024
2 changes: 1 addition & 1 deletion frontend/src/app/(client)/hackathon/sw-contest/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PageTitle from '@/components/common/PageTitle';
const Page = () => {
return (
<div className="flex w-full flex-col gap-4 rounded-sm bg-white p-5">
<PageTitle title="SW문제 해결 경진대회" description="구글 사이트에 게시된 학생들의 작품을 감상해보세요!" />
<PageTitle title="SW문제해결 경진대회" description="구글 사이트에 게시된 학생들의 작품을 감상해보세요!" />
<div className="h-0 w-full border border-border" />

<div className="flex h-40 w-full items-center justify-center text-comment">개발 중인 기능입니다.</div>
Expand Down
147 changes: 145 additions & 2 deletions frontend/src/app/admin/hackathon/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,150 @@
export default function ContestListPage() {
import { headers } from 'next/headers';

import PageTitle from '@/components/common/PageTitle';
import AdminHackathonManageTable from '@/components/ui/admin/hackathon/AdminHackathonManageTable';
import AdminHackathonSearchBox from '@/components/ui/admin/hackathon/AdminHackathonSearchBox';

import { HackathonManagePageableDto } from '@/types/common.dto';
import { getAuthFromCookie } from '@/lib/utils/auth';
import { AuthSliceState } from '@/store/auth.slice';
import Pagination from '@/components/common/Pagination';

export interface HackathonListPageProps {
searchParams?: { [key: string]: string | undefined };
}

export default function HackathonListPage({ searchParams }: HackathonListPageProps) {
const headersList = headers();
const pathname = headersList.get('x-pathname') || '';

const auth: AuthSliceState = getAuthFromCookie();

const page = searchParams?.page ? parseInt(searchParams.page, 10) : 1;
const keyword = searchParams?.keyword ? searchParams.keyword : '';

// TODO: api 연결
const hackathonInfos: HackathonManagePageableDto = {
totalPages: hackathonInfoContent.length / 8,
totalElements: hackathonInfoContent.length,
size: 8,
number: page,
pageable: '{"page":1,"size":8}',
numberOfElements: 0,
empty: false,
content: hackathonInfoContent,
};

return (
<div className="w-full">
<div className="flex h-40 w-full items-center justify-center text-comment">개발 중인 기능입니다.</div>
<PageTitle title="해커톤 목록" />
<AdminHackathonSearchBox count={hackathonInfos.content.length} page={page} keyword={keyword} status={1} />
<AdminHackathonManageTable hackathonInfos={hackathonInfos.content.slice(8 * (page - 1), 8 * page)} />
<Pagination
currentPage={page}
totalItems={hackathonInfos.totalElements}
pathname={pathname}
pageSize={8}
query={JSON.stringify(searchParams)}
/>
</div>
);
}

const hackathonInfoContent = [
{
id: 1,
title: '제 8회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
{
id: 2,
title: '제 7회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
{
id: 3,
title: '제 7회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
{
id: 4,
title: '제 7회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
{
id: 5,
title: '제 7회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
{
id: 6,
title: '제 7회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
{
id: 7,
title: '제 7회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
{
id: 8,
title: '제 7회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
{
id: 9,
title: '제 7회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
{
id: 10,
title: '제 7회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
{
id: 11,
title: '제 7회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
{
id: 12,
title: '제 7회 2024-2 PNU SW+X 문제해결 경진대회',
hackathonStartDate: '2024-09-01',
hackathonEndDate: '2024-12-01',
teamCode: '1234',
isActive: false,
},
];
19 changes: 8 additions & 11 deletions frontend/src/app/admin/hackathon/register/page.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,25 @@
'use client';

import { useState } from 'react';
import * as Yup from 'yup';
import { Form, Formik } from 'formik';

import PageTitle from '@/components/common/PageTitle';
import { HackathonDto } from '@/types/common.dto';
import TextInput from '@/components/common/formik/TextInput';
import ImageUploader from '@/components/common/formik/ImageUploader';
import { DatePicker } from '@/components/common/formik/DatePicker';
import MarkdownEditor from '@/components/common/formik/MarkdownEditor';
import AdminHackathonInputSection from '@/components/ui/admin/hackathon/AdminHackathonInputSection';
import { MdImage } from '@react-icons/all-files/md/MdImage';
import ImageUploader from '@/components/common/formik/ImageUploader';
import { MdTextFields } from '@react-icons/all-files/md/MdTextFields';
import { MdDateRange } from '@react-icons/all-files/md/MdDateRange';
import MarkdownEditor from '@/components/common/formik/MarkdownEditor';
import { DatePicker } from '@/components/common/formik/DatePicker';
import { HackathonDto } from '@/types/common.dto';

export default function HackathonCreatePage() {
const [hackathonInfo, setHackathonInfo] = useState<HackathonInfo>(hackathonInfoInitialValue);

return (
<div className="w-full">
<PageTitle title="해커톤 등록" />
<Formik
initialValues={hackathonInfo}
initialValues={hackathonInfoInitialValue}
validationSchema={hackathonValidationSchema}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(false);
Expand All @@ -37,10 +34,10 @@ export default function HackathonCreatePage() {
inputElement={
<TextInput
name="name"
value={values.name}
value={values.title}
onChange={handleChange}
onBlur={handleBlur}
errorText={touched.name && errors.name ? errors.name : undefined}
errorText={touched.title && errors.title ? errors.title : undefined}
placeholder="대회명을 입력해주세요."
/>
}
Expand Down Expand Up @@ -161,7 +158,7 @@ export default function HackathonCreatePage() {

export type HackathonInfo = Omit<HackathonDto, 'bannerImage' | 'id'> & { bannerImage: File | null };
const hackathonInfoInitialValue: HackathonInfo = {
name: '',
title: '',
content: '',
bannerImage: null,
applyStartDate: '',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/common/formik/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ const Dropdown = ({ isRequired = false, size = 'md', ...props }: DropdownProps)
<button
className={`m-0 w-full rounded-sm border-[1px] ${isAdmin ? 'border-admin-border' : 'border-border'} bg-white ${FORM_SIZE[size].padding} text-left ${FORM_SIZE[size].textSize} ${hasError && 'border-red-400'} ${typeof selectedId === 'number' && selectedId <= 0 && 'text-comment'}`}
type="button"
onBlur={() => {
setTimeout(() => setIsOpen(false), 200);
}}
onClick={() => setIsOpen((prev) => !prev)}
>
{selectedValue[0]?.name ?? selectOptionText}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use client';

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';

import { MdDeleteForever } from '@react-icons/all-files/md/MdDeleteForever';
import { MdFileDownload } from '@react-icons/all-files/md/MdFileDownload';
import { MdEdit } from '@react-icons/all-files/md/MdEdit';

import { HackathonManageDto } from '@/types/common.dto';

export interface AdminHackathonManageTableProps {
hackathonInfos: HackathonManageDto[];
}

export default function AdminHackathonManageTable({ hackathonInfos }: AdminHackathonManageTableProps) {
const [hackathons, setHackathon] = useState<HackathonManageDto[]>([]);
const router = useRouter();

function handleActiveStatusClick(hackathonId: number) {
console.log(hackathonId);
// TODO: API 연결
setHackathon((prev) =>
prev.map((hackathon) =>
hackathon.id === hackathonId ? { ...hackathon, isActive: !hackathon.isActive } : hackathon,
),
);
}

function handleEditContestClick(hackathonId: number) {
router.push(`/admin/hackathon/${hackathonId}`);
}

function handleDeleteContestClick(selectedHackathon: HackathonManageDto) {
const willDelete = window.confirm(selectedHackathon.title + '을/를 정말 삭제하겠습니까?');
if (!willDelete) return;

// TODO: API 연결
setHackathon((prev) =>
prev.map((hackathon) => (hackathon.id === selectedHackathon.id ? null : hackathon)).filter((v) => v !== null),
);
}

function handleDownloadVoteResultClick(hackathonId: number) {
// TODO: API 연결
console.log(hackathonId, 'clicked');
}

useEffect(() => {
setHackathon(hackathonInfos);
}, [hackathonInfos, setHackathon]);

return (
<div className="min-h-[380px]">
<table className="my-4 w-full table-fixed text-center text-sm [&_*]:cursor-default">
<thead className="border-y-2 border-admin-border [&_th]:px-2 [&_th]:py-4">
<tr>
<th className="w-20">대회ID</th>
<th>대회명</th>
<th className="w-60">대회기간</th>
<th className="w-32">활성 여부</th>
<th className="w-20">대회 수정</th>
<th className="w-20">대회 삭제</th>
<th className="w-20">투표 결과</th>
</tr>
</thead>
<tbody>
{hackathons.map((hackathon) => {
return (
<tr key={hackathon.id} className="border-b border-admin-border">
<td>{hackathon.id}</td>
<td className="py-4 pl-3 text-left font-semibold">{hackathon.title}</td>
<td className="text-sm">
{hackathon.hackathonStartDate} ~ {hackathon.hackathonEndDate}
</td>
<td>
<button onClick={() => handleActiveStatusClick(hackathon.id)} className="relative mt-1">
<div
className={`h-6 w-12 rounded-lg transition-colors ${hackathon.isActive ? 'bg-green-400' : 'bg-gray-300'}`}
/>
<div
className={`absolute top-1 h-4 w-4 rounded-full bg-white transition-all ${hackathon.isActive ? 'left-7' : 'left-1'}`}
/>
</button>
</td>
<td>
<button
onClick={() => handleEditContestClick(hackathon.id)}
className="rounded-sm px-3 py-2 text-lg text-admin-secondary-main hover:bg-gray-200 hover:text-admin-primary-main"
>
<MdEdit />
</button>
</td>
<td>
<button
onClick={() => handleDeleteContestClick(hackathon)}
className="rounded-sm px-3 py-2 text-lg text-admin-secondary-main hover:bg-gray-200 hover:text-admin-semantic-error-light"
>
<MdDeleteForever />
</button>
</td>
<td>
<button
onClick={() => handleDownloadVoteResultClick(hackathon.id)}
className="rounded-sm px-3 py-2 text-lg text-admin-secondary-main hover:bg-gray-200 hover:text-admin-secondary-dark"
>
<MdFileDownload />
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
Loading