Skip to content

Commit

Permalink
Feature/#234 대회 상세 페이지 구현 (#235)
Browse files Browse the repository at this point in the history
* feat: 해커톤 정보 수정 페이지 구현

#232

* feat: 공통 컴포넌트 모달 구현

#234

* design: 해커톤 관리 테이블에서 관리 버튼의 색상 변경

#234

* feat: 해커톤 팀 목록 검색창 구현

#234

* feat: 해커톤 팀 관리 테이블 구현

#234

* refactor: 전공 dropdown의 속성명 정확하게 변경

#234

* feat: 해커톤 팀 생성 모달 구현

#234

* feat: 해커톤 팀 수정 모달 구현

#234

* feat: 해커톤 팀 공통 디자인 컴포넌트로 구현

#234

* feat: 해커톤 팀 멤버 인풋 컴포넌트로 구현

#234

* feat: 해커톤 상 관련 상태 정의

#234

* refactor: 적절한 변수명으로 수정

#234

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

#234

* fix: 빌드 에러 수정

#234
  • Loading branch information
llddang authored Dec 17, 2024
1 parent 71f85bf commit a497f20
Show file tree
Hide file tree
Showing 24 changed files with 1,313 additions and 100 deletions.
5 changes: 5 additions & 0 deletions frontend/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const nextConfig = {
hostname: 'localhost',
pathname: '**',
},
{
protocol: 'https',
hostname: 'www.pusan.ac.kr',
pathname: '**',
},
{
protocol: 'https',
hostname: 'swcss.pusan.ac.kr',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ const HackathonTeamCreateModal = ({ hackathonId, open, onClose }: HackathonTeamC
registerTeam(
{
hackathonId: hackathonId,
image: values.image,
name: values.name,
work: values.work,
thumbnailImage: values.image,
teamName: values.name,
projectTitle: values.work,
githubUrl: values.githubUrl,
members: [values.leader, ...values.members],
password: values.password,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const HackathonTeamReadModal = ({ selectedTeam, onClose }: HackathonTeamReadModa
className="flex h-[600px] w-full max-w-[900px] flex-col items-center gap-2 rounded bg-white p-5 sm:h-[700px]"
>
<div className="flex w-full flex-wrap items-center justify-between gap-y-2">
<PageTitle title={selectedTeam.name} />
<PageTitle title={selectedTeam.teamName} description={selectedTeam.projectTitle} />
<div className="flex flex-grow justify-end gap-2">
<span className="flex items-center gap-1 font-bold text-primary-main">
<VscHeart />
Expand All @@ -67,7 +67,7 @@ const HackathonTeamReadModal = ({ selectedTeam, onClose }: HackathonTeamReadModa
<tr key={member.id} className={classNames(member.isLeader && 'font-bold')}>
<td className="min-w-[4em]">{member.name}</td>
<td className="min-w-[10em]">{member.id}</td>
<td className="hidden min-w-[10em] md:table-cell">{member.majorName}</td>
<td className="hidden min-w-[10em] md:table-cell">{member.major}</td>
{member.isLeader && <td className="min-w-[4em]">(팀장)</td>}
</tr>
))}
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/app/(client)/hackathon/[slug]/vote/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Image from 'next/image';
import { useState } from 'react';
import HackathonTeamCreateModal from './components/HackathonTeamCreateModal';
import HackathonTeamReadModal from './components/HackathonTeamReadModal';
import { mockHackathonTeamPageableData } from '@/mocks/hackathon';

interface HackathonVotePageProps {
params: {
Expand All @@ -20,7 +21,9 @@ const Page = ({ params: { slug }, searchParams }: HackathonVotePageProps) => {
const [teamCreateModalOpen, setTeamCreateModalOpen] = useState<boolean>(false);

const page = searchParams?.page ? parseInt(searchParams.page, 10) : 1;
const { data: teams } = useHackathonTeamsQuery(slug, page, 8);
// const { data: teams } = useHackathonTeamsQuery(slug, page, 8);
const teams = mockHackathonTeamPageableData;

return (
<div className="flex flex-col gap-4">
<HackathonTeamReadModal selectedTeam={selectedTeam} onClose={() => setSelectedTeam(null)} />
Expand Down Expand Up @@ -48,7 +51,7 @@ const Page = ({ params: { slug }, searchParams }: HackathonVotePageProps) => {
>
<div className="relative h-28 w-full">
<Image
src={process.env.NEXT_PUBLIC_FILE_URL + '/' + team.thumbnailImageName}
src={process.env.NEXT_PUBLIC_FILE_URL + '/' + team.thumbnailImage}
alt="팀 섬네일"
className="rounded-t-sm"
layout="fill"
Expand All @@ -57,7 +60,7 @@ const Page = ({ params: { slug }, searchParams }: HackathonVotePageProps) => {
quality={100}
/>
</div>
<p className="m-2 font-bold">{team.name}</p>
<p className="m-2 font-bold">{team.teamName}</p>
<div className="flex justify-end">
<span className="ml-4 flex-grow rounded-l-2xl border border-r-0 border-primary-main p-2 text-left text-lg text-primary-main">
{team.voteCount}표 득표
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/app/(client)/hackathon/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ const Page = async ({ searchParams }: { searchParams?: { [key: string]: string |
/>
<div className="h-0 w-full border border-border" />

<div className="flex h-40 w-full items-center justify-center text-comment">개발 중인 기능입니다.</div>
{/* <div className="flex h-40 w-full items-center justify-center text-comment">개발 중인 기능입니다.</div> */}

{/* {hackathons?.content && hackathons.content.length > 0 ? (
{hackathons?.content && hackathons.content.length > 0 ? (
<div className="grid grid-cols-1 gap-6 py-4 sm:grid-cols-2 md:grid-cols-3">
{hackathons.content.map((hackathon) => (
<Link
Expand Down Expand Up @@ -104,7 +104,7 @@ const Page = async ({ searchParams }: { searchParams?: { [key: string]: string |
<div className="flex h-40 w-full items-center justify-center text-comment">해커톤 정보가 없습니다.</div>
)}

<Pagination currentPage={page} totalItems={hackathons?.totalElements ?? 0} pathname={pathname} pageSize={6} /> */}
<Pagination currentPage={page} totalItems={hackathons?.totalElements ?? 0} pathname={pathname} pageSize={6} />
</div>
);
};
Expand Down
197 changes: 197 additions & 0 deletions frontend/src/app/admin/hackathon/edit/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
'use client';

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

import PageTitle from '@/components/common/PageTitle';
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 { MdTextFields } from '@react-icons/all-files/md/MdTextFields';
import { MdDateRange } from '@react-icons/all-files/md/MdDateRange';
import { HackathonDto } from '@/types/common.dto';

export interface AdminHackathonEditPageProps {
params: { slug: number };
}

export type HackathonInfo = Omit<HackathonDto, 'bannerImage' | 'id'> & { bannerImage: File | null };

export default function AdminHackathonEditPage({ params: { slug } }: AdminHackathonEditPageProps) {
// TODO: API 연결
const hackathonInfoInitialValue: HackathonInfo = {
title: '',
content: '',
bannerImage: null,
applyStartDate: '',
applyEndDate: '',
hackathonStartDate: '',
hackathonEndDate: '',
teamCode: '',
};

return (
<div className="w-full">
<PageTitle title="해커톤 수정" />
<Formik
initialValues={hackathonInfoInitialValue}
validationSchema={hackathonValidationSchema}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(false);
// TODO: API 연결
}}
>
{({ isSubmitting, values, touched, handleChange, handleBlur, setFieldValue, errors }) => (
<Form className="m-5 flex flex-col gap-6" autoComplete="off">
<AdminHackathonInputSection
icon={MdTextFields}
label="대회명"
inputElement={
<TextInput
name="name"
value={values.title}
onChange={handleChange}
onBlur={handleBlur}
errorText={touched.title && errors.title ? errors.title : undefined}
placeholder="대회명을 입력해주세요."
/>
}
/>
<AdminHackathonInputSection
icon={MdImage}
label="배너 이미지"
inputElement={
<div className="h-[120px] w-full">
<ImageUploader
name="bannerImage"
image={values.bannerImage}
fitStand="height"
setFieldValue={setFieldValue}
errorText={touched.bannerImage && errors.bannerImage ? errors.bannerImage : undefined}
/>
</div>
}
/>
<AdminHackathonInputSection
icon={MdTextFields}
label="대회명"
inputElement={
<MarkdownEditor
name="content"
value={values.content}
height="400px"
onChange={handleChange}
onBlur={handleBlur}
errorText={touched.content && errors.content ? errors.content : undefined}
/>
}
/>
<AdminHackathonInputSection
icon={MdDateRange}
label="신청 기간"
inputElement={
<div className="flex items-center justify-center gap-4">
<DatePicker
style={{ width: '200px' }}
name="applyStartDate"
value={values.applyStartDate}
onChange={handleChange}
onBlur={handleBlur}
errorText={touched.applyStartDate && errors.applyStartDate ? errors.applyStartDate : undefined}
/>
~
<DatePicker
style={{ width: '200px' }}
name="applyEndDate"
value={values.applyEndDate}
onChange={handleChange}
onBlur={handleBlur}
errorText={touched.applyEndDate && errors.applyEndDate ? errors.applyEndDate : undefined}
/>
</div>
}
/>
<AdminHackathonInputSection
icon={MdDateRange}
label="대회 기간"
inputElement={
<div className="flex items-center justify-center gap-4">
<DatePicker
style={{ width: '200px' }}
name="hackathonStartDate"
value={values.hackathonStartDate}
onChange={handleChange}
onBlur={handleBlur}
errorText={
touched.hackathonStartDate && errors.hackathonStartDate ? errors.hackathonStartDate : undefined
}
/>
~
<DatePicker
style={{ width: '200px' }}
name="hackathonEndDate"
value={values.hackathonEndDate}
onChange={handleChange}
onBlur={handleBlur}
errorText={
touched.hackathonEndDate && errors.hackathonEndDate ? errors.hackathonEndDate : undefined
}
/>
</div>
}
/>
<AdminHackathonInputSection
icon={MdTextFields}
label="팀 등록 코드"
tooltip="대회에 팀을 등록할 때 필요한 비밀번호입니다."
inputElement={
<TextInput
style={{ width: '441px' }}
name="teamCode"
value={values.teamCode}
onChange={handleChange}
errorText={touched.teamCode && errors.teamCode ? errors.teamCode : undefined}
placeholder="팀 등록 코드"
/>
}
/>
<div className="pt-4">
<button
type="submit"
className="rounded-sm bg-admin-primary-main px-8 py-2 text-lg font-bold text-white transition-colors hover:bg-admin-primary-dark"
disabled={isSubmitting}
>
수정하기
</button>
</div>
</Form>
)}
</Formik>
</div>
);
}

const hackathonValidationSchema = Yup.object().shape({
name: Yup.string().max(15, '대회명을 50자 이내로 입력해주세요.').required('등록할 대회명을 입력해주세요.'),
content: Yup.string().required('등록할 대회의 상세정보를 입력해주세요.'),
bannerImage: Yup.mixed()
.required('대회의 배너 이미지를 첨부해주세요.')
.test(
'fileFormat',
'이미지 파일(.jpg, .jpeg, .png), PDF 파일(.pdf)만 업로드 가능합니다.',
(value) =>
value instanceof File && ['image/jpeg', 'image/jpg', 'image/png', 'application/pdf'].includes(value.type),
),
applyStartDate: Yup.date().required('대회 신청 시작일을 입력해주세요.'),
applyEndDate: Yup.date()
.min(Yup.ref('applyStartDate'), '신청 시작일보다 늦은 날짜로 지정해야 합니다')
.required('대회 신청 마지막 날을 입력해주세요.'),
hackathonStartDate: Yup.date().required('대회 시작일을 입력해주세요.'),
hackathonEndDate: Yup.date()
.min(Yup.ref('hackathonStartDate'), '대회 시작일보다 늦은 날짜로 지정해야 합니다')
.required('대회 마지막 일을 입력해주세요.'),
teamCode: Yup.string().required('대회에 참여할 수 있는 팀 등록 코드를 입력해주세요.'),
});
33 changes: 33 additions & 0 deletions frontend/src/app/admin/hackathon/manage/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import PageTitle from '@/components/common/PageTitle';
import AdminHackathonManageTeamSearchBox from '@/components/ui/admin/hackathon/AdminHackathonManageTeamSearchBox';
import AdminHackathonManageTeamTable from '@/components/ui/admin/hackathon/AdminHackathonManageTeamTable';
import { getAuthFromCookie } from '@/lib/utils/auth';
import { mockHackathonTeamPageableData } from '@/mocks/hackathon';
import { AuthSliceState } from '@/store/auth.slice';
import { headers } from 'next/headers';

export interface AdminHackathonEditPageProps {
params: { slug: number };
searchParams?: { [key: string]: string | undefined };
}

export default function AdminHackathonManagePage({ params: { slug }, searchParams }: AdminHackathonEditPageProps) {
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 hackathonTeamInfos = mockHackathonTeamPageableData.content;

return (
<>
<PageTitle title="해커톤 관리" className="mb-4" />
<AdminHackathonManageTeamSearchBox hackathonId={slug} count={hackathonTeamInfos.length} keyword={keyword} />
<AdminHackathonManageTeamTable hackathonId={slug} teamInfos={hackathonTeamInfos} />
</>
);
}
4 changes: 2 additions & 2 deletions frontend/src/app/admin/hackathon/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { getAuthFromCookie } from '@/lib/utils/auth';
import { AuthSliceState } from '@/store/auth.slice';
import Pagination from '@/components/common/Pagination';

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

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

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/admin/hackathon/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { MdTextFields } from '@react-icons/all-files/md/MdTextFields';
import { MdDateRange } from '@react-icons/all-files/md/MdDateRange';
import { HackathonDto } from '@/types/common.dto';

export default function HackathonCreatePage() {
export default function AdminHackathonCreatePage() {
return (
<div className="w-full">
<PageTitle title="해커톤 등록" />
Expand Down
Loading

0 comments on commit a497f20

Please sign in to comment.