Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[백승렬] Week14 #463

Merged
merged 17 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
238 changes: 209 additions & 29 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,26 @@
"lint": "next lint"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"axios": "^1.6.8",
"classnames": "^2.5.1",
"date-fns": "^3.6.0",
"next": "13.5.6",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.51.4",
"react-icons": "^5.2.1",
"react-popper": "^2.3.0",
"react-query": "^3.39.3",
"react-transition-group": "^4.4.5",
"sass": "^1.77.0"
},
"devDependencies": {
"@svgr/webpack": "^8.1.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-transition-group": "^4.4.10",
"eslint": "^8",
"eslint-config-next": "13.5.6",
"typescript": "^5"
Expand Down
21 changes: 21 additions & 0 deletions pages/signin/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { SignInForm, Oauth } from "@/src/feature";
import { SignHeader } from "@/src/ui";
import { SignLayout } from "@/src/page-layout/SignLayout";
import { ROUTE } from "@/src/util";

const SignInPage = () => {
return (
<SignLayout
header={
<SignHeader
message="회원이 아니신가요?"
link={{ text: "회원 가입하기", href: ROUTE.회원가입 }}
/>
}
form={<SignInForm />}
oauth={<Oauth description="소셜 로그인" />}
/>
Comment on lines +8 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

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

Page에 Layout 컴포넌트 하나만 들어가있군요.

Layout은 "제한된 공간에 요소를 배치"하는 데에 사용되며 보편적으로 children을 가지게 됩니다 ! 혹시, Page 내에 Layout의 요소를 작성하지 않은 이유가 있으실까요?
페이지에 직접 컴포넌트를 작성하지 않으신 이유가 궁금합니다 !

);
};

export default SignInPage;
21 changes: 21 additions & 0 deletions pages/signup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Oauth, SignUpForm } from "@/src/feature";
import { SignHeader } from "@/src/ui";
import { SignLayout } from "@/src/page-layout/SignLayout";
import { ROUTE } from "@/src/util";

const SignUpPage = () => {
return (
<SignLayout
header={
<SignHeader
message="이미 회원이신가요?"
link={{ text: "로그인 하기", href: ROUTE.로그인 }}
/>
}
form={<SignUpForm />}
oauth={<Oauth description="다른 방식으로 가입하기" />}
/>
);
};

export default SignUpPage;
Binary file added public/images/google-oauth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/kakao-oauth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 0 additions & 7 deletions public/images/kebab.svg

This file was deleted.

8 changes: 0 additions & 8 deletions public/images/search.svg

This file was deleted.

10 changes: 0 additions & 10 deletions public/images/star.svg

This file was deleted.

3 changes: 3 additions & 0 deletions src/data-access/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ export * from "./useGetUser";
export * from "./useGetFolder";
export * from "./useGetFolders";
export * from "./useGetLinks"
export * from "./useSignIn";
export * from "./useSignUp";
export * from "./useCheckEmailDuplicate";
37 changes: 37 additions & 0 deletions src/data-access/useCheckEmailDuplicate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { instance, useAsync } from "@/src/util";
import { useCallback } from "react";

/**
* useCheckEmailDuplicate 훅은 이메일 중복 확인을 위한 비동기 요청을 처리합니다.
*
* @param email - 중복 확인할 이메일 주소입니다.
* @returns 훅의 반환 객체입니다.
* @returns return.execute - 이메일 중복 확인을 실행하는 함수입니다.
* @returns return.loading - 이메일 중복 확인 요청의 로딩 상태입니다.
* @returns return.error - 이메일 중복 확인 요청 중 발생한 오류입니다.
* @returns return.data - 이메일 중복 확인 응답 데이터입니다.
*
* @example
* const { execute, loading, error, data } = useCheckEmailDuplicate("example@example.com");
*
* useEffect(() => {
* execute();
* }, [execute]);
*/
Comment on lines +4 to +20
Copy link
Collaborator

Choose a reason for hiding this comment

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

와아... ㄷㄷㄷ jsdoc 곳수 시군요 ..

정말 친절하네요.. 동료 개발자셨으면 좋겠다..

export const useCheckEmailDuplicate = (email: string) => {
const checkEmailDuplicate = useCallback(
() =>
instance.post<{ data: { isUsableNickname: boolean } }>("check-email", {
Copy link
Collaborator

Choose a reason for hiding this comment

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

타입 제네릭도 놓치지 않았군요 👍👍👍

타입스크립트도 금방금방 적응하시는군요 😊

email,
}),
[email]
);
const { execute, loading, error, data } = useAsync(checkEmailDuplicate, true);

return {
execute,
loading,
error,
data,
};
};
38 changes: 38 additions & 0 deletions src/data-access/useGetFolder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
import { useAsync, instance, mapFolderData } from "@/src/util";
import { SampleFolderRawData } from "@/src/type";

/**
* useGetFolder 훅은 샘플 폴더 데이터를 가져와서 매핑된 폴더 데이터를 반환합니다.
*
* @returns 훅의 반환 객체입니다.
* @returns return.loading - 폴더 데이터를 가져오는 요청의 로딩 상태입니다.
* @returns return.error - 폴더 데이터를 가져오는 요청 중 발생한 오류입니다.
* @returns return.data - 매핑된 폴더 데이터 객체입니다.
* @returns return.data.profileImage - 폴더 소유자의 프로필 이미지 URL입니다.
* @returns return.data.ownerName - 폴더 소유자의 이름입니다.
* @returns return.data.folderName - 폴더의 이름입니다.
* @returns return.data.links - 폴더에 포함된 링크의 배열입니다.
*
* @example
* const { loading, error, data } = useGetFolder();
*
* if (loading) {
* return <div>Loading...</div>;
* }
*
* if (error) {
* return <div>Error occurred: {error.message}</div>;
* }
*
* return (
* <div>
* <h1>{data.folderName}</h1>
* <p>{data.ownerName}</p>
* <img src={data.profileImage} alt={`${data.ownerName}'s profile`} />
* <ul>
* {data.links.map(link => (
* <li key={link.id}>{link.title}</li>
* ))}
* </ul>
* </div>
* );
*/
export const useGetFolder = () => {
const getFolder = () =>
instance.get<{ folder: SampleFolderRawData }>("sample/folder");
Expand All @@ -10,3 +46,5 @@ export const useGetFolder = () => {

return { loading, error, data: folderData };
};


44 changes: 42 additions & 2 deletions src/data-access/useGetFolders.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,53 @@
import { useMemo } from "react";
import { instance, useAsync, mapFoldersData } from "@/src/util";
import { FolderRawData } from "@/src/type";

/**
* useGetFolders 훅은 사용자 폴더 데이터를 가져와서 매핑된 폴더 데이터 배열을 반환합니다.
*
* @returns 훅의 반환 객체입니다.
* @returns return.loading - 폴더 데이터를 가져오는 요청의 로딩 상태입니다.
* @returns return.error - 폴더 데이터를 가져오는 요청 중 발생한 오류입니다.
* @returns return.data - 매핑되고 정렬된 폴더 데이터 배열입니다.
* @returns return.data[].id - 폴더의 고유 ID입니다.
* @returns return.data[].createdAt - 폴더 생성 일자입니다.
* @returns return.data[].name - 폴더의 이름입니다.
* @returns return.data[].userId - 폴더 소유자의 사용자 ID입니다.
* @returns return.data[].linkCount - 폴더에 포함된 링크의 개수입니다.
*
* @example
* const { loading, error, data } = useGetFolders();
*
* if (loading) {
* return <div>Loading...</div>;
* }
*
* if (error) {
* return <div>Error occurred: {error.message}</div>;
* }
*
* return (
* <div>
* {data.map(folder => (
* <div key={folder.id}>
* <h1>{folder.name}</h1>
* <p>{folder.createdAt}</p>
* <p>{folder.linkCount} links</p>
* </div>
* ))}
* </div>
* );
*/
export const useGetFolders = () => {
const getFolders = () =>
instance.get<{ data: FolderRawData[] }>("users/1/folders");
const { loading, error, data } = useAsync(getFolders);

const folders = mapFoldersData(data?.data);
const sortedFolders = folders.sort((a, b) => a?.id - b?.id);
const folders = useMemo(() => mapFoldersData(data?.data), [data?.data]);
const sortedFolders = useMemo(
() => folders.sort((a, b) => a?.id - b?.id),
[folders]
);

return { loading, error, data: sortedFolders };
};
46 changes: 46 additions & 0 deletions src/data-access/useGetLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,52 @@ import { useCallback, useEffect } from "react";
import { instance, useAsync, mapLinksData, ALL_LINKS_ID } from "@/src/util";
import { SelectedFolderId, LinkRawData } from "@/src/type";


/**
* useGetLinks 훅은 주어진 폴더 ID에 따라 링크 데이터를 가져와 매핑된 링크 데이터를 반환합니다.
*
* @param [folderId=ALL_LINKS_ID] - 링크를 가져올 폴더의 ID입니다. 기본값은 모든 링크를 가져오는 "ALL_LINKS_ID"입니다.
* @returns 훅의 반환 객체입니다.
* @returns return.execute - 링크 데이터를 수동으로 가져오는 함수입니다.
* @returns return.loading - 링크 데이터를 가져오는 요청의 로딩 상태입니다.
* @returns return.error - 링크 데이터를 가져오는 요청 중 발생한 오류입니다.
* @returns return.data - 매핑된 링크 데이터 배열입니다.
* @returns return.data[].id - 링크의 고유 ID입니다.
* @returns return.data[].createdAt - 링크 생성 일자입니다.
* @returns return.data[].updatedAt - 링크 업데이트 일자입니다.
* @returns return.data[].url - 링크 URL입니다.
* @returns return.data[].imageSource - 링크 이미지 소스입니다.
* @returns return.data[].title - 링크의 제목입니다.
* @returns return.data[].description - 링크의 설명입니다.
* @returns return.data[].elapsedTime - 링크 생성 이후 경과 시간입니다.
*
* @example
* const { execute, loading, error, data } = useGetLinks("folderId");
*
* useEffect(() => {
* execute();
* }, [execute]);
*
* if (loading) {
* return <div>Loading...</div>;
* }
*
* if (error) {
* return <div>Error occurred: {error.message}</div>;
* }
*
* return (
* <div>
* {data.map(link => (
* <div key={link.id}>
* <h1>{link.title}</h1>
* <p>{link.description}</p>
* <a href={link.url}>{link.url}</a>
* </div>
* ))}
* </div>
* );
*/
export const useGetLinks = (folderId: SelectedFolderId = ALL_LINKS_ID) => {
const queryString = folderId === ALL_LINKS_ID ? "" : `?folderId=${folderId}`;
const getLinks = useCallback(
Expand Down
27 changes: 27 additions & 0 deletions src/data-access/useGetUser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
import { useAsync, instance } from "@/src/util";
import { UserRawData } from "@/src/type";


/**
* useGetUser 훅은 사용자 데이터를 가져와 반환합니다.
*
* @returns 훅의 반환 객체입니다.
* @returns return.loading - 사용자 데이터를 가져오는 요청의 로딩 상태입니다.
* @returns return.error - 사용자 데이터를 가져오는 요청 중 발생한 오류입니다.
* @returns return.data - 가져온 사용자 데이터입니다.
*
* @example
* const { loading, error, data } = useGetUser();
*
* if (loading) {
* return <div>Loading...</div>;
* }
*
* if (error) {
* return <div>Error occurred: {error.message}</div>;
* }
*
* return (
* <div>
* <h1>{data.name}</h1>
* <p>{data.email}</p>
* </div>
* );
*/
Comment on lines +4 to +30
Copy link
Collaborator

Choose a reason for hiding this comment

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

아니 ㅋㅋㅋ 정말 너무 친절한데요 ..? 🫢

웬만한 라이브러리보다 친절하군요 ㄷㄷㄷ

export const useGetUser = () => {
const getUser = () => instance.get<{ data: UserRawData }>("sample/user");
const { loading, error, data } = useAsync(getUser);
Expand Down
55 changes: 55 additions & 0 deletions src/data-access/useSignIn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { instance, useAsync } from "@/src/util";
import { useCallback, useEffect } from "react";
import { Token } from "@/src/type";

type UseSignInParams = { email: string; password: string };

/**
* useSignIn 훅은 사용자 인증을 처리하고, 토큰을 로컬 스토리지에 저장합니다.
*
* @param params - 사용자 인증에 필요한 이메일과 비밀번호입니다.
* @param params.email - 사용자 이메일 주소입니다.
* @param params.password - 사용자 비밀번호입니다.
* @returns 훅의 반환 객체입니다.
* @returns return.execute - 사용자 인증을 수동으로 실행하는 함수입니다.
* @returns return.loading - 사용자 인증 요청의 로딩 상태입니다.
* @returns return.error - 사용자 인증 요청 중 발생한 오류입니다.
* @returns return.data - 사용자 인증 응답 데이터입니다.
*
* @example
* const { execute, loading, error, data } = useSignIn({ email: "user@example.com", password: "password" });
*
* const handleSignIn = async () => {
* await execute();
* };
*
* useEffect(() => {
* if (data) {
* console.log("로그인 성공:", data);
* }
* }, [data]);
*/
export const useSignIn = ({ email, password }: UseSignInParams) => {
const signIn = useCallback(
() =>
instance.post<{ data: Token }>("sign-in", {
email,
password,
}),
[email, password]
);
const { execute, loading, error, data } = useAsync(signIn, true);

useEffect(() => {
if (data?.data.accessToken) {
localStorage.setItem("accessToken", data.data.accessToken);
}
}, [data?.data.accessToken]);

return {
execute,
loading,
error,
data,
};
};
Loading
Loading