Skip to content

Commit

Permalink
Feature/#232 대회 관리 페이지 (#233)
Browse files Browse the repository at this point in the history
* feat: 클라이언트의 헤더에서 '문제 해결 경진대회'를 '문제해결 경진대회'로 수정

#232

* refactor: 대회 등록 페이지 리펙토링

#232

* feat: 해커톤 관리 타입 정의

#232

* feat: 드롭 박스 focus out 되었을 때, 닫히도록 수정

#232

* feat: 해커톤 검색창 컴포넌트 구현

#232

* feat: 해커톤 테이블 컴포넌트 등록

#232

* feat: 해커톤 관리 페이지 구현

#232

* refactor: import 구문 순서 정리

#232

* feat: 해커톤 목록 페이지에 페이지네이션 추가

#232

* design: table의 최소 height 고정

#232

* chore: 사용하지 않는 HackathonDTO에서 속성 삭제

#232
  • Loading branch information
llddang authored Dec 16, 2024
1 parent 50c8cfd commit 71f85bf
Show file tree
Hide file tree
Showing 10 changed files with 377 additions and 44 deletions.
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

0 comments on commit 71f85bf

Please sign in to comment.