Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
6b75c81
feat: 현재 선택된 대회명 가져오는 공통 훅 생성
koty08 Nov 24, 2025
036ac9f
feat: 대회명 수정 컴포넌트 생성
koty08 Nov 24, 2025
de1a459
feat: 대회 삭제 컴포넌트 생성
koty08 Nov 24, 2025
d6d6f19
feat: 대회 관리 페이지 생성 및 라우팅 설정
koty08 Nov 24, 2025
bdb674b
fix: AdminContestLayout 컨텐츠 영역 flex-1 적용
koty08 Nov 24, 2025
0375366
feat: 현재 진행중인 대회 설정 컴포넌트 추가
koty08 Nov 25, 2025
9a6fcba
feat: 모달 위한 radix-ui dialog 라이브러리 설치 및 커스터마이징
koty08 Nov 25, 2025
bf6d49b
feat: admin 페이지에서 사용할 합성 컴포넌트 모음 추가
koty08 Nov 25, 2025
6e21141
feat: 대회 카테고리 섹션 및 추가/수정/삭제 모달 컴포넌트 작업
koty08 Nov 25, 2025
52cc2cd
feat: 대회 목록 섹션 컴포넌트 추가
koty08 Nov 25, 2025
c46c2f7
feat: 전체 공지사항 섹션 추가 및 추가/수정 모달 컴포넌트 작업
koty08 Nov 25, 2025
b4259c0
feat: 서비스 관련 정보 섹션 컴포넌트 추가
koty08 Nov 25, 2025
a17fd3d
feat: 어드민 메인(대시보드)페이지 추가 및 라우팅 설정
koty08 Nov 25, 2025
1684a59
feat: 어드민 공통 삭제 확인 모달 추가
koty08 Nov 30, 2025
ac738f1
design: 모달 X 버튼 위치, 패딩 수정
koty08 Nov 30, 2025
95bf063
fix: 카테고리 생성 모달도 Controlled 하게 설정, 삭제 확인 모달 공통으로 수정
koty08 Nov 30, 2025
a03c9b1
fix: 모달 Title 컴포넌트 추가 및 적용
koty08 Nov 30, 2025
41eacf4
feat: 대회 카테고리 DTO 및 Fetcher 정의
koty08 Nov 30, 2025
8785117
feat: 카테고리 CRUD API 연결
koty08 Nov 30, 2025
a321df4
feat: 대회 전체 조회에 DTO 현재 진행 상태, 카테고리 추가
koty08 Dec 1, 2025
b52194e
feat: 전체 대회 조회 queryOption 추가, 현재 대회 상태 변경 Fetcher 추가
koty08 Dec 1, 2025
e32ce15
feat: 전체 대회 조회 및 현재 대회 상태 변경 API 컴포넌트에 연결, ContestSlots 컴포넌트 별도 분리
koty08 Dec 1, 2025
e1fee23
fix: 기존 공지사항 DTO 내 날짜 형식 Date -> string으로 수정
koty08 Dec 23, 2025
dae7482
refactor: 공지사항 및 공지사항 상세 API QueryOption으로 별도 분리
koty08 Dec 23, 2025
5ff73ed
feat: 대회 생성 단계 및 생성된 대회 ID 관리 위한 ContextProvider 생성
koty08 Jan 12, 2026
827066e
feat: 카테고리 선택 드롭다운 컴포넌트 생성 (대회 관리에서 추후 활용)
koty08 Jan 12, 2026
8787256
feat: 대회 생성 단계 UI 컴포넌츠 생성
koty08 Jan 12, 2026
4166e72
feat: 대회 생성 1단계 컴포넌트 생성
koty08 Jan 12, 2026
b2f9299
chore: 템플릿 파일 public 폴더 내 추가
koty08 Jan 12, 2026
69cef09
feat: 대회 생성 2단계 - 대회 참여자 설정 컴포넌트 생성
koty08 Jan 12, 2026
8a053eb
feat: 대회 생성 3단계 - 필수 항목 설정 컴포넌트 틀만 우선 생성
koty08 Jan 12, 2026
8dd84b4
feat: 대회 생성 페이지 및 라우팅 설정
koty08 Jan 12, 2026
486e3a6
Merge branch 'feat/contest-create' into feat/contest-manage
koty08 Jan 13, 2026
fbcb076
feat: 대회 수정 부분 대회 카테고리 컴포넌트 사용, 카테고리도 함께 수정할 수 있게 변경
koty08 Jan 13, 2026
58a093e
style: 대회 삭제 버튼 스타일 수정
koty08 Jan 13, 2026
097e226
style: 상단 Progress UI 애니메이션 추가
koty08 Jan 14, 2026
0c1b095
Merge branch 'feat/contest-manage' into feat/contest-create
koty08 Jan 14, 2026
28f18e4
feat: 대회 생성 단계 생성 API 연결
koty08 Jan 14, 2026
a9d2a15
feat: 대회 참여자 설정 단계 API 연결
koty08 Jan 14, 2026
0e8136a
fix: 전체 공지사항 모달 닫기 버그 수정, mutation 관련 로직 수정
koty08 Jan 20, 2026
99b3ed4
fix: 카테고리 삭제 모달 onDelete 잘못 설정된 부분 수정
koty08 Jan 20, 2026
9d4eb4d
feat: 공지사항 삭제 확인 모달 추가
koty08 Jan 20, 2026
f88cc3a
fix: 카테고리 CUD 이후 리스트 API 리패칭하도록 수정
koty08 Jan 23, 2026
e791185
design: 공지사항 목록 border 색상 수정
koty08 Jan 23, 2026
db2931e
fix: 전체 공지사항 삭제 후 리패칭 하도록 수정, 삭제 모달 안열리는 버그 수정
koty08 Jan 23, 2026
1e04f06
feat: 카테고리 Select 컴포넌트에 API 연결
koty08 Jan 23, 2026
1bba16e
feat: 대회 생성 취소시 뒤로가기 추가
koty08 Jan 23, 2026
521b4d9
fix: 진행중 대회 해제 시 refetch로 인한 select 값 초기화 버그 수정
koty08 Jan 23, 2026
a6bad4b
design: 대회 목록 아이템에 카테고리명 추가
koty08 Jan 23, 2026
97c6e70
fix: 대회 목록 부분 누락된 key 추가
koty08 Jan 23, 2026
5801bea
Merge branch 'feat/contest-create' into feat/contest-manage
koty08 Jan 25, 2026
7db2180
feat: 대회 삭제 시 확인 모달 추가
koty08 Jan 27, 2026
9735071
chore: 콘솔로그 삭제
koty08 Jan 27, 2026
c0d03d4
fix: 대회 수정 시 카테고리 선택의 defaultValue값 현재 대회의 값으로 선택되게 수정
koty08 Jan 27, 2026
96c531b
feat: 대회명 수정 시 입력 값 trim 처리
koty08 Jan 27, 2026
a68bfff
chore: text-[16px] -> text-base로 변경
koty08 Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
Expand Down
Binary file added public/팀등록_템플릿파일.xlsx
Binary file not shown.
22 changes: 22 additions & 0 deletions src/apis/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { CategoryDto } from 'types/DTO';
import apiClient from './apiClient';

export const getAllCategory = async () => {
const res = await apiClient.get<CategoryDto[]>('/categories');
return res.data;
};

export const postCategory = async (categoryName: string) => {
const res = await apiClient.post('/categories', { categoryName });
return res.data;
};

export const patchCategory = async (categoryId: number, categoryName: string) => {
const res = await apiClient.patch(`/categories/${categoryId}`, { categoryName });
return res.data;
};

export const deleteCategory = async (categoryId: number) => {
const res = await apiClient.delete(`/categories/${categoryId}`);
return res.data;
};
25 changes: 20 additions & 5 deletions src/apis/contests.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { ContestResponseDto, VoteTermDto } from 'types/DTO';
import { ContestRequestDto, ContestResponseDto, VoteTermDto } from 'types/DTO';
import apiClient from './apiClient';
import { mockContestsResponse } from 'mocks/data/contests';
import { TeamListItemResponseDto } from 'types/DTO/teams/teamListDto';

export const postContest = async (payload: ContestRequestDto): Promise<ContestResponseDto> => {
const res = await apiClient.post('/contests', payload);
return res.data;
};

export const getAllContests = async (): Promise<ContestResponseDto[]> => {
const res = await apiClient.get('/contests');
return res.data.map((contest: ContestResponseDto) => ({
Expand All @@ -18,12 +22,11 @@ export const postAllContests = async (contestName: string) => {

export const deleteContest = async (contestId: number) => {
const res = await apiClient.delete(`/contests/${contestId}`);
console.log(res);
return res.data;
};

export const patchContest = async (contestId: number, contestName: string) => {
const res = await apiClient.patch(`/contests/${contestId}`, { contestName });
export const patchContest = async (contestId: number, payload: ContestRequestDto) => {
const res = await apiClient.patch(`/contests/${contestId}`, payload);
return res.data;
};

Expand All @@ -32,11 +35,23 @@ export const getCurrentContestTeams = async (): Promise<TeamListItemResponseDto[
return res.data;
};

export const patchChangeOngoingContest = async (contestId: number, isCurrent: boolean) => {
const res = await apiClient.patch(`/contests/${contestId}/current`, { isCurrent });
return res.data;
};

export const getContestTeams = async (contestId: number): Promise<TeamListItemResponseDto[]> => {
const res = await apiClient.get(`/contests/${contestId}/teams`);
return res.data;
};

export const postBulkAddTeams = async (contestId: number, formData: FormData) => {
const res = await apiClient.post(`/contests/${contestId}/teams/bulk`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return res.data;
};

export const getVoteTerm = async (contestId: number): Promise<VoteTermDto> => {
const res = await apiClient.get(`/contests/${contestId}/vote`);
return res.data;
Expand Down
13 changes: 4 additions & 9 deletions src/apis/notices.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import { NoticeDetailDto, NoticeListDto, NoticeRequestDto } from 'types/DTO/noticeDto';
import apiClient from './apiClient';
import { NoticeResponseDto } from 'types/DTO/notices/NoticeResponseDto';
import { NoticeDetailDto } from '../types/DTO/notices/NoticeDetailDto';
import { NoticeRequestDto } from 'types/DTO/notices/NoticeRequestDto';

export const getNotices = async (): Promise<NoticeResponseDto[]> => {
export const getNotices = async (): Promise<NoticeListDto[]> => {
const { data } = await apiClient.get('/notices');
return data.map((notice: NoticeResponseDto) => ({
...notice,
createdAt: new Date(notice.createdAt),
}));
return data;
};

export const getNoticeDetail = async (noticeId: number): Promise<NoticeDetailDto> => {
const { data } = await apiClient.get(`/notices/${noticeId}`);
return { ...data, updatedAt: new Date(data.updatedAt), createdAt: new Date(data.createdAt) };
return data;
};

export const postCreateNotice = async (request: NoticeRequestDto) => {
Expand Down
6 changes: 5 additions & 1 deletion src/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { InputHTMLAttributes } from 'react';
import { twMerge } from 'tailwind-merge';

interface Props extends InputHTMLAttributes<HTMLInputElement> {}

const Input = ({ className = '', ...props }: Props) => {
return (
<input
className={`border-lightGray focus:outline-mainGreen w-full rounded-lg border p-3 text-lg focus:outline-2 ${className}`}
className={twMerge(
`border-lightGray focus:outline-mainGreen w-full rounded-lg border p-3 text-lg focus:outline-2`,
className,
)}
{...props}
/>
);
Expand Down
83 changes: 83 additions & 0 deletions src/components/ui/admin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { VscKebabVertical } from 'react-icons/vsc';
import { LuPencil } from 'react-icons/lu';
import { FaRegTrashAlt } from 'react-icons/fa';
import { twMerge } from 'tailwind-merge';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
import { DialogClose, DialogContent, DialogTitle } from './dialog';
import Button from '@components/Button';

export const AdminCard = ({ children }: React.ComponentProps<'div'>) => {
return <div className="border-lightGray flex flex-col gap-0.5 rounded-xl border-2">{children}</div>;
};

export const AdminCardTop = ({ children }: React.ComponentProps<'div'>) => {
return <div className="border-lightGray flex items-center justify-between border-b px-5 py-4">{children}</div>;
};

export const AdminCardCreateButton = ({ children, ...props }: React.ComponentProps<'button'>) => {
return (
<button className="text-midGray hover:text-mainBlue rounded-xl px-[15px] py-2.5 font-bold" {...props}>
{children}
</button>
);
};

export const AdminCardRow = ({ children, className }: React.ComponentProps<'div'>) => {
return <div className={twMerge('flex items-center justify-between px-5 py-3', className)}>{children}</div>;
};

export const AdminPopoverMenu = ({ children, ...props }: React.ComponentProps<'div'>) => {
return (
<Popover>
<PopoverTrigger asChild>
<button className="group rounded-4xl px-2 py-2 hover:bg-blue-100">
<VscKebabVertical size={16} className="group-hover:fill-mainBlue fill-midGray" />
</button>
</PopoverTrigger>
<PopoverContent className="w-fit p-1.5">
<div className="flex w-[70px] flex-col gap-1" {...props}>
{children}
</div>
</PopoverContent>
</Popover>
);
};

export const AdminPopoverEditButton = ({ onEdit }: { onEdit: () => void }) => {
return (
<button className="hover:bg-whiteGray flex items-center gap-2.5 rounded-sm p-1 text-sm" onClick={onEdit}>
<LuPencil size={16} className="mt-1" />
수정
</button>
);
};

export const AdminPopoverDeleteButton = ({ onDelete }: { onDelete: () => void }) => {
return (
<button
className="hover:bg-whiteGray flex items-center gap-2.5 rounded-sm p-1 text-sm text-red-500"
onClick={onDelete}
>
<FaRegTrashAlt size={16} className="mt-1 fill-red-500" />
삭제
</button>
);
};

export const AdminDeleteConfirmModal = ({ title, onDelete }: { title: string; onDelete: () => void }) => {
return (
<DialogContent className="gap-6">
<DialogTitle className="text-center text-lg font-semibold text-gray-800">{title}</DialogTitle>
<div className="flex justify-center gap-4">
<DialogClose asChild>
<Button className="border-lightGray text-midGray rounded-full border px-4 py-2 hover:bg-gray-100">
{'닫기'}
</Button>
</DialogClose>
<Button className="rounded-full bg-red-700 px-4 py-2" onClick={onDelete}>
{'삭제'}
</Button>
</div>
</DialogContent>
);
};
58 changes: 58 additions & 0 deletions src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { RxCross2 } from 'react-icons/rx';

import { cn } from '@components/lib/utils';

function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}

function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.DialogTrigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}

function DialogContent({ className, children, ...props }: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/30"
/>
<DialogPrimitive.Content
data-slot="dialog-content"
aria-describedby={undefined}
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-1/2 left-1/2 z-50 grid w-fit -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-4 border p-7 pt-9 pr-9 shadow-lg duration-200 sm:rounded-lg',
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close
asChild
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-2 right-2 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none"
>
<button>
<RxCross2 size={20} />
<span className="sr-only">Close</span>
</button>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
);
}

function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title data-slot="dialog-title" asChild>
<h3 className={cn('"text-center text-gray-800" text-lg font-semibold', className)} {...props} />
</DialogPrimitive.Title>
);
}

function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}

export { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogClose };
12 changes: 12 additions & 0 deletions src/hooks/useContestName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useParams } from 'react-router-dom';
import useContests from './useContests';

const useContestName = () => {
const { contestId: contestIdParam } = useParams();
const { data: contests } = useContests();
const contestName = contests?.find((contest) => contest.contestId === Number(contestIdParam))?.contestName;

return contestName;
};

export default useContestName;
2 changes: 1 addition & 1 deletion src/layout/admin/contest/AdminContestLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const AdminContestLayout = () => {
<LayoutTopBar />
<div className="flex">
<LayoutSideBar />
<div className="border-l">
<div className="flex-1 border-l">
<FullContainerLayout />
</div>
</div>
Expand Down
Loading