From 6c044a83b906d0eb1ee98ff6db181545f776d8e2 Mon Sep 17 00:00:00 2001 From: seobbang Date: Mon, 16 Jun 2025 19:57:17 +0900 Subject: [PATCH 01/33] chore: update git ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c699b65..7f4661f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ dist-ssr # Editor directories and files .vscode/* +.cursor/* !.vscode/extensions.json .idea .DS_Store From e295d9289488a17995bf9e504414b6dfe7db782b Mon Sep 17 00:00:00 2001 From: seobbang Date: Mon, 16 Jun 2025 19:57:35 +0900 Subject: [PATCH 02/33] feat: add route --- src/router.tsx | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 src/router.tsx diff --git a/src/router.tsx b/src/router.tsx deleted file mode 100644 index 34722b5..0000000 --- a/src/router.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { LoginPage } from "@/Login/page"; -import MenuPage from "@/Menu/page"; -import GlobalLayout from "@/common/components/GlobalLayout"; -import { authLoader } from "@/loaders/authLoader"; -import { createBrowserRouter } from "react-router"; - -export const router = createBrowserRouter([ - { - path: "/", - element: , - children: [ - { - path: "login", - element: , - }, - { - loader: authLoader, - children: [ - { - path: "menu", - element: , - }, - ], - }, - ], - }, -]); From c7ac4a573aa51095c86d6bc438da2ccef0fbc616 Mon Sep 17 00:00:00 2001 From: seobbang Date: Mon, 16 Jun 2025 20:00:17 +0900 Subject: [PATCH 03/33] feat: follow ui --- src/Follow/components/FollowList.tsx | 36 +++++++++++++++++++++ src/Follow/components/FollowStats.tsx | 46 +++++++++++++++++++++++++++ src/Follow/components/Tab.tsx | 28 ++++++++++++++++ src/Follow/page.tsx | 43 +++++++++++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 src/Follow/components/FollowList.tsx create mode 100644 src/Follow/components/FollowStats.tsx create mode 100644 src/Follow/components/Tab.tsx create mode 100644 src/Follow/page.tsx diff --git a/src/Follow/components/FollowList.tsx b/src/Follow/components/FollowList.tsx new file mode 100644 index 0000000..de36ca2 --- /dev/null +++ b/src/Follow/components/FollowList.tsx @@ -0,0 +1,36 @@ +import UserListItemCard from "@/common/components/UserListItemCard"; +import { Button, Flex } from "@radix-ui/themes"; +import UserProfileAvatar from "@/common/components/UserProfileAvatar"; +import { Text } from "@radix-ui/themes"; +import type { FollowTab } from "@/Follow/page"; + +interface FollowListProps { + type: FollowTab; +} + +export function FollowList({ type }: FollowListProps) { + const users = Array.from({ length: 20 }); + + return ( + + {users.map((_, idx) => ( + + + + + + + 이름 + + + + + + + + ))} + + ); +} diff --git a/src/Follow/components/FollowStats.tsx b/src/Follow/components/FollowStats.tsx new file mode 100644 index 0000000..6aeef45 --- /dev/null +++ b/src/Follow/components/FollowStats.tsx @@ -0,0 +1,46 @@ +import { colors } from "@/common/styles/theme"; +import { Flex, Text } from "@radix-ui/themes"; + +const STATS = [ + { + title: "맞팔로우", + count: 25, + }, + { + title: "팔로워", + count: 3, + }, + { + title: "팔로잉", + count: 3, + }, +]; + +export function FollowStats() { + return ( + + {STATS.map((item) => ( + + + {item.title} + + + {item.count} + + + ))} + + ); +} diff --git a/src/Follow/components/Tab.tsx b/src/Follow/components/Tab.tsx new file mode 100644 index 0000000..b5642b9 --- /dev/null +++ b/src/Follow/components/Tab.tsx @@ -0,0 +1,28 @@ +import { colors } from "@/common/styles/theme"; +import { SegmentedControl } from "@radix-ui/themes"; +import type { FollowTab } from "../page"; + +interface TabProps { + onChange: (value: FollowTab) => void; +} + +export function Tab({ onChange }: TabProps) { + return ( + + 맞팔 + 팔로워 + 팔로잉 + + ); +} diff --git a/src/Follow/page.tsx b/src/Follow/page.tsx new file mode 100644 index 0000000..f982572 --- /dev/null +++ b/src/Follow/page.tsx @@ -0,0 +1,43 @@ +import Header from "@/common/components/Header"; +import Spacer from "@/common/components/Spacer"; +import { colors } from "@/common/styles/theme"; +import { FollowStats } from "@/Follow/components/FollowStats"; +import { Tab } from "@/Follow/components/Tab"; +import { FollowList } from "@/Follow/components/FollowList"; +import styled from "@emotion/styled"; +import { Button, Flex, Text } from "@radix-ui/themes"; +import { useState } from "react"; + +export type FollowTab = "co-following" | "follower" | "following"; + +export function FollowPage() { + const [selectedTab, setSelectedTab] = useState("co-following"); + + return ( + <> +
새로고침} /> + + + + + + 마지막 업데이트: 10분 전 + + + + + + + + + + + + ); +} + +const Container = styled.div({ + overflowY: "auto", + height: "100vh", + backgroundColor: colors.gray2, +}); From 7bb1dc8bb2762810c346b9283f510d8de9c3e28c Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 6 Jul 2025 16:15:09 +0900 Subject: [PATCH 04/33] =?UTF-8?q?feat:=20=EB=A7=9E=ED=8C=94=20=ED=83=90?= =?UTF-8?q?=EC=A7=80=EA=B8=B0=20=ED=83=80=EC=9E=85=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=94=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/components/FollowList.tsx | 17 +++++++------- src/Follow/components/FollowStats.tsx | 3 +++ src/Follow/page.tsx | 11 +++++---- src/Router.tsx | 32 +++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 src/Router.tsx diff --git a/src/Follow/components/FollowList.tsx b/src/Follow/components/FollowList.tsx index de36ca2..656bac4 100644 --- a/src/Follow/components/FollowList.tsx +++ b/src/Follow/components/FollowList.tsx @@ -1,11 +1,11 @@ +import type { FollowDetectType } from "@/Follow/apis/follow"; import UserListItemCard from "@/common/components/UserListItemCard"; -import { Button, Flex } from "@radix-ui/themes"; import UserProfileAvatar from "@/common/components/UserProfileAvatar"; +import { Button, Flex } from "@radix-ui/themes"; import { Text } from "@radix-ui/themes"; -import type { FollowTab } from "@/Follow/page"; interface FollowListProps { - type: FollowTab; + type: FollowDetectType; } export function FollowList({ type }: FollowListProps) { @@ -14,17 +14,18 @@ export function FollowList({ type }: FollowListProps) { return ( {users.map((_, idx) => ( - - + // TODO: key user id로 변경 + + - - 이름 + + seobbang - diff --git a/src/Follow/components/FollowStats.tsx b/src/Follow/components/FollowStats.tsx index 6aeef45..18d1b07 100644 --- a/src/Follow/components/FollowStats.tsx +++ b/src/Follow/components/FollowStats.tsx @@ -3,14 +3,17 @@ import { Flex, Text } from "@radix-ui/themes"; const STATS = [ { + type: "mutual", title: "맞팔로우", count: 25, }, { + type: "follow-only", title: "팔로워", count: 3, }, { + type: "followed-only", title: "팔로잉", count: 3, }, diff --git a/src/Follow/page.tsx b/src/Follow/page.tsx index f982572..5929f45 100644 --- a/src/Follow/page.tsx +++ b/src/Follow/page.tsx @@ -1,17 +1,16 @@ +import type { FollowDetectType } from "@/Follow/apis/follow"; +import { FollowList } from "@/Follow/components/FollowList"; +import { FollowStats } from "@/Follow/components/FollowStats"; +import { Tab } from "@/Follow/components/Tab"; import Header from "@/common/components/Header"; import Spacer from "@/common/components/Spacer"; import { colors } from "@/common/styles/theme"; -import { FollowStats } from "@/Follow/components/FollowStats"; -import { Tab } from "@/Follow/components/Tab"; -import { FollowList } from "@/Follow/components/FollowList"; import styled from "@emotion/styled"; import { Button, Flex, Text } from "@radix-ui/themes"; import { useState } from "react"; -export type FollowTab = "co-following" | "follower" | "following"; - export function FollowPage() { - const [selectedTab, setSelectedTab] = useState("co-following"); + const [selectedTab, setSelectedTab] = useState("mutual"); return ( <> diff --git a/src/Router.tsx b/src/Router.tsx new file mode 100644 index 0000000..df0927d --- /dev/null +++ b/src/Router.tsx @@ -0,0 +1,32 @@ +import { FollowPage } from "@/Follow/page"; +import { LoginPage } from "@/Login/page"; +import MenuPage from "@/Menu/page"; +import GlobalLayout from "@/common/components/GlobalLayout"; +import { authLoader } from "@/loaders/authLoader"; +import { createBrowserRouter } from "react-router"; + +export const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { + path: "login", + element: , + }, + { + loader: authLoader, + children: [ + { + path: "menu", + element: , + }, + ], + }, + { + path: "follow", + element: , + }, + ], + }, +]); From d5dfc9569f852292151f567c49509b28f5e523ea Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 6 Jul 2025 16:18:44 +0900 Subject: [PATCH 05/33] =?UTF-8?q?feat:=20route=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/components/FollowList.tsx | 1 + src/Router.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Follow/components/FollowList.tsx b/src/Follow/components/FollowList.tsx index 656bac4..7d05d32 100644 --- a/src/Follow/components/FollowList.tsx +++ b/src/Follow/components/FollowList.tsx @@ -9,6 +9,7 @@ interface FollowListProps { } export function FollowList({ type }: FollowListProps) { + // TODO: 실제 user data로 변경 const users = Array.from({ length: 20 }); return ( diff --git a/src/Router.tsx b/src/Router.tsx index df0927d..6e06e46 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -21,12 +21,12 @@ export const router = createBrowserRouter([ path: "menu", element: , }, + { + path: "follow", + element: , + }, ], }, - { - path: "follow", - element: , - }, ], }, ]); From 1c64f210e028ba3ca52771eab5fa38aa208b937b Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 17 Aug 2025 23:37:22 +0900 Subject: [PATCH 06/33] =?UTF-8?q?feat:=20follow=20=EC=A1=B0=ED=9A=8C=20api?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/apis/follow.ts | 21 +++++++ src/Follow/components/DetectTypeSelector.tsx | 29 ++++++++++ src/Follow/components/FollowList.tsx | 18 +++--- src/Follow/components/Tab.tsx | 28 ---------- src/Follow/hooks/apis/queryKeys.ts | 6 ++ src/Follow/hooks/apis/useQueryFollowDetect.ts | 10 ++++ src/Follow/page.tsx | 55 +++++++++++++++---- 7 files changed, 116 insertions(+), 51 deletions(-) create mode 100644 src/Follow/apis/follow.ts create mode 100644 src/Follow/components/DetectTypeSelector.tsx delete mode 100644 src/Follow/components/Tab.tsx create mode 100644 src/Follow/hooks/apis/queryKeys.ts create mode 100644 src/Follow/hooks/apis/useQueryFollowDetect.ts diff --git a/src/Follow/apis/follow.ts b/src/Follow/apis/follow.ts new file mode 100644 index 0000000..c2400e9 --- /dev/null +++ b/src/Follow/apis/follow.ts @@ -0,0 +1,21 @@ +import { apiClient } from "@/common/api"; +import type { ApiResponse } from "@/common/types/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: FollowDetectType) { + return apiClient.get>(`api/v1/users/me/followings/${detectType}`).json(); +} diff --git a/src/Follow/components/DetectTypeSelector.tsx b/src/Follow/components/DetectTypeSelector.tsx new file mode 100644 index 0000000..c489431 --- /dev/null +++ b/src/Follow/components/DetectTypeSelector.tsx @@ -0,0 +1,29 @@ +import type { FollowDetectType } from "@/Follow/apis/follow"; +import { colors } from "@/common/styles/theme"; +import { Flex, SegmentedControl } from "@radix-ui/themes"; + +interface Props { + onChange: (value: FollowDetectType) => void; +} + +export function DetectTypeSelector({ onChange }: Props) { + return ( + + 맞팔 + 상대만 팔로우 + 나만 팔로우 + + ); +} diff --git a/src/Follow/components/FollowList.tsx b/src/Follow/components/FollowList.tsx index 7d05d32..5bcca04 100644 --- a/src/Follow/components/FollowList.tsx +++ b/src/Follow/components/FollowList.tsx @@ -1,28 +1,24 @@ -import type { FollowDetectType } from "@/Follow/apis/follow"; +import type { FollowDetectResponse } from "@/Follow/apis/follow"; 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"; interface FollowListProps { - type: FollowDetectType; + userList: FollowDetectResponse["userList"]; } -export function FollowList({ type }: FollowListProps) { - // TODO: 실제 user data로 변경 - const users = Array.from({ length: 20 }); - +export function FollowList({ userList }: FollowListProps) { return ( - {users.map((_, idx) => ( - // TODO: key user id로 변경 - + {userList.map((user) => ( + - + - seobbang + {user.username} diff --git a/src/Follow/components/Tab.tsx b/src/Follow/components/Tab.tsx deleted file mode 100644 index b5642b9..0000000 --- a/src/Follow/components/Tab.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { colors } from "@/common/styles/theme"; -import { SegmentedControl } from "@radix-ui/themes"; -import type { FollowTab } from "../page"; - -interface TabProps { - onChange: (value: FollowTab) => void; -} - -export function Tab({ onChange }: TabProps) { - return ( - - 맞팔 - 팔로워 - 팔로잉 - - ); -} diff --git a/src/Follow/hooks/apis/queryKeys.ts b/src/Follow/hooks/apis/queryKeys.ts new file mode 100644 index 0000000..cf514b4 --- /dev/null +++ b/src/Follow/hooks/apis/queryKeys.ts @@ -0,0 +1,6 @@ +import type { FollowDetectType } from "@/Follow/apis/follow"; + +export const followQueryKeys = { + all: () => ["follow"], + list: (type: FollowDetectType) => [...followQueryKeys.all(), type], +}; diff --git a/src/Follow/hooks/apis/useQueryFollowDetect.ts b/src/Follow/hooks/apis/useQueryFollowDetect.ts new file mode 100644 index 0000000..11c1d7a --- /dev/null +++ b/src/Follow/hooks/apis/useQueryFollowDetect.ts @@ -0,0 +1,10 @@ +import { type FollowDetectType, queryFollowDetect } from "@/Follow/apis/follow"; +import { followQueryKeys } from "@/Follow/hooks/apis/queryKeys"; +import { useSuspenseQuery } from "@tanstack/react-query"; + +export function useQueryFollowDetect(detectType: FollowDetectType) { + return useSuspenseQuery({ + queryKey: followQueryKeys.list(detectType), + queryFn: () => queryFollowDetect(detectType), + }); +} diff --git a/src/Follow/page.tsx b/src/Follow/page.tsx index 5929f45..2f75f82 100644 --- a/src/Follow/page.tsx +++ b/src/Follow/page.tsx @@ -1,35 +1,66 @@ import type { FollowDetectType } from "@/Follow/apis/follow"; +import { DetectTypeSelector } from "@/Follow/components/DetectTypeSelector"; import { FollowList } from "@/Follow/components/FollowList"; -import { FollowStats } from "@/Follow/components/FollowStats"; -import { Tab } from "@/Follow/components/Tab"; +import { useQueryFollowDetect } from "@/Follow/hooks/apis/useQueryFollowDetect"; import Header from "@/common/components/Header"; import Spacer from "@/common/components/Spacer"; import { colors } from "@/common/styles/theme"; +import { rem } from "@/common/utils/rem"; import styled from "@emotion/styled"; import { Button, Flex, Text } from "@radix-ui/themes"; import { useState } from "react"; export function FollowPage() { - const [selectedTab, setSelectedTab] = useState("mutual"); + const [selectedType, setSelectedType] = useState("mutual"); + const { + data: { + data: { userList, totalUserCount, lastSyncAt }, + }, + } = useQueryFollowDetect(selectedType); return ( <> -
새로고침} /> +
+ 새로고침 + + {lastSyncAt} + + + } + /> - - 마지막 업데이트: 10분 전 - - - + + + + + {selectedType === "mutual" && "맞팔로우"} + {selectedType === "follow-only" && "나만 팔로우"} + {selectedType === "followed-only" && "상대만 팔로우"} + + + {totalUserCount} + + + - - - + ); From 29818bf3c2dc5e839d1bcb9027f49e9b257298b6 Mon Sep 17 00:00:00 2001 From: seobbang Date: Mon, 18 Aug 2025 00:04:21 +0900 Subject: [PATCH 07/33] =?UTF-8?q?feat:=20=ED=8C=94=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EC=96=B8=ED=8C=94=EB=A1=9C=EC=9A=B0=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/apis/follow.ts | 8 ++++ src/Follow/components/FollowList.tsx | 44 ++++++++++++++++--- src/Follow/hooks/apis/useCreateFollowUser.ts | 14 ++++++ .../hooks/apis/useDeleteUnfollowUser.ts | 14 ++++++ src/Follow/page.tsx | 2 +- 5 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/Follow/hooks/apis/useCreateFollowUser.ts create mode 100644 src/Follow/hooks/apis/useDeleteUnfollowUser.ts diff --git a/src/Follow/apis/follow.ts b/src/Follow/apis/follow.ts index c2400e9..5ad36bd 100644 --- a/src/Follow/apis/follow.ts +++ b/src/Follow/apis/follow.ts @@ -19,3 +19,11 @@ export interface FollowDetectResponse { export async function queryFollowDetect(detectType: FollowDetectType) { return apiClient.get>(`api/v1/users/me/followings/${detectType}`).json(); } + +export async function createFollowUser(githubUserId: FollowDetectUser["githubUserId"]) { + return apiClient.post>(`api/v1/follow/${githubUserId}`).json(); +} + +export async function deleteUnfollowUser(githubUserId: FollowDetectUser["githubUserId"]) { + return apiClient.delete>(`api/v1/unfollow/${githubUserId}`).json(); +} diff --git a/src/Follow/components/FollowList.tsx b/src/Follow/components/FollowList.tsx index 5bcca04..287bb5c 100644 --- a/src/Follow/components/FollowList.tsx +++ b/src/Follow/components/FollowList.tsx @@ -1,14 +1,20 @@ -import type { FollowDetectResponse } from "@/Follow/apis/follow"; +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"; interface FollowListProps { + type: FollowDetectType; userList: FollowDetectResponse["userList"]; } -export function FollowList({ userList }: FollowListProps) { +export function FollowList({ type, userList }: FollowListProps) { + const { mutateAsync: unfollow } = useDeleteUnfollowUser(); + const { mutateAsync: follow } = useCreateFollowUser(); + return ( {userList.map((user) => ( @@ -22,9 +28,37 @@ export function FollowList({ userList }: FollowListProps) { - + {type === "followed-only" ? ( + + ) : ( + + )} diff --git a/src/Follow/hooks/apis/useCreateFollowUser.ts b/src/Follow/hooks/apis/useCreateFollowUser.ts new file mode 100644 index 0000000..d7edb61 --- /dev/null +++ b/src/Follow/hooks/apis/useCreateFollowUser.ts @@ -0,0 +1,14 @@ +import { createFollowUser } from "@/Follow/apis/follow"; +import { followQueryKeys } from "@/Follow/hooks/apis/queryKeys"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +export function useCreateFollowUser() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createFollowUser, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: followQueryKeys.all() }); + }, + }); +} diff --git a/src/Follow/hooks/apis/useDeleteUnfollowUser.ts b/src/Follow/hooks/apis/useDeleteUnfollowUser.ts new file mode 100644 index 0000000..2e06ad9 --- /dev/null +++ b/src/Follow/hooks/apis/useDeleteUnfollowUser.ts @@ -0,0 +1,14 @@ +import { deleteUnfollowUser } from "@/Follow/apis/follow"; +import { followQueryKeys } from "@/Follow/hooks/apis/queryKeys"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +export function useDeleteUnfollowUser() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: deleteUnfollowUser, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: followQueryKeys.all() }); + }, + }); +} diff --git a/src/Follow/page.tsx b/src/Follow/page.tsx index 2f75f82..d454c80 100644 --- a/src/Follow/page.tsx +++ b/src/Follow/page.tsx @@ -60,7 +60,7 @@ export function FollowPage() { - + ); From f2fd1522fb70ec6a2508afad4ae04d9b3bc32b2b Mon Sep 17 00:00:00 2001 From: seobbang Date: Fri, 29 Aug 2025 01:26:03 +0900 Subject: [PATCH 08/33] =?UTF-8?q?chore:=20api=20base=20url=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.development b/.env.development index c4f3432..5922473 100644 --- a/.env.development +++ b/.env.development @@ -1 +1 @@ -VITE_API_BASE_URL='https://api.cafehub.site' \ No newline at end of file +VITE_API_BASE_URL='https://api.moyoy-test.shop' \ No newline at end of file From 7abe6090d7c98125ce08786deccf51e4ed913072 Mon Sep 17 00:00:00 2001 From: seobbang Date: Fri, 29 Aug 2025 01:26:28 +0900 Subject: [PATCH 09/33] =?UTF-8?q?feat:=20mutation=20default=20throwOnError?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 28defd4..1f88a6a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,6 @@ const queryClient = new QueryClient({ }, mutations: { retry: 0, - throwOnError: true, }, }, }); From 5cc741929ca6270c293ae7bc98f5ae8fbc1e1761 Mon Sep 17 00:00:00 2001 From: seobbang Date: Fri, 29 Aug 2025 01:48:26 +0900 Subject: [PATCH 10/33] =?UTF-8?q?feat:=20=EB=AC=B4=ED=95=9C=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/hooks/apis/useQueryFollowDetect.ts | 24 ++++++++++-- src/Follow/page.tsx | 38 +++++++++++++++---- src/common/hooks/useInfiniteScroll.ts | 34 +++++++++++++++++ 3 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 src/common/hooks/useInfiniteScroll.ts diff --git a/src/Follow/hooks/apis/useQueryFollowDetect.ts b/src/Follow/hooks/apis/useQueryFollowDetect.ts index 11c1d7a..3702c03 100644 --- a/src/Follow/hooks/apis/useQueryFollowDetect.ts +++ b/src/Follow/hooks/apis/useQueryFollowDetect.ts @@ -1,10 +1,26 @@ -import { type FollowDetectType, queryFollowDetect } from "@/Follow/apis/follow"; +import { type FollowDetectResponse, type FollowDetectType, queryFollowDetect } from "@/Follow/apis/follow"; import { followQueryKeys } from "@/Follow/hooks/apis/queryKeys"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import type { ApiResponse } from "@/common/types/api"; +import { type InfiniteData, useInfiniteQuery } from "@tanstack/react-query"; export function useQueryFollowDetect(detectType: FollowDetectType) { - return useSuspenseQuery({ + return useInfiniteQuery< + ApiResponse, + Error, + InfiniteData>, + string[], + number | undefined + >({ queryKey: followQueryKeys.list(detectType), - queryFn: () => queryFollowDetect(detectType), + queryFn: ({ pageParam }) => queryFollowDetect(detectType, pageParam), + initialPageParam: undefined, + getNextPageParam: (prevPage) => { + const lastPage = prevPage.data.lastPage; + if (lastPage) return undefined; + + const userList = prevPage.data.userList; + return userList[userList.length - 1].githubUserId; + }, + staleTime: 60000, }); } diff --git a/src/Follow/page.tsx b/src/Follow/page.tsx index d454c80..01de155 100644 --- a/src/Follow/page.tsx +++ b/src/Follow/page.tsx @@ -1,22 +1,24 @@ import type { FollowDetectType } from "@/Follow/apis/follow"; import { DetectTypeSelector } from "@/Follow/components/DetectTypeSelector"; import { FollowList } from "@/Follow/components/FollowList"; +import { RefreshButton } from "@/Follow/components/RefreshButton"; import { useQueryFollowDetect } from "@/Follow/hooks/apis/useQueryFollowDetect"; import Header from "@/common/components/Header"; import Spacer from "@/common/components/Spacer"; +import UserListItemCard from "@/common/components/UserListItemCard"; +import UserProfileAvatar from "@/common/components/UserProfileAvatar"; +import { useInfiniteScroll } from "@/common/hooks/useInfiniteScroll"; import { colors } from "@/common/styles/theme"; import { rem } from "@/common/utils/rem"; import styled from "@emotion/styled"; -import { Button, Flex, Text } from "@radix-ui/themes"; +import { Flex, Skeleton, Spinner, Text } from "@radix-ui/themes"; import { useState } from "react"; export function FollowPage() { const [selectedType, setSelectedType] = useState("mutual"); - const { - data: { - data: { userList, totalUserCount, lastSyncAt }, - }, - } = useQueryFollowDetect(selectedType); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useQueryFollowDetect(selectedType); + + const observerRef = useInfiniteScroll({ fetchNextPage, hasNextPage, isFetchingNextPage }); return ( <> @@ -60,7 +62,29 @@ export function FollowPage() { - + + {data ? ( + <> + userList)} /> +
+ + {isFetchingNextPage && ( + + + + )} + + ) : ( + + {Array.from({ length: 10 }, (_, idx) => idx).map((idx) => ( + + + + + + ))} + + )} ); diff --git a/src/common/hooks/useInfiniteScroll.ts b/src/common/hooks/useInfiniteScroll.ts new file mode 100644 index 0000000..8ede20c --- /dev/null +++ b/src/common/hooks/useInfiniteScroll.ts @@ -0,0 +1,34 @@ +import { useEffect, useRef } from "react"; + +interface Params { + fetchNextPage: () => void; + hasNextPage: boolean; + isFetchingNextPage: boolean; +} + +export function useInfiniteScroll({ fetchNextPage, hasNextPage, isFetchingNextPage }: Params) { + const observerRef = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + const entry = entries[0]; + if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, + { + threshold: 0.1, + rootMargin: "100px", + } + ); + + if (observerRef.current) { + observer.observe(observerRef.current); + } + + return () => observer.disconnect(); + }, [fetchNextPage, hasNextPage, isFetchingNextPage]); + + return observerRef; +} From 3fb7740521dee1cddeffd40c2d59b8abdc5f09fb Mon Sep 17 00:00:00 2001 From: seobbang Date: Fri, 29 Aug 2025 02:01:17 +0900 Subject: [PATCH 11/33] =?UTF-8?q?feat:=20type=20selector=EC=97=90=20value?= =?UTF-8?q?=20=EC=A0=84=EB=8B=AC=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/components/DetectTypeSelector.tsx | 6 ++++-- src/Follow/page.tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Follow/components/DetectTypeSelector.tsx b/src/Follow/components/DetectTypeSelector.tsx index c489431..53e8606 100644 --- a/src/Follow/components/DetectTypeSelector.tsx +++ b/src/Follow/components/DetectTypeSelector.tsx @@ -1,17 +1,19 @@ import type { FollowDetectType } from "@/Follow/apis/follow"; import { colors } from "@/common/styles/theme"; -import { Flex, SegmentedControl } from "@radix-ui/themes"; +import { SegmentedControl } from "@radix-ui/themes"; interface Props { + value: FollowDetectType; onChange: (value: FollowDetectType) => void; } -export function DetectTypeSelector({ onChange }: Props) { +export function DetectTypeSelector({ value, onChange }: Props) { return ( - + From 7f1f48768ae3f80e98507beea664dece1475323a Mon Sep 17 00:00:00 2001 From: seobbang Date: Fri, 29 Aug 2025 02:01:46 +0900 Subject: [PATCH 12/33] =?UTF-8?q?feat:=20follow/unfollow=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=EC=97=90=20cursor=20pointer=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/components/FollowList.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Follow/components/FollowList.tsx b/src/Follow/components/FollowList.tsx index 287bb5c..acf0da1 100644 --- a/src/Follow/components/FollowList.tsx +++ b/src/Follow/components/FollowList.tsx @@ -40,6 +40,7 @@ export function FollowList({ type, userList }: FollowListProps) { alert("팔로우에 실패했습니다. 다시 시도해주세요"); } }} + css={{ cursor: "pointer" }} > Follow @@ -55,6 +56,7 @@ export function FollowList({ type, userList }: FollowListProps) { alert("언팔로우에 실패했습니다. 다시 시도해주세요"); } }} + css={{ cursor: "pointer" }} > Unfollow From 2971dd263b8dc33cd6cfe9f875974337440c0c9c Mon Sep 17 00:00:00 2001 From: seobbang Date: Fri, 29 Aug 2025 02:04:07 +0900 Subject: [PATCH 13/33] =?UTF-8?q?feat:=20=EB=A7=9E=ED=8C=94=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20polling=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/apis/follow.ts | 20 +++++++++++++++++-- src/Follow/hooks/apis/useQueryFollowDetect.ts | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Follow/apis/follow.ts b/src/Follow/apis/follow.ts index 5ad36bd..9cc068d 100644 --- a/src/Follow/apis/follow.ts +++ b/src/Follow/apis/follow.ts @@ -16,8 +16,24 @@ export interface FollowDetectResponse { lastSyncAt: string; } -export async function queryFollowDetect(detectType: FollowDetectType) { - return apiClient.get>(`api/v1/users/me/followings/${detectType}`).json(); +export async function queryFollowDetect({ + detectType, + lastGithubUserId, +}: { detectType: FollowDetectType; lastGithubUserId?: number }) { + const poll = async (): Promise> => { + const response = await apiClient.get(`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.json(); + }; + + return poll(); } export async function createFollowUser(githubUserId: FollowDetectUser["githubUserId"]) { diff --git a/src/Follow/hooks/apis/useQueryFollowDetect.ts b/src/Follow/hooks/apis/useQueryFollowDetect.ts index 3702c03..af30d1c 100644 --- a/src/Follow/hooks/apis/useQueryFollowDetect.ts +++ b/src/Follow/hooks/apis/useQueryFollowDetect.ts @@ -12,7 +12,7 @@ export function useQueryFollowDetect(detectType: FollowDetectType) { number | undefined >({ queryKey: followQueryKeys.list(detectType), - queryFn: ({ pageParam }) => queryFollowDetect(detectType, pageParam), + queryFn: ({ pageParam }) => queryFollowDetect({ detectType, lastGithubUserId: pageParam }), initialPageParam: undefined, getNextPageParam: (prevPage) => { const lastPage = prevPage.data.lastPage; From 7b5d4da0b69e6c37b2075e7ff04d3081dedd018f Mon Sep 17 00:00:00 2001 From: seobbang Date: Fri, 29 Aug 2025 02:04:57 +0900 Subject: [PATCH 14/33] =?UTF-8?q?feat:=20totalUserCount=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EC=A0=91=EA=B7=BC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=9C=EB=8C=80=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/page.tsx | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Follow/page.tsx b/src/Follow/page.tsx index f9ca836..cda2316 100644 --- a/src/Follow/page.tsx +++ b/src/Follow/page.tsx @@ -46,18 +46,24 @@ export function FollowPage() { {selectedType === "follow-only" && "나만 팔로우"} {selectedType === "followed-only" && "상대만 팔로우"} - - {totalUserCount} - + {data && ( + + {data.pages[0].data.totalUserCount} + + )} From 99d8ff721da3ffa316bc25e0205c2056c4f5250d Mon Sep 17 00:00:00 2001 From: seobbang Date: Fri, 29 Aug 2025 16:43:20 +0900 Subject: [PATCH 15/33] =?UTF-8?q?feat:=20=EA=B0=95=EC=A0=9C=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0=20=EA=B5=AC=ED=98=84=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/apis/follow.ts | 4 + src/Follow/components/RefreshButton.tsx | 140 ++++++++++++++++++ .../hooks/apis/useCreateFollowRefresh.ts | 14 ++ src/Follow/page.tsx | 12 +- src/assets/icons/index.ts | 3 +- src/assets/icons/refresh_arrow.svg | 3 + src/common/utils/formatDate.ts | 12 ++ 7 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 src/Follow/components/RefreshButton.tsx create mode 100644 src/Follow/hooks/apis/useCreateFollowRefresh.ts create mode 100644 src/assets/icons/refresh_arrow.svg create mode 100644 src/common/utils/formatDate.ts diff --git a/src/Follow/apis/follow.ts b/src/Follow/apis/follow.ts index 9cc068d..81a8a76 100644 --- a/src/Follow/apis/follow.ts +++ b/src/Follow/apis/follow.ts @@ -40,6 +40,10 @@ export async function createFollowUser(githubUserId: FollowDetectUser["githubUse return apiClient.post>(`api/v1/follow/${githubUserId}`).json(); } +export async function createFollowRefresh() { + return apiClient.post>("api/v1/users/me/followings/refresh").json(); +} + export async function deleteUnfollowUser(githubUserId: FollowDetectUser["githubUserId"]) { return apiClient.delete>(`api/v1/unfollow/${githubUserId}`).json(); } diff --git a/src/Follow/components/RefreshButton.tsx b/src/Follow/components/RefreshButton.tsx new file mode 100644 index 0000000..e1bba18 --- /dev/null +++ b/src/Follow/components/RefreshButton.tsx @@ -0,0 +1,140 @@ +import { useCreateFollowRefresh } from "@/Follow/hooks/apis/useCreateFollowRefresh"; +import { IconRefreshArrow } from "@/assets/icons"; +import { colors } from "@/common/styles/theme"; +import { formatDate } from "@/common/utils/formatDate"; +import styled from "@emotion/styled"; +import { Button, Flex, Text } from "@radix-ui/themes"; +import { useEffect, useRef, useState } from "react"; + +interface Props { + lastSyncAt: string; +} + +export function RefreshButton({ lastSyncAt }: Props) { + const [isRefreshEnabled, setIsRefreshEnabled] = useState(() => checkRefreshEnabled(lastSyncAt)); + + const [showTooltip, setShowTooltip] = useState(false); + const tooltipRef = useRef(null); + + const { mutate: refresh } = useCreateFollowRefresh(); + + useEffect(() => { + if (isRefreshEnabled) return; + + const updateCanRefresh = () => { + const enabled = checkRefreshEnabled(lastSyncAt); + setIsRefreshEnabled(enabled); + + if (enabled) { + clearInterval(intervalId); + } + }; + + const intervalId = setInterval(updateCanRefresh, 30000); + + return () => clearInterval(intervalId); + }, [lastSyncAt, isRefreshEnabled]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (tooltipRef.current && !tooltipRef.current.contains(event.target as Node)) { + setShowTooltip(false); + } + }; + + if (showTooltip) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [showTooltip]); + + return ( + +
+ setShowTooltip(!showTooltip)} + > + {getTimeAgo(lastSyncAt)} + + + {showTooltip && {formatDate(lastSyncAt)}} +
+ + +
+ ); +} + +function getTimeAgo(timestamp: string) { + const now = new Date(); + const timestampDate = new Date(timestamp); + const diffInMinutes = Math.round((now.getTime() - timestampDate.getTime()) / (1000 * 60)); + + if (diffInMinutes < 1) { + return "방금 전"; + } + + if (diffInMinutes < 60) { + return `${diffInMinutes}분 전`; + } + + if (diffInMinutes < 1440) { + const hours = Math.floor(diffInMinutes / 60); + return `${hours}시간 전`; + } + + const days = Math.floor(diffInMinutes / 1440); + return `${days}일 전`; +} + +function checkRefreshEnabled(timestamp: string) { + const timestampDate = new Date(timestamp); + // 서버 제한(5분) + 오차 대비 10초 버퍼 두기 + const fiveMinutesTenSecondsLater = new Date(timestampDate.getTime() + 5 * 60 * 1000 + 10 * 1000); + const now = new Date(); + + return fiveMinutesTenSecondsLater < now; +} + +const ToolTip = styled.div({ + position: "absolute", + top: "100%", + right: 0, + backgroundColor: colors.gray11, + color: colors.gray1, + padding: "8px 12px", + borderRadius: "6px", + fontSize: "12px", + whiteSpace: "nowrap", + zIndex: 1000, + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", + marginTop: "4px", +}); diff --git a/src/Follow/hooks/apis/useCreateFollowRefresh.ts b/src/Follow/hooks/apis/useCreateFollowRefresh.ts new file mode 100644 index 0000000..7587ade --- /dev/null +++ b/src/Follow/hooks/apis/useCreateFollowRefresh.ts @@ -0,0 +1,14 @@ +import { createFollowRefresh } from "@/Follow/apis/follow"; +import { followQueryKeys } from "@/Follow/hooks/apis/queryKeys"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +export function useCreateFollowRefresh() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createFollowRefresh, + onSuccess: () => { + queryClient.resetQueries({ queryKey: followQueryKeys.all() }); + }, + }); +} diff --git a/src/Follow/page.tsx b/src/Follow/page.tsx index cda2316..b399b1a 100644 --- a/src/Follow/page.tsx +++ b/src/Follow/page.tsx @@ -22,17 +22,7 @@ export function FollowPage() { return ( <> -
- 새로고침 - - {lastSyncAt} - - - } - /> +
} /> diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index d13edf6..a59d6f3 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -1,3 +1,4 @@ import IconGithubLogo from "./github_logo.svg?react"; +import IconRefreshArrow from "./refresh_arrow.svg?react"; -export { IconGithubLogo }; +export { IconGithubLogo, IconRefreshArrow }; diff --git a/src/assets/icons/refresh_arrow.svg b/src/assets/icons/refresh_arrow.svg new file mode 100644 index 0000000..046149c --- /dev/null +++ b/src/assets/icons/refresh_arrow.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/common/utils/formatDate.ts b/src/common/utils/formatDate.ts new file mode 100644 index 0000000..1b6a763 --- /dev/null +++ b/src/common/utils/formatDate.ts @@ -0,0 +1,12 @@ +export function formatDate(timestamp: string) { + const date = new Date(timestamp); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const hour = date.getHours(); + const minute = String(date.getMinutes()).padStart(2, "0"); + + const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; + + return `${year}년 ${month}월 ${day}일 ${hour >= 12 ? "오후" : "오전"} ${displayHour}:${minute}`; +} From 911a7d14dd13ba52602dcd2eb6e539755beeee43 Mon Sep 17 00:00:00 2001 From: seobbang Date: Fri, 29 Aug 2025 16:43:30 +0900 Subject: [PATCH 16/33] =?UTF-8?q?chore:=20biome=20react=20hook=20rule=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- biome.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index d7184cf..6eacbb1 100644 --- a/biome.json +++ b/biome.json @@ -12,7 +12,10 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "correctness": { + "useExhaustiveDependencies": "warn" + } } }, "javascript": { From 3c1228ddea6451ecabb42dd08c9445318fca96f0 Mon Sep 17 00:00:00 2001 From: seobbang Date: Fri, 29 Aug 2025 16:54:28 +0900 Subject: [PATCH 17/33] =?UTF-8?q?feat:=20useInfiniteScroll=20param=20optio?= =?UTF-8?q?ns=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/hooks/useInfiniteScroll.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/hooks/useInfiniteScroll.ts b/src/common/hooks/useInfiniteScroll.ts index 8ede20c..6819158 100644 --- a/src/common/hooks/useInfiniteScroll.ts +++ b/src/common/hooks/useInfiniteScroll.ts @@ -4,9 +4,10 @@ interface Params { fetchNextPage: () => void; hasNextPage: boolean; isFetchingNextPage: boolean; + options?: IntersectionObserverInit; } -export function useInfiniteScroll({ fetchNextPage, hasNextPage, isFetchingNextPage }: Params) { +export function useInfiniteScroll({ fetchNextPage, hasNextPage, isFetchingNextPage, options }: Params) { const observerRef = useRef(null); useEffect(() => { @@ -20,6 +21,7 @@ export function useInfiniteScroll({ fetchNextPage, hasNextPage, isFetchingNextPa { threshold: 0.1, rootMargin: "100px", + ...options, } ); From 23a6e1f8ed07bf492bdc9da7fb8188d6b6b62a2c Mon Sep 17 00:00:00 2001 From: seobbang Date: Fri, 29 Aug 2025 16:54:43 +0900 Subject: [PATCH 18/33] =?UTF-8?q?feat:=20spinner=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=EB=A7=81=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/page.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Follow/page.tsx b/src/Follow/page.tsx index b399b1a..c2514c6 100644 --- a/src/Follow/page.tsx +++ b/src/Follow/page.tsx @@ -62,13 +62,10 @@ export function FollowPage() { {data ? ( <> userList)} /> -
- {isFetchingNextPage && ( - - - - )} + + {isFetchingNextPage && } + ) : ( From de7398249cf4c51d6f57998f7030bf64d508912c Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 31 Aug 2025 15:39:57 +0900 Subject: [PATCH 19/33] =?UTF-8?q?chore:=20env.development=20git=20ignore?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 1 - .gitignore | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 .env.development diff --git a/.env.development b/.env.development deleted file mode 100644 index 5922473..0000000 --- a/.env.development +++ /dev/null @@ -1 +0,0 @@ -VITE_API_BASE_URL='https://api.moyoy-test.shop' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7f4661f..d83d6e1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ dist-ssr *.local .env +.env.development *.pem # Editor directories and files From dd862c38dedbbb958130f4076a745ec21c26ee3d Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 31 Aug 2025 15:53:30 +0900 Subject: [PATCH 20/33] =?UTF-8?q?chore:=20=ED=95=84=EC=9A=94=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/components/FollowStats.tsx | 49 --------------------------- 1 file changed, 49 deletions(-) delete mode 100644 src/Follow/components/FollowStats.tsx diff --git a/src/Follow/components/FollowStats.tsx b/src/Follow/components/FollowStats.tsx deleted file mode 100644 index 18d1b07..0000000 --- a/src/Follow/components/FollowStats.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { colors } from "@/common/styles/theme"; -import { Flex, Text } from "@radix-ui/themes"; - -const STATS = [ - { - type: "mutual", - title: "맞팔로우", - count: 25, - }, - { - type: "follow-only", - title: "팔로워", - count: 3, - }, - { - type: "followed-only", - title: "팔로잉", - count: 3, - }, -]; - -export function FollowStats() { - return ( - - {STATS.map((item) => ( - - - {item.title} - - - {item.count} - - - ))} - - ); -} From 37bd90d08dbf7441e6f8c5622374385c7dd268bc Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 31 Aug 2025 21:49:22 +0900 Subject: [PATCH 21/33] =?UTF-8?q?fix:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20apiC?= =?UTF-8?q?lient=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/apis/follow.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Follow/apis/follow.ts b/src/Follow/apis/follow.ts index 81a8a76..e71786c 100644 --- a/src/Follow/apis/follow.ts +++ b/src/Follow/apis/follow.ts @@ -20,8 +20,8 @@ export async function queryFollowDetect({ detectType, lastGithubUserId, }: { detectType: FollowDetectType; lastGithubUserId?: number }) { - const poll = async (): Promise> => { - const response = await apiClient.get(`api/v1/users/me/followings/${detectType}`, { + const poll = async () => { + const response = await apiClient.get(`api/v1/users/me/followings/${detectType}`, { searchParams: lastGithubUserId ? { lastGithubUserId } : undefined, }); @@ -30,20 +30,20 @@ export async function queryFollowDetect({ return poll(); } - return response.json(); + return response; }; return poll(); } export async function createFollowUser(githubUserId: FollowDetectUser["githubUserId"]) { - return apiClient.post>(`api/v1/follow/${githubUserId}`).json(); + return apiClient.post(`api/v1/follow/${githubUserId}`); } export async function createFollowRefresh() { - return apiClient.post>("api/v1/users/me/followings/refresh").json(); + return apiClient.post("api/v1/users/me/followings/refresh"); } export async function deleteUnfollowUser(githubUserId: FollowDetectUser["githubUserId"]) { - return apiClient.delete>(`api/v1/unfollow/${githubUserId}`).json(); + return apiClient.delete(`api/v1/unfollow/${githubUserId}`); } From ad0d747a9969d29ac083f2db40171232bd201d9b Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 31 Aug 2025 21:49:53 +0900 Subject: [PATCH 22/33] =?UTF-8?q?fix:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20Head?= =?UTF-8?q?er=20=EC=97=90=20=EB=A7=9E=EA=B2=8C=20props=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Follow/page.tsx b/src/Follow/page.tsx index c2514c6..1ddb967 100644 --- a/src/Follow/page.tsx +++ b/src/Follow/page.tsx @@ -22,7 +22,10 @@ export function FollowPage() { return ( <> -
} /> +
<>"팔로우 관리"} + renderRight={data ? () => : undefined} + /> From cdbdbee24cba199719bd663095df0fcc7a93dc68 Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 31 Aug 2025 21:50:58 +0900 Subject: [PATCH 23/33] =?UTF-8?q?chore:=20=ED=81=B0=EB=94=B0=EC=98=B4?= =?UTF-8?q?=ED=91=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Follow/page.tsx b/src/Follow/page.tsx index 1ddb967..5cbd75d 100644 --- a/src/Follow/page.tsx +++ b/src/Follow/page.tsx @@ -23,7 +23,7 @@ export function FollowPage() { return ( <>
<>"팔로우 관리"} + renderCenter={() => <>팔로우 관리} renderRight={data ? () => : undefined} /> From 34695db11342033407c0253b002d368d8b31754d Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 31 Aug 2025 21:54:47 +0900 Subject: [PATCH 24/33] =?UTF-8?q?feat:=20follow/unfollow=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20toast=EB=A1=9C=20=EC=B2=98=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 26 +++++++ src/Follow/components/FollowList.tsx | 100 ++++++++++++++------------- 3 files changed, 79 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 57c722d..3ab39bf 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9de9780..cd9b98b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + react-hot-toast: + specifier: ^2.6.0 + version: 2.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react-router: specifier: ^7.1.1 version: 7.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1501,6 +1504,11 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + goober@2.1.16: + resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==} + peerDependencies: + csstype: ^3.0.10 + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -1656,6 +1664,13 @@ packages: peerDependencies: react: ^19.0.0 + react-hot-toast@2.6.0: + resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3292,6 +3307,10 @@ snapshots: globrex@0.1.2: {} + goober@2.1.16(csstype@3.1.3): + dependencies: + csstype: 3.1.3 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -3437,6 +3456,13 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 + react-hot-toast@2.6.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + csstype: 3.1.3 + goober: 2.1.16(csstype@3.1.3) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-is@16.13.1: {} react-remove-scroll-bar@2.3.8(@types/react@18.3.18)(react@19.0.0): diff --git a/src/Follow/components/FollowList.tsx b/src/Follow/components/FollowList.tsx index acf0da1..cfe9bbd 100644 --- a/src/Follow/components/FollowList.tsx +++ b/src/Follow/components/FollowList.tsx @@ -5,6 +5,7 @@ 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; @@ -16,55 +17,58 @@ export function FollowList({ type, userList }: FollowListProps) { const { mutateAsync: follow } = useCreateFollowUser(); return ( - - {userList.map((user) => ( - - - - + <> + + {userList.map((user) => ( + + + + - - {user.username} - + + {user.username} + + + + {type === "followed-only" ? ( + + ) : ( + + )} + - - {type === "followed-only" ? ( - - ) : ( - - )} - - - - ))} - + + ))} + + + ); } From 50e04c54303b5312cba1aaf1650529876d780324 Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 31 Aug 2025 22:00:55 +0900 Subject: [PATCH 25/33] =?UTF-8?q?chore:=20=EC=95=88=20=EC=93=B0=EB=8A=94?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/apis/follow.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Follow/apis/follow.ts b/src/Follow/apis/follow.ts index e71786c..66dd763 100644 --- a/src/Follow/apis/follow.ts +++ b/src/Follow/apis/follow.ts @@ -1,5 +1,4 @@ import { apiClient } from "@/common/api"; -import type { ApiResponse } from "@/common/types/api"; export type FollowDetectType = "mutual" | "follow-only" | "followed-only"; From b0ae058de2473fbb501698e84b0267dac68a3459 Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 31 Aug 2025 22:06:24 +0900 Subject: [PATCH 26/33] =?UTF-8?q?chore:=20=ED=95=84=EC=9A=94=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/apis/index.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 src/common/apis/index.ts diff --git a/src/common/apis/index.ts b/src/common/apis/index.ts deleted file mode 100644 index 8355887..0000000 --- a/src/common/apis/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import ky from "ky"; - -export const apiClient = ky.create({ - prefixUrl: import.meta.env.VITE_API_BASE_URL, -}); From ac15ffaa8ffaa4cb2e41b3c63a6e6799224ab9c4 Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 31 Aug 2025 22:14:06 +0900 Subject: [PATCH 27/33] =?UTF-8?q?fix:=20import=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 1f88a6a..f910d66 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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"; From cd52f5b4e4944ee78f4892015cbcb49199ea2a04 Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 31 Aug 2025 22:14:32 +0900 Subject: [PATCH 28/33] =?UTF-8?q?fix:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EB=B0=B0=EC=97=B4=20options=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/hooks/useInfiniteScroll.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/hooks/useInfiniteScroll.ts b/src/common/hooks/useInfiniteScroll.ts index 6819158..8b0db1c 100644 --- a/src/common/hooks/useInfiniteScroll.ts +++ b/src/common/hooks/useInfiniteScroll.ts @@ -30,7 +30,7 @@ export function useInfiniteScroll({ fetchNextPage, hasNextPage, isFetchingNextPa } return () => observer.disconnect(); - }, [fetchNextPage, hasNextPage, isFetchingNextPage]); + }, [fetchNextPage, hasNextPage, isFetchingNextPage, options]); return observerRef; } From 9900db8857a1cedc24d2f022ca3d0ca07469dd9e Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 31 Aug 2025 22:24:23 +0900 Subject: [PATCH 29/33] =?UTF-8?q?feat:=20=ED=8C=94=EB=A1=9C=EC=9A=B0/?= =?UTF-8?q?=EC=96=B8=ED=8C=94=EB=A1=9C=EC=9A=B0=20=EC=84=B1=EA=B3=B5?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/components/FollowList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Follow/components/FollowList.tsx b/src/Follow/components/FollowList.tsx index cfe9bbd..aaa1bfc 100644 --- a/src/Follow/components/FollowList.tsx +++ b/src/Follow/components/FollowList.tsx @@ -38,7 +38,8 @@ export function FollowList({ type, userList }: FollowListProps) { css={{ cursor: "pointer" }} onClick={async () => { try { - follow(user.githubUserId); + await follow(user.githubUserId); + toast.success("팔로우에 성공했습니다."); } catch (e) { toast.error("팔로우에 실패했습니다. 다시 시도해주세요"); } @@ -54,7 +55,8 @@ export function FollowList({ type, userList }: FollowListProps) { css={{ cursor: "pointer" }} onClick={async () => { try { - unfollow(user.githubUserId); + await unfollow(user.githubUserId); + toast.success("언팔로우에 성공했습니다."); } catch (e) { toast.error("언팔로우에 실패했습니다. 다시 시도해주세요"); } From 0d324bd345838e628f2299eeef5560a2ca9e7272 Mon Sep 17 00:00:00 2001 From: seobbang Date: Sun, 21 Sep 2025 22:11:56 +0900 Subject: [PATCH 30/33] =?UTF-8?q?feat:=20staleTime=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/hooks/apis/useQueryFollowDetect.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Follow/hooks/apis/useQueryFollowDetect.ts b/src/Follow/hooks/apis/useQueryFollowDetect.ts index af30d1c..c249440 100644 --- a/src/Follow/hooks/apis/useQueryFollowDetect.ts +++ b/src/Follow/hooks/apis/useQueryFollowDetect.ts @@ -21,6 +21,5 @@ export function useQueryFollowDetect(detectType: FollowDetectType) { const userList = prevPage.data.userList; return userList[userList.length - 1].githubUserId; }, - staleTime: 60000, }); } From 30ebef1bfaae964ce18a52fa77949b4ce83cda0a Mon Sep 17 00:00:00 2001 From: seobbang Date: Tue, 23 Sep 2025 00:34:34 +0900 Subject: [PATCH 31/33] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20defaultValue=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/components/DetectTypeSelector.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Follow/components/DetectTypeSelector.tsx b/src/Follow/components/DetectTypeSelector.tsx index 53e8606..6bacd16 100644 --- a/src/Follow/components/DetectTypeSelector.tsx +++ b/src/Follow/components/DetectTypeSelector.tsx @@ -10,7 +10,6 @@ interface Props { export function DetectTypeSelector({ value, onChange }: Props) { return ( Date: Tue, 23 Sep 2025 00:41:32 +0900 Subject: [PATCH 32/33] =?UTF-8?q?fix:=20=EB=B0=B0=EC=97=B4=20length=200?= =?UTF-8?q?=EC=9D=BC=EB=95=8C=20early=20return?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/hooks/apis/useQueryFollowDetect.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Follow/hooks/apis/useQueryFollowDetect.ts b/src/Follow/hooks/apis/useQueryFollowDetect.ts index c249440..2e089d6 100644 --- a/src/Follow/hooks/apis/useQueryFollowDetect.ts +++ b/src/Follow/hooks/apis/useQueryFollowDetect.ts @@ -19,6 +19,8 @@ export function useQueryFollowDetect(detectType: FollowDetectType) { if (lastPage) return undefined; const userList = prevPage.data.userList; + if (userList.length === 0) return; + return userList[userList.length - 1].githubUserId; }, }); From 51ef6b39a8c53b2a15944d4e07595990633e04d1 Mon Sep 17 00:00:00 2001 From: seobbang Date: Tue, 23 Sep 2025 22:03:33 +0900 Subject: [PATCH 33/33] =?UTF-8?q?feat:=20poll=20=EC=B5=9C=EB=8C=80=20?= =?UTF-8?q?=ED=9A=9F=EC=88=98=20=EC=A0=9C=ED=95=9C=20=EB=91=90=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Follow/apis/follow.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Follow/apis/follow.ts b/src/Follow/apis/follow.ts index 66dd763..61b61d2 100644 --- a/src/Follow/apis/follow.ts +++ b/src/Follow/apis/follow.ts @@ -15,17 +15,25 @@ export interface FollowDetectResponse { lastSyncAt: string; } +const MAX_RETRY = 40; +const POLL_DELAY_MS = 500; + export async function queryFollowDetect({ detectType, lastGithubUserId, }: { detectType: FollowDetectType; lastGithubUserId?: number }) { + let retry = 0; + const poll = async () => { const response = await apiClient.get(`api/v1/users/me/followings/${detectType}`, { searchParams: lastGithubUserId ? { lastGithubUserId } : undefined, }); - if (response.status === 202) { - await new Promise((resolve) => setTimeout(resolve, 500)); + if (retry > MAX_RETRY) { + throw new Error("Follow detect polling exceeded max retries"); + } + await new Promise((resolve) => setTimeout(resolve, POLL_DELAY_MS)); + retry++; return poll(); }