Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6c044a8
chore: update git ignore
seobbang Jun 16, 2025
e295d92
feat: add route
seobbang Jun 16, 2025
c7ac4a5
feat: follow ui
seobbang Jun 16, 2025
7bb1dc8
feat: 맞팔 탐지기 타입 서버에 맞추기
seobbang Jul 6, 2025
d5dfc95
feat: route 수정
seobbang Jul 6, 2025
1c64f21
feat: follow 조회 api 연결
seobbang Aug 17, 2025
29818bf
feat: 팔로우 언팔로우 버튼 구현
seobbang Aug 17, 2025
f2fd152
chore: api base url 변경
seobbang Aug 28, 2025
7abe609
feat: mutation default throwOnError 제거
seobbang Aug 28, 2025
5cc7419
feat: 무한스크롤 구현
seobbang Aug 28, 2025
3fb7740
feat: type selector에 value 전달하기
seobbang Aug 28, 2025
7f1f487
feat: follow/unfollow 버튼에 cursor pointer 추가
seobbang Aug 28, 2025
2971dd2
feat: 맞팔 리스트 데이터 polling 추가
seobbang Aug 28, 2025
7b5d4da
feat: totalUserCount 프로퍼티 접근 변경된대로 수정
seobbang Aug 28, 2025
99d8ff7
feat: 강제 갱신 구현하기
seobbang Aug 29, 2025
911a7d1
chore: biome react hook rule 추가
seobbang Aug 29, 2025
3c1228d
feat: useInfiniteScroll param options 추가
seobbang Aug 29, 2025
23a6e1f
feat: spinner 스타일링 조정
seobbang Aug 29, 2025
de73982
chore: env.development git ignore 추가
seobbang Aug 31, 2025
dd862c3
chore: 필요 없는 파일 삭제
seobbang Aug 31, 2025
37bd90d
fix: 변경된 apiClient에 맞게 수정
seobbang Aug 31, 2025
ad0d747
fix: 변경된 Header 에 맞게 props 수정
seobbang Aug 31, 2025
cdbdbee
chore: 큰따옴표 제거
seobbang Aug 31, 2025
34695db
feat: follow/unfollow 오류 toast로 처리하기
seobbang Aug 31, 2025
50e04c5
chore: 안 쓰는 코드 제거
seobbang Aug 31, 2025
b0ae058
chore: 필요 없는 코드 제거
seobbang Aug 31, 2025
ac15ffa
fix: import 파일 이름 오타 수정
seobbang Aug 31, 2025
cd52f5b
fix: 의존성 배열 options 추가
seobbang Aug 31, 2025
9900db8
feat: 팔로우/언팔로우 성공시 토스트
seobbang Aug 31, 2025
0d324bd
feat: staleTime 제거
seobbang Sep 21, 2025
30ebef1
chore: 불필요한 defaultValue 제거
seobbang Sep 22, 2025
964fa64
fix: 배열 length 0일때 early return
seobbang Sep 22, 2025
51ef6b3
feat: poll 최대 횟수 제한 두기
seobbang Sep 23, 2025
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
1 change: 0 additions & 1 deletion .env.development

This file was deleted.

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ dist-ssr
*.local

.env
.env.development

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

환경별 env 공유해서 사용하기로 했던것 같은데, 깃에 안올라가게 처리한 이유가 있나요 -?!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

요거 제가 긴가민가한데.. ㅎㅎ 지금은 BASE_URL만 있어서 노출되어도 되지만, 생각해보니 노출되면 안되는 시크릿 키 같은걸 들고 있게되면 파일이 올라가면 안되겠더라구요! 그래서 깃에 안올라가도록 처리해줬는데 요 부분 어떻게 생각하시나요 ~?

*.pem

# Editor directories and files
.vscode/*
.cursor/*
!.vscode/extensions.json
.idea
.DS_Store
Expand Down
5 changes: 4 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"linter": {
"enabled": true,
"rules": {
"recommended": true
"recommended": true,
"correctness": {
"useExhaustiveDependencies": "warn"
}
}
},
"javascript": {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"ky": "^1.7.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hot-toast": "^2.6.0",
"react-router": "^7.1.1"
},
"devDependencies": {
Expand Down
26 changes: 26 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { router } from "@/Router";
import { GlobalStyles } from "@/common/styles/GlobalStyles";
import { router } from "@/router";
import { Theme } from "@radix-ui/themes";
import "@radix-ui/themes/styles.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
Expand All @@ -15,7 +15,6 @@ const queryClient = new QueryClient({
},
mutations: {
retry: 0,
throwOnError: true,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

mutation은 사용처에서 각각 맞는 에러 처리 방식 사용하기 위해 throwOnError 제거합니다

},
},
});
Expand Down
48 changes: 48 additions & 0 deletions src/Follow/apis/follow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { apiClient } from "@/common/api";

export type FollowDetectType = "mutual" | "follow-only" | "followed-only";

export interface FollowDetectUser {
githubUserId: number;
username: string;
profileImgUrl: string;
}

export interface FollowDetectResponse {
userList: FollowDetectUser[];
lastPage: boolean;
totalUserCount: number;
lastSyncAt: string;
}

export async function queryFollowDetect({
detectType,
lastGithubUserId,
}: { detectType: FollowDetectType; lastGithubUserId?: number }) {
const poll = async () => {
const response = await apiClient.get<FollowDetectResponse>(`api/v1/users/me/followings/${detectType}`, {
searchParams: lastGithubUserId ? { lastGithubUserId } : undefined,
});

if (response.status === 202) {
await new Promise((resolve) => setTimeout(resolve, 500));
return poll();
}

return response;
};

return poll();
Comment on lines +27 to +43

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

맞팔 리스트 조회시 status가 202이면 202가 아닐때까지 polling합니다

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

서버에서 status가 지속적으로 202를 반환한다면 무제한 polling이 발생할것 같은데, 최대 횟수나 타임아웃 등의 제한을 두어서 처리해주어도 좋을것 같아요

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

너무 좋은데요 ?! 👍 이 부분 백엔드 담당자분과 횟수 제한 어떻게 둘지 논의 먼저 해보겠습니다!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

반영 완료 ~ 51ef6b3
논의해서 40번으로 결정했습니다!

}

export async function createFollowUser(githubUserId: FollowDetectUser["githubUserId"]) {
return apiClient.post<FollowDetectResponse>(`api/v1/follow/${githubUserId}`);
}

export async function createFollowRefresh() {
return apiClient.post<null>("api/v1/users/me/followings/refresh");
}

export async function deleteUnfollowUser(githubUserId: FollowDetectUser["githubUserId"]) {
return apiClient.delete<FollowDetectResponse>(`api/v1/unfollow/${githubUserId}`);
}
31 changes: 31 additions & 0 deletions src/Follow/components/DetectTypeSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { FollowDetectType } from "@/Follow/apis/follow";
import { colors } from "@/common/styles/theme";
import { SegmentedControl } from "@radix-ui/themes";

interface Props {
value: FollowDetectType;
onChange: (value: FollowDetectType) => void;
}

export function DetectTypeSelector({ value, onChange }: Props) {
return (
<SegmentedControl.Root
defaultValue="mutual"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

defaultValue는 현 value에 상관없이 항상 고정으로 넣어주나요?!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

value에 state값 넣어주면서 지워도 되는데 놓쳤네요! ㅋ-ㅋ 30ebef1

radius="large"
size="3"
value={value}
onValueChange={onChange}
variant="classic"
css={{
position: "sticky",
top: 0,
border: `1px solid ${colors.gray4}`,
boxShadow: "0 0 3px 0 rgba(0, 0, 0, 0.1)",
}}
>
<SegmentedControl.Item value="mutual">맞팔</SegmentedControl.Item>
<SegmentedControl.Item value="followed-only">상대만 팔로우</SegmentedControl.Item>
<SegmentedControl.Item value="follow-only">나만 팔로우</SegmentedControl.Item>
</SegmentedControl.Root>
);
}
76 changes: 76 additions & 0 deletions src/Follow/components/FollowList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { FollowDetectResponse, FollowDetectType } from "@/Follow/apis/follow";
import { useCreateFollowUser } from "@/Follow/hooks/apis/useCreateFollowUser";
import { useDeleteUnfollowUser } from "@/Follow/hooks/apis/useDeleteUnfollowUser";
import UserListItemCard from "@/common/components/UserListItemCard";
import UserProfileAvatar from "@/common/components/UserProfileAvatar";
import { Button, Flex } from "@radix-ui/themes";
import { Text } from "@radix-ui/themes";
import toast, { Toaster } from "react-hot-toast";

interface FollowListProps {
type: FollowDetectType;
userList: FollowDetectResponse["userList"];
}

export function FollowList({ type, userList }: FollowListProps) {
const { mutateAsync: unfollow } = useDeleteUnfollowUser();
const { mutateAsync: follow } = useCreateFollowUser();

return (
<>
<Flex direction="column" gap="2" css={{ padding: "0 2rem" }}>
{userList.map((user) => (
<UserListItemCard key={user.githubUserId}>
<Flex justify="between" align="center">
<Flex gap="4" align="center">
<UserProfileAvatar src={user.profileImgUrl} />

<Text size="2" weight="medium">
{user.username}
</Text>
</Flex>
<Flex gap="3">
{type === "followed-only" ? (
<Button
color="indigo"
variant="soft"
size="1"
css={{ cursor: "pointer" }}
onClick={async () => {
try {
await follow(user.githubUserId);
toast.success("팔로우에 성공했습니다.");
} catch (e) {
toast.error("팔로우에 실패했습니다. 다시 시도해주세요");
Comment on lines +39 to +44

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

팔로우/언팔로우시 버튼 onClick 내부 로직이 동일하므로 핸들러로 추상화하고, mutate function이랑 toast 텍스트만 각 버튼에 따라 적용되도록 수정해줘도 좋을것 같아요~

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

고것도 좋은 것 같네용 👀 그런데 아직은 추상화하지 않아도 되는 정도의 로직이라고 생각해요!

핸들러를 만들게 되면 type에 따라 api 요청 함수 / success / error 토스트 텍스트 분기가 다 들어가야 하다보니
결국 현재 담고 있는 모든 로직에 분기가 들어가는 것과 같아서 추상화하면 불필요하게 복잡도가 높아질 것 같아요 🤔

그래서 지금은 이대로 쓰는게 더 직관적이지 않을까 하는 의견입니다!!

나중에 좀 더 공통 처리 로직이나 부가적인 로직이 들어갔을 때 추상화 고려해봐도 좋을 것 같은데 어떠세요 ~?!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

저두 지금은 옵션이 2개라 괜찮다고 보긴 하는데 나중에 옵션들이 추가된다면 핸들러로 추상화가 필요하긴 할 것 같아요. 그러면 옵션 개수만큼 분기가 필요해질거라 옵션이 많아지면 그만큼 핸들러 로직이 비대해질수도 있을 것 같아요.
이럴 때 핸들러 내부를 추상화시키고 액션값들은 map으로 빼는 방법도 있을 것 같네요~~ 모든 옵션 케이스의 액션 데이터를 map 한 곳 에서 관리하기 때문에 응집성 측면에서도 이점이 있을 것 같아용

 const map = {
    "followed-only": {
      mutate: follow,
      success: "팔로우에 성공했습니다.",
      error: "팔로우에 실패했습니다. 다시 시도해주세요",
      buttonText: "Follow",
    },
    "following-only": {
      mutate: unfollow,
      success: "언팔로우에 성공했습니다.",
      error: "언팔로우에 실패했습니다. 다시 시도해주세요",
      buttonText: "Unfollow",
    },
  } as const;

  const currentAction = map[type];

  const handleClick = async (userId: string) => {
    try {
      await currentAction.mutate(userId);
      toast.success(currentAction.success);
    } catch {
      toast.error(currentAction.error);
    }
  };

}
}}
>
Follow
</Button>
) : (
<Button
color="indigo"
variant="soft"
size="1"
css={{ cursor: "pointer" }}
onClick={async () => {
try {
await unfollow(user.githubUserId);
toast.success("언팔로우에 성공했습니다.");
} catch (e) {
toast.error("언팔로우에 실패했습니다. 다시 시도해주세요");
}
}}
>
Unfollow
</Button>
)}
</Flex>
</Flex>
</UserListItemCard>
))}
</Flex>
<Toaster position="bottom-center" />
</>
);
}
Loading