From 85459f15ecc369992959c6451b4aa7be9adbca05 Mon Sep 17 00:00:00 2001 From: leegwae Date: Sat, 26 Aug 2023 22:45:48 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix(auth-api):=20Auth=20API=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81=ED=95=9C?= =?UTF-8?q?=EB=8B=A4=20(#389)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/queries/auth.ts | 12 ++-- src/lib/api/auth.ts | 101 ++++++++++++----------------- src/lib/api/types.ts | 15 +++-- src/mocks/handlers/auth.ts | 12 ++-- src/pages/Login.tsx | 2 +- src/pages/__tests__/Login.test.tsx | 24 +++---- 6 files changed, 75 insertions(+), 91 deletions(-) diff --git a/src/hooks/queries/auth.ts b/src/hooks/queries/auth.ts index 4a5ddee0..ff708a01 100644 --- a/src/hooks/queries/auth.ts +++ b/src/hooks/queries/auth.ts @@ -2,11 +2,11 @@ import type { LogInBody, LogInResult, SignUpBody, - SignUpResult, SignUpError, LoginError, - LogoutResult, LogoutError, + SignUpResponse, + LogoutResponse, } from '@/lib/api/auth'; import type { ApiError } from '@/lib/apiClient'; import type { UseMutationResult } from '@tanstack/react-query'; @@ -17,13 +17,13 @@ import { login, logout, signUp } from '@/lib/api/auth'; import { queryKey } from '@/lib/queryKey'; export const useSignUpMutation = (): UseMutationResult< - SignUpResult, + SignUpResponse, ApiError | SyntaxError, SignUpBody > => { const key = queryKey.auth.signup(); const mutation = useMutation< - SignUpResult, + SignUpResponse, ApiError | SyntaxError, SignUpBody >(key, signUp); @@ -45,13 +45,13 @@ export const useLoginMutation = (): UseMutationResult< }; export const useLogoutMutation = (): UseMutationResult< - LogoutResult, + LogoutResponse, ApiError | SyntaxError, void > => { const key = queryKey.auth.logout(); const mutation = useMutation< - LogoutResult, + LogoutResponse, ApiError | SyntaxError >(key, logout); diff --git a/src/lib/api/auth.ts b/src/lib/api/auth.ts index f66b866c..a19cd6b6 100644 --- a/src/lib/api/auth.ts +++ b/src/lib/api/auth.ts @@ -3,16 +3,14 @@ import type { SuccessResponse, ErrorResponse } from '@/lib/api/types'; import { apiUrl } from '@/lib/api/apiUrl'; import { apiClient } from '@/lib/apiClient'; -export const signUp = async (body: SignUpBody): Promise => - apiClient - .post( - apiUrl.auth.signup(), - body, - undefined, - undefined, - false, - ) - .then((data) => data.result); +export const signUp = async (body: SignUpBody): Promise => + apiClient.post( + apiUrl.auth.signup(), + body, + undefined, + undefined, + false, + ); export const login = async (body: LogInBody): Promise => apiClient @@ -25,10 +23,8 @@ export const login = async (body: LogInBody): Promise => ) .then((data) => data.result); -export const logout = async (): Promise => - apiClient - .post(apiUrl.auth.logout()) - .then((data) => data.result); +export const logout = async (): Promise => + apiClient.post(apiUrl.auth.logout()); export const me = async (): Promise => apiClient @@ -46,78 +42,56 @@ export const refresh = async (): Promise => ) .then((data) => data.result); -/* ================================ [회원가입 API] ====================================== */ +/* ================================ [ 회원가입 API ] ====================================== */ export interface SignUpBody { email: Member['email']; password: Member['password']; nickname: Member['nickname']; } - -export interface SignUpResult { - message: string; -} - -export type SignUpResponse = SuccessResponse; - +export type SignUpResponse = SuccessResponse; export type SignUpError = DuplicateSignUpError | InvalidParameterSignUpError; - -export type DuplicateSignUpError = Omit & { +export type DuplicateSignUpError = ErrorResponse & { code: 'DUPLICATE_EMAIL' | 'DUPLICATE_NICKNAME'; + statusCode: 400; }; - -export type InvalidParameterSignUpError = Omit< - ErrorResponse, - 'code' | 'errors' -> & { +export type InvalidParameterSignUpError = Omit & { code: 'INVALID_PARAMETER'; + statusCode: 400; errors: Array<{ field: 'email' | 'password' | 'nickname'; message: string }>; }; -/* ================================ [로그인 API] ====================================== */ +/* ================================ [ 로그인 API ] ====================================== */ export interface LogInBody { email: Member['email']; password: Member['password']; } - export interface LogInResult { email: Member['email']; nickname: Member['nickname']; createdAt: Member['createdAt']; accessToken: string; } - export type LogInResponse = SuccessResponse; - -export type LoginError = NotExistMemberLoginError | InvalidPasswordLoginError; - -export type NotExistMemberLoginError = Omit< - ErrorResponse, - 'code' | 'statusCode' -> & { +export type LoginError = NotExistMemberLoginError | NotMatchPasswordLoginError; +export type InvalidParameterLoginError = Omit & { + code: 'INVALID_PARAMETER'; + statusCode: 400; + errors: Array<{ field: 'email' | 'password' | 'nickname'; message: string }>; +}; +export type NotExistMemberLoginError = ErrorResponse & { code: 'NOT_EXIST_MEMBER'; + statusCode: 404; }; - -export type InvalidPasswordLoginError = Omit< - ErrorResponse, - 'code' | 'statusCode' -> & { - code: 'INVALID_PASSWORD'; +export type NotMatchPasswordLoginError = ErrorResponse & { + code: 'NOT_MATCH_PASSWORD'; statusCode: 400; }; -/* ================================ [회원 정보 API] ====================================== */ -export type LogoutResult = undefined; -export type LogoutResponse = SuccessResponse; -export type LogoutError = InvalidAccessTokenLogoutError; -export type InvalidAccessTokenLogoutError = Omit< - ErrorResponse, - 'code' | 'statusCode' -> & { - code: 'INVALID_ACCESS_TOKEN'; - statusCode: 401; -}; +/* ================================ [ 로그아웃 API ] ====================================== */ +export type LogoutResponse = SuccessResponse; +export type LogoutError = InvalidAccessTokenError; -/* ================================ [회원 정보 API] ====================================== */ +/* ================================ [ 회원 정보 API ] ====================================== */ export interface GetMyInfoResult { email: Member['email']; nickname: Member['nickname']; @@ -125,11 +99,10 @@ export interface GetMyInfoResult { createdAt: Member['createdAt']; updatedAt: Member['updatedAt']; } - export type GetMyInfoResponse = SuccessResponse; +export type GetMyInfoError = InvalidAccessTokenError; -/* ================================ [토큰 리프레쉬 API] ====================================== */ - +/* ================================ [ 토큰 리프레쉬 API ] ====================================== */ export interface RefreshResult { email: Member['email']; nickname: Member['nickname']; @@ -149,7 +122,13 @@ export type InvalidRefreshTokenRefreshError = ErrorResponse & { statusCode: 401; }; -/* ================================ [사용자 엔티티] ====================================== */ +/* ================================ [ 유효하지 않은 토큰 에러 ] ====================================== */ +export type InvalidAccessTokenError = ErrorResponse & { + code: 'INVALID_ACCESS_TOKEN'; + statusCode: 401; +}; + +/* ================================ [ 사용자 엔티티 ] ====================================== */ export interface Member { email: string; diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts index 0e6474b5..77a998fd 100644 --- a/src/lib/api/types.ts +++ b/src/lib/api/types.ts @@ -1,8 +1,13 @@ -export interface SuccessResponse { - statusCode: number; - message?: string; - result: T; -} +export type SuccessResponse = T extends undefined + ? { + statusCode: number; + message?: string; + } + : { + statusCode: number; + message?: string; + result: T; + }; export interface ErrorResponse { code: string; diff --git a/src/mocks/handlers/auth.ts b/src/mocks/handlers/auth.ts index 075c383c..7a0cc1a5 100644 --- a/src/mocks/handlers/auth.ts +++ b/src/mocks/handlers/auth.ts @@ -26,7 +26,7 @@ export const signUp = rest.post( ctx.status(200), ctx.json({ statusCode: 200, - result: { message: `${nickname}이 회원가입에 성공했다` }, + message: '회원가입에 성공하였습니다.', }), ); }, @@ -61,7 +61,7 @@ export const logout = rest.post( ctx.cookie('refreshToken', '', { expires: new Date() }), ctx.json({ statusCode: 200, - result: undefined, + message: '로그아웃에 성공하였습니다.', }), ), ); @@ -92,9 +92,9 @@ export const me = rest.get( return res( ctx.status(401), ctx.json({ - code: 'unauthorized error', + code: 'INVALID_ACCESS_TOKEN', statusCode: 401, - message: 'unauthorized error', + message: '액세스 토큰이 유효하지 않습니다.', }), ); } @@ -127,9 +127,9 @@ export const refresh = rest.post( return res( ctx.status(401), ctx.json({ - code: 'unauthorized error', + code: 'INVALID_REFRESH_TOKEN', statusCode: 401, - message: 'unauthorized error', + message: '리프레시 토큰이 유효하지 않습니다.', }), ); } diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 628f02ce..0d39fa01 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -65,7 +65,7 @@ export function Login(): JSX.Element { if (code === 'NOT_EXIST_MEMBER') { setError('email', `${email}은 존재하지 않는 계정입니다`); - } else if (code === 'INVALID_PASSWORD') { + } else if (code === 'NOT_MATCH_PASSWORD') { setError('password', '이메일과 비밀번호가 일치하지 않습니다'); } }, diff --git a/src/pages/__tests__/Login.test.tsx b/src/pages/__tests__/Login.test.tsx index 2132ba79..223dea34 100644 --- a/src/pages/__tests__/Login.test.tsx +++ b/src/pages/__tests__/Login.test.tsx @@ -1,4 +1,4 @@ -import type { ErrorResponse } from '@/lib/api/types'; +import type { LoginError } from '@/lib/api/auth'; import React from 'react'; @@ -8,7 +8,7 @@ import { rest } from 'msw'; import { MemoryRouter } from 'react-router-dom'; import { apiUrl } from '@/lib/api/apiUrl'; -import * as apis from '@/lib/api/auth'; +import * as authApis from '@/lib/api/auth'; import { renderWithQueryClient } from '@/lib/test-utils'; import { server } from '@/mocks/server'; import { Login } from '@/pages/Login'; @@ -27,7 +27,7 @@ vi.mock('react-router-dom', async (importOriginal) => { describe('로그인 페이지', () => { it('유효한 형식의 이메일, 비밀번호를 제출하면 로그인 API를 호출하고 스토어에 저장한 후 홈 페이지로 이동한다', async () => { - const spyOnLogin = vi.spyOn(apis, 'login'); + const spyOnLogin = vi.spyOn(authApis, 'login'); const { emailInput, passwordInput, submitButton } = setup(); fireEvent.change(emailInput, { target: { value: '유효한 이메일' } }); @@ -48,13 +48,13 @@ describe('로그인 페이지', () => { }); it('사용자가 제출한 이메일로 된 계정이 존재하지 않으면 에러 메시지를 보여준다', async () => { - overrideLoginResultWithError({ - statusCode: 400, + overrideLogintWithError({ code: 'NOT_EXIST_MEMBER', - message: '계정이 존재하지 않습니다', + statusCode: 404, + message: '아이디가 존재하지 않습니다.', }); - const spyOnLogin = vi.spyOn(apis, 'login'); + const spyOnLogin = vi.spyOn(authApis, 'login'); const { emailInput, passwordInput, submitButton } = setup(); fireEvent.change(emailInput, { @@ -76,13 +76,13 @@ describe('로그인 페이지', () => { }); it('사용자가 제출한 비밀번호가 일치하지 않으면 에러 메시지를 보여준다', async () => { - overrideLoginResultWithError({ + overrideLogintWithError({ + code: 'NOT_MATCH_PASSWORD', statusCode: 400, - code: 'INVALID_PASSWORD', - message: '비밀번호가 일치하지 않습니다', + message: '비밀번호가 일치하지 않습니다.', }); - const spyOnLogin = vi.spyOn(apis, 'login'); + const spyOnLogin = vi.spyOn(authApis, 'login'); const { emailInput, passwordInput, submitButton } = setup(); fireEvent.change(emailInput, { @@ -104,7 +104,7 @@ describe('로그인 페이지', () => { }); }); -function overrideLoginResultWithError(data: ErrorResponse): void { +function overrideLogintWithError(data: LoginError): void { server.use( rest.post(apiUrl.auth.login(), async (req, res, ctx) => res(ctx.status(data.statusCode), ctx.json(data)), From f333c76f63b976d4d8f584bdc4fca54bda7631a2 Mon Sep 17 00:00:00 2001 From: leegwae Date: Sat, 26 Aug 2023 23:35:04 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix(panel-api):=20Panel=20API=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81=ED=95=9C?= =?UTF-8?q?=EB=8B=A4=20(#389)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/home/CreatePanelModal.tsx | 4 +- src/components/home/DeletePanelModal.tsx | 4 +- src/components/home/PanelGrid.tsx | 4 +- src/components/home/PanelItem.tsx | 4 +- src/components/home/UpdatePanelModal.tsx | 2 +- .../home/__tests__/DeletePanelModal.test.tsx | 5 +- .../home/__tests__/PanelGrid.test.tsx | 18 +++-- .../home/__tests__/PanelItem.test.tsx | 5 +- .../home/__tests__/UpdatePanelModal.test.tsx | 7 +- src/components/panel/CreateQuestionModal.tsx | 2 +- src/components/panel/InfiniteQuestionList.tsx | 2 +- src/components/panel/PanelSidebar.tsx | 5 +- .../__tests__/CreateQuestionModal.test.tsx | 3 +- .../__tests__/InfiniteQuestionList.test.tsx | 3 +- .../panel/__tests__/PanelSidebar.test.tsx | 3 +- src/hooks/queries/__tests__/question.test.tsx | 5 +- src/hooks/queries/active-info.ts | 2 +- src/hooks/queries/panel.ts | 12 ++-- src/hooks/queries/question.ts | 4 +- src/lib/api/__tests__/active-info.test.ts | 4 +- src/lib/api/__tests__/panel.test.ts | 12 ++-- src/lib/api/__tests__/question.test.ts | 6 +- src/lib/api/active-Info.ts | 4 +- src/lib/api/apiUrl.ts | 14 ++-- src/lib/api/panel.ts | 68 ++++++------------- src/lib/api/question.ts | 8 +-- src/lib/queryKey.ts | 7 +- src/mocks/data/panel.ts | 7 +- src/mocks/handlers/panel.ts | 10 ++- src/pages/Panel.tsx | 10 ++- src/pages/__tests__/panel-route.test.tsx | 4 +- 31 files changed, 115 insertions(+), 133 deletions(-) diff --git a/src/components/home/CreatePanelModal.tsx b/src/components/home/CreatePanelModal.tsx index 1d36b2ad..d52fb026 100644 --- a/src/components/home/CreatePanelModal.tsx +++ b/src/components/home/CreatePanelModal.tsx @@ -31,8 +31,8 @@ export function CreatePanelModal({ close }: Props): JSX.Element { createPanelMutation.mutate( { title, description }, { - onSuccess: ({ id }) => { - navigate(`/panel/${id}`); + onSuccess: ({ sid }) => { + navigate(`/panel/${sid}`); }, }, ); diff --git a/src/components/home/DeletePanelModal.tsx b/src/components/home/DeletePanelModal.tsx index 87b2c6e7..8c17bc87 100644 --- a/src/components/home/DeletePanelModal.tsx +++ b/src/components/home/DeletePanelModal.tsx @@ -10,7 +10,7 @@ interface Props { panel: Panel; } export function DeletePanelModal({ close, panel }: Props): JSX.Element { - const { id, title } = panel; + const { sid, title } = panel; const deletePanelMutation = useDeletePanelMutation(); return ( @@ -27,7 +27,7 @@ export function DeletePanelModal({ close, panel }: Props): JSX.Element { variant="danger" type="button" onClick={() => { - deletePanelMutation.mutate(id, { + deletePanelMutation.mutate(sid, { onSuccess: () => { close(); }, diff --git a/src/components/home/PanelGrid.tsx b/src/components/home/PanelGrid.tsx index 54a0c69f..1f3a1e00 100644 --- a/src/components/home/PanelGrid.tsx +++ b/src/components/home/PanelGrid.tsx @@ -66,7 +66,7 @@ export function PanelGrid({ panelPages }: Props): JSX.Element { const handlePanelTitleClick = useCallback( (panel: Panel) => () => { - navigate(`/panel/${panel.id}`); + navigate(`/panel/${panel.sid}`); }, [navigate], ); @@ -89,7 +89,7 @@ export function PanelGrid({ panelPages }: Props): JSX.Element { {panelPages.map((page) => page.panels.map((panel) => ( { - const { id, title, createdAt, description } = panel; + const { sid, title, createdAt, description } = panel; return (
  • diff --git a/src/components/home/UpdatePanelModal.tsx b/src/components/home/UpdatePanelModal.tsx index 761d5238..735192cc 100644 --- a/src/components/home/UpdatePanelModal.tsx +++ b/src/components/home/UpdatePanelModal.tsx @@ -14,7 +14,7 @@ type Props = CreateOverlayContentProps & { }; export function UpdatePanelModal({ close, panel }: Props): JSX.Element { - const updateMutation = useUpdatePanelMutation(panel.id); + const updateMutation = useUpdatePanelMutation(panel.sid); const messageRef = useRef(null); const { inputProps, errors, formProps, hasError } = useForm({ diff --git a/src/components/home/__tests__/DeletePanelModal.test.tsx b/src/components/home/__tests__/DeletePanelModal.test.tsx index 14e97ba5..9a262c79 100644 --- a/src/components/home/__tests__/DeletePanelModal.test.tsx +++ b/src/components/home/__tests__/DeletePanelModal.test.tsx @@ -2,6 +2,7 @@ import type { Panel } from '@/lib/api/panel'; import React from 'react'; +import { faker } from '@faker-js/faker'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -12,10 +13,10 @@ import { DeletePanelModal } from '../DeletePanelModal'; const handleClose = vi.fn(); const panel: Panel = { - id: 1, - author: 'test@email.com', + sid: faker.datatype.uuid(), title: '테스트 패널 제목', description: '테스트 패널 설명', + authorId: 0, createdAt: new Date().toDateString(), updatedAt: new Date().toDateString(), }; diff --git a/src/components/home/__tests__/PanelGrid.test.tsx b/src/components/home/__tests__/PanelGrid.test.tsx index a30f1220..008d4e34 100644 --- a/src/components/home/__tests__/PanelGrid.test.tsx +++ b/src/components/home/__tests__/PanelGrid.test.tsx @@ -1,5 +1,3 @@ -import type { GetMyPanelsResult } from '@/lib/api/panel'; - import React from 'react'; import { screen } from '@testing-library/react'; @@ -7,7 +5,7 @@ import userEvent from '@testing-library/user-event'; import { PanelGrid } from '@/components/home/PanelGrid'; import { renderWithQueryClient } from '@/lib/test-utils'; -import { createMockPanel, createMockPanleList } from '@/mocks/data/panel'; +import { createMockPanleList } from '@/mocks/data/panel'; const navigateMockFn = vi.fn(); vi.mock('react-router-dom', () => ({ @@ -18,13 +16,12 @@ vi.mock('@/hooks/useOverlay', () => ({ useOverlay: vi.fn() })); describe('PanelGrid', () => { it('패널 목록을 렌더링한다', () => { - const panelPages: GetMyPanelsResult[] = [ - { panels: createMockPanleList(10) }, - ]; + const panelPage = { panels: createMockPanleList(10), nextPage: -1 }; + const panelPages = [panelPage]; renderWithQueryClient(); expect( - screen.getAllByText(panelPages[0].panels[0].title)[0], + screen.getAllByText(panelPage.panels[0].title)[0], ).toBeInTheDocument(); }); @@ -35,13 +32,14 @@ describe('PanelGrid', () => { }); it('패널 제목을 누르면 해당 아이디의 패널 페이지로 이동한다', async () => { - const panel = createMockPanel(); - const panelPages: GetMyPanelsResult[] = [{ panels: [panel] }]; + const panelPage = { panels: createMockPanleList(10), nextPage: -1 }; + const panelPages = [panelPage]; renderWithQueryClient(); + const panel = panelPage.panels[0]; const title = screen.getByText(panel.title); await userEvent.click(title); - expect(navigateMockFn).toHaveBeenCalledWith(`/panel/${panel.id}`); + expect(navigateMockFn).toHaveBeenCalledWith(`/panel/${panel.sid}`); }); }); diff --git a/src/components/home/__tests__/PanelItem.test.tsx b/src/components/home/__tests__/PanelItem.test.tsx index 8694d124..a7b74d3e 100644 --- a/src/components/home/__tests__/PanelItem.test.tsx +++ b/src/components/home/__tests__/PanelItem.test.tsx @@ -2,16 +2,17 @@ import type { Panel } from '@/lib/api/panel'; import React from 'react'; +import { faker } from '@faker-js/faker'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { PanelItem } from '@/components/home/PanelItem'; const panel: Panel = { - id: 1, - author: 'test@email.com', + sid: faker.datatype.uuid(), title: '테스트 패널 제목', description: '테스트 패널 설명', + authorId: 0, createdAt: new Date().toDateString(), updatedAt: new Date().toDateString(), }; diff --git a/src/components/home/__tests__/UpdatePanelModal.test.tsx b/src/components/home/__tests__/UpdatePanelModal.test.tsx index 20a9ee37..4744a64b 100644 --- a/src/components/home/__tests__/UpdatePanelModal.test.tsx +++ b/src/components/home/__tests__/UpdatePanelModal.test.tsx @@ -3,6 +3,7 @@ import type * as Vi from 'vitest'; import React from 'react'; +import { faker } from '@faker-js/faker'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -19,10 +20,10 @@ vi.mock('@/lib/validator', () => ({ const handleClose = vi.fn(); const panel: Panel = { - id: 1, - author: 'test@email.com', + sid: faker.datatype.uuid(), title: '테스트 패널 제목', description: '테스트 패널 설명', + authorId: 0, createdAt: new Date().toDateString(), updatedAt: new Date().toDateString(), }; @@ -77,7 +78,7 @@ describe('UpdatePanelModal', () => { await userEvent.click(submitButton); - expect(spyOnUpdatePanel).toHaveBeenCalledWith(panel.id, { + expect(spyOnUpdatePanel).toHaveBeenCalledWith(panel.sid, { title: '유효한 패널 제목', description: '유효한 패널 설명', }); diff --git a/src/components/panel/CreateQuestionModal.tsx b/src/components/panel/CreateQuestionModal.tsx index a4c03918..129b1b4f 100644 --- a/src/components/panel/CreateQuestionModal.tsx +++ b/src/components/panel/CreateQuestionModal.tsx @@ -9,7 +9,7 @@ import { Button } from '@/components/system/Button'; import { useCreateQuestionMutation } from '@/hooks/queries/question'; interface Props { - panelId: Panel['id']; + panelId: Panel['sid']; close: () => void; } export function CreateQuestionModal({ panelId, close }: Props): JSX.Element { diff --git a/src/components/panel/InfiniteQuestionList.tsx b/src/components/panel/InfiniteQuestionList.tsx index 7e97aa3e..0ca25b59 100644 --- a/src/components/panel/InfiniteQuestionList.tsx +++ b/src/components/panel/InfiniteQuestionList.tsx @@ -26,7 +26,7 @@ import { IntersectionArea } from '../system/IntersectionArea'; import { QuestionList } from './QuestionList'; interface Props { - panelId: Panel['id']; + panelId: Panel['sid']; } type Sort = GetQuestionsParams['sort']; export function InfiniteQuestionList({ panelId }: Props): JSX.Element { diff --git a/src/components/panel/PanelSidebar.tsx b/src/components/panel/PanelSidebar.tsx index 98d98b22..41cf328f 100644 --- a/src/components/panel/PanelSidebar.tsx +++ b/src/components/panel/PanelSidebar.tsx @@ -9,14 +9,13 @@ interface Props { panel: Panel; } export function PanelSidebar({ panel }: Props): JSX.Element { - const { id, title, author, createdAt, description } = panel; + const { sid, title, createdAt, description } = panel; return (

    {title}

    -
    {author}
    {formatToKRLocaleString(createdAt)}
    @@ -29,7 +28,7 @@ export function PanelSidebar({ panel }: Props): JSX.Element { type="button" onClick={() => { window.navigator.clipboard.writeText( - `${window.location.origin}/panel/${id}`, + `${window.location.origin}/panel/${sid}`, ); }} > diff --git a/src/components/panel/__tests__/CreateQuestionModal.test.tsx b/src/components/panel/__tests__/CreateQuestionModal.test.tsx index b8d415a6..647ecfa0 100644 --- a/src/components/panel/__tests__/CreateQuestionModal.test.tsx +++ b/src/components/panel/__tests__/CreateQuestionModal.test.tsx @@ -2,6 +2,7 @@ import type { Panel } from '@/lib/api/panel'; import React from 'react'; +import { faker } from '@faker-js/faker'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -9,7 +10,7 @@ import { CreateQuestionModal } from '@/components/panel/CreateQuestionModal'; import * as questionApis from '@/lib/api/question'; import { renderWithQueryClient } from '@/lib/test-utils'; -const panelId: Panel['id'] = -1; +const panelId: Panel['sid'] = faker.datatype.uuid(); const handleClose = vi.fn(); describe('CreatePanelModal', () => { it('사용자가 입력하는 글자수를 보여준다', () => { diff --git a/src/components/panel/__tests__/InfiniteQuestionList.test.tsx b/src/components/panel/__tests__/InfiniteQuestionList.test.tsx index c3c2e3c8..91c84b09 100644 --- a/src/components/panel/__tests__/InfiniteQuestionList.test.tsx +++ b/src/components/panel/__tests__/InfiniteQuestionList.test.tsx @@ -4,6 +4,7 @@ import type { QueryClient } from '@tanstack/react-query'; import React from 'react'; +import { faker } from '@faker-js/faker'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { rest } from 'msw'; @@ -16,7 +17,7 @@ import { delay } from '@/lib/test-utils/delay'; import { createMockQuestion } from '@/mocks/data/question'; import { server } from '@/mocks/server'; -const panelId: Panel['id'] = 0; +const panelId: Panel['sid'] = faker.datatype.uuid(); describe('InfiniteQuestionList', () => { describe('질문 목록 렌더링', () => { diff --git a/src/components/panel/__tests__/PanelSidebar.test.tsx b/src/components/panel/__tests__/PanelSidebar.test.tsx index 101c876b..d4c59ac6 100644 --- a/src/components/panel/__tests__/PanelSidebar.test.tsx +++ b/src/components/panel/__tests__/PanelSidebar.test.tsx @@ -17,7 +17,6 @@ describe('PanelSidebar', () => { render(); expect(screen.getByText(panel.title)).toBeInTheDocument(); - expect(screen.getByText(panel.author)).toBeInTheDocument(); expect( screen.getByText(formatToKRLocaleString(panel.createdAt)), ).toBeInTheDocument(); @@ -37,7 +36,7 @@ describe('PanelSidebar', () => { await userEvent.click(copyUrlButton); expect(window.navigator.clipboard.writeText).toHaveBeenCalledWith( - `${window.location.origin}/panel/${panel.id}`, + `${window.location.origin}/panel/${panel.sid}`, ); }); }); diff --git a/src/hooks/queries/__tests__/question.test.tsx b/src/hooks/queries/__tests__/question.test.tsx index fb3cfcce..c9718ccd 100644 --- a/src/hooks/queries/__tests__/question.test.tsx +++ b/src/hooks/queries/__tests__/question.test.tsx @@ -5,6 +5,7 @@ import type { GetQuestionsResult, } from '@/lib/api/question'; +import { faker } from '@faker-js/faker'; import { act, renderHook, waitFor } from '@testing-library/react'; import { rest } from 'msw'; @@ -20,7 +21,7 @@ import { server } from '@/mocks/server'; describe('question API queries', () => { describe('useQuestionsInfiniteQuery', () => { - const panelId: Panel['id'] = -1; + const panelId: Panel['sid'] = faker.datatype.uuid(); it('getQuestions가 nextPage를 -1로 반환하면 hasNextPage는 false이다', async () => { overrideGetQuestionsWith200({ questions: [], nextPage: -1 }); @@ -64,7 +65,7 @@ describe('question API queries', () => { describe('createQuestionMutation', () => { it('mutate를 호출하면 createQuestion 함수를 호출한다', async () => { - const panelId: Panel['id'] = -1; + const panelId: Panel['sid'] = faker.datatype.uuid(); const body: questionApis.CreateQuestionBody = { content: '안녕하세요', }; diff --git a/src/hooks/queries/active-info.ts b/src/hooks/queries/active-info.ts index 2a6cb108..044df0a5 100644 --- a/src/hooks/queries/active-info.ts +++ b/src/hooks/queries/active-info.ts @@ -22,7 +22,7 @@ export type ActiveInfoDetailQueryOptions = NonNullableKeys<_ActiveInfoDetailQueryOptions>; export const activeInfoDetailQuery = ( - panelId: Panel['id'], + panelId: Panel['sid'], ): ActiveInfoDetailQueryOptions => ({ queryKey: queryKey.activeInfo.detail(panelId), queryFn: async () => getMyActiveInfo(panelId).then((res) => res.result), diff --git a/src/hooks/queries/panel.ts b/src/hooks/queries/panel.ts index b8001e2f..54297ff2 100644 --- a/src/hooks/queries/panel.ts +++ b/src/hooks/queries/panel.ts @@ -5,8 +5,8 @@ import type { UpdatePanelBody, UpdatePanelResult, GetMyPanelsParams, - GetMyPanelsResult, Panel, + MyPanelPage, } from '@/lib/api/panel'; import type { ApiError } from '@/lib/apiClient'; import type { @@ -25,11 +25,11 @@ import { import { queryKey } from '@/lib/queryKey'; export const useMyPanelsInfiniteQuery = (): UseInfiniteQueryResult< - GetMyPanelsResult, + MyPanelPage, ApiError | SyntaxError > => { const key = queryKey.panel.lists(); - const query = useInfiniteQuery( + const query = useInfiniteQuery( key, async ({ pageParam = 0 }) => getMyPanels({ page: pageParam as GetMyPanelsParams['page'] }), @@ -57,7 +57,7 @@ export const useCreatePanelMutation = (): UseMutationResult< }; export const useUpdatePanelMutation = ( - panelId: Panel['id'], + panelId: Panel['sid'], ): UseMutationResult< UpdatePanelResult, ApiError | SyntaxError, @@ -76,13 +76,13 @@ export const useUpdatePanelMutation = ( export const useDeletePanelMutation = (): UseMutationResult< DeletePanelResponse, ApiError | SyntaxError, - Panel['id'] + Panel['sid'] > => { const key = queryKey.panel.delete(); const mutation = useMutation< DeletePanelResponse, ApiError | SyntaxError, - Panel['id'] + Panel['sid'] >(key, deletePanel); return mutation; diff --git a/src/hooks/queries/question.ts b/src/hooks/queries/question.ts index b9a312ee..b1ffa409 100644 --- a/src/hooks/queries/question.ts +++ b/src/hooks/queries/question.ts @@ -23,7 +23,7 @@ import { createQuestion, getQuestions, likeQuestion } from '@/lib/api/question'; import { queryKey } from '@/lib/queryKey'; export const useQuestionsInfiniteQuery = ( - panelId: Panel['id'], + panelId: Panel['sid'], sort: GetQuestionsParams['sort'] = undefined, ): UseInfiniteQueryResult => { const key = queryKey.question.list(panelId, sort); @@ -46,7 +46,7 @@ export const useQuestionsInfiniteQuery = ( }; export const useCreateQuestionMutation = ( - panelId: Panel['id'], + panelId: Panel['sid'], ): UseMutationResult< CreateQuestionResult, ApiError | SyntaxError, diff --git a/src/lib/api/__tests__/active-info.test.ts b/src/lib/api/__tests__/active-info.test.ts index 5cb18d84..1674b3dc 100644 --- a/src/lib/api/__tests__/active-info.test.ts +++ b/src/lib/api/__tests__/active-info.test.ts @@ -1,5 +1,7 @@ import type { Panel } from '@/lib/api/panel'; +import { faker } from '@faker-js/faker'; + import { getMyActiveInfo } from '@/lib/api/active-Info'; import { apiUrl } from '@/lib/api/apiUrl'; import { myActiveInfoMock } from '@/mocks/data/active-info'; @@ -8,7 +10,7 @@ describe('activeInfo api', () => { it(`getMyActiveInfo를 호출하면 내 활동 정보 가져오기 API(${apiUrl.panel.getMyActiveInfo( ':panelId', )})`, async () => { - const panelId: Panel['id'] = 1; + const panelId: Panel['sid'] = faker.datatype.uuid(); const res = await getMyActiveInfo(panelId); expect(res.result).toEqual(myActiveInfoMock); }); diff --git a/src/lib/api/__tests__/panel.test.ts b/src/lib/api/__tests__/panel.test.ts index a7f0a288..02e71d48 100644 --- a/src/lib/api/__tests__/panel.test.ts +++ b/src/lib/api/__tests__/panel.test.ts @@ -1,5 +1,7 @@ import type { UpdatePanelBody, CreatePanelBody, Panel } from '@/lib/api/panel'; +import { faker } from '@faker-js/faker'; + import { apiUrl } from '@/lib/api/apiUrl'; import { createPanel, @@ -23,7 +25,7 @@ describe('panel api', () => { it(`updatePanel을 호출하면 패널 생성 API(${apiUrl.panel.update( ':panelId', )})로 요청한다`, async () => { - const panelId: Panel['id'] = 1; + const panelId: Panel['sid'] = faker.datatype.uuid(); const body: UpdatePanelBody = { title: '새로운 패널 이름', description: '새로운 패널 설명', @@ -32,13 +34,13 @@ describe('panel api', () => { const res = await updatePanel(panelId, body); expect(res.title).toBe(body.title); expect(res.description).toBe(body.description); - expect(res.id).toBe(panelId); + expect(res.sid).toBe(panelId); }); it(`deletePanel을 호출하면 패널 삭제 API(${apiUrl.panel.delete( ':panelId', )})로 요청한다`, async () => { - const panelId: Panel['id'] = 1; + const panelId: Panel['sid'] = faker.datatype.uuid(); const res = await deletePanel(panelId); expect(res.message).toBe('패널 삭제에 성공하였습니다.'); @@ -47,9 +49,9 @@ describe('panel api', () => { it(`getPanel을 호출하면 패널 가져오기 API(${apiUrl.panel.get( ':panelId', )})로 요청한다`, async () => { - const panelId: Panel['id'] = 1; + const panelId: Panel['sid'] = faker.datatype.uuid(); const res = await getPanel(panelId); - expect(res.result.id).toBe(panelId); + expect(res.result.sid).toBe(panelId); }); }); diff --git a/src/lib/api/__tests__/question.test.ts b/src/lib/api/__tests__/question.test.ts index 5a08eaab..1d07a811 100644 --- a/src/lib/api/__tests__/question.test.ts +++ b/src/lib/api/__tests__/question.test.ts @@ -1,6 +1,8 @@ import type { Panel } from '@/lib/api/panel'; import type { Question } from '@/lib/api/question'; +import { faker } from '@faker-js/faker'; + import { apiUrl } from '@/lib/api/apiUrl'; import { createQuestion, getQuestions, likeQuestion } from '@/lib/api/question'; import { mockQuestionList } from '@/mocks/data/question'; @@ -9,7 +11,7 @@ describe('question api', () => { it(`getQuestions를 호출하면 질문 목록 가져오기 API(${apiUrl.question.getQuestions( ':panelId', )})로 요청한다`, async () => { - const panelId: Panel['id'] = 0; + const panelId: Panel['sid'] = faker.datatype.uuid(); const res = await getQuestions(panelId, { page: 0, @@ -20,7 +22,7 @@ describe('question api', () => { it(`createQuestion을 호출하면 질문 생성 API (${apiUrl.question.create( ':panelId', )})로 요청한다`, async () => { - const panelId: Panel['id'] = 0; + const panelId: Panel['sid'] = faker.datatype.uuid(); const content: Question['content'] = '안녕하세요'; const res = await createQuestion(panelId, { content }); diff --git a/src/lib/api/active-Info.ts b/src/lib/api/active-Info.ts index 66fa0acb..d3ab8d1f 100644 --- a/src/lib/api/active-Info.ts +++ b/src/lib/api/active-Info.ts @@ -6,10 +6,10 @@ import { apiUrl } from '@/lib/api/apiUrl'; import { apiClient } from '@/lib/apiClient'; export const getMyActiveInfo = async ( - panelId: Panel['id'], + panelId: Panel['sid'], ): Promise => apiClient.get( - apiUrl.panel.getMyActiveInfo(String(panelId)), + apiUrl.panel.getMyActiveInfo(panelId), undefined, undefined, false, diff --git a/src/lib/api/apiUrl.ts b/src/lib/api/apiUrl.ts index e2365b8e..4bf739ce 100644 --- a/src/lib/api/apiUrl.ts +++ b/src/lib/api/apiUrl.ts @@ -1,3 +1,5 @@ +import type { Panel } from '@/lib/api/panel'; + const auth = { signup: () => `/api/auth/signup`, login: () => `/api/auth/login`, @@ -8,18 +10,18 @@ const auth = { const panel = { create: () => `/api/panel`, - get: (panelId: string) => `/api/panels/${panelId}`, - update: (panelId: string) => `/api/panels/${panelId}`, - delete: (panelId: string) => `/api/panels/${panelId}`, + get: (panelId: Panel['sid']) => `/api/panels/${panelId}`, + update: (panelId: Panel['sid']) => `/api/panels/${panelId}`, + delete: (panelId: Panel['sid']) => `/api/panels/${panelId}`, getMyPanels: () => `/api/panels`, - getMyActiveInfo: (panelId: string) => + getMyActiveInfo: (panelId: Panel['sid']) => `/api/panels/${panelId}/active-info` as const, } as const; const question = { - getQuestions: (panelId: string) => + getQuestions: (panelId: Panel['sid']) => `/api/panels/${panelId}/questions` as const, - create: (panelId: string) => `/api/panels/${panelId}/question`, + create: (panelId: Panel['sid']) => `/api/panels/${panelId}/question`, like: (questionId: string) => `/api/questions/${questionId}/like` as const, } as const; diff --git a/src/lib/api/panel.ts b/src/lib/api/panel.ts index 1786ee4d..fc35069d 100644 --- a/src/lib/api/panel.ts +++ b/src/lib/api/panel.ts @@ -1,4 +1,3 @@ -import type { Member } from '@/lib/api/auth'; import type { SuccessResponse } from '@/lib/api/types'; import { apiUrl } from '@/lib/api/apiUrl'; @@ -12,26 +11,26 @@ export const createPanel = async ( .then((data) => data.result); export const updatePanel = async ( - panelId: Panel['id'], + panelId: Panel['sid'], body: UpdatePanelBody, ): Promise => apiClient .patch( - apiUrl.panel.update(String(panelId)), + apiUrl.panel.update(panelId), body, ) .then((data) => data.result); export const deletePanel = async ( - panelId: Panel['id'], + panelId: Panel['sid'], ): Promise => - apiClient.delete(apiUrl.panel.delete(String(panelId))); + apiClient.delete(apiUrl.panel.delete(panelId)); export const getPanel = async ( - panelId: Panel['id'], + panelId: Panel['sid'], ): Promise => apiClient.get( - apiUrl.panel.get(String(panelId)), + apiUrl.panel.get(panelId), undefined, undefined, false, @@ -47,74 +46,49 @@ export const getMyPanels = async ( ) .then((data) => data.result); -/* ================================ [패널 생성 API] ====================================== */ +/* ================================ [ 패널 생성 API ] ====================================== */ export interface CreatePanelBody { - title: string; - description?: string; -} - -export interface CreatePanelResult { - id: Panel['id']; - author: Panel['author']; title: Panel['title']; - description: Panel['description']; - createdAt: Panel['createdAt']; + description?: Panel['description']; } - +export type CreatePanelResult = Panel; export type CreatePanelResponse = SuccessResponse; -/* ================================ [패널 수정 API] ====================================== */ +/* ================================ [ 패널 수정 API ] ====================================== */ export interface UpdatePanelBody { title: string; description?: string; } - export type UpdatePanelResult = Panel; - export type UpdatePanelResponse = SuccessResponse; - export type UpdatePanelPathParams = Record<'panelId', string>; -/* ================================ [패널 삭제 API] ====================================== */ -export interface DeletePanelBody { - title: string; - description?: string; -} - +/* ================================ [ 패널 삭제 API ] ====================================== */ export type DeletePanelResponse = SuccessResponse; - export type DeletePanelPathParams = Record<'panelId', string>; -/* ================================ [패널 가져오기 API] ====================================== */ -export interface GetPanelResult { - id: Panel['id']; - author: Panel['author']; - title: Panel['title']; - description?: Panel['description']; - createdAt: Panel['createdAt']; - updatedAt: Panel['updatedAt']; -} +/* ================================ [ 패널 가져오기 API ] ====================================== */ +export type GetPanelResult = Panel; export type GetPanelResponse = SuccessResponse; - export type GetPanelPathParams = Record<'panelId', string>; -/* ================================ [패널 목록 가져오기 API] ====================================== */ +/* ================================ [ 패널 목록 가져오기 API ] ====================================== */ export interface GetMyPanelsParams { - page?: number; + page: number; } -export interface GetMyPanelsResult { - nextPage?: number; +export interface MyPanelPage { + nextPage: number; panels: Panel[]; } - +export type GetMyPanelsResult = MyPanelPage; export type GetMyPanelsResponse = SuccessResponse; -/* ================================ [패널 엔티티] ====================================== */ +/* ================================ [ 패널 엔티티 ] ====================================== */ export interface Panel { - id: number; - author: Member['email']; + sid: string; title: string; description?: string; + authorId: number; createdAt: string; updatedAt: string; } diff --git a/src/lib/api/question.ts b/src/lib/api/question.ts index 7be3621e..c6d55ea8 100644 --- a/src/lib/api/question.ts +++ b/src/lib/api/question.ts @@ -6,22 +6,22 @@ import { apiClient } from '../apiClient'; import { apiUrl } from './apiUrl'; export const getQuestions = async ( - panelId: Panel['id'], + panelId: Panel['sid'], params: GetQuestionsParams, ): Promise => apiClient.get( - apiUrl.question.getQuestions(String(panelId)), + apiUrl.question.getQuestions(panelId), params, undefined, false, ); export const createQuestion = async ( - panelId: Panel['id'], + panelId: Panel['sid'], body: CreateQuestionBody, ): Promise => apiClient.post( - apiUrl.question.create(String(panelId)), + apiUrl.question.create(panelId), body, undefined, undefined, diff --git a/src/lib/queryKey.ts b/src/lib/queryKey.ts index da6fd763..c93866aa 100644 --- a/src/lib/queryKey.ts +++ b/src/lib/queryKey.ts @@ -18,13 +18,16 @@ const panelQueryKey = { const activeInfoQueryKeys = { all: ['activeInfos'] as const, details: () => [...activeInfoQueryKeys.all, 'details'], - detail: (panelId: Panel['id']) => [...activeInfoQueryKeys.details(), panelId], + detail: (panelId: Panel['sid']) => [ + ...activeInfoQueryKeys.details(), + panelId, + ], }; const questionQuerykey = { all: ['questions'] as const, lists: () => [...questionQuerykey.all, 'list'] as const, - list: (panelId: Panel['id'], sort: GetQuestionsParams['sort'] = undefined) => + list: (panelId: Panel['sid'], sort: GetQuestionsParams['sort'] = undefined) => [...questionQuerykey.lists(), panelId, { sort }] as const, create: () => ['createQuestion'] as const, like: () => ['likeQuestion'] as const, diff --git a/src/mocks/data/panel.ts b/src/mocks/data/panel.ts index b52101fa..87ef493a 100644 --- a/src/mocks/data/panel.ts +++ b/src/mocks/data/panel.ts @@ -2,14 +2,11 @@ import type { Panel } from '@/lib/api/panel'; import { faker } from '@faker-js/faker'; -import { myAccount } from '@/mocks/data/auth'; - -let id = 0; export const createMockPanel = (): Panel => ({ - id: id++, - author: myAccount.email, + sid: faker.datatype.uuid(), title: faker.music.songName(), description: faker.lorem.sentences().slice(0, 50), + authorId: 0, createdAt: new Date().toDateString(), updatedAt: new Date().toDateString(), }); diff --git a/src/mocks/handlers/panel.ts b/src/mocks/handlers/panel.ts index cf7960f3..0a21221f 100644 --- a/src/mocks/handlers/panel.ts +++ b/src/mocks/handlers/panel.ts @@ -17,8 +17,6 @@ import { apiUrl } from '@/lib/api/apiUrl'; import { API_BASE_URL } from '@/lib/apiClient'; import { createMockPanel, myPanelList } from '@/mocks/data/panel'; -import { myAccount } from '../data/auth'; - export const getMyPanels = rest.get( `${API_BASE_URL}${apiUrl.panel.getMyPanels()}`, async (req, res, ctx) => { @@ -79,8 +77,8 @@ export const updatePanel = rest.patch< ctx.json({ statusCode: 200, result: { - id: Number(panelId), - author: myAccount.email, + sid: panelId, + authorId: 0, title, description, createdAt: new Date().toDateString(), @@ -110,9 +108,9 @@ export const getPanel = rest.get( `${API_BASE_URL}${apiUrl.panel.get(':panelId')}`, async (req, res, ctx) => { const { panelId } = req.params; - const panel = myPanelList.find((panel) => panel.id === Number(panelId)) ?? { + const panel = myPanelList.find((panel) => panel.sid === panelId) ?? { ...createMockPanel(), - id: Number(panelId), + sid: panelId, }; return res( diff --git a/src/pages/Panel.tsx b/src/pages/Panel.tsx index a25fe8c8..6d941983 100644 --- a/src/pages/Panel.tsx +++ b/src/pages/Panel.tsx @@ -32,10 +32,8 @@ export const panelLoader = async ({ params }: LoaderFunctionArgs): Promise => { const panelId = params.id!; try { - const _panelId = Number(panelId); - if (Number.isNaN(_panelId)) throw json({ id: panelId }, { status: 404 }); - const { result: panel } = await getPanel(_panelId); - await queryClient.prefetchQuery(activeInfoDetailQuery(_panelId)); + const { result: panel } = await getPanel(panelId); + await queryClient.prefetchQuery(activeInfoDetailQuery(panelId)); return panel; } catch (error) { if (error instanceof ApiError) { @@ -59,7 +57,7 @@ export function Panel(): JSX.Element { function handleOpenModal(): void { overlay.open(({ close }) => ( - + )); } @@ -67,7 +65,7 @@ export function Panel(): JSX.Element {
    - +