Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
139b0e9
feat: exclude test files from biome scanner includes
ohprettyhak Aug 17, 2025
ad786f9
feat: add player management page with edit functionality
ohprettyhak Aug 17, 2025
031258e
feat: implement player management functionality with player list and …
ohprettyhak Aug 17, 2025
0f566e9
feat: enhance player management page with search functionality and im…
ohprettyhak Aug 17, 2025
a5a60b1
feat: add Modal component and integrate with player management page
ohprettyhak Aug 17, 2025
d02f42f
feat: add AlertDialog component for player deletion confirmation
ohprettyhak Aug 17, 2025
dea7c99
feat: enhance player list display with student number and adjust typo…
ohprettyhak Aug 18, 2025
ae69278
feat: add spinner component with customizable size and color, and enh…
ohprettyhak Aug 18, 2025
6d42bd6
feat: add loading state to Button component and integrate Spinner for…
ohprettyhak Aug 18, 2025
68eeb32
feat: add search functionality to player list with input field for fi…
ohprettyhak Aug 19, 2025
b28eb8f
feat: add player management page with add player button and update pl…
ohprettyhak Aug 19, 2025
113270c
feat: add player creation and update functionality with form handling…
ohprettyhak Aug 19, 2025
d3b612e
feat: refactor player delete menu import path for better module organ…
ohprettyhak Aug 19, 2025
315b916
feat: update player form to display message when no teams are available
ohprettyhak Aug 19, 2025
d4c5916
feat: improve loading state handling in alert dialog and refactor pla…
ohprettyhak Aug 19, 2025
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
4 changes: 3 additions & 1 deletion apps/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"@vercel/analytics": "^1.5.0",
"next": "^15.4.6",
"react": "^19.1.1",
"react-dom": "^19.1.1"
"react-dom": "^19.1.1",
"react-hook-form": "^7.62.0",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@hcc/typescript-config": "workspace:*",
Expand Down
3 changes: 3 additions & 0 deletions apps/manager/src/api/mutations/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from './useCreatePlayers';
export * from './useDeletePlayers';
export * from './useLogin';
export * from './useUpdatePlayers';
20 changes: 20 additions & 0 deletions apps/manager/src/api/mutations/useCreatePlayers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { fetcher, useMutation, useQueryClient } from '@hcc/api-base';
import type { PlayerType } from '~/api';
import { queryKeys } from '~/api/queryKey';

export type PlayerFormType = Pick<PlayerType, 'name' | 'studentNumber'>;

export const postPlayers = (request: PlayerFormType) => {
return fetcher.post<void>('players', { json: request });
};

export const useCreatePlayers = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: postPlayers,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: queryKeys.players._def });
},
});
};
21 changes: 21 additions & 0 deletions apps/manager/src/api/mutations/useDeletePlayers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { fetcher, useMutation, useQueryClient } from '@hcc/api-base';
import { queryKeys } from '~/api/queryKey';

type Request = {
id: number;
};

export const deletePlayers = ({ id }: Request) => {
return fetcher.delete<void>(`players/${id}`, { json: null });
};

export const useDeletePlayers = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: deletePlayers,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: queryKeys.players._def });
},
});
};
22 changes: 22 additions & 0 deletions apps/manager/src/api/mutations/useUpdatePlayers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { fetcher, useMutation, useQueryClient } from '@hcc/api-base';
import type { PlayerFormType } from '~/api';
import { queryKeys } from '~/api/queryKey';

type Request = {
id: number;
} & PlayerFormType;

export const patchPlayers = ({ id, ...request }: Request) => {
return fetcher.patch<void>(`players/${id}`, { json: request });
};

export const useUpdatePlayers = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: patchPlayers,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: queryKeys.players._def });
},
});
};
2 changes: 2 additions & 0 deletions apps/manager/src/api/queries/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './useLeaguesHome';
export * from './useLeaguesLeague';
export * from './usePlayer';
export * from './usePlayers';
9 changes: 9 additions & 0 deletions apps/manager/src/api/queries/usePlayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useQuery, useSuspenseQuery } from '@hcc/api-base';
import type { PlayerDetailPayload } from '~/api';
import { queryKeys } from '../queryKey';

export const usePlayer = (payload: PlayerDetailPayload) =>
useQuery(queryKeys.players.detail(payload));

export const useSuspensePlayer = (payload: PlayerDetailPayload) =>
useSuspenseQuery(queryKeys.players.detail(payload));
6 changes: 6 additions & 0 deletions apps/manager/src/api/queries/usePlayers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useQuery, useSuspenseQuery } from '@hcc/api-base';
import { queryKeys } from '../queryKey';

export const usePlayers = () => useQuery(queryKeys.players.list);

export const useSuspensePlayers = () => useSuspenseQuery(queryKeys.players.list);
15 changes: 13 additions & 2 deletions apps/manager/src/api/queryKey.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fetcher } from '@hcc/api-base';
import { createQueryKeys, mergeQueryKeys } from '@lukemorales/query-key-factory';
import type { LeagueDetailType, LeagueType } from './types';
import type { LeagueDetailType, LeagueType, PlayerDetailPayload, PlayerType } from './types';

const leagueQueryKeys = createQueryKeys('leagues', {
home: {
Expand All @@ -13,4 +13,15 @@ const leagueQueryKeys = createQueryKeys('leagues', {
},
});

export const queryKeys = mergeQueryKeys(leagueQueryKeys);
const playerQueryKeys = createQueryKeys('players', {
list: {
queryKey: null,
queryFn: () => fetcher.get<PlayerType[]>('players'),
},
detail: (payload: PlayerDetailPayload) => ({
queryKey: [payload],
queryFn: () => fetcher.get<PlayerType>(`players/${payload.id}`),
}),
});

export const queryKeys = mergeQueryKeys(leagueQueryKeys, playerQueryKeys);
6 changes: 3 additions & 3 deletions apps/manager/src/api/types/games.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { TeamType } from '~/api/types/teams';
import type { GameTeamType } from '~/api/types/teams';

export type GameType = {
id: number;
isPkTaken: boolean;
startTime: string;
gameQuarter: string;
gameName: string;
round: number;
videoId: string;
gameTeams: TeamType[];
isPkTaken: boolean;
gameTeams: GameTeamType[];
};
1 change: 1 addition & 0 deletions apps/manager/src/api/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './games';
export * from './leagues';
export * from './players';
export * from './teams';
13 changes: 13 additions & 0 deletions apps/manager/src/api/types/players.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { TeamType } from '~/api';

export type PlayerType = {
playerId: number;
name: string;
studentNumber: string;
totalGoalCount: number;
teams: TeamType[];
};

export type PlayerDetailPayload = {
id: number;
};
8 changes: 8 additions & 0 deletions apps/manager/src/api/types/teams.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
export type TeamType = {
id: number;
name: string;
logoImageUrl: string;
unit: string;
teamColor: string;
};

export type GameTeamType = {
gameTeamId: number;
gameTeamName: string;
logoImageUrl: string;
Expand Down
7 changes: 4 additions & 3 deletions apps/manager/src/app/_components/game-card.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ChevronForwardIcon } from '@hcc/icons';
import { formatTime } from '@hcc/toolkit';
import { Badge, Button, Typography } from '@hcc/ui';
import Image from 'next/image';
import Link from 'next/link';
import type { PropsWithChildren } from 'react';
import type { GameType, TeamType } from '~/api';
import type { GameTeamType, GameType } from '~/api';
import { ROUTES } from '~/constants/routes';

export const GameCardRoot = ({ children }: PropsWithChildren) => {
Expand All @@ -22,7 +23,7 @@ const GameHeader = ({ leagueId, id: gameId, gameQuarter, startTime }: GameCardPr
</Badge>
<Typography className="center-y" color="var(--color-neutral-500)" fontSize={14} asChild>
<Link href={`${ROUTES.LEAGUE}/${leagueId}/${gameId}`}>
{startTime}
{formatTime(startTime, { format: 'YYYY.MM.DD. HH:mm' })}
<ChevronForwardIcon size={20} />
</Link>
</Typography>
Expand All @@ -34,7 +35,7 @@ const GameTeamGroup = ({ children }: PropsWithChildren) => {
return <div className="column mt-4 gap-2">{children}</div>;
};

const GameTeam = ({ gameTeamName, logoImageUrl, score }: TeamType) => {
const GameTeam = ({ gameTeamName, logoImageUrl, score }: GameTeamType) => {
return (
<div className="row-between">
<div className="center-y flex-1 gap-2">
Expand Down
11 changes: 9 additions & 2 deletions apps/manager/src/app/auth/login/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ROUTES } from '~/constants/routes';

export const LoginForm = () => {
const router = useRouter();
const { mutate } = useLogin();
const { mutate, isPending } = useLogin();

const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
Expand Down Expand Up @@ -49,7 +49,14 @@ export const LoginForm = () => {
placeholder="비밀번호"
autoComplete="current-password"
/>
<Button className="mt-6" size="xl" color="black" variant="solid" type="submit">
<Button
type="submit"
className="mt-6"
size="xl"
color="black"
variant="solid"
loading={isPending}
>
로그인
</Button>
</form>
Expand Down
30 changes: 30 additions & 0 deletions apps/manager/src/app/players/[id]/form-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import { toast } from '@hcc/ui';
import { useRouter } from 'next/navigation';
import { type PlayerFormType, useSuspensePlayer, useUpdatePlayers } from '~/api';
import { PlayerForm } from '../_components/player-form';

type Props = {
id: number;
};

export const FormSection = ({ id }: Props) => {
const router = useRouter();

const { mutateAsync } = useUpdatePlayers();
const handleSubmit = async (data: PlayerFormType) => {
try {
await mutateAsync({ id, ...data });
toast.success('선수가 수정되었어요.');
router.back();
} catch (error) {
console.error(error);
toast.error('선수 수정에 실패했어요.');
}
};

const { data } = useSuspensePlayer({ id });

return <PlayerForm className="p-5" onSubmit={handleSubmit} initialData={data} />;
};
43 changes: 43 additions & 0 deletions apps/manager/src/app/players/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Spinner } from '@hcc/ui';
import { Suspense } from '@suspensive/react';
import { notFound } from 'next/navigation';
import { Header } from '~/components/layout';
import { TipBanner } from '../_components/tip-banner';
import { FormSection } from './form-section';
import { PlayerDeleteMenu } from './player-delete-menu';

type Props = {
params: Promise<{ id: number }>;
};

const Page = async ({ params }: Props) => {
const { id: _id } = await params;

if (!_id || Number.isNaN(_id)) {
notFound();
}

const id: number = Number(_id);

return (
<>
<Header title="선수 수정" menu={<PlayerDeleteMenu id={id} />} arrow />

<div className="column-between h-full overflow-hidden">
<Suspense
fallback={
<div className="center p-5">
<Spinner size="lg" color="neutral" />
</div>
}
clientOnly
>
<FormSection id={id} />
</Suspense>
<TipBanner />
</div>
</>
);
};

export default Page;
35 changes: 35 additions & 0 deletions apps/manager/src/app/players/[id]/player-delete-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client';

import { Typography, toast } from '@hcc/ui';
import { useRouter } from 'next/navigation';
import { useDeletePlayers } from '~/api';
import { AlertDialog } from '~/components/ui';

export const PlayerDeleteMenu = ({ id }: { id: number }) => {
const router = useRouter();
const { mutateAsync } = useDeletePlayers();

const handlePlayerDelete = async (playerId: number): Promise<void> => {
try {
await mutateAsync({ id: playerId });
toast.success('선수가 삭제되었어요.');
router.back();
} catch (error) {
console.error(error);
toast.error('선수 삭제에 실패했어요.');
}
};

return (
<AlertDialog
title="삭제한 선수는 다시 복구할 수 없어요"
description="정말 삭제할까요?"
primaryTitle="삭제"
onPrimaryClick={() => handlePlayerDelete(id)}
>
<Typography className="cursor-pointer" color="var(--color-danger-600)" weight="semibold">
삭제
</Typography>
</AlertDialog>
);
};
Loading