Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
f4171e3
init: nextjs 초기세팅
Arooming Mar 29, 2024
bc94f96
remove: 불필요한 파일 삭제
Arooming Mar 29, 2024
4dba641
init: 파비콘 변경 및 프로젝트 초기화
Arooming Mar 29, 2024
7b236b1
init: 초기세팅
Arooming Apr 1, 2024
92ccc44
init: 패키지 매니저 변경 및 라이브러리 재설치
Arooming Apr 1, 2024
bde244a
style: reset.css 및 globalStyle 설정
Arooming Apr 1, 2024
60b961f
init: 패키지 매니저 변경
Arooming Apr 4, 2024
34842b0
chore: .gitignore 내용 추가
Arooming Apr 4, 2024
1a4ca2f
feat: axios instance 및 interceptor 설정
Arooming Apr 4, 2024
1b9767b
test: 테스트 코드 작성
Arooming Apr 4, 2024
dfd8310
chore: 폴더 구조 변경
Arooming Apr 9, 2024
2642ecd
chore: npm install
Arooming Apr 9, 2024
d128515
feat: 공통 컴포넌트 적용
Arooming Apr 9, 2024
7f4964b
feat: 헤더 생성
Arooming Apr 9, 2024
8f56101
feat: 팔로잉/ 팔로워 목록 불러오는 api
Arooming Apr 9, 2024
29826e4
feat: 커스텀 훅 생성
Arooming Apr 9, 2024
646e07d
feat: 타입 정의
Arooming Apr 9, 2024
499be93
style: 공통 스타일 입히기
Arooming Apr 9, 2024
273003f
style: 로고 에셋 추가
Arooming Apr 9, 2024
04f854e
style: 스타일 정의
Arooming Apr 9, 2024
b8b741d
style: settings.json 수정
Arooming Apr 9, 2024
275e832
chore: 불필요한 코드 삭제
Arooming Apr 9, 2024
a602313
chore: .gitignore 내용 추가
Arooming Apr 9, 2024
9badc68
feat: 홈화면 구현
Arooming Apr 9, 2024
691b826
chore: 라이브러리 설치
Arooming Apr 9, 2024
a00e1ac
style: 로티 에셋 추가
Arooming Apr 9, 2024
85de9e4
feat: 로딩 컴포넌트 생성
Arooming Apr 9, 2024
e83a81d
style: settings.json 수정
Arooming Apr 9, 2024
1783a75
fix: full reload 이슈 해결
Arooming Apr 10, 2024
8f80ffb
chore: 주석 추가
Arooming Apr 10, 2024
ca5bfac
feat: 팔로우/ 팔로잉 정보 불러오는 페이지
Arooming Apr 10, 2024
1055837
feat: 컴포넌트 분기처리
Arooming Apr 10, 2024
7baba10
feat: 팔로잉/ 팔로워 정보를 보여줄 컴포넌트
Arooming Apr 10, 2024
80533ee
feat: 토큰 입력한 사용자의 정보를 보여줄 컴포넌트
Arooming Apr 10, 2024
cc83351
feat: 사용자 프로필 정보 불러오는 api
Arooming Apr 10, 2024
a43400f
feat: 불필요한 코드 삭제
Arooming Apr 10, 2024
56d5d66
feat: 타입 정의
Arooming Apr 10, 2024
224a147
feat: next/Image 관련 설정
Arooming Apr 10, 2024
8993136
feat: 사용자의 팔로우 목록을 보여주는 컴포넌트
Arooming Apr 10, 2024
19a436d
feat: 공통 컴포넌트 분리
Arooming Apr 10, 2024
01fc2d8
feat: 공통 컴포넌트 분리 및 스타일링 적용
Arooming Apr 10, 2024
3b44247
style: 스타일 정의
Arooming Apr 10, 2024
1028f71
feat: 유저의 정보를 보여주는 컴포넌트
Arooming Apr 10, 2024
a8f3e1c
chore: 불필요한 타입 삭제
Arooming Apr 10, 2024
b0bebc2
feat: 팔로우/ 언팔로우 버튼 생성
Arooming Apr 10, 2024
29753d0
feat: 팔로우 수 표시
Arooming Apr 10, 2024
0783d34
chore: 타입 추가
Arooming Apr 10, 2024
d6e76c7
style: 스타일링 수정
Arooming Apr 10, 2024
9877220
feat: 버튼 클릭 시 팔로우/ 언팔로우 기능 구현
Arooming Apr 10, 2024
9f2f8c5
feat: 언팔로우 api
Arooming Apr 10, 2024
5cad9fd
feat: 팔로우 api
Arooming Apr 10, 2024
a06d3ac
chore: 디스크 캐시 관련 주석 추가 및 코드 변경
Arooming Apr 10, 2024
b1b5a35
chore: 불필요한 코드 삭제
Arooming Apr 10, 2024
1f38612
fix: image/domain deprecated 이슈 해결
Arooming Apr 10, 2024
cf65e20
fix: CORS 에러 해결
Arooming Apr 11, 2024
8a014af
Merge branch 'empty' into review
Arooming Apr 13, 2024
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
45 changes: 45 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history
.next

# Optional add
.DS_Store

.env.local

.vscode
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
Binary file added app/favicon.ico
Binary file not shown.
25 changes: 25 additions & 0 deletions app/follow-list/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import Loading from "@/common/Loading";
import FollowList from "@/component/FollowList";
import UserInfo from "@/component/UserInfo";
import useGetFollowInfo from "@/libs/hook/useGetFollowInfo";
import useGetUserInfo from "@/libs/hook/useGetUserInfo";
import React from "react";

const page = () => {
const { isLoading: userInfoLoading, data: userInfoData } = useGetUserInfo();
const { isLoading: followInfoLoading, data: followInfoData } =
Comment on lines +11 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저는 여기서 두 useQuery문을 병렬적으로 실행하기 위해 useQueires문을 사용했는데요,
데이터 return 되는 타입이 복잡해진다는 단점이 있지만 loading, data 값을 한 번에 관리할 수 있고(tanstack query v5의 combine 문법 사용) 지금처럼 한 뷰에 보여져야 하는 데이터들을 병렬적으로 호출할 수 있다는 장점이 있는것 같습니다!

useQuries 공식 문서 링크 보고 한 번 비교해봐도 좋을것 가타요~

Copy link
Copy Markdown
Owner Author

@Arooming Arooming Apr 16, 2024

Choose a reason for hiding this comment

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

안 그래도 연서님 코드 리뷰하면서 useQueries에 대해 더 깊게 알아보고 또 직접 코드에 적용해봐야겠다고 생각했습니다!

useQuery를 사용하면서 쿼리 수가 증가함에 따라 loading과 data 값의 수 역시 함께 증가해서 관리가 복잡하다고 느꼈는데요..!
이런 상황에 사용하는게 useQueries인 것 같네요!! 참고하겠습니당 :)

useGetFollowInfo();

return userInfoLoading || followInfoLoading ? (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

isLoading이라는 것을 판단하는 변수를 만들어서 써주면 더 직관적일것 같아요!

Suggested change
return userInfoLoading || followInfoLoading ? (
const isLoading = useInfoLoading || followInfoLoading
return isLoading ? (

<Loading />
) : (
<React.Fragment>
{userInfoData && <UserInfo userInfoData={userInfoData} />}
{followInfoData && <FollowList followInfoData={followInfoData} />}
</React.Fragment>
);
};

export default page;
26 changes: 26 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import CommonLayout from "@/common/CommonLayout";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import React from "react";
import "../style/global.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Follow Detector",
description: "Following by Follow Detector App",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<CommonLayout>
<body className={inter.className}>{children}</body>
</CommonLayout>
Comment on lines +21 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CommonLayout이 body 태그로 이루어져 있는데, 하위에 body 태그가 또 있어서 중복 코드 같습니다!
폰트 provide 해주는 body 태그까지 한번에 CommonLayout에 넣어주고 여기 body 태그는 삭제하면 더 좋을것 같습니당💪🏻

Suggested change
<CommonLayout>
<body className={inter.className}>{children}</body>
</CommonLayout>
<CommonLayout>
{children}
</CommonLayout>

</html>
);
}
66 changes: 66 additions & 0 deletions app/page.tsx
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Home 부분 (url : / )은 유저 액션이 들어가는 부분이

  1. 토큰 입력하는 input 부분
  2. follow-list로 navigate 시키는 button 부분 두가지인데요!
    이 두가지를 컴포넌트로 분리시키고, home page 자체는 서버 컴포넌트로 사용할 수 있게 구조화 해봐도 좋을것 같아요!

유저 액션이 들어가는 위 두 부분 외에는 단순 ui이기 때문에 next.js의 작동 원리를 생각해 봤을 때, 서버 컴포넌트를 사용하면 더 빠르게 유저가 초기 화면을 볼 수 있다는 장점이 있다고 생각합니다!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

맞네요! next.js의 작동 원리에 대해 더 고민해보도록 하겠습니다 !

Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";

import Link from "next/link";
import { ChangeEvent, useState } from "react";
import * as styles from "../style/Home/Home.css";

export interface UserTypes {
login?: string;
bio?: string;
avatar_url?: string;
Comment on lines +8 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

요기 type들을 다 nullable로 정의한 이유가 있나유?!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

타입 수정하는 과정에서 제대로 반영이 안된 것 같아요! 리팩토링하면서 함께 수정할 예정입니다 !

}

export default function Home() {
const [token, setToken] = useState("");

const handleChangeInput = (e: ChangeEvent<HTMLInputElement>) => {
setToken(e.target.value);
};

const handleClickBtn = () => {
sessionStorage.setItem("token", token);
};

return (
<main className={styles.HomeWrapper}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

요기는 vanilla extract의 css 정의 네이밍이 PascalCase로 되어 있네유
수정해주면 좋을것 같습니다!

<section className={styles.HomeContents}>
<div className={styles.TokenLinkBox}>
<Link
href={"https://github.com/settings/tokens"}
className={styles.TokenLinkBtn}
>
Github Token 만들러 가기
</Link>
<p className={styles.TokenInfoText}>
﹒ 토큰 발급 시 권한 user(Update ALL user data)를 체크해주세요!
</p>
</div>

<article className={styles.TokenInputBox}>
<input
type="text"
placeholder="Github Token을 입력해주세요"
onChange={handleChangeInput}
className={styles.TokenInput}
/>

{/* 추후 수정 방향 */}
{/* <Link
href={{ pathname: "/follow-list", query: { token: token } }}
as={"/follow-list"}
> */}
<Link href={"/follow-list"}>
<button
type="button"
onClick={handleClickBtn}
className={styles.TokenInputNextBtn}
disabled={token.length === 0}
>
나의 맞팔 확인하기
</button>
</Link>
</article>
</section>
</main>
);
}
22 changes: 22 additions & 0 deletions common/CommonLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import { Inter } from "next/font/google";
import React from "react";
import { QueryClient, QueryClientProvider } from "react-query";
import Header from "./Header";
const inter = Inter({ subsets: ["latin"] });

const queryClient = new QueryClient();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nextjs에서 root layout 계층에 선언하는 queryClient는, 성능적인 측면과 참조 동일성 유지(라이프사이클에서 한 번만 초기화 될 수 있도록)를 위해 useState를 사용해 설정해주는게 좋다고 합니다!

참고한 자료 링크도 함께 남겨요~

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

오!! 참고해볼게요 감사합니다 !


const CommonLayout = ({ children }: { children: React.ReactNode }) => {
return (
<body className={inter.className}>
<QueryClientProvider client={queryClient}>
<Header />
{children}
</QueryClientProvider>
</body>
);
};

export default CommonLayout;
19 changes: 19 additions & 0 deletions common/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import img_github_logo_white from "@/public/image/img_github_logo_white.png";
import Image from "next/image";
import * as styles from "../style/Common/Header.css";

const Header = () => {
return (
<header className={styles.HeaderWrapper}>
<Image
src={img_github_logo_white}
alt="깃허브-로고"
width={30}
height={30}
/>
<h1 className={styles.HeaderTitle}>깃허브 팔로우 탐지기</h1>
</header>
);
};

export default Header;
54 changes: 54 additions & 0 deletions common/ListLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import useDeleteFollower from "@/libs/hook/useDeleteFollower";
import usePutFollower from "@/libs/hook/usePutFollower";
import { ListLayoutTypes } from "@/type/user";
import Image from "next/image";
import * as styles from "../style/Common/ListLayout.css";

const ListLayout = ({ list, isUserInfo, isFollowingBtn }: ListLayoutTypes) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

흐음 요건 개인적인 의견인데...
맞팔 중인 리스트 / 아닌 리스트를 구분해서 1) 버튼에 들어갈 텍스트 구분 2) mutate function 구분을 하는 플래그인 isFollowingBtn이 살짝 버튼 네이밍에 의존적인것 같아요...!
ListType 이런식으로 바꿔보면 더 가독성 있고, 내부 로직을 보여주지 않는 플래그 네이밍이 될수 있을것 같습니당!

const putMuation = usePutFollower();
const deleteMutation = useDeleteFollower();

const handleClickFollowBtn = ({
isFollowingBtn,
login,
}: {
isFollowingBtn?: boolean;
login: string;
}) => {
isFollowingBtn ? putMuation.mutate(login) : deleteMutation.mutate(login);
};

return list.map(({ login, avatar_url }) => {
return (
<ul key={login} className={styles.contentsWrapper}>
{avatar_url && (
<Image
width={130}
height={130}
src={avatar_url}
alt={"유저-이미지"}
priority={true}
style={{ marginTop: "1rem" }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

여기만 스타일링에 style 속성을 사용한 이유가 있을까용 ?!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

헙 이 부분도 추후에 수정하려 했는데, 놓치고 넘어갔네요.. ㅜ_ㅜ 리팩토링하면서 같이 수정할게요 !

/>
)}
<div className={styles.followWrapper}>
<p className={styles.loginInfo}>{login}</p>

{!isUserInfo && (
<button
type="button"
className={isFollowingBtn ? styles.followBtn : styles.unfollowBtn}
onClick={() =>
login && handleClickFollowBtn({ isFollowingBtn, login })
}
>
{isFollowingBtn ? "팔로우" : "언팔로우"}
</button>
)}
</div>
</ul>
);
});
};

export default ListLayout;
17 changes: 17 additions & 0 deletions common/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use client";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

요기 혹시 클라이언트 컴포넌트로 선언하지 않으면 오류 나나요 ?! 아니라면 서버 컴포넌트로 사용해도 좋을것 같습니당!


import Lottie from "lottie-react";
import animationData from "../public/lottie/lottie.json";
import * as styles from "../style/Common/Loading.css";

const Loading = () => {
return (
<section className={styles.LoadingPageItemContainer}>
<article className={styles.LottieWrapper}>
<Lottie animationData={animationData} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

lottie 너무 잘쓴당😎

</article>
</section>
);
};

export default Loading;
46 changes: 46 additions & 0 deletions component/FollowList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import ListLayout from "@/common/ListLayout";
import { FollowInfoDataTypes } from "@/type/user";
import * as styles from "../style/FollowList/FollowList.css";

const FollowList = ({
followInfoData,
}: {
followInfoData: FollowInfoDataTypes;
}) => {
const { followingData, followersData } = followInfoData;

const matchedList = followersData.filter((follower) => {
return followingData.some(
(following) => following.login === follower.login
);
});

const unfollowingList = followersData.filter((follower) => {
return !matchedList.includes(follower);
});
Comment on lines +12 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이전 멘토 리뷰에서 남겨주셨듯이 useMemo로 메모이제이션 해주면 좋을것 같아요!


const user = [matchedList, unfollowingList];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

user를 위와 같은 array 형식으로 사용할거라면,
matchedList와 unfollowingList를 return 해주는 함수를 정의해줘도 좋을것 같아요!

Suggested change
const user = [matchedList, unfollowingList];
const getUserList = useMemo( () => {
const matchedList = followersData.filter((follower) => {
return followingData.some(
(following) => following.login === follower.login
);
});
const unfollowingList = followersData.filter((follower) => {
return !matchedList.includes(follower);
});
return [matchedList, unfollowingList]
}, [followersData, followingData])


return (
<section className={styles.followListWrapper}>
{user.map((list, idx) => {
return (
<article key={idx} className={styles.listWrapper}>
<div className={styles.titleWrapper}>
<p className={styles.title}>
{list === matchedList
? "맞팔 중인 사용자"
: "내가 팔로우 안 한 사용자"}
</p>
<p className={styles.title}>{`${list.length} 명`}</p>
</div>

<ListLayout list={list} isUserInfo={false} isFollowingBtn={list !== matchedList} />
Comment on lines +30 to +38
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

list === matchedList 로직을 여러 곳에서 사용하고 있는것 같아요!
플래그를 만들어줘서 활용하면 더 가독성이 좋아질것 같습니당

Suggested change
<p className={styles.title}>
{list === matchedList
? "맞팔 중인 사용자"
: "내가 팔로우 안 한 사용자"}
</p>
<p className={styles.title}>{`${list.length} 명`}</p>
</div>
<ListLayout list={list} isUserInfo={false} isFollowingBtn={list !== matchedList} />
const isMatchedList = list ===matchedList
<p className={styles.title}>
{isMatchedList
? "맞팔 중인 사용자"
: "내가 팔로우 안 한 사용자"}
</p>
<p className={styles.title}>{`${list.length} 명`}</p>
</div>
<ListLayout list={list} isUserInfo={false} isFollowingBtn={!isMtachedList} />

</article>
);
})}
</section>
);
};

export default FollowList;
13 changes: 13 additions & 0 deletions component/UserInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ListLayout from "@/common/ListLayout";
import { UserProfileInfoTypes } from "@/type/user";
import * as styles from "../style/User/User.css";

const UserInfo = ({ userInfoData }: { userInfoData: UserProfileInfoTypes }) => {
return (
<section className={styles.userInfoWrapper}>
<ListLayout list={[userInfoData]} isUserInfo={true} />
</section>
);
};

export default UserInfo;
8 changes: 8 additions & 0 deletions libs/api/deleteFollower.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { client } from ".";

const deleteFollower = async (login: string) => {
const { data } = await client().delete(`/user/following/${login}`);
return data;
};

export default deleteFollower;
Loading