Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 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
09398c8
refactor: 미사용 레거시 코드 삭제
koty08 Jan 14, 2026
8fe9681
refactor: queryOption으로 변경
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
521b4d9
fix: 진행중 대회 해제 시 refetch로 인한 select 값 초기화 버그 수정
koty08 Jan 23, 2026
a6bad4b
design: 대회 목록 아이템에 카테고리명 추가
koty08 Jan 23, 2026
97c6e70
fix: 대회 목록 부분 누락된 key 추가
koty08 Jan 23, 2026
722d1a3
Merge branch 'feat/contest-manage' into feat/admin-dashboard
koty08 Jan 23, 2026
4ba1a74
feat: 데이터 없는 경우 공통 UI 추가, 어드민 메인 페이지에 적용
koty08 Jan 26, 2026
ece253e
Merge branch 'develop' into feat/admin-dashboard
koty08 Jan 26, 2026
0567c5f
refactor: 기존 twMerge 부분 cn 공통 유틸함수로 변경
koty08 Jan 27, 2026
35c5ecf
fix: AdminNoData 컴포넌트 className 부분 style로 잘못 작성된 부분 수정
koty08 Jan 27, 2026
dda330b
fix: 대회 리스트 아이템의 링크 영역 AdminCardRow 전체로 변경
koty08 Jan 27, 2026
93b945b
feat: 카테고리명 API 호출 전 trim 처리
koty08 Jan 27, 2026
69b2a8d
style: 진행 중 대회 설정 삭제 버튼 스타일 수정, 서비스 관련 정보 스타일 수정
koty08 Jan 28, 2026
ebf90fe
feat: 전체 공지사항 목록 ID 출력 부분 제거
koty08 Jan 28, 2026
0ed4c7b
fix: 카테고리, 전체 공지사항 추가 이후 입력값 초기화 처리
koty08 Jan 28, 2026
958d1fc
style: 전체 공지사항 영역에 스크롤 추가
koty08 Jan 28, 2026
372a822
Merge branch 'develop' into feat/admin-dashboard
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;
};
11 changes: 8 additions & 3 deletions src/apis/contests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ContestResponseDto, CurrentContestResponseDto, VoteTermDto } from 'types/DTO';
import { ContestRequestDto, ContestResponseDto, CurrentContestResponseDto, VoteTermDto } from 'types/DTO';
import apiClient from './apiClient';
import { TeamListItemResponseDto } from 'types/DTO/teams/teamListDto';

Expand All @@ -21,8 +21,8 @@ export const deleteContest = async (contestId: number) => {
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 @@ -31,6 +31,11 @@ export const getCurrentContest = async (): Promise<CurrentContestResponseDto[]>
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;
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: 3 additions & 3 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ButtonHTMLAttributes, ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
import { cn } from 'utils/classname';

interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
Expand All @@ -9,8 +9,8 @@ interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
const Button = ({ className = '', children, ...props }: Props) => {
return (
<button
className={twMerge(
'flex items-center justify-center rounded-md px-2 py-1 text-center text-sm text-white hover:cursor-pointer text-nowrap',
className={cn(
'flex items-center justify-center rounded-md px-2 py-1 text-center text-sm text-nowrap text-white hover:cursor-pointer',
className,
)}
{...props}
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 { cn } from 'utils/classname';

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={cn(
`border-lightGray focus:outline-mainGreen w-full rounded-lg border p-3 text-lg focus:outline-2`,
className,
)}
{...props}
/>
);
Expand Down
91 changes: 91 additions & 0 deletions src/components/ui/admin.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/components/ui 디렉토리는 shadcn 컴포넌트들이 위치하는 곳으로 알고 있습니다. 현재 디렉토리에 해당 파일이 위치할 경우 헷갈릴 수 있을 것 같아요 다른 적절한 위치로 파일을 이동하는 것은 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 @redzzzi 님께서도 해당 admin 파일 수정 사항이 feature 브랜치에 존재하여, 추후 머지된 이후에 위치 수정하도록 하겠습니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { VscKebabVertical } from 'react-icons/vsc';
import { LuPencil } from 'react-icons/lu';
import { FaRegTrashAlt } from 'react-icons/fa';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
import { DialogClose, DialogContent, DialogTitle } from './dialog';
import Button from '@components/Button';
import { cn } from '@components/lib/utils';

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={cn('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>
);
};

export const AdminNoData = ({ className }: React.ComponentProps<'div'>) => {
return (
<div className={cn('text-midGray my-10 flex items-center justify-center font-bold', className)}>
데이터가 없습니다.
</div>
);
};
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;
Loading