From 0ee600b21838edaf9a4c9b0ff4c45c649364ad55 Mon Sep 17 00:00:00 2001 From: Jungu Lee <100949102+jobkaeHenry@users.noreply.github.com> Date: Thu, 9 Nov 2023 22:22:49 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8-=EA=B8=B0=EB=8A=A5-?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * New : 로그인 관련 타입정의 * New : React Query Dev tool 설치 * New : React Query Dev tool 설치 * New : Custom Error handler 추가 * Bug : Storybook 빌드 에러 해결 * New : 로그인 기능 서버 연결 --- client/package-lock.json | 27 +++++++++++++ client/package.json | 1 + client/src/app/layout.tsx | 2 + .../src/components/user/signin/SigninForm.tsx | 40 ++++++++++++++----- client/src/const/serverPath.ts | 8 ++++ client/src/hooks/useLogin.ts | 22 ++++++---- client/src/hooks/useSetCookie.ts | 15 +++++++ client/src/mocks/handlers/getPostDetail.ts | 2 +- client/src/mocks/handlers/getPostList.ts | 2 +- client/src/queries/auth/useLoginMutation.tsx | 40 +++++++++++++++++++ client/src/queries/auth/useUserInfoQuery.tsx | 33 +++++++++++++++ .../queries/post/useGetPostDetailQuery.tsx | 3 +- .../src/queries/post/useGetPostListQuery.tsx | 4 +- .../Components/Post/PostCard.stories.tsx | 39 ++++++++++++------ client/src/types/auth/myInfo.ts | 13 ++++++ client/src/types/auth/signin.ts | 4 -- client/src/types/auth/signinRequirement.ts | 10 +++++ client/src/types/auth/signinResponse.ts | 9 +++++ client/src/types/auth/signupRequirement.ts | 18 +++++++++ client/src/utils/errorHandler.ts | 3 ++ 20 files changed, 256 insertions(+), 39 deletions(-) create mode 100644 client/src/const/serverPath.ts create mode 100644 client/src/hooks/useSetCookie.ts create mode 100644 client/src/queries/auth/useLoginMutation.tsx create mode 100644 client/src/queries/auth/useUserInfoQuery.tsx create mode 100644 client/src/types/auth/myInfo.ts delete mode 100644 client/src/types/auth/signin.ts create mode 100644 client/src/types/auth/signinRequirement.ts create mode 100644 client/src/types/auth/signinResponse.ts create mode 100644 client/src/types/auth/signupRequirement.ts create mode 100644 client/src/utils/errorHandler.ts diff --git a/client/package-lock.json b/client/package-lock.json index 73d26ca..54f63de 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,6 +13,7 @@ "@mui/icons-material": "^5.14.15", "@mui/material": "^5.14.15", "@tanstack/react-query": "^5.8.1", + "@tanstack/react-query-devtools": "^5.8.1", "axios": "^1.6.0", "framer-motion": "^10.16.4", "next": "14.0.0", @@ -7162,6 +7163,15 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/query-devtools": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.7.4.tgz", + "integrity": "sha512-wx+gwRZUZq7ISXKFJgaAYvVPt+DwTaLKlzaVLB9SLBFIzcL8Jz8jDQGR9ZQp7/O7SVj0TohS8aFuiiGzQUvWyg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/react-query": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.8.1.tgz", @@ -7187,6 +7197,23 @@ } } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.8.1.tgz", + "integrity": "sha512-sE6El2Yj98LlPDzf2Ju9IjWPZvqh2RgoQZ+Jomrfm80uA1HlHUvhXuOB0mOcyc706ZpNpL54no59JbgpHcMuUg==", + "dependencies": { + "@tanstack/query-devtools": "5.7.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.8.1", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.3", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", diff --git a/client/package.json b/client/package.json index 6341c78..acf7906 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "@mui/icons-material": "^5.14.15", "@mui/material": "^5.14.15", "@tanstack/react-query": "^5.8.1", + "@tanstack/react-query-devtools": "^5.8.1", "axios": "^1.6.0", "framer-motion": "^10.16.4", "next": "14.0.0", diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx index 4eaccf4..1ea7660 100644 --- a/client/src/app/layout.tsx +++ b/client/src/app/layout.tsx @@ -8,6 +8,7 @@ import NavigationBar from "~/components/NavigationBar"; import "./globals.css"; import MSWInit from "@/components/mock/MSWInit"; import CustomQueryClientProvider from "@/components/queryClient/CustomQueryClientProvider"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; export const metadata: Metadata = { title: `${nameOfApp} | ${oneLineMessage}`, @@ -42,6 +43,7 @@ export default function RootLayout({ + diff --git a/client/src/components/user/signin/SigninForm.tsx b/client/src/components/user/signin/SigninForm.tsx index e5ddcbd..d158fb8 100644 --- a/client/src/components/user/signin/SigninForm.tsx +++ b/client/src/components/user/signin/SigninForm.tsx @@ -1,33 +1,46 @@ "use client"; -import useLogin from "@/hooks/useLogin"; -import { Box, Button, TextField } from "@mui/material"; +import HOME from "@/const/clientPath"; +import useLoginMutation from "@/queries/auth/useLoginMutation"; +import errorHandler from "@/utils/errorHandler"; +import { Box, Button, TextField, Typography } from "@mui/material"; +import { useRouter } from "next/navigation"; import { useState } from "react"; const SigninForm = () => { - const [email, setEmail] = useState(""); + const [id, setId] = useState(""); const [password, setPassword] = useState(""); - const handleSubmit = useLogin(); + + const router = useRouter(); + const { mutate: loginHandler, isError } = useLoginMutation(); return ( { event.preventDefault(); - handleSubmit({ email, password }); + if (!id || !password) { + return; + } + try { + loginHandler({ id, password }); + router.push(HOME); + } catch { + errorHandler("로그인에 실패했습다다"); + } }} sx={{ mt: 1 }} > setEmail(target.value)} + value={id} + onChange={({ target }) => setId(target.value)} margin="normal" required fullWidth - id="email" - label="이메일 / 아이디" - name="email" - autoComplete="email" + id="id" + label="아이디" + name="id" + autoComplete="id" autoFocus /> { id="password" autoComplete="current-password" /> + {isError && ( + + 아이디 혹은 비밀번호를 확인해주세요 + + )} diff --git a/client/src/const/serverPath.ts b/client/src/const/serverPath.ts new file mode 100644 index 0000000..f1d85a2 --- /dev/null +++ b/client/src/const/serverPath.ts @@ -0,0 +1,8 @@ +/** + * 로그인 API Path + */ +export const LOGIN_API_PATH = '/user/login' as const +/** + * 내 정보를 받아오는 Path + */ +export const MY_INFO = '/user/me' as const \ No newline at end of file diff --git a/client/src/hooks/useLogin.ts b/client/src/hooks/useLogin.ts index f4873e9..fb8813d 100644 --- a/client/src/hooks/useLogin.ts +++ b/client/src/hooks/useLogin.ts @@ -1,13 +1,21 @@ -import { SigninRequirement } from "@/types/auth/signin" +import { LOGIN_API_PATH } from "@/const/serverPath"; +import axios from "@/libs/axios"; +import { SigninRequirement } from "@/types/auth/signinRequirement"; +import { SigninResponseInterface } from "@/types/auth/signinResponse"; /** * 로그인 관련 로직들이 모여있는 Hook * @returns login Handler */ -export default function useLogin () { - const loginHandler = (props:SigninRequirement)=>{ - const {email,password} = props - console.log(`email : ${email}, password : ${password}`) - } - return loginHandler +export default function useLogin() { + const loginHandler = async (props: SigninRequirement) => { + const { id, password } = props; + const { data } = await axios.post(LOGIN_API_PATH, { + id, + password, + }); + return data; + }; + + return {loginHandler}; } diff --git a/client/src/hooks/useSetCookie.ts b/client/src/hooks/useSetCookie.ts new file mode 100644 index 0000000..102e2f8 --- /dev/null +++ b/client/src/hooks/useSetCookie.ts @@ -0,0 +1,15 @@ +"use server"; +import { cookies } from "next/headers"; + +interface SetCookieInterface { + key: string; + value: string; + httpOnly?: boolean; +} +export async function setCookie({ key, value, httpOnly }: SetCookieInterface) { + cookies().set({ + name: key, + value: value, + httpOnly: httpOnly ?? true, + }); +} diff --git a/client/src/mocks/handlers/getPostDetail.ts b/client/src/mocks/handlers/getPostDetail.ts index bfcda67..868d839 100644 --- a/client/src/mocks/handlers/getPostDetail.ts +++ b/client/src/mocks/handlers/getPostDetail.ts @@ -4,7 +4,7 @@ import { randomBoolean, randomNumber, randomSelect } from "../utils/random"; /** * 포스트 상세보기 정보를 받아오는 핸들러 */ -export default http.get(`${process.env.NEXT_PUBLIC_BASE_URL}/posts/1`, () => { +export default http.get(`${process.env.NEXT_PUBLIC_DEV_BASE_URL}/posts/1`, () => { return HttpResponse.json({ nickname: "testNick", id: "userID", diff --git a/client/src/mocks/handlers/getPostList.ts b/client/src/mocks/handlers/getPostList.ts index 989b929..6fd70d1 100644 --- a/client/src/mocks/handlers/getPostList.ts +++ b/client/src/mocks/handlers/getPostList.ts @@ -4,7 +4,7 @@ import { randomBoolean, randomNumber, randomSelect } from "../utils/random"; /** * 포스트 리스트를 받아오는 핸들러 */ -export default http.get(`${process.env.NEXT_PUBLIC_BASE_URL}/posts`, () => { +export default http.get(`${process.env.NEXT_PUBLIC_DEV_BASE_URL}/posts`, () => { return HttpResponse.json({ content: Array.from(new Array(5)).map((_data, i): PostInterface => { return { diff --git a/client/src/queries/auth/useLoginMutation.tsx b/client/src/queries/auth/useLoginMutation.tsx new file mode 100644 index 0000000..4f103d2 --- /dev/null +++ b/client/src/queries/auth/useLoginMutation.tsx @@ -0,0 +1,40 @@ +"use client"; +import useLogin from "@/hooks/useLogin"; +import { SigninRequirement } from "@/types/auth/signinRequirement"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { userInfoQueryKeys } from "./useUserInfoQuery"; + +const useLoginMutation = () => { + const { loginHandler } = useLogin(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: LoginMuataionKey.all, + mutationFn: async ({ id, password }: SigninRequirement) => + await loginHandler({ id, password }), + // 로그인에 성공한 경우, 토큰을 로컬스토리지에 저장, 이전 로그인 쿼리를 인벨리데이트 + onSuccess: async ({ token }) => { + localStorage?.setItem("accessToken", token); + queryClient.invalidateQueries({ queryKey: userInfoQueryKeys.all }); + }, + }); +}; + +/** + * + * @param id + * @returns + */ +export const LoginMuataionKey = { + /** + * 모든 로그인 관련 키 + */ + all: ["login"] as const, + /** + * Id 를 기반으로 로그인뮤테이션 키를 리턴 + * @param id 유저아이디 + * @returns 로그인뮤테이션 키 + */ + byId: (id: SigninRequirement["id"]) => [...LoginMuataionKey.all, id] as const, +}; +export default useLoginMutation; diff --git a/client/src/queries/auth/useUserInfoQuery.tsx b/client/src/queries/auth/useUserInfoQuery.tsx new file mode 100644 index 0000000..e51b5d3 --- /dev/null +++ b/client/src/queries/auth/useUserInfoQuery.tsx @@ -0,0 +1,33 @@ +"use client"; +import { MY_INFO } from "@/const/serverPath"; +import axios from "@/libs/axios"; +import { MyInfoInterface } from "@/types/auth/myInfo"; +import { SigninRequirement } from "@/types/auth/signinRequirement"; +import { useSuspenseQuery } from "@tanstack/react-query"; + +export const useUserInfoQuery = () => + useSuspenseQuery({ + queryKey: userInfoQueryKeys.all, + queryFn: getMyInfoByLocalStorage, + }); + +export const getMyInfoByLocalStorage = async () => { + const accessToken = localStorage.get("accessToken"); + const { data } = await axios.get(MY_INFO, { + headers: { Authorization: accessToken } + }); + return data; +}; + +export const userInfoQueryKeys = { + /** + * 모든 로그인 관련 쿼리키 + */ + all: ["me"], + /** + * Id 를 기반으로 로그인뮤테이션 키를 리턴 + * @param id 유저아이디 + * @returns 로그인뮤테이션 키 + */ + byId: (id: SigninRequirement["id"]) => ["me", id] as const, +}; diff --git a/client/src/queries/post/useGetPostDetailQuery.tsx b/client/src/queries/post/useGetPostDetailQuery.tsx index e268127..b888ba1 100644 --- a/client/src/queries/post/useGetPostDetailQuery.tsx +++ b/client/src/queries/post/useGetPostDetailQuery.tsx @@ -12,7 +12,8 @@ const useGetPostDetailQuery = (postId: string) => { export const getPostListQueryFn = async (postId: string) => { const { data } = await axios.get( - `/posts/${postId}` + //FIXME 수정해야함 + `/posts/${postId}`,{'baseURL':process.env.NEXT_PUBLIC_DEV_BASE_URL} ); return data; }; diff --git a/client/src/queries/post/useGetPostListQuery.tsx b/client/src/queries/post/useGetPostListQuery.tsx index 1e3b49a..9c4cd64 100644 --- a/client/src/queries/post/useGetPostListQuery.tsx +++ b/client/src/queries/post/useGetPostListQuery.tsx @@ -11,7 +11,9 @@ const useGetPostListQuery = (initialData: { content: PostInterface[] }) => { }; export const getPostListQueryFn = async () => { - const { data } = await axios.get<{ content: PostInterface[] }>("/posts"); + const { data } = await axios.get<{ content: PostInterface[] }>("/posts", { + baseURL: process.env.NEXT_PUBLIC_DEV_BASE_URL, + }); return data; }; diff --git a/client/src/stories/Components/Post/PostCard.stories.tsx b/client/src/stories/Components/Post/PostCard.stories.tsx index 6515456..c2068cf 100644 --- a/client/src/stories/Components/Post/PostCard.stories.tsx +++ b/client/src/stories/Components/Post/PostCard.stories.tsx @@ -3,16 +3,29 @@ import { Container } from "@mui/material"; import { Meta, StoryObj } from "@storybook/react"; const mockData = { - id: "123458", - createdAt: "Mon Nov 06 2023 00:13:07", - nickname: "testNick", - userId: "userID", - userImage: "https://source.unsplash.com/random?wallpapers", - content: - "Lorem ipsum dolor, sit amet consectetur adipisicing elit. Eos ullam aut minus aliquam quis officia, non dolore omnis, magnam totam tenetur ad harum? Mollitia omnis odit atque blanditiis exercitationem! Voluptatum.", - image: ["https://source.unsplash.com/random?wallpapers"], - tags: ["해시태그1", "해시태그2"], -}; + "nickname": "testNick", + "id": "userID", + "updateDt": "2023-11-08T13:05:09.531Z", + "createdAt": "2023-11-08T13:05:09.531Z", + "edited": true, + "postNo": 135, + "postContent": "Lorem ipsum dolor, sit amet consectetur adipisicing elit. Eos ullam aut minus aliquam quis officia, non dolore omnis, magnam totam tenetur ad harum? Mollitia omnis odit atque blanditiis exercitationem! Voluptatum.", + "positionInfo": "울릉도 동남쪽 뱃길따라 200리", + "alcoholName": "string", + "postAttachUrl": [ + "https://source.unsplash.com/random?wallpapers" + ], + "tagList": [ + "tag1", + "tag2" + ], + "quoteInfo": [], + "likeCount": 6, + "quoteCount": 4, + "followedByMe": true, + "likedByme": false, + "profileImgUrls": "https://source.unsplash.com/random?wallpapers" +} const meta = { title: "Components/Post/PostCard", @@ -40,18 +53,18 @@ export const Default: Story = { export const withoutImage: Story = { args: { ...mockData, - image: [], + postAttachUrl: [], }, }; export const withoutUserImage: Story = { args: { ...mockData, - userImage: undefined, + profileImgUrls: '', }, }; export const withoutTags: Story = { args: { ...mockData, - tags: [], + tagList: [], }, }; diff --git a/client/src/types/auth/myInfo.ts b/client/src/types/auth/myInfo.ts new file mode 100644 index 0000000..59c1962 --- /dev/null +++ b/client/src/types/auth/myInfo.ts @@ -0,0 +1,13 @@ +export interface MyInfoInterface { + id: string; + nickname: string; + profileImages: ProfileImagesType[]; + introduction: string; + followerCount: number; +} + +export interface ProfileImagesType { + attachNo: number; + attachUrl: string; + attachType: string; +} diff --git a/client/src/types/auth/signin.ts b/client/src/types/auth/signin.ts deleted file mode 100644 index 0a8dcd0..0000000 --- a/client/src/types/auth/signin.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface SigninRequirement { - email: string; - password: string; -} diff --git a/client/src/types/auth/signinRequirement.ts b/client/src/types/auth/signinRequirement.ts new file mode 100644 index 0000000..8ab8ec2 --- /dev/null +++ b/client/src/types/auth/signinRequirement.ts @@ -0,0 +1,10 @@ +export interface SigninRequirement { + /** + * 유저의 ID + */ + id: string; + /** + * 유저의 패스워드 + */ + password: string; +} diff --git a/client/src/types/auth/signinResponse.ts b/client/src/types/auth/signinResponse.ts new file mode 100644 index 0000000..00b761e --- /dev/null +++ b/client/src/types/auth/signinResponse.ts @@ -0,0 +1,9 @@ +/** + * 로그인 API 응답 Interface + */ +export interface SigninResponseInterface { + /** + * 액세스 토큰 + */ + token: string; +} diff --git a/client/src/types/auth/signupRequirement.ts b/client/src/types/auth/signupRequirement.ts new file mode 100644 index 0000000..4b789e0 --- /dev/null +++ b/client/src/types/auth/signupRequirement.ts @@ -0,0 +1,18 @@ +export interface SignupRequirement { + /** + * 유저의 이메일 (회원가입 전용) + */ + email: string; + /** + * 유저의 패스워드 + */ + password: string; + /** + * 유저의 ID + */ + id:string; + /** + * 유저의 닉네임 + */ + nickname:string; +} diff --git a/client/src/utils/errorHandler.ts b/client/src/utils/errorHandler.ts new file mode 100644 index 0000000..b3ee737 --- /dev/null +++ b/client/src/utils/errorHandler.ts @@ -0,0 +1,3 @@ +export default function errorHandler(error: string) { + console.log(error); +}