Skip to content

Commit

Permalink
로그인-기능-구현 (#21)
Browse files Browse the repository at this point in the history
* New : 로그인 관련 타입정의

* New : React Query Dev tool 설치

* New : React Query Dev tool 설치

* New : Custom Error handler 추가

* Bug : Storybook 빌드 에러 해결

* New  : 로그인 기능 서버 연결
  • Loading branch information
jobkaeHenry authored Nov 9, 2023
1 parent 8ac003a commit 0ee600b
Show file tree
Hide file tree
Showing 20 changed files with 256 additions and 39 deletions.
27 changes: 27 additions & 0 deletions client/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 client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down Expand Up @@ -42,6 +43,7 @@ export default function RootLayout({
</Box>
<NavigationBar />
</ThemeRegistry>
<ReactQueryDevtools initialIsOpen={false} />
</CustomQueryClientProvider>
</body>
</html>
Expand Down
40 changes: 29 additions & 11 deletions client/src/components/user/signin/SigninForm.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box
component="form"
onSubmit={(event) => {
event.preventDefault();
handleSubmit({ email, password });
if (!id || !password) {
return;
}
try {
loginHandler({ id, password });
router.push(HOME);
} catch {
errorHandler("로그인에 실패했습다다");
}
}}
sx={{ mt: 1 }}
>
<TextField
value={email}
onChange={({ target }) => 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
/>
<TextField
Expand All @@ -42,6 +55,11 @@ const SigninForm = () => {
id="password"
autoComplete="current-password"
/>
{isError && (
<Typography variant="caption1" color="error">
아이디 혹은 비밀번호를 확인해주세요
</Typography>
)}
<Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}>
Sign In
</Button>
Expand Down
8 changes: 8 additions & 0 deletions client/src/const/serverPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* 로그인 API Path
*/
export const LOGIN_API_PATH = '/user/login' as const
/**
* 내 정보를 받아오는 Path
*/
export const MY_INFO = '/user/me' as const
22 changes: 15 additions & 7 deletions client/src/hooks/useLogin.ts
Original file line number Diff line number Diff line change
@@ -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<SigninResponseInterface>(LOGIN_API_PATH, {
id,
password,
});
return data;
};

return {loginHandler};
}
15 changes: 15 additions & 0 deletions client/src/hooks/useSetCookie.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
2 changes: 1 addition & 1 deletion client/src/mocks/handlers/getPostDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PostInterface>({
nickname: "testNick",
id: "userID",
Expand Down
2 changes: 1 addition & 1 deletion client/src/mocks/handlers/getPostList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions client/src/queries/auth/useLoginMutation.tsx
Original file line number Diff line number Diff line change
@@ -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;
33 changes: 33 additions & 0 deletions client/src/queries/auth/useUserInfoQuery.tsx
Original file line number Diff line number Diff line change
@@ -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<MyInfoInterface>(MY_INFO, {
headers: { Authorization: accessToken }
});
return data;
};

export const userInfoQueryKeys = {
/**
* 모든 로그인 관련 쿼리키
*/
all: ["me"],
/**
* Id 를 기반으로 로그인뮤테이션 키를 리턴
* @param id 유저아이디
* @returns 로그인뮤테이션 키
*/
byId: (id: SigninRequirement["id"]) => ["me", id] as const,
};
3 changes: 2 additions & 1 deletion client/src/queries/post/useGetPostDetailQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const useGetPostDetailQuery = (postId: string) => {

export const getPostListQueryFn = async (postId: string) => {
const { data } = await axios.get<PostInterface>(
`/posts/${postId}`
//FIXME 수정해야함
`/posts/${postId}`,{'baseURL':process.env.NEXT_PUBLIC_DEV_BASE_URL}
);
return data;
};
Expand Down
4 changes: 3 additions & 1 deletion client/src/queries/post/useGetPostListQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
39 changes: 26 additions & 13 deletions client/src/stories/Components/Post/PostCard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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: [],
},
};
13 changes: 13 additions & 0 deletions client/src/types/auth/myInfo.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 0 additions & 4 deletions client/src/types/auth/signin.ts

This file was deleted.

Loading

0 comments on commit 0ee600b

Please sign in to comment.