-
Notifications
You must be signed in to change notification settings - Fork 4
[Feat] 기업 상세페이지 북마크 기능 추가 및 API 연동 #166
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
base: dev
Are you sure you want to change the base?
Changes from all commits
2e052b7
6e42803
7c96345
519e85b
b17c745
0d7f908
836a02b
1922bf7
adb109c
ddc1adc
b622deb
497f6fe
5011401
ae1e8fc
ae7dc87
117cd29
458db14
13af087
5516abe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { useMutation, useQueryClient } from "@tanstack/react-query"; | ||
|
|
||
| import { api } from "@/shared/api/axios-instance"; | ||
| import { meQueryKey } from "@/shared/api/config/query-key"; | ||
|
|
||
| export const deleteBookmark = async (companyId: number) => { | ||
| await api.me.removeBookmark(companyId); | ||
| }; | ||
|
|
||
| interface UseDeleteBookmarkOptions { | ||
| onSuccess?: () => void; | ||
| onError?: (error: unknown) => void; | ||
| } | ||
|
|
||
| export const useDeleteBookmark = (options?: UseDeleteBookmarkOptions) => { | ||
| const queryClient = useQueryClient(); | ||
|
|
||
| return useMutation({ | ||
| mutationFn: (companyId: number) => deleteBookmark(companyId), | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ | ||
| queryKey: meQueryKey.bookmarkCompanyLists(), | ||
| }); | ||
| options?.onSuccess?.(); | ||
| }, | ||
| onError: options?.onError, | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { useQuery } from "@tanstack/react-query"; | ||
|
|
||
| import { api } from "@/shared/api/axios-instance"; | ||
| import { meQueryKey } from "@/shared/api/config/query-key"; | ||
|
|
||
| import type { BookmarkRow } from "../types/bookmark.type"; | ||
|
|
||
| export type BookmarkCompanySort = "NAME" | "LIKE" | "LATEST" | "OLDEST"; | ||
|
|
||
| export interface BookmarkCompaniesResponse { | ||
| content: BookmarkRow[]; | ||
| currentPage: number; | ||
| totalPage: number; | ||
| totalElements: number; | ||
| } | ||
|
|
||
| interface BookmarkCompanyItemApiResponse { | ||
| id?: number; | ||
| companyId?: number; | ||
| name?: string; | ||
| createdAt?: string; | ||
| isConnected?: boolean; | ||
| } | ||
|
|
||
| interface BookmarkCompaniesApiResponse { | ||
| content?: BookmarkCompanyItemApiResponse[]; | ||
| currentPage?: number; | ||
| totalPage?: number; | ||
| totalElements?: number; | ||
| } | ||
|
|
||
| const isValidBookmarkCompany = ( | ||
| bookmarkCompany: BookmarkCompanyItemApiResponse | ||
| ): bookmarkCompany is BookmarkCompanyItemApiResponse & { | ||
| id: number; | ||
| companyId: number; | ||
| } => { | ||
| const { id, companyId } = bookmarkCompany; | ||
|
|
||
| return ( | ||
| typeof id === "number" && | ||
| Number.isInteger(id) && | ||
| id > 0 && | ||
| typeof companyId === "number" && | ||
| Number.isInteger(companyId) && | ||
| companyId > 0 | ||
| ); | ||
| }; | ||
|
|
||
| export const getBookmarkCompanies = async ( | ||
| page: number, | ||
| sort: BookmarkCompanySort = "LATEST" | ||
| ): Promise<BookmarkCompaniesResponse> => { | ||
| const response = await api.me.getBookmarkCompany({ page, sort }); | ||
| const result = | ||
| (response.result as BookmarkCompaniesApiResponse | undefined) ?? {}; | ||
|
|
||
| return { | ||
| content: (result.content ?? []) | ||
| .filter(isValidBookmarkCompany) | ||
| .map((bookmarkCompany) => ({ | ||
| id: bookmarkCompany.id, | ||
| companyId: bookmarkCompany.companyId, | ||
| companyName: bookmarkCompany.name ?? "", | ||
| scrapedAt: bookmarkCompany.createdAt ?? "", | ||
| isConnected: bookmarkCompany.isConnected ?? false, | ||
| })), | ||
| currentPage: result.currentPage ?? page, | ||
| totalPage: result.totalPage ?? 0, | ||
| totalElements: result.totalElements ?? 0, | ||
| }; | ||
| }; | ||
|
|
||
| export const useGetBookmarkCompaniesQuery = ( | ||
| page: number, | ||
| sort: BookmarkCompanySort = "LATEST" | ||
| ) => { | ||
| return useQuery({ | ||
| queryKey: meQueryKey.bookmarkCompanyList(page, sort), | ||
| queryFn: () => getBookmarkCompanies(page, sort), | ||
| enabled: Number.isFinite(page) && page > 0, | ||
| staleTime: 0, | ||
| refetchOnMount: "always", | ||
| refetchOnWindowFocus: true, | ||
| }); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { useMutation, useQueryClient } from "@tanstack/react-query"; | ||
|
|
||
| import { api } from "@/shared/api/axios-instance"; | ||
| import { meQueryKey } from "@/shared/api/config/query-key"; | ||
|
|
||
| export const postBookmark = async (companyId: number) => { | ||
| const response = await api.me.addBookmark(companyId); | ||
| return response.result; | ||
| }; | ||
|
|
||
| interface UsePostBookmarkOptions { | ||
| onSuccess?: (bookmarkId: number) => void; | ||
| onError?: (error: unknown) => void; | ||
| } | ||
|
|
||
| export const usePostBookmark = (options?: UsePostBookmarkOptions) => { | ||
| const queryClient = useQueryClient(); | ||
|
|
||
| return useMutation({ | ||
| mutationFn: (companyId: number) => postBookmark(companyId), | ||
| onSuccess: (bookmarkId: number) => { | ||
| queryClient.invalidateQueries({ | ||
| queryKey: meQueryKey.bookmarkCompanyLists(), | ||
| }); | ||
|
Comment on lines
+22
to
+24
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기서는 북마크 변경 이후 현재 직접적으로 갱신이 필요한 건 북마크 목록 쪽이라서, 가능하다면 이미 query key가 세분화되어 있어서, invalidate도 그 단위에 맞춰주면 불필요한 refetch를 줄이고 의도도 더 잘 드러날 것 같아요!
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 유진님 의견에 동의하는 입장이에요! |
||
| options?.onSuccess?.(bookmarkId); | ||
| }, | ||
| onError: options?.onError, | ||
| }); | ||
| }; | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { useQueryClient } from "@tanstack/react-query"; | ||
| import { useCallback, useEffect, useState } from "react"; | ||
|
|
||
| import { companyQueryKey } from "@/shared/api/config/query-key"; | ||
| import { isValidCustomError } from "@/shared/api/error-handler"; | ||
|
|
||
| import { useDeleteBookmark } from "../api/use-delete-bookmark.mutation"; | ||
| import { usePostBookmark } from "../api/use-post-bookmark.mutation"; | ||
| import { useBookmarkStore } from "../store/bookmark.store"; | ||
|
|
||
| import type { GetCompanyResponseDto } from "@/shared/api/generate/http-client"; | ||
|
|
||
| interface UseCompanyBookmarkParams { | ||
| companyId: number; | ||
| initialIsBookmarked: boolean; | ||
| } | ||
|
|
||
| const isDuplicateBookmarkError = (error: unknown) => | ||
| isValidCustomError(error) && | ||
| error.response.status === 400 && | ||
| error.response.data.prefix.startsWith("BOOKMARK_"); | ||
|
|
||
| const updateCompanyBookmarkCache = ( | ||
| previousData: GetCompanyResponseDto | undefined, | ||
| isBookmarked: boolean | ||
| ) => { | ||
| if (!previousData) { | ||
| return previousData; | ||
| } | ||
|
|
||
| return { | ||
| ...previousData, | ||
| isLiked: isBookmarked, | ||
| }; | ||
| }; | ||
|
|
||
| export const useCompanyBookmark = ({ | ||
| companyId, | ||
| initialIsBookmarked, | ||
| }: UseCompanyBookmarkParams) => { | ||
| const queryClient = useQueryClient(); | ||
| const [isBookmarkErrorOpen, setIsBookmarkErrorOpen] = useState(false); | ||
| const bookmarkOverride = useBookmarkStore( | ||
| (state) => state.bookmarkOverrides[companyId] | ||
| ); | ||
| const setBookmarkOverride = useBookmarkStore( | ||
| (state) => state.setBookmarkOverride | ||
| ); | ||
|
|
||
| const isBookmarked = bookmarkOverride ?? initialIsBookmarked; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| useEffect(() => { | ||
| if (bookmarkOverride === undefined && initialIsBookmarked) { | ||
| setBookmarkOverride(companyId, true); | ||
| } | ||
| }, [bookmarkOverride, companyId, initialIsBookmarked, setBookmarkOverride]); | ||
|
|
||
| const updateDetailQuery = useCallback( | ||
| (nextIsBookmarked: boolean) => { | ||
| queryClient.setQueryData<GetCompanyResponseDto | undefined>( | ||
| companyQueryKey.detail(companyId), | ||
| (previousData) => | ||
| updateCompanyBookmarkCache(previousData, nextIsBookmarked) | ||
| ); | ||
| }, | ||
| [companyId, queryClient] | ||
| ); | ||
|
|
||
| const { mutate: addBookmark, isPending: isAddingBookmark } = usePostBookmark({ | ||
| onSuccess: () => { | ||
| setBookmarkOverride(companyId, true); | ||
| updateDetailQuery(true); | ||
| }, | ||
| onError: (error) => { | ||
| if (isDuplicateBookmarkError(error)) { | ||
| setBookmarkOverride(companyId, true); | ||
| updateDetailQuery(true); | ||
| return; | ||
| } | ||
|
|
||
| setBookmarkOverride(companyId, false); | ||
| setIsBookmarkErrorOpen(true); | ||
| }, | ||
| }); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const { mutate: removeBookmark, isPending: isRemovingBookmark } = | ||
| useDeleteBookmark({ | ||
| onSuccess: () => { | ||
| setBookmarkOverride(companyId, false); | ||
| updateDetailQuery(false); | ||
| queryClient.invalidateQueries({ | ||
| queryKey: companyQueryKey.detail(companyId), | ||
| }); | ||
| }, | ||
| onError: () => { | ||
| setBookmarkOverride(companyId, true); | ||
| setIsBookmarkErrorOpen(true); | ||
| }, | ||
| }); | ||
|
|
||
| const isBookmarkPending = isAddingBookmark || isRemovingBookmark; | ||
|
|
||
| const handleBookmarkClick = useCallback(() => { | ||
| if (isBookmarkPending) { | ||
| return; | ||
| } | ||
|
|
||
| if (isBookmarked) { | ||
| setBookmarkOverride(companyId, false); | ||
| removeBookmark(companyId); | ||
| return; | ||
| } | ||
|
|
||
| setBookmarkOverride(companyId, true); | ||
| addBookmark(companyId); | ||
| }, [ | ||
| addBookmark, | ||
| companyId, | ||
| isBookmarked, | ||
| isBookmarkPending, | ||
| removeBookmark, | ||
| setBookmarkOverride, | ||
| ]); | ||
|
|
||
| const closeBookmarkError = useCallback(() => { | ||
| setIsBookmarkErrorOpen(false); | ||
| }, []); | ||
|
|
||
| return { | ||
| isBookmarked, | ||
| isBookmarkPending, | ||
| isBookmarkErrorOpen, | ||
| handleBookmarkClick, | ||
| closeBookmarkError, | ||
| }; | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
기본 정렬값
"LATEST"는 상수로 통합해 중복을 제거하는 것을 권장합니다.동일 리터럴이 여러 곳에 있어 이후 변경 시 누락 가능성이 있습니다. 상수화하면 유지보수가 쉬워집니다.
♻️ 제안 수정
As per coding guidelines, Use UPPER_SNAKE_CASE for constants (e.g.,
VITE_API_KEY,ROTATE_DELAY).Also applies to: 52-53, 76-77
🤖 Prompt for AI Agents