Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2e052b7
design: 기업 상세 페이지 디자인 수정 및 북마크 UI 추가 (#161)
qowjdals23 Apr 3, 2026
6e42803
Merge branch 'dev' of https://github.com/TEAM-COMFIT/COMFIT-CLIENT in…
qowjdals23 Apr 4, 2026
7c96345
chore: swagger http-client 업데이트 (#161)
qowjdals23 Apr 4, 2026
519e85b
feat: 기업 북마크 조회 API 연동 (#161)
qowjdals23 Apr 4, 2026
b17c745
feat: 기업 북마크 추가 API 연동 (#161)
qowjdals23 Apr 4, 2026
0d7f908
feat: 기업 북마크 삭제 API 연동 (#161)
qowjdals23 Apr 4, 2026
836a02b
fix: 북마크 중복 요청 시 상세 상태 동기화 보정 (#161)
qowjdals23 Apr 4, 2026
1922bf7
refactor: 기업 상세 섹션 구조 정리 및 스타일 중복 제거 (#161)
qowjdals23 Apr 4, 2026
adb109c
fix: 온보딩 페이지 타입 에러 수정 (#161)
qowjdals23 Apr 4, 2026
ddc1adc
fix: 북마크 기업 응답 가드 및 invalid row 필터링 (#161)
qowjdals23 Apr 4, 2026
b622deb
fix: 북마크 중복 에러 prefix 기반 판별 및 상세 재조회 최적화 (#161)
qowjdals23 Apr 4, 2026
497f6fe
fix: 북마크 override 세션 영속화 제거 (#161)
qowjdals23 Apr 4, 2026
5011401
fix: 북마크 에러 문구 수정 (#161)
qowjdals23 Apr 4, 2026
ae1e8fc
fix: 북마크 성공 후 override 상태 정리 (#161)
qowjdals23 Apr 4, 2026
ae7dc87
refactor: 북마크 쿼리 invalidate 범위 축소 (#161)
qowjdals23 Apr 5, 2026
117cd29
refactor: 북마크 페이지 불필요한 모달 상태 제거 (#161)
qowjdals23 Apr 5, 2026
458db14
refactor: 북마크 캐시 갱신 네이밍 통일 (#161)
qowjdals23 Apr 5, 2026
13af087
refactor: 북마크 목록 상태 query data 기반 정리 및 페이지 크기 기준 주석 추가 (#161)
qowjdals23 Apr 5, 2026
5516abe
fix: 기업 상세 북마크 상태 유지 (#161)
qowjdals23 Apr 5, 2026
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
28 changes: 28 additions & 0 deletions src/features/bookmark/api/use-delete-bookmark.mutation.ts
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,
});
};
86 changes: 86 additions & 0 deletions src/features/bookmark/api/use-get-bookmark-companies.query.ts
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";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

기본 정렬값 "LATEST"는 상수로 통합해 중복을 제거하는 것을 권장합니다.

동일 리터럴이 여러 곳에 있어 이후 변경 시 누락 가능성이 있습니다. 상수화하면 유지보수가 쉬워집니다.

♻️ 제안 수정
 export type BookmarkCompanySort = "NAME" | "LIKE" | "LATEST" | "OLDEST";
+const DEFAULT_BOOKMARK_COMPANY_SORT: BookmarkCompanySort = "LATEST";
@@
 export const getBookmarkCompanies = async (
   page: number,
-  sort: BookmarkCompanySort = "LATEST"
+  sort: BookmarkCompanySort = DEFAULT_BOOKMARK_COMPANY_SORT
 ): Promise<BookmarkCompaniesResponse> => {
@@
 export const useGetBookmarkCompaniesQuery = (
   page: number,
-  sort: BookmarkCompanySort = "LATEST"
+  sort: BookmarkCompanySort = DEFAULT_BOOKMARK_COMPANY_SORT
 ) => {

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
Verify each finding against the current code and only fix it if needed.

In `@src/features/bookmark/api/use-get-bookmark-companies.query.ts` at line 8, The
literal default sort "LATEST" is duplicated; define a single UPPER_SNAKE_CASE
constant (e.g., BOOKMARK_COMPANY_DEFAULT_SORT = "LATEST") and replace all
occurrences of the string literal in this file (references related to the
BookmarkCompanySort type, default parameter values, and any
comparisons/assignments currently using "LATEST") with that constant to remove
duplication and follow the constant naming guideline.


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,
});
};
29 changes: 29 additions & 0 deletions src/features/bookmark/api/use-post-bookmark.mutation.ts
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
Copy link
Copy Markdown
Collaborator

@u-zzn u-zzn Apr 4, 2026

Choose a reason for hiding this comment

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

여기서는 북마크 변경 이후 meQueryKey.all() 전체를 invalidate하고 있는데, 영향이 미치는 범위에 비해 invalidate 범위가 조금 넓은 것 같아요 🥹

현재 직접적으로 갱신이 필요한 건 북마크 목록 쪽이라서, 가능하다면 bookmarkCompanyList 관련 query만 invalidate하는 쪽이 더 좋을 것 같은데, 어떻게 생각하시나요? :)

이미 query key가 세분화되어 있어서, invalidate도 그 단위에 맞춰주면 불필요한 refetch를 줄이고 의도도 더 잘 드러날 것 같아요!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

유진님 의견에 동의하는 입장이에요!
현재 query-key.ts 파일에는 북마크를 위한 별도의 키를 설정해주셨기 때문에, 해당 계층 키를 추가하여 적용하면 좋을 것 같습니다! (delete api에도!!)

options?.onSuccess?.(bookmarkId);
},
onError: options?.onError,
});
};
101 changes: 0 additions & 101 deletions src/features/bookmark/config/bookmark-page.constants.ts

This file was deleted.

11 changes: 6 additions & 5 deletions src/features/bookmark/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export {
BOOKMARK_MOCK_ROWS,
BOOKMARK_PAGE_SIZE,
} from "./config/bookmark-page.constants";
export type { BookmarkRow } from "./config/bookmark-page.constants";
export type { BookmarkRow } from "./types/bookmark.type";
export { useDeleteBookmark } from "./api/use-delete-bookmark.mutation";
export { useGetBookmarkCompaniesQuery } from "./api/use-get-bookmark-companies.query";
export { usePostBookmark } from "./api/use-post-bookmark.mutation";
export { useCompanyBookmark } from "./model/use-company-bookmark";
export { useBookmarkStore } from "./store/bookmark.store";

export { BookmarkCheckbox } from "./ui/bookmark-checkbox";
export { BookmarkEmptyState } from "./ui/bookmark-empty-state";
Expand Down
136 changes: 136 additions & 0 deletions src/features/bookmark/model/use-company-bookmark.ts
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;

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);
},
});

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,
};
};
Loading
Loading