From 2e052b75c2e96d0f5819f53c47bf65e789fa07c5 Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sat, 4 Apr 2026 00:34:26 +0900 Subject: [PATCH 01/18] =?UTF-8?q?design:=20=EA=B8=B0=EC=97=85=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=B6=81=EB=A7=88?= =?UTF-8?q?=ED=81=AC=20UI=20=EC=B6=94=EA=B0=80=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/company-detail-section.css.ts | 78 ++++++++++++++++-- .../ui/company-detail-section.tsx | 81 ++++++++++++------- src/shared/assets/icons/icon_bookmark.svg | 3 + src/shared/assets/icons/index.ts | 1 + 4 files changed, 128 insertions(+), 35 deletions(-) create mode 100644 src/shared/assets/icons/icon_bookmark.svg diff --git a/src/pages/company-detail/ui/company-detail-section.css.ts b/src/pages/company-detail/ui/company-detail-section.css.ts index b0ec81b4..df2f2a55 100644 --- a/src/pages/company-detail/ui/company-detail-section.css.ts +++ b/src/pages/company-detail/ui/company-detail-section.css.ts @@ -1,4 +1,5 @@ -import { style } from "@vanilla-extract/css"; +import { globalStyle, style } from "@vanilla-extract/css"; +import { recipe } from "@vanilla-extract/recipes"; import { themeVars } from "@/app/styles"; @@ -21,7 +22,7 @@ export const header = style({ export const headerLeft = style({ display: "flex", alignItems: "center", - gap: "2rem", + gap: "2.4rem", minWidth: 0, }); @@ -39,14 +40,14 @@ export const logo = style({ export const headerMeta = style({ display: "flex", flexDirection: "column", - gap: "1.2rem", + gap: "0.8rem", minWidth: 0, }); -export const nameRow = style({ +export const titleRow = style({ display: "flex", alignItems: "center", - gap: "0.8rem", + gap: "0.4rem", minWidth: 0, }); @@ -58,17 +59,68 @@ export const companyName = style({ textOverflow: "ellipsis", }); +export const bookmarkButton = style({ + width: "4.4rem", + height: "4.4rem", + padding: 0, + border: "none", + backgroundColor: "transparent", + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + color: themeVars.color.black, + cursor: "pointer", + flexShrink: 0, +}); + +export const bookmarkIcon = recipe({ + base: { + width: "4.4rem", + height: "4.4rem", + display: "block", + color: themeVars.color.black, + flexShrink: 0, + }, + variants: { + active: { + true: {}, + false: {}, + }, + }, + defaultVariants: { + active: false, + }, +}); + +globalStyle(`${bookmarkIcon.classNames.base} path`, { + stroke: "currentColor", +}); + +globalStyle(`${bookmarkIcon.classNames.variants.active.true} path`, { + fill: "currentColor", +}); + +globalStyle(`${bookmarkIcon.classNames.variants.active.false} path`, { + fill: "transparent", +}); + +export const statusRow = style({ + display: "flex", + alignItems: "center", + gap: "0.8rem", +}); + export const dot = style({ width: "0.4rem", height: "0.4rem", borderRadius: "999px", - backgroundColor: themeVars.color.gray800, + backgroundColor: themeVars.color.black, display: "inline-block", flexShrink: 0, }); export const hireStatus = style({ - color: themeVars.color.gray800, + color: themeVars.color.black, ...themeVars.fontStyles.body_m_16, flexShrink: 0, }); @@ -89,6 +141,16 @@ export const sectionBase = style({ flexDirection: "column", }); +export const keywordSection = style({ + marginTop: "6rem", + gap: "1.6rem", +}); + +export const keywordTitle = style({ + color: themeVars.color.black, + ...themeVars.fontStyles.hline_b_18, +}); + export const sectionTitleRow = style({ display: "flex", width: "88.3rem", @@ -110,7 +172,7 @@ export const sectionTitle = style({ }); export const summarySection = style({ - marginTop: "8rem", + marginTop: "5.2rem", gap: "1.6rem", }); diff --git a/src/pages/company-detail/ui/company-detail-section.tsx b/src/pages/company-detail/ui/company-detail-section.tsx index 107a715b..4581d681 100644 --- a/src/pages/company-detail/ui/company-detail-section.tsx +++ b/src/pages/company-detail/ui/company-detail-section.tsx @@ -1,13 +1,16 @@ +import { useState } from "react"; + import { CompanyCtaBanner, CompanyIssue, CompanyLinkButton, } from "@/features/company-detail"; import { + IconBookmark, IconIdeal, IconIssue, IconSummary, -} from "@/shared/assets/icons/index.ts"; +} from "@/shared/assets/icons"; import { getIndustryLabel, getScaleLabel, @@ -42,7 +45,21 @@ interface CompanyDetailSectionProps { companyData: CompanyDetailSummary; } +const getSectionClassName = (sectionStyle: string) => + [styles.sectionBase, sectionStyle].join(" "); + const CompanyDetailSection = ({ companyData }: CompanyDetailSectionProps) => { + const [isBookmarked, setIsBookmarked] = useState(false); + + const keywordTags = [ + companyData.industry ? `#${getIndustryLabel(companyData.industry)}` : null, + companyData.scale ? `#${getScaleLabel(companyData.scale)}` : null, + ].filter((keyword): keyword is string => keyword !== null); + + const handleBookmarkClick = () => { + setIsBookmarked((prev) => !prev); + }; + return (
@@ -54,40 +71,50 @@ const CompanyDetailSection = ({ companyData }: CompanyDetailSectionProps) => { />
-
+

{companyData.name}

- {companyData.isRecruiting ? ( - <> -
-
- {companyData.industry ? ( - - #{getIndustryLabel(companyData.industry)} - - ) : null} - {companyData.scale ? ( - #{getScaleLabel(companyData.scale)} - ) : null} -
+ {companyData.isRecruiting ? ( +
+
+ ) : null}
- + +
+
+ +
+

기업 관련 키워드

+ +
+ {keywordTags.map((keywordTag) => ( + + {keywordTag} + + ))}
-
+
{
-
+
{
-
+
+ + diff --git a/src/shared/assets/icons/index.ts b/src/shared/assets/icons/index.ts index 02e4b8d4..5e7cd572 100644 --- a/src/shared/assets/icons/index.ts +++ b/src/shared/assets/icons/index.ts @@ -53,6 +53,7 @@ export { default as IconJob } from "./icon_job.svg?react"; export { default as IconCopy } from "./icon_copy.svg?react"; export { default as IconCheckOn } from "./icon_check_on.svg?react"; +export { default as IconBookmark } from "./icon_bookmark.svg?react"; export { default as IconPen } from "./icon_pen.svg?react"; export { default as IconWarn } from "./icon_warning.svg?react"; From 7c96345de8223a8f4803f11ab7163a7a67d4df68 Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sat, 4 Apr 2026 17:51:43 +0900 Subject: [PATCH 02/18] =?UTF-8?q?chore:=20swagger=20http-client=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/api/use-get-companies.query.ts | 4 +- src/shared/api/generate/http-client.ts | 258 ++++++++++-------- 2 files changed, 152 insertions(+), 110 deletions(-) diff --git a/src/features/home/api/use-get-companies.query.ts b/src/features/home/api/use-get-companies.query.ts index 738c030d..8c4a6294 100644 --- a/src/features/home/api/use-get-companies.query.ts +++ b/src/features/home/api/use-get-companies.query.ts @@ -16,8 +16,8 @@ const getCompanies = async ({ const response = await api.companies.getCompanyList( { keyword, - industry, - scale, + industry: industry ? [industry] : undefined, + scale: scale ? [scale] : undefined, sort, page, isRecruited, diff --git a/src/shared/api/generate/http-client.ts b/src/shared/api/generate/http-client.ts index 9e83ec38..e17cd05c 100644 --- a/src/shared/api/generate/http-client.ts +++ b/src/shared/api/generate/http-client.ts @@ -10,12 +10,7 @@ * --------------------------------------------------------------- */ -export interface ReIssueTokenRequestDto { - /** @example "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." */ - refreshToken: string; -} - -export interface ReIssueTokenResponseDto { +export interface AccessTokenResponseDto { accessToken?: string; } @@ -28,88 +23,19 @@ export interface CustomErrorResponse { export interface OnBoardingRequestDTO { /** @example "HIGH_SCHOOL" */ - educationLevel: - | "HIGH_SCHOOL" - | "BACHELOR_STUDENT" - | "BACHELOR" - | "MASTER_STUDENT" - | undefined; + educationLevel: string; /** @example "IT" */ - firstIndustry: - | "CONSUMER_GOODS" - | "IT" - | "MEDIA_CONTENTS" - | "RETAIL" - | "LIFESTYLE" - | "FOOD" - | "TRAVEL" - | "FINANCE" - | "FITNESS" - | undefined; + firstIndustry: string; /** @example "MEDIA_CONTENTS" */ - secondIndustry?: - | "CONSUMER_GOODS" - | "IT" - | "MEDIA_CONTENTS" - | "RETAIL" - | "LIFESTYLE" - | "FOOD" - | "TRAVEL" - | "FINANCE" - | "FITNESS" - | undefined; + secondIndustry?: string; /** @example "RETAIL" */ - thirdIndustry?: - | "CONSUMER_GOODS" - | "IT" - | "MEDIA_CONTENTS" - | "RETAIL" - | "LIFESTYLE" - | "FOOD" - | "TRAVEL" - | "FINANCE" - | "FITNESS" - | undefined; + thirdIndustry?: string; /** @example "MARKETING_STRATEGY" */ - firstJob: - | "MARKETING_STRATEGY" - | "BRAND_MARKETING" - | "DIGITAL_MARKETING" - | "CONTENT_MARKETING" - | "VIRAL_MARKETING" - | "PERFORMANCE_MARKETING" - | "B2B_MARKETING" - | "CRM_MARKETING" - | "PRODUCT_MARKETING" - | "PARTNERSHIP_MARKETING" - | "GLOBAL_MARKETING" - | undefined; + firstJob: string; /** @example "BRAND_MARKETING" */ - secondJob?: - | "MARKETING_STRATEGY" - | "BRAND_MARKETING" - | "DIGITAL_MARKETING" - | "CONTENT_MARKETING" - | "VIRAL_MARKETING" - | "PERFORMANCE_MARKETING" - | "B2B_MARKETING" - | "CRM_MARKETING" - | "PRODUCT_MARKETING" - | "PARTNERSHIP_MARKETING" - | "GLOBAL_MARKETING"; + secondJob?: string; /** @example "DIGITAL_MARKETING" */ - thirdJob?: - | "MARKETING_STRATEGY" - | "BRAND_MARKETING" - | "DIGITAL_MARKETING" - | "CONTENT_MARKETING" - | "VIRAL_MARKETING" - | "PERFORMANCE_MARKETING" - | "B2B_MARKETING" - | "CRM_MARKETING" - | "PRODUCT_MARKETING" - | "PARTNERSHIP_MARKETING" - | "GLOBAL_MARKETING"; + thirdJob?: string; /** * @format int64 * @example 1 @@ -194,24 +120,20 @@ export interface MatchExperienceRequestDto { */ experienceId: number; /** - * @example "[직무 설명 (JD 원문)] - * - * CJ ENM 엔터테인먼트부문은 - * 콘텐츠 기획 및 운영 전반을 담당할 인재를 모집합니다. - * - * 주요 업무 - * - 콘텐츠 기획 및 운영 업무 지원 - * - 디지털 콘텐츠 성과 분석 및 인사이트 도출 - * - 유관 부서 및 외부 파트너와의 협업 - * + * @example "업무 내용 + * 고객센터의 각 채널 (Call Chat Mail App) 로 유입되는 고객 문의 운영 , 관리 + * VOC , Inquiry 분석 및 개선 + * 신규사업, 마케팅, 이벤트 관련 고객 서비스 운영 지원 * 자격 요건 - * - 콘텐츠 및 엔터테인먼트 산업에 대한 관심 - * - 데이터 기반으로 문제를 분석하고 개선안을 도출한 경험 - * - 원활한 커뮤니케이션 및 협업 능력 - * - * 우대 사항 - * - 디지털 콘텐츠 또는 마케팅 관련 프로젝트 경험 - * - 글로벌 콘텐츠 트렌드에 대한 이해" + * 3년 이상의 유관업무 경력이 있는 분 또는 프로세스 수립/개선 업무 경험이 있는분 + * 유연한 사고와 원활한 커뮤니케이션 능력이 있는 분 + * 주말 스케줄 근무 가능하신 분 - 1~2개월마다 1회 주말 근무 (주말근무시 : 11:00~20:00(휴게포함) + * 우대사항 + * 온라인 커머스 또는 배달서비스 비즈니스에 대한 이해도가 있으신 분 + * 고객 중심의 서비스 마인드 보유하신 분 + * 변화에 빠르게 적응 가능하신 분 + * 일본어 가능하신 분 + * " */ jobDescription: string; } @@ -221,6 +143,10 @@ export interface AIReportResponseDto { id?: number; companyName?: string; experienceTitle?: string; + situation?: string; + task?: string; + action?: string; + result?: string; jobDescription?: string; perspectives?: Perspective[]; density?: Density[]; @@ -382,7 +308,7 @@ export interface GetReportCompanyResponseDto { logo?: string; } -export type ReissueTokenData = ReIssueTokenResponseDto; +export type ReissueTokenData = AccessTokenResponseDto; export type AddUserInfoData = CommonApiResponse; @@ -402,8 +328,19 @@ export type CreateExperienceData = number; export type GetReportListData = PageDto; +export type MatchExperienceVirtualThreadData = AIReportResponseDto; + export type MatchExperienceData = AIReportResponseDto; +export type MatchAsyncData = AIReportResponseDto; + +export type MatchAsyncWebClientData = AIReportResponseDto; + +export type MatchExperienceWebfluxParallelData = AIReportResponseDto; + +/** @format int64 */ +export type MatchExperienceJobData = number; + export type GetExperienceData = GetExperienceResponseDto; export type DeleteExperienceData = CommonApiResponse; @@ -647,12 +584,10 @@ export class Api< * @summary 액세스 토큰 재발급 * @request POST:/api/v1/re-issued */ - reissueToken: (data: ReIssueTokenRequestDto, params: RequestParams = {}) => + reissueToken: (params: RequestParams = {}) => this.request({ path: `/api/v1/re-issued`, method: "POST", - body: data, - type: ContentType.Json, format: "json", ...params, }), @@ -964,16 +899,16 @@ export class Api< * @description AI 리포트 생성 API입니다 * * @tags AI-Report - * @name MatchExperience + * @name MatchExperienceVirtualThread * @summary AI 리포트 생성 API * @request POST:/api/v1/ai-reports * @secure */ - matchExperience: ( + matchExperienceVirtualThread: ( data: MatchExperienceRequestDto, params: RequestParams = {} ) => - this.request({ + this.request({ path: `/api/v1/ai-reports`, method: "POST", body: data, @@ -983,6 +918,113 @@ export class Api< ...params, }), + /** + * No description + * + * @tags AI-Report + * @name MatchExperience + * @request POST:/api/v1/ai-reports/match/sync + * @secure + */ + matchExperience: ( + data: MatchExperienceRequestDto, + params: RequestParams = {} + ) => + this.request({ + path: `/api/v1/ai-reports/match/sync`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags AI-Report + * @name MatchAsync + * @request POST:/api/v1/ai-reports/match/async + * @secure + */ + matchAsync: (data: MatchExperienceRequestDto, params: RequestParams = {}) => + this.request({ + path: `/api/v1/ai-reports/match/async`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags AI-Report + * @name MatchAsyncWebClient + * @request POST:/api/v1/ai-reports/match/async/webclient + * @secure + */ + matchAsyncWebClient: ( + data: MatchExperienceRequestDto, + params: RequestParams = {} + ) => + this.request({ + path: `/api/v1/ai-reports/match/async/webclient`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags AI-Report + * @name MatchExperienceWebfluxParallel + * @request POST:/api/v1/ai-reports/match/async/parallel + * @secure + */ + matchExperienceWebfluxParallel: ( + data: MatchExperienceRequestDto, + params: RequestParams = {} + ) => + this.request({ + path: `/api/v1/ai-reports/match/async/parallel`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + + /** + * No description + * + * @tags AI-Report + * @name MatchExperienceJob + * @request POST:/api/v1/ai-reports/match/async/jobs + * @secure + */ + matchExperienceJob: ( + data: MatchExperienceRequestDto, + params: RequestParams = {} + ) => + this.request({ + path: `/api/v1/ai-reports/match/async/jobs`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params, + }), + /** * @description Report 단일 조회 API입니다 * @@ -1098,8 +1140,8 @@ export class Api< getCompanyList: ( query?: { keyword?: string; - industry?: string; - scale?: string; + industry?: string[]; + scale?: string[]; sort?: string; /** * @format int32 From 519e85be4ef32f97e6513044dd1f006c0974bc56 Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sat, 4 Apr 2026 18:01:42 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20=EA=B8=B0=EC=97=85=20=EB=B6=81?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EC=A1=B0=ED=9A=8C=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/use-get-bookmark-companies.query.ts | 62 +++++++++++ .../config/bookmark-page.constants.ts | 101 ------------------ src/features/bookmark/index.ts | 7 +- src/features/bookmark/types/bookmark.type.ts | 7 ++ src/features/bookmark/ui/bookmark-table.tsx | 4 +- src/pages/bookmark/bookmark-page.tsx | 35 ++++-- src/shared/api/config/query-key.ts | 4 + 7 files changed, 104 insertions(+), 116 deletions(-) create mode 100644 src/features/bookmark/api/use-get-bookmark-companies.query.ts delete mode 100644 src/features/bookmark/config/bookmark-page.constants.ts create mode 100644 src/features/bookmark/types/bookmark.type.ts diff --git a/src/features/bookmark/api/use-get-bookmark-companies.query.ts b/src/features/bookmark/api/use-get-bookmark-companies.query.ts new file mode 100644 index 00000000..586c1321 --- /dev/null +++ b/src/features/bookmark/api/use-get-bookmark-companies.query.ts @@ -0,0 +1,62 @@ +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; +} + +export const getBookmarkCompanies = async ( + page: number, + sort: BookmarkCompanySort = "LATEST" +): Promise => { + const response = await api.me.getBookmarkCompany({ page, sort }); + const result = response.result as unknown as BookmarkCompaniesApiResponse; + + return { + content: (result.content ?? []).map((bookmarkCompany) => ({ + id: bookmarkCompany.id ?? 0, + companyId: bookmarkCompany.companyId ?? 0, + 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, + }); +}; diff --git a/src/features/bookmark/config/bookmark-page.constants.ts b/src/features/bookmark/config/bookmark-page.constants.ts deleted file mode 100644 index 09bb7a09..00000000 --- a/src/features/bookmark/config/bookmark-page.constants.ts +++ /dev/null @@ -1,101 +0,0 @@ -export interface BookmarkRow { - id: number; - companyName: string; - scrapedAt: string; - isConnected: boolean; -} - -export const BOOKMARK_PAGE_SIZE = 4; - -export const BOOKMARK_MOCK_ROWS: BookmarkRow[] = [ - { - id: 1, - companyName: "쿠팡", - scrapedAt: "2025-11-21", - isConnected: true, - }, - { - id: 2, - companyName: "SK네트웍스서비스", - scrapedAt: "2025-11-20", - isConnected: false, - }, - { - id: 3, - companyName: "레진엔터테인먼트", - scrapedAt: "2025-11-19", - isConnected: false, - }, - { - id: 4, - companyName: "컴퓨존", - scrapedAt: "2025-11-19", - isConnected: false, - }, - { - id: 5, - companyName: "CJ ENM", - scrapedAt: "2025-11-18", - isConnected: true, - }, - { - id: 6, - companyName: "삼성전자", - scrapedAt: "2025-11-16", - isConnected: false, - }, - { - id: 7, - companyName: "네이버", - scrapedAt: "2025-11-15", - isConnected: true, - }, - { - id: 8, - companyName: "카카오", - scrapedAt: "2025-11-14", - isConnected: false, - }, - { - id: 9, - companyName: "우아한형제들", - scrapedAt: "2025-11-12", - isConnected: true, - }, - { - id: 10, - companyName: "토스", - scrapedAt: "2025-11-11", - isConnected: true, - }, - { - id: 11, - companyName: "라인플러스", - scrapedAt: "2025-11-10", - isConnected: false, - }, - { - id: 12, - companyName: "당근", - scrapedAt: "2025-11-09", - isConnected: true, - }, - { - id: 13, - companyName: "현대자동차", - scrapedAt: "2025-11-08", - isConnected: true, - }, - { - id: 14, - companyName: "LG전자", - scrapedAt: "2025-11-07", - isConnected: false, - }, - { - id: 15, - companyName: "포스코", - scrapedAt: "2025-11-05", - isConnected: true, - }, -]; diff --git a/src/features/bookmark/index.ts b/src/features/bookmark/index.ts index 5025f047..c2a92a0d 100644 --- a/src/features/bookmark/index.ts +++ b/src/features/bookmark/index.ts @@ -1,8 +1,5 @@ -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 { useGetBookmarkCompaniesQuery } from "./api/use-get-bookmark-companies.query"; export { BookmarkCheckbox } from "./ui/bookmark-checkbox"; export { BookmarkEmptyState } from "./ui/bookmark-empty-state"; diff --git a/src/features/bookmark/types/bookmark.type.ts b/src/features/bookmark/types/bookmark.type.ts new file mode 100644 index 00000000..136c8fa5 --- /dev/null +++ b/src/features/bookmark/types/bookmark.type.ts @@ -0,0 +1,7 @@ +export interface BookmarkRow { + id: number; + companyId: number; + companyName: string; + scrapedAt: string; + isConnected: boolean; +} diff --git a/src/features/bookmark/ui/bookmark-table.tsx b/src/features/bookmark/ui/bookmark-table.tsx index 88d5aa00..5d5755eb 100644 --- a/src/features/bookmark/ui/bookmark-table.tsx +++ b/src/features/bookmark/ui/bookmark-table.tsx @@ -1,7 +1,7 @@ import { BookmarkCheckbox } from "./bookmark-checkbox"; import * as styles from "./bookmark-table.css"; -import type { BookmarkRow } from "../config/bookmark-page.constants"; +import type { BookmarkRow } from "../types/bookmark.type"; interface BookmarkTableProps { rows: BookmarkRow[]; @@ -61,7 +61,7 @@ const BookmarkTable = ({ diff --git a/src/pages/bookmark/bookmark-page.tsx b/src/pages/bookmark/bookmark-page.tsx index 90b33980..b29c91bf 100644 --- a/src/pages/bookmark/bookmark-page.tsx +++ b/src/pages/bookmark/bookmark-page.tsx @@ -3,10 +3,9 @@ import { useNavigate, useSearchParams } from "react-router-dom"; import { ROUTES } from "@/app/routes/paths"; import { - BOOKMARK_MOCK_ROWS, - BOOKMARK_PAGE_SIZE, BookmarkEmptyState, BookmarkTable, + useGetBookmarkCompaniesQuery, } from "@/features/bookmark"; import { IconBookmarkBefore, IconTrash } from "@/shared/assets/icons"; import { modalStore } from "@/shared/model/store"; @@ -14,15 +13,17 @@ import { Button, ModalBasic, Pagination, Search } from "@/shared/ui"; import * as styles from "./bookmark-page.css"; +import type { BookmarkRow } from "@/features/bookmark"; + const BOOKMARK_QUERY_KEY = "keyword"; const BOOKMARK_PAGE_QUERY_KEY = "page"; const BOOKMARK_DELETE_MODAL_ID = "bookmark-delete-modal"; +const BOOKMARK_PAGE_SIZE = 4; const BookmarkPage = () => { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); - - const [rows, setRows] = useState(BOOKMARK_MOCK_ROWS); + const [rows, setRows] = useState([]); const [selectedIds, setSelectedIds] = useState>(new Set()); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -32,12 +33,22 @@ const BookmarkPage = () => { Number.isInteger(currentPageParam) && currentPageParam > 0 ? currentPageParam : 1; + + const { + data: bookmarkCompanies, + isLoading, + isFetching, + } = useGetBookmarkCompaniesQuery(currentPage); const [searchInput, setSearchInput] = useState(keyword); useEffect(() => { setSearchInput(keyword); }, [keyword]); + useEffect(() => { + setRows(bookmarkCompanies?.content ?? []); + }, [bookmarkCompanies?.content]); + useEffect(() => { const unsubscribe = modalStore.subscribe((modals) => { setIsDeleteModalOpen( @@ -49,7 +60,9 @@ const BookmarkPage = () => { }, []); const filteredRows = useMemo(() => { - if (!keyword) return rows; + if (!keyword) { + return rows; + } const normalizedKeyword = keyword.toLowerCase(); return rows.filter((row) => @@ -57,14 +70,20 @@ const BookmarkPage = () => { ); }, [keyword, rows]); - const totalPage = Math.ceil(filteredRows.length / BOOKMARK_PAGE_SIZE); + const totalPage = keyword + ? Math.ceil(filteredRows.length / BOOKMARK_PAGE_SIZE) + : (bookmarkCompanies?.totalPage ?? 0); const paginationTotalPage = Math.max(totalPage, 1); const resolvedCurrentPage = Math.min(currentPage, paginationTotalPage); const currentPageRows = useMemo(() => { + if (!keyword) { + return rows; + } + const startIndex = (resolvedCurrentPage - 1) * BOOKMARK_PAGE_SIZE; return filteredRows.slice(startIndex, startIndex + BOOKMARK_PAGE_SIZE); - }, [filteredRows, resolvedCurrentPage]); + }, [filteredRows, keyword, resolvedCurrentPage, rows]); const visibleIds = useMemo( () => currentPageRows.map((row) => row.id), @@ -75,7 +94,7 @@ const BookmarkPage = () => { visibleIds.length > 0 && visibleIds.every((id) => selectedIds.has(id)); const isDeleteDisabled = selectedIds.size === 0; - const isBookmarkEmpty = rows.length === 0; + const isBookmarkEmpty = !isLoading && !isFetching && rows.length === 0; const isSearchResultEmpty = rows.length > 0 && filteredRows.length === 0; const showPagination = !isBookmarkEmpty && !isSearchResultEmpty; diff --git a/src/shared/api/config/query-key.ts b/src/shared/api/config/query-key.ts index 3fb99be4..0fd82611 100644 --- a/src/shared/api/config/query-key.ts +++ b/src/shared/api/config/query-key.ts @@ -21,6 +21,10 @@ export const meQueryKey = { all: () => ["me"], profile: () => [...meQueryKey.all(), "profile"], // 사용자 프로필 조회 onboarding: () => [...meQueryKey.all(), "onboarding"], // 온보딩 상태 조회 + bookmarkCompanyList: ( + page: number, + sort: "NAME" | "LIKE" | "LATEST" | "OLDEST" + ) => [...meQueryKey.all(), "bookmark-company-list", page, sort], }; // AI-Report 관련 API (AI-Report) From b17c74530b9c7ecb66df15e38ccd11896d9695c2 Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sat, 4 Apr 2026 19:15:44 +0900 Subject: [PATCH 04/18] =?UTF-8?q?feat:=20=EA=B8=B0=EC=97=85=20=EB=B6=81?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EC=B6=94=EA=B0=80=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/use-get-bookmark-companies.query.ts | 3 + .../api/use-post-bookmark.mutation.ts | 29 +++++++ src/features/bookmark/index.ts | 2 + .../bookmark/model/use-company-bookmark.ts | 81 +++++++++++++++++++ .../api/use-get-company-detail.query.ts | 3 + .../company-detail/company-detail-page.tsx | 1 + .../ui/company-detail-section.tsx | 37 ++++++--- 7 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 src/features/bookmark/api/use-post-bookmark.mutation.ts create mode 100644 src/features/bookmark/model/use-company-bookmark.ts diff --git a/src/features/bookmark/api/use-get-bookmark-companies.query.ts b/src/features/bookmark/api/use-get-bookmark-companies.query.ts index 586c1321..d70dd221 100644 --- a/src/features/bookmark/api/use-get-bookmark-companies.query.ts +++ b/src/features/bookmark/api/use-get-bookmark-companies.query.ts @@ -58,5 +58,8 @@ export const useGetBookmarkCompaniesQuery = ( queryKey: meQueryKey.bookmarkCompanyList(page, sort), queryFn: () => getBookmarkCompanies(page, sort), enabled: Number.isFinite(page) && page > 0, + staleTime: 0, + refetchOnMount: "always", + refetchOnWindowFocus: true, }); }; diff --git a/src/features/bookmark/api/use-post-bookmark.mutation.ts b/src/features/bookmark/api/use-post-bookmark.mutation.ts new file mode 100644 index 00000000..2e29bda0 --- /dev/null +++ b/src/features/bookmark/api/use-post-bookmark.mutation.ts @@ -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.all(), + }); + options?.onSuccess?.(bookmarkId); + }, + onError: options?.onError, + }); +}; diff --git a/src/features/bookmark/index.ts b/src/features/bookmark/index.ts index c2a92a0d..1ee529c7 100644 --- a/src/features/bookmark/index.ts +++ b/src/features/bookmark/index.ts @@ -1,5 +1,7 @@ export type { BookmarkRow } from "./types/bookmark.type"; 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 { BookmarkCheckbox } from "./ui/bookmark-checkbox"; export { BookmarkEmptyState } from "./ui/bookmark-empty-state"; diff --git a/src/features/bookmark/model/use-company-bookmark.ts b/src/features/bookmark/model/use-company-bookmark.ts new file mode 100644 index 00000000..b5b6781e --- /dev/null +++ b/src/features/bookmark/model/use-company-bookmark.ts @@ -0,0 +1,81 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useCallback, useState } from "react"; + +import { companyQueryKey } from "@/shared/api/config/query-key"; + +import { usePostBookmark } from "../api/use-post-bookmark.mutation"; + +import type { GetCompanyResponseDto } from "@/shared/api/generate/http-client"; + +interface UseCompanyBookmarkParams { + companyId: number; + initialIsBookmarked: boolean; +} + +const updateCompanyBookmarkCache = ( + previousData: GetCompanyResponseDto | undefined, + isLiked: boolean +) => { + if (!previousData) { + return previousData; + } + + return { + ...previousData, + isLiked, + }; +}; + +export const useCompanyBookmark = ({ + companyId, + initialIsBookmarked, +}: UseCompanyBookmarkParams) => { + const queryClient = useQueryClient(); + const [localIsBookmarked, setLocalIsBookmarked] = useState( + null + ); + const [isBookmarkErrorOpen, setIsBookmarkErrorOpen] = useState(false); + + const isBookmarked = localIsBookmarked ?? initialIsBookmarked; + + const updateDetailQuery = useCallback(() => { + queryClient.setQueryData( + companyQueryKey.detail(companyId), + (previousData) => updateCompanyBookmarkCache(previousData, true) + ); + }, [companyId, queryClient]); + + const { mutate: addBookmark, isPending: isAddingBookmark } = usePostBookmark({ + onSuccess: () => { + updateDetailQuery(); + queryClient.invalidateQueries({ + queryKey: companyQueryKey.detail(companyId), + }); + }, + onError: () => { + setLocalIsBookmarked(null); + setIsBookmarkErrorOpen(true); + }, + }); + + const handleBookmarkClick = useCallback(() => { + if (isBookmarked || isAddingBookmark) { + return; + } + + setLocalIsBookmarked(true); + addBookmark(companyId); + }, [addBookmark, companyId, isAddingBookmark, isBookmarked]); + + const closeBookmarkError = useCallback(() => { + setIsBookmarkErrorOpen(false); + }, []); + + return { + isBookmarked, + isAddingBookmark, + isBookmarkErrorOpen, + handleBookmarkClick, + closeBookmarkError, + }; +}; diff --git a/src/features/company-detail/api/use-get-company-detail.query.ts b/src/features/company-detail/api/use-get-company-detail.query.ts index f694b028..64561c1b 100644 --- a/src/features/company-detail/api/use-get-company-detail.query.ts +++ b/src/features/company-detail/api/use-get-company-detail.query.ts @@ -16,6 +16,9 @@ export const useGetCompanyDetail = (companyId: number) => { queryKey: companyQueryKey.detail(companyId), queryFn: () => getCompanyDetail(companyId), enabled: Number.isFinite(companyId) && companyId > 0, + staleTime: 0, + refetchOnMount: "always", + refetchOnWindowFocus: true, // secure: tr throwOnError: (error) => !(isAxiosError(error) && error.response?.status === 404), diff --git a/src/pages/company-detail/company-detail-page.tsx b/src/pages/company-detail/company-detail-page.tsx index ee95fb1c..e3136e81 100644 --- a/src/pages/company-detail/company-detail-page.tsx +++ b/src/pages/company-detail/company-detail-page.tsx @@ -26,6 +26,7 @@ const CompanyDetailPage = () => { ? { companyId, name: companyDetail.name ?? "", + isBookmarked: companyDetail.isLiked ?? false, isRecruiting: companyDetail.isRecruiting, logo: companyDetail.logo ?? "", industry: companyDetail.industry as IndustryCode | undefined, diff --git a/src/pages/company-detail/ui/company-detail-section.tsx b/src/pages/company-detail/ui/company-detail-section.tsx index 4581d681..805b009f 100644 --- a/src/pages/company-detail/ui/company-detail-section.tsx +++ b/src/pages/company-detail/ui/company-detail-section.tsx @@ -1,5 +1,4 @@ -import { useState } from "react"; - +import { useCompanyBookmark } from "@/features/bookmark"; import { CompanyCtaBanner, CompanyIssue, @@ -17,7 +16,7 @@ import { type IndustryCode, type ScaleCode, } from "@/shared/config"; -import { Tag, Textbox } from "@/shared/ui"; +import { Alert, Tag, Textbox } from "@/shared/ui"; import * as styles from "./company-detail-section.css.ts"; @@ -31,6 +30,7 @@ type IssueItem = { type CompanyDetailSummary = { companyId: number; name: string; + isBookmarked: boolean; isRecruiting?: boolean; logo: string; industry?: IndustryCode; @@ -49,16 +49,25 @@ const getSectionClassName = (sectionStyle: string) => [styles.sectionBase, sectionStyle].join(" "); const CompanyDetailSection = ({ companyData }: CompanyDetailSectionProps) => { - const [isBookmarked, setIsBookmarked] = useState(false); + const { + isBookmarked, + isAddingBookmark, + isBookmarkErrorOpen, + handleBookmarkClick, + closeBookmarkError, + } = useCompanyBookmark({ + companyId: companyData.companyId, + initialIsBookmarked: companyData.isBookmarked, + }); const keywordTags = [ companyData.industry ? `#${getIndustryLabel(companyData.industry)}` : null, companyData.scale ? `#${getScaleLabel(companyData.scale)}` : null, ].filter((keyword): keyword is string => keyword !== null); - const handleBookmarkClick = () => { - setIsBookmarked((prev) => !prev); - }; + const bookmarkAriaLabel = isBookmarked + ? "기업 북마크 완료" + : "기업 북마크 추가"; return (
@@ -75,12 +84,11 @@ const CompanyDetailSection = ({ companyData }: CompanyDetailSectionProps) => {

{companyData.name}

); }; From 0d7f908107f177083151c43574169c78b07ca926 Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sat, 4 Apr 2026 19:56:24 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20=EA=B8=B0=EC=97=85=20=EB=B6=81?= =?UTF-8?q?=EB=A7=88=ED=81=AC=20=EC=82=AD=EC=A0=9C=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/use-delete-bookmark.mutation.ts | 28 +++++++ src/features/bookmark/index.ts | 2 + .../bookmark/model/use-company-bookmark.ts | 73 +++++++++++++++---- src/features/bookmark/store/bookmark.store.ts | 36 +++++++++ src/pages/bookmark/bookmark-page.tsx | 50 ++++++++++++- .../ui/company-detail-section.tsx | 6 +- 6 files changed, 172 insertions(+), 23 deletions(-) create mode 100644 src/features/bookmark/api/use-delete-bookmark.mutation.ts create mode 100644 src/features/bookmark/store/bookmark.store.ts diff --git a/src/features/bookmark/api/use-delete-bookmark.mutation.ts b/src/features/bookmark/api/use-delete-bookmark.mutation.ts new file mode 100644 index 00000000..350a155c --- /dev/null +++ b/src/features/bookmark/api/use-delete-bookmark.mutation.ts @@ -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.all(), + }); + options?.onSuccess?.(); + }, + onError: options?.onError, + }); +}; diff --git a/src/features/bookmark/index.ts b/src/features/bookmark/index.ts index 1ee529c7..cd950e84 100644 --- a/src/features/bookmark/index.ts +++ b/src/features/bookmark/index.ts @@ -1,7 +1,9 @@ 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"; diff --git a/src/features/bookmark/model/use-company-bookmark.ts b/src/features/bookmark/model/use-company-bookmark.ts index b5b6781e..5e85a62d 100644 --- a/src/features/bookmark/model/use-company-bookmark.ts +++ b/src/features/bookmark/model/use-company-bookmark.ts @@ -3,7 +3,9 @@ import { useCallback, useState } from "react"; import { companyQueryKey } from "@/shared/api/config/query-key"; +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"; @@ -31,41 +33,80 @@ export const useCompanyBookmark = ({ initialIsBookmarked, }: UseCompanyBookmarkParams) => { const queryClient = useQueryClient(); - const [localIsBookmarked, setLocalIsBookmarked] = useState( - null - ); const [isBookmarkErrorOpen, setIsBookmarkErrorOpen] = useState(false); + const bookmarkOverride = useBookmarkStore( + (state) => state.bookmarkOverrides[companyId] + ); + const setBookmarkOverride = useBookmarkStore( + (state) => state.setBookmarkOverride + ); + const clearBookmarkOverride = useBookmarkStore( + (state) => state.clearBookmarkOverride + ); - const isBookmarked = localIsBookmarked ?? initialIsBookmarked; + const isBookmarked = bookmarkOverride ?? initialIsBookmarked; - const updateDetailQuery = useCallback(() => { - queryClient.setQueryData( - companyQueryKey.detail(companyId), - (previousData) => updateCompanyBookmarkCache(previousData, true) - ); - }, [companyId, queryClient]); + const updateDetailQuery = useCallback( + (nextIsBookmarked: boolean) => { + queryClient.setQueryData( + companyQueryKey.detail(companyId), + (previousData) => + updateCompanyBookmarkCache(previousData, nextIsBookmarked) + ); + }, + [companyId, queryClient] + ); const { mutate: addBookmark, isPending: isAddingBookmark } = usePostBookmark({ onSuccess: () => { - updateDetailQuery(); + updateDetailQuery(true); queryClient.invalidateQueries({ queryKey: companyQueryKey.detail(companyId), }); }, onError: () => { - setLocalIsBookmarked(null); + clearBookmarkOverride(companyId); setIsBookmarkErrorOpen(true); }, }); + const { mutate: removeBookmark, isPending: isRemovingBookmark } = + useDeleteBookmark({ + onSuccess: () => { + updateDetailQuery(false); + queryClient.invalidateQueries({ + queryKey: companyQueryKey.detail(companyId), + }); + }, + onError: () => { + clearBookmarkOverride(companyId); + setIsBookmarkErrorOpen(true); + }, + }); + + const isBookmarkPending = isAddingBookmark || isRemovingBookmark; + const handleBookmarkClick = useCallback(() => { - if (isBookmarked || isAddingBookmark) { + if (isBookmarkPending) { + return; + } + + if (isBookmarked) { + setBookmarkOverride(companyId, false); + removeBookmark(companyId); return; } - setLocalIsBookmarked(true); + setBookmarkOverride(companyId, true); addBookmark(companyId); - }, [addBookmark, companyId, isAddingBookmark, isBookmarked]); + }, [ + addBookmark, + companyId, + isBookmarked, + isBookmarkPending, + removeBookmark, + setBookmarkOverride, + ]); const closeBookmarkError = useCallback(() => { setIsBookmarkErrorOpen(false); @@ -73,7 +114,7 @@ export const useCompanyBookmark = ({ return { isBookmarked, - isAddingBookmark, + isBookmarkPending, isBookmarkErrorOpen, handleBookmarkClick, closeBookmarkError, diff --git a/src/features/bookmark/store/bookmark.store.ts b/src/features/bookmark/store/bookmark.store.ts new file mode 100644 index 00000000..b535be17 --- /dev/null +++ b/src/features/bookmark/store/bookmark.store.ts @@ -0,0 +1,36 @@ +import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; + +interface BookmarkState { + bookmarkOverrides: Record; + setBookmarkOverride: (companyId: number, isBookmarked: boolean) => void; + clearBookmarkOverride: (companyId: number) => void; +} + +export const useBookmarkStore = create( + persist( + (set) => ({ + bookmarkOverrides: {}, + setBookmarkOverride: (companyId, isBookmarked) => + set((state) => ({ + bookmarkOverrides: { + ...state.bookmarkOverrides, + [companyId]: isBookmarked, + }, + })), + clearBookmarkOverride: (companyId) => + set((state) => { + const nextOverrides = { ...state.bookmarkOverrides }; + delete nextOverrides[companyId]; + + return { + bookmarkOverrides: nextOverrides, + }; + }), + }), + { + name: "bookmark", + storage: createJSONStorage(() => sessionStorage), + } + ) +); diff --git a/src/pages/bookmark/bookmark-page.tsx b/src/pages/bookmark/bookmark-page.tsx index b29c91bf..6ab781fa 100644 --- a/src/pages/bookmark/bookmark-page.tsx +++ b/src/pages/bookmark/bookmark-page.tsx @@ -5,11 +5,13 @@ import { ROUTES } from "@/app/routes/paths"; import { BookmarkEmptyState, BookmarkTable, + useBookmarkStore, + useDeleteBookmark, useGetBookmarkCompaniesQuery, } from "@/features/bookmark"; import { IconBookmarkBefore, IconTrash } from "@/shared/assets/icons"; import { modalStore } from "@/shared/model/store"; -import { Button, ModalBasic, Pagination, Search } from "@/shared/ui"; +import { Alert, Button, ModalBasic, Pagination, Search } from "@/shared/ui"; import * as styles from "./bookmark-page.css"; @@ -26,6 +28,10 @@ const BookmarkPage = () => { const [rows, setRows] = useState([]); const [selectedIds, setSelectedIds] = useState>(new Set()); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isDeleteErrorOpen, setIsDeleteErrorOpen] = useState(false); + const setBookmarkOverride = useBookmarkStore( + (state) => state.setBookmarkOverride + ); const keyword = searchParams.get(BOOKMARK_QUERY_KEY)?.trim() ?? ""; const currentPageParam = Number(searchParams.get(BOOKMARK_PAGE_QUERY_KEY)); @@ -39,6 +45,8 @@ const BookmarkPage = () => { isLoading, isFetching, } = useGetBookmarkCompaniesQuery(currentPage); + const { mutateAsync: deleteBookmark, isPending: isDeletingBookmark } = + useDeleteBookmark(); const [searchInput, setSearchInput] = useState(keyword); useEffect(() => { @@ -164,10 +172,35 @@ const BookmarkPage = () => { }); }; - const handleDeleteSelected = () => { - setRows((prev) => prev.filter((row) => !selectedIds.has(row.id))); + const handleDeleteSelected = async () => { + const rowsToDelete = rows.filter((row) => selectedIds.has(row.id)); + const deleteResults = await Promise.allSettled( + rowsToDelete.map((row) => deleteBookmark(row.companyId)) + ); + + const succeededRows = rowsToDelete.filter( + (_, index) => deleteResults[index]?.status === "fulfilled" + ); + const hasFailedDelete = deleteResults.some( + (result) => result.status === "rejected" + ); + + if (succeededRows.length > 0) { + const succeededRowIds = new Set(succeededRows.map((row) => row.id)); + + succeededRows.forEach((row) => { + setBookmarkOverride(row.companyId, false); + }); + + setRows((prev) => prev.filter((row) => !succeededRowIds.has(row.id))); + } + setSelectedIds(new Set()); modalStore.close(BOOKMARK_DELETE_MODAL_ID); + + if (hasFailedDelete) { + setIsDeleteErrorOpen(true); + } }; const handleOpenDeleteModal = () => { @@ -228,7 +261,7 @@ const BookmarkPage = () => {
)} + + {isDeleteErrorOpen ? ( + setIsDeleteErrorOpen(false)} + /> + ) : null} ); }; diff --git a/src/pages/company-detail/ui/company-detail-section.tsx b/src/pages/company-detail/ui/company-detail-section.tsx index 805b009f..a1acb25b 100644 --- a/src/pages/company-detail/ui/company-detail-section.tsx +++ b/src/pages/company-detail/ui/company-detail-section.tsx @@ -51,7 +51,7 @@ const getSectionClassName = (sectionStyle: string) => const CompanyDetailSection = ({ companyData }: CompanyDetailSectionProps) => { const { isBookmarked, - isAddingBookmark, + isBookmarkPending, isBookmarkErrorOpen, handleBookmarkClick, closeBookmarkError, @@ -66,7 +66,7 @@ const CompanyDetailSection = ({ companyData }: CompanyDetailSectionProps) => { ].filter((keyword): keyword is string => keyword !== null); const bookmarkAriaLabel = isBookmarked - ? "기업 북마크 완료" + ? "기업 북마크 해제" : "기업 북마크 추가"; return ( @@ -88,7 +88,7 @@ const CompanyDetailSection = ({ companyData }: CompanyDetailSectionProps) => { aria-pressed={isBookmarked} className={styles.bookmarkButton} onClick={handleBookmarkClick} - disabled={isAddingBookmark} + disabled={isBookmarkPending} > Date: Sat, 4 Apr 2026 19:58:38 +0900 Subject: [PATCH 06/18] =?UTF-8?q?fix:=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=83=81=ED=83=9C=20=EB=8F=99=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EB=B3=B4=EC=A0=95=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bookmark/model/use-company-bookmark.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/features/bookmark/model/use-company-bookmark.ts b/src/features/bookmark/model/use-company-bookmark.ts index 5e85a62d..29b193c4 100644 --- a/src/features/bookmark/model/use-company-bookmark.ts +++ b/src/features/bookmark/model/use-company-bookmark.ts @@ -1,4 +1,5 @@ import { useQueryClient } from "@tanstack/react-query"; +import { isAxiosError } from "axios"; import { useCallback, useState } from "react"; import { companyQueryKey } from "@/shared/api/config/query-key"; @@ -14,6 +15,9 @@ interface UseCompanyBookmarkParams { initialIsBookmarked: boolean; } +const isDuplicateBookmarkError = (error: unknown) => + isAxiosError(error) && error.response?.status === 400; + const updateCompanyBookmarkCache = ( previousData: GetCompanyResponseDto | undefined, isLiked: boolean @@ -64,7 +68,16 @@ export const useCompanyBookmark = ({ queryKey: companyQueryKey.detail(companyId), }); }, - onError: () => { + onError: (error) => { + if (isDuplicateBookmarkError(error)) { + setBookmarkOverride(companyId, true); + updateDetailQuery(true); + queryClient.invalidateQueries({ + queryKey: companyQueryKey.detail(companyId), + }); + return; + } + clearBookmarkOverride(companyId); setIsBookmarkErrorOpen(true); }, From 1922bf7d54f1b8b960548ea8381bc5c8bdd900dc Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sat, 4 Apr 2026 20:37:26 +0900 Subject: [PATCH 07/18] =?UTF-8?q?refactor:=20=EA=B8=B0=EC=97=85=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=84=B9=EC=85=98=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/company-detail-section.css.ts | 74 ++++++++++++------- .../ui/company-detail-section.tsx | 23 ++---- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/src/pages/company-detail/ui/company-detail-section.css.ts b/src/pages/company-detail/ui/company-detail-section.css.ts index df2f2a55..084842a9 100644 --- a/src/pages/company-detail/ui/company-detail-section.css.ts +++ b/src/pages/company-detail/ui/company-detail-section.css.ts @@ -136,15 +136,18 @@ export const headerRight = style({ alignItems: "center", }); -export const sectionBase = style({ +const sectionBase = style({ display: "flex", flexDirection: "column", }); -export const keywordSection = style({ - marginTop: "6rem", - gap: "1.6rem", -}); +export const keywordSection = style([ + sectionBase, + { + marginTop: "6rem", + gap: "1.6rem", + }, +]); export const keywordTitle = style({ color: themeVars.color.black, @@ -171,20 +174,29 @@ export const sectionTitle = style({ ...themeVars.fontStyles.hline_b_18, }); -export const summarySection = style({ - marginTop: "5.2rem", - gap: "1.6rem", -}); +export const summarySection = style([ + sectionBase, + { + marginTop: "5.2rem", + gap: "1.6rem", + }, +]); -export const talentSection = style({ - marginTop: "5.2rem", - gap: "1.6rem", -}); +export const talentSection = style([ + sectionBase, + { + marginTop: "5.2rem", + gap: "1.6rem", + }, +]); -export const issueSection = style({ - marginTop: "5.2rem", - gap: "1.6rem", -}); +export const issueSection = style([ + sectionBase, + { + marginTop: "5.2rem", + gap: "1.6rem", + }, +]); export const issueList = style({ display: "flex", @@ -192,21 +204,27 @@ export const issueList = style({ gap: "1.2rem", }); -export const textboxContent = style({ +const textboxContent = style({ whiteSpace: "pre-line", textAlign: "justify", }); -export const summaryBox = style({ - display: "flex", - alignItems: "center", -}); - -export const talentBox = style({ - minHeight: "8rem", - display: "flex", - alignItems: "center", -}); +export const summaryBox = style([ + textboxContent, + { + display: "flex", + alignItems: "center", + }, +]); + +export const talentBox = style([ + textboxContent, + { + minHeight: "8rem", + display: "flex", + alignItems: "center", + }, +]); export const ctaBanner = style({ marginTop: "7.8rem", diff --git a/src/pages/company-detail/ui/company-detail-section.tsx b/src/pages/company-detail/ui/company-detail-section.tsx index a1acb25b..4310b431 100644 --- a/src/pages/company-detail/ui/company-detail-section.tsx +++ b/src/pages/company-detail/ui/company-detail-section.tsx @@ -18,7 +18,7 @@ import { } from "@/shared/config"; import { Alert, Tag, Textbox } from "@/shared/ui"; -import * as styles from "./company-detail-section.css.ts"; +import * as styles from "./company-detail-section.css"; type IssueItem = { href: string; @@ -45,9 +45,6 @@ interface CompanyDetailSectionProps { companyData: CompanyDetailSummary; } -const getSectionClassName = (sectionStyle: string) => - [styles.sectionBase, sectionStyle].join(" "); - const CompanyDetailSection = ({ companyData }: CompanyDetailSectionProps) => { const { isBookmarked, @@ -110,7 +107,7 @@ const CompanyDetailSection = ({ companyData }: CompanyDetailSectionProps) => {
-
+

기업 관련 키워드

@@ -122,7 +119,7 @@ const CompanyDetailSection = ({ companyData }: CompanyDetailSectionProps) => {
-
+
{

회사 한줄 요약

- + {companyData.summary}
-
+
{

인재상

- + {companyData.talentProfile}
-
+
Date: Sat, 4 Apr 2026 20:48:37 +0900 Subject: [PATCH 08/18] =?UTF-8?q?fix:=20=EC=98=A8=EB=B3=B4=EB=94=A9=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=83=80=EC=9E=85=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/onboarding/onboarding-page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/onboarding/onboarding-page.tsx b/src/pages/onboarding/onboarding-page.tsx index c4b196cb..5c83a331 100644 --- a/src/pages/onboarding/onboarding-page.tsx +++ b/src/pages/onboarding/onboarding-page.tsx @@ -55,11 +55,11 @@ const OnboardingPage = () => { educationLevel: selectedEducation ?? "HIGH_SCHOOL", universityId: Number(selectedUniversity?.id ?? 0), - firstIndustry: labelToCodeIndustry(industry[1]), + firstIndustry: labelToCodeIndustry(industry[1]) ?? "CONSUMER_GOODS", secondIndustry: labelToCodeIndustry(industry[2]) || undefined, thirdIndustry: labelToCodeIndustry(industry[3]) || undefined, - firstJob: labelToCodeJob(job[1]), + firstJob: labelToCodeJob(job[1]) ?? "MARKETING_STRATEGY", secondJob: labelToCodeJob(job[2]) || undefined, thirdJob: labelToCodeJob(job[3]) || undefined, }; From ddc1adca2206629e8717a1640fb77810f120aa99 Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sat, 4 Apr 2026 21:57:25 +0900 Subject: [PATCH 09/18] =?UTF-8?q?fix:=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EA=B8=B0=EC=97=85=20=EC=9D=91=EB=8B=B5=20=EA=B0=80=EB=93=9C=20?= =?UTF-8?q?=EB=B0=8F=20invalid=20row=20=ED=95=84=ED=84=B0=EB=A7=81=20(#161?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/use-get-bookmark-companies.query.ts | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/features/bookmark/api/use-get-bookmark-companies.query.ts b/src/features/bookmark/api/use-get-bookmark-companies.query.ts index d70dd221..c1526afc 100644 --- a/src/features/bookmark/api/use-get-bookmark-companies.query.ts +++ b/src/features/bookmark/api/use-get-bookmark-companies.query.ts @@ -29,21 +29,42 @@ interface BookmarkCompaniesApiResponse { 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 => { const response = await api.me.getBookmarkCompany({ page, sort }); - const result = response.result as unknown as BookmarkCompaniesApiResponse; + const result = + (response.result as BookmarkCompaniesApiResponse | undefined) ?? {}; return { - content: (result.content ?? []).map((bookmarkCompany) => ({ - id: bookmarkCompany.id ?? 0, - companyId: bookmarkCompany.companyId ?? 0, - companyName: bookmarkCompany.name ?? "", - scrapedAt: bookmarkCompany.createdAt ?? "", - isConnected: bookmarkCompany.isConnected ?? false, - })), + 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, From b622deb01b4a75e8e9f0c3be67d6fd5713367661 Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sat, 4 Apr 2026 22:48:03 +0900 Subject: [PATCH 10/18] =?UTF-8?q?fix:=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=97=90=EB=9F=AC=20prefix=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=ED=8C=90=EB=B3=84=20=EB=B0=8F=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=20=EC=9E=AC=EC=A1=B0=ED=9A=8C=20=EC=B5=9C=EC=A0=81=ED=99=94=20?= =?UTF-8?q?(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/bookmark/model/use-company-bookmark.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/features/bookmark/model/use-company-bookmark.ts b/src/features/bookmark/model/use-company-bookmark.ts index 29b193c4..ce0e8bac 100644 --- a/src/features/bookmark/model/use-company-bookmark.ts +++ b/src/features/bookmark/model/use-company-bookmark.ts @@ -1,8 +1,8 @@ import { useQueryClient } from "@tanstack/react-query"; -import { isAxiosError } from "axios"; import { useCallback, 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"; @@ -16,7 +16,9 @@ interface UseCompanyBookmarkParams { } const isDuplicateBookmarkError = (error: unknown) => - isAxiosError(error) && error.response?.status === 400; + isValidCustomError(error) && + error.response.status === 400 && + error.response.data.prefix.startsWith("BOOKMARK_"); const updateCompanyBookmarkCache = ( previousData: GetCompanyResponseDto | undefined, @@ -64,17 +66,11 @@ export const useCompanyBookmark = ({ const { mutate: addBookmark, isPending: isAddingBookmark } = usePostBookmark({ onSuccess: () => { updateDetailQuery(true); - queryClient.invalidateQueries({ - queryKey: companyQueryKey.detail(companyId), - }); }, onError: (error) => { if (isDuplicateBookmarkError(error)) { setBookmarkOverride(companyId, true); updateDetailQuery(true); - queryClient.invalidateQueries({ - queryKey: companyQueryKey.detail(companyId), - }); return; } From 497f6fe952254b9f3caba94648c27a44b3ab55a6 Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sat, 4 Apr 2026 22:52:54 +0900 Subject: [PATCH 11/18] =?UTF-8?q?fix:=20=EB=B6=81=EB=A7=88=ED=81=AC=20over?= =?UTF-8?q?ride=20=EC=84=B8=EC=85=98=20=EC=98=81=EC=86=8D=ED=99=94=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/bookmark/store/bookmark.store.ts | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/features/bookmark/store/bookmark.store.ts b/src/features/bookmark/store/bookmark.store.ts index b535be17..6b5bf597 100644 --- a/src/features/bookmark/store/bookmark.store.ts +++ b/src/features/bookmark/store/bookmark.store.ts @@ -1,5 +1,4 @@ import { create } from "zustand"; -import { createJSONStorage, persist } from "zustand/middleware"; interface BookmarkState { bookmarkOverrides: Record; @@ -7,30 +6,22 @@ interface BookmarkState { clearBookmarkOverride: (companyId: number) => void; } -export const useBookmarkStore = create( - persist( - (set) => ({ - bookmarkOverrides: {}, - setBookmarkOverride: (companyId, isBookmarked) => - set((state) => ({ - bookmarkOverrides: { - ...state.bookmarkOverrides, - [companyId]: isBookmarked, - }, - })), - clearBookmarkOverride: (companyId) => - set((state) => { - const nextOverrides = { ...state.bookmarkOverrides }; - delete nextOverrides[companyId]; +export const useBookmarkStore = create((set) => ({ + bookmarkOverrides: {}, + setBookmarkOverride: (companyId, isBookmarked) => + set((state) => ({ + bookmarkOverrides: { + ...state.bookmarkOverrides, + [companyId]: isBookmarked, + }, + })), + clearBookmarkOverride: (companyId) => + set((state) => { + const nextOverrides = { ...state.bookmarkOverrides }; + delete nextOverrides[companyId]; - return { - bookmarkOverrides: nextOverrides, - }; - }), + return { + bookmarkOverrides: nextOverrides, + }; }), - { - name: "bookmark", - storage: createJSONStorage(() => sessionStorage), - } - ) -); +})); From 50114019514de642f25e570cad44ab2e6fd2324e Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sat, 4 Apr 2026 23:03:48 +0900 Subject: [PATCH 12/18] =?UTF-8?q?fix:=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/company-detail/ui/company-detail-section.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/company-detail/ui/company-detail-section.tsx b/src/pages/company-detail/ui/company-detail-section.tsx index 4310b431..0456fe3c 100644 --- a/src/pages/company-detail/ui/company-detail-section.tsx +++ b/src/pages/company-detail/ui/company-detail-section.tsx @@ -187,7 +187,7 @@ const CompanyDetailSection = ({ companyData }: CompanyDetailSectionProps) => { ) : null} From ae1e8fc7f80af268e606dde357597991ff252d5f Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sat, 4 Apr 2026 23:46:59 +0900 Subject: [PATCH 13/18] =?UTF-8?q?fix:=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=ED=9B=84=20override=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/bookmark/model/use-company-bookmark.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/features/bookmark/model/use-company-bookmark.ts b/src/features/bookmark/model/use-company-bookmark.ts index ce0e8bac..4b5fd038 100644 --- a/src/features/bookmark/model/use-company-bookmark.ts +++ b/src/features/bookmark/model/use-company-bookmark.ts @@ -66,11 +66,13 @@ export const useCompanyBookmark = ({ const { mutate: addBookmark, isPending: isAddingBookmark } = usePostBookmark({ onSuccess: () => { updateDetailQuery(true); + clearBookmarkOverride(companyId); }, onError: (error) => { if (isDuplicateBookmarkError(error)) { setBookmarkOverride(companyId, true); updateDetailQuery(true); + clearBookmarkOverride(companyId); return; } @@ -83,6 +85,7 @@ export const useCompanyBookmark = ({ useDeleteBookmark({ onSuccess: () => { updateDetailQuery(false); + clearBookmarkOverride(companyId); queryClient.invalidateQueries({ queryKey: companyQueryKey.detail(companyId), }); From ae7dc87e0624bba20d4ff913ee6e87d7dc12305c Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sun, 5 Apr 2026 13:05:09 +0900 Subject: [PATCH 14/18] =?UTF-8?q?refactor:=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=EC=BF=BC=EB=A6=AC=20invalidate=20=EB=B2=94=EC=9C=84=20?= =?UTF-8?q?=EC=B6=95=EC=86=8C=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/bookmark/api/use-delete-bookmark.mutation.ts | 2 +- src/features/bookmark/api/use-post-bookmark.mutation.ts | 2 +- src/shared/api/config/query-key.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/features/bookmark/api/use-delete-bookmark.mutation.ts b/src/features/bookmark/api/use-delete-bookmark.mutation.ts index 350a155c..d7fabb05 100644 --- a/src/features/bookmark/api/use-delete-bookmark.mutation.ts +++ b/src/features/bookmark/api/use-delete-bookmark.mutation.ts @@ -19,7 +19,7 @@ export const useDeleteBookmark = (options?: UseDeleteBookmarkOptions) => { mutationFn: (companyId: number) => deleteBookmark(companyId), onSuccess: () => { queryClient.invalidateQueries({ - queryKey: meQueryKey.all(), + queryKey: meQueryKey.bookmarkCompanyLists(), }); options?.onSuccess?.(); }, diff --git a/src/features/bookmark/api/use-post-bookmark.mutation.ts b/src/features/bookmark/api/use-post-bookmark.mutation.ts index 2e29bda0..146ad15d 100644 --- a/src/features/bookmark/api/use-post-bookmark.mutation.ts +++ b/src/features/bookmark/api/use-post-bookmark.mutation.ts @@ -20,7 +20,7 @@ export const usePostBookmark = (options?: UsePostBookmarkOptions) => { mutationFn: (companyId: number) => postBookmark(companyId), onSuccess: (bookmarkId: number) => { queryClient.invalidateQueries({ - queryKey: meQueryKey.all(), + queryKey: meQueryKey.bookmarkCompanyLists(), }); options?.onSuccess?.(bookmarkId); }, diff --git a/src/shared/api/config/query-key.ts b/src/shared/api/config/query-key.ts index 0fd82611..505c8c29 100644 --- a/src/shared/api/config/query-key.ts +++ b/src/shared/api/config/query-key.ts @@ -21,10 +21,11 @@ export const meQueryKey = { all: () => ["me"], profile: () => [...meQueryKey.all(), "profile"], // 사용자 프로필 조회 onboarding: () => [...meQueryKey.all(), "onboarding"], // 온보딩 상태 조회 + bookmarkCompanyLists: () => [...meQueryKey.all(), "bookmark-company-list"], bookmarkCompanyList: ( page: number, sort: "NAME" | "LIKE" | "LATEST" | "OLDEST" - ) => [...meQueryKey.all(), "bookmark-company-list", page, sort], + ) => [...meQueryKey.bookmarkCompanyLists(), page, sort], }; // AI-Report 관련 API (AI-Report) From 117cd29708f8937881e798e4f513b5a278c62aad Mon Sep 17 00:00:00 2001 From: qowjdals23 Date: Sun, 5 Apr 2026 13:08:07 +0900 Subject: [PATCH 15/18] =?UTF-8?q?refactor:=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=AA=A8=EB=8B=AC=20=EC=83=81=ED=83=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/bookmark/bookmark-page.css.ts | 8 -------- src/pages/bookmark/bookmark-page.tsx | 17 +---------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/pages/bookmark/bookmark-page.css.ts b/src/pages/bookmark/bookmark-page.css.ts index 9aa5ea6d..40aed10d 100644 --- a/src/pages/bookmark/bookmark-page.css.ts +++ b/src/pages/bookmark/bookmark-page.css.ts @@ -60,8 +60,6 @@ export const deleteButtonWrap = style({ display: "inline-flex", }); -export const deleteButtonWrapActive = style({}); - export const trashIcon = style({ width: "2.4rem", height: "2.4rem", @@ -96,12 +94,6 @@ globalStyle(`${deleteButtonWrap} > button:active:not(:disabled)`, { color: themeVars.color.gray300, }); -globalStyle(`${deleteButtonWrapActive} > button:not(:disabled)`, { - borderColor: themeVars.color.gray300, - backgroundColor: themeVars.color.white, - color: themeVars.color.gray300, -}); - globalStyle(`${deleteButtonWrap} > button:disabled`, { borderColor: themeVars.color.blue600, backgroundColor: themeVars.color.blue600, diff --git a/src/pages/bookmark/bookmark-page.tsx b/src/pages/bookmark/bookmark-page.tsx index 6ab781fa..17387ffb 100644 --- a/src/pages/bookmark/bookmark-page.tsx +++ b/src/pages/bookmark/bookmark-page.tsx @@ -27,7 +27,6 @@ const BookmarkPage = () => { const [searchParams, setSearchParams] = useSearchParams(); const [rows, setRows] = useState([]); const [selectedIds, setSelectedIds] = useState>(new Set()); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteErrorOpen, setIsDeleteErrorOpen] = useState(false); const setBookmarkOverride = useBookmarkStore( (state) => state.setBookmarkOverride @@ -57,16 +56,6 @@ const BookmarkPage = () => { setRows(bookmarkCompanies?.content ?? []); }, [bookmarkCompanies?.content]); - useEffect(() => { - const unsubscribe = modalStore.subscribe((modals) => { - setIsDeleteModalOpen( - modals.some((modal) => modal.id === BOOKMARK_DELETE_MODAL_ID) - ); - }); - - return unsubscribe; - }, []); - const filteredRows = useMemo(() => { if (!keyword) { return rows; @@ -253,11 +242,7 @@ const BookmarkPage = () => { />
-
+