diff --git a/src/features/experience-detail/types/experience-detail.types.ts b/src/features/experience-detail/types/experience-detail.types.ts index a0a6bbf3..55ef20ac 100644 --- a/src/features/experience-detail/types/experience-detail.types.ts +++ b/src/features/experience-detail/types/experience-detail.types.ts @@ -7,7 +7,7 @@ export type ExperienceType = ExperienceTypeCode; export interface ExperienceUpsertBody { title: string; - type: ExperienceType | null; + type: string | null; startAt: string | null; diff --git a/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx b/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx index acd34eac..68e1ef1d 100644 --- a/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx +++ b/src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx @@ -1,4 +1,7 @@ -import { EXPERIENCE_TYPE } from "@/shared/config/experience"; +import { + EXPERIENCE_TYPE, + type ExperienceTypeCode, +} from "@/shared/config/experience"; import { parseYMD } from "@/shared/lib/format-date"; import { ModalBasic, Tooltip } from "@/shared/ui"; import { Button } from "@/shared/ui/button/button"; @@ -40,7 +43,10 @@ const ExperienceViewer = () => { ); } - const typeLabel = current.type ? EXPERIENCE_TYPE[current.type] : "미지정"; + const typeLabel = + current.type && current.type in EXPERIENCE_TYPE + ? EXPERIENCE_TYPE[current.type as ExperienceTypeCode] + : "미지정"; return (
diff --git a/src/features/experience/api/use-experience-list.query.ts b/src/features/experience/api/use-experience-list.query.ts index 7f78d3ce..0f150471 100644 --- a/src/features/experience/api/use-experience-list.query.ts +++ b/src/features/experience/api/use-experience-list.query.ts @@ -4,13 +4,12 @@ import { api } from "@/shared/api/axios-instance"; import { experienceQueryKey } from "@/shared/api/config/query-key"; import type { ExperienceList } from "../type/experience"; -import type { ExperienceTypeCode } from "@/shared/config"; export const getExperienceList = async ({ type, page, }: { - type?: ExperienceTypeCode | undefined; + type?: string; page: number; }) => { const response = await api.experiences.getSummaryExperienceList({ @@ -24,7 +23,7 @@ export const useGetExperienceList = ({ type, page, }: { - type: ExperienceTypeCode | null; + type?: string | null; page: number; }) => { return useQuery({ diff --git a/src/pages/experience/experience-page.tsx b/src/pages/experience/experience-page.tsx index 8a0ce9d3..86697b1c 100644 --- a/src/pages/experience/experience-page.tsx +++ b/src/pages/experience/experience-page.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useSearchParams, useNavigate } from "react-router-dom"; import { ROUTES } from "@/app/routes/paths"; import { useGetExperienceList } from "@/features/experience/api/use-experience-list.query"; @@ -9,24 +9,33 @@ import { ExperienceFilter } from "@/widgets"; import * as styles from "./experience-page.css"; import { ExperienceListContainer } from "./ui/experience-list-container"; -import type { ExperienceTypeCode } from "@/shared/config/experience"; - const ExperiencePage = () => { - const [filter, setFilter] = useState(null); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + + const currentPage = Number(searchParams.get("page")) || 1; + const type = searchParams.get("type") || ""; const [isExpTouched, setIsExpTouched] = useState(false); - const [currentPage, setCurrentPage] = useState(1); - const navigate = useNavigate(); const { data } = useGetExperienceList({ - type: filter, + type, page: currentPage, }); - const handleFilterChange = (value: ExperienceTypeCode | null) => { + const handleFilterChange = (value: string) => { setIsExpTouched(true); - setFilter(value); - setCurrentPage(1); + setSearchParams({ + type: value, + page: "1", + }); + }; + + const handlePageChange = (page: number) => { + setSearchParams({ + type: type ?? "", + page: String(page), + }); }; return ( @@ -53,7 +62,7 @@ const ExperiencePage = () => { { - + ); }; diff --git a/src/pages/home/search-section/search-section.tsx b/src/pages/home/search-section/search-section.tsx index f6c628a4..56f917a9 100644 --- a/src/pages/home/search-section/search-section.tsx +++ b/src/pages/home/search-section/search-section.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; import { useGetCompanies } from "@/features/home"; import { ScaleFilter, IndustryFilter } from "@/features/home/ui"; @@ -11,42 +12,66 @@ import * as styles from "./search-section.css"; import type { IndustryCode, ScaleCode } from "@/shared/config"; -interface CompanySearchParamsType { - keyword?: string; - industry?: IndustryCode; - scale?: ScaleCode; - sort?: string; - page?: number; - isRecruited?: boolean; -} - const SearchSection = () => { - const [params, setParams] = useState({ - page: 1, - isRecruited: true, - }); + const [searchParams, setSearchParams] = useSearchParams(); + + const keyword = searchParams.get("keyword") || ""; + const industry = (searchParams.get("industry") as IndustryCode) || undefined; + const scale = (searchParams.get("scale") as ScaleCode) || undefined; + const page = Number(searchParams.get("page")) || 1; + const isRecruited = searchParams.get("isRecruited") !== "false"; + + const params = { + keyword, + industry, + scale, + page, + isRecruited, + }; const { data, isLoading, isPlaceholderData } = useGetCompanies(params); const content = data?.content || []; const hasResult = content.length > 0; - const [searchValue, setSearchValue] = useState(""); - const currentPage = params.page ?? 1; + + const [searchValue, setSearchValue] = useState(keyword); const [isScaleTouched, setIsScaleTouched] = useState(false); const [isIndustryTouched, setIsIndustryTouched] = useState(false); - const updateParams = (patch: Partial) => { - setParams((prev) => ({ - ...prev, - ...patch, - })); + const updateSearchParams = ( + patch: Record + ) => { + const newParams = new URLSearchParams(searchParams); + + Object.entries(patch).forEach(([key, value]) => { + if (value === undefined || value === "") { + newParams.delete(key); + } else { + newParams.set(key, String(value)); + } + }); + + if (!patch.page) { + newParams.set("page", "1"); + } + + setSearchParams(newParams); }; const handlePageChange = (newPage: number) => { if (isPlaceholderData) return; - updateParams({ page: newPage }); + updateSearchParams({ page: newPage }); + }; + + const handleSearch = (newKeyword: string) => { + updateSearchParams({ keyword: newKeyword }); }; + // 검색값 유지 + useEffect(() => { + setSearchValue(keyword); + }, [keyword]); + return ( <>
{ placeholder="지원하고 싶은 기업을 검색해보세요" value={searchValue} onChange={setSearchValue} - onSearch={(keyword) => updateParams({ keyword, page: 1 })} + onSearch={handleSearch} /> @@ -78,20 +103,20 @@ const SearchSection = () => {
{ + onChange={(newIndustry) => { setIsIndustryTouched(true); - updateParams({ industry, page: 1 }); + updateSearchParams({ industry: newIndustry }); }} /> { + onChange={(newScale) => { setIsScaleTouched(true); - updateParams({ scale, page: 1 }); + updateSearchParams({ scale: newScale }); }} /> @@ -100,9 +125,9 @@ const SearchSection = () => {

- updateParams({ isRecruited, page: 1 }) + checked={isRecruited} + onCheckedChange={(checked) => + updateSearchParams({ isRecruited: checked }) } />
@@ -122,7 +147,7 @@ const SearchSection = () => { ))}
diff --git a/src/pages/matching-list/matching-list-page.tsx b/src/pages/matching-list/matching-list-page.tsx index 4a644ab4..154237df 100644 --- a/src/pages/matching-list/matching-list-page.tsx +++ b/src/pages/matching-list/matching-list-page.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; import { useGetAiReportList } from "@/features/matching-list/api/use-get-matching-list"; import { ICON_MATCH, ERROR } from "@/shared/assets/images"; @@ -7,39 +8,45 @@ import { Search } from "@/shared/ui"; import { ListSection } from "./list-section/list-section"; import * as styles from "./matching-list-page.css"; -interface MatchingListParams { - keyword?: string; - page: number; -} const MatchingListPage = () => { - const [params, setParams] = useState({ - keyword: "", - page: 1, - }); + const [searchParams, setSearchParams] = useSearchParams(); + const currentPage = Number(searchParams.get("page")) || 1; + const keyword = searchParams.get("keyword") || ""; - const { data, isLoading } = useGetAiReportList(params); - const { content = [], currentPage = 1, totalPage = 1 } = data ?? {}; + const { data, isLoading } = useGetAiReportList({ + page: currentPage, + keyword, + }); + const { content = [], totalPage = 1 } = data ?? {}; const showEmptyState = !isLoading && content.length === 0; const showList = content.length > 0; - const [searchValue, setSearchValue] = useState(""); + const [searchValue, setSearchValue] = useState(keyword); - const handleSearch = (keyword: string) => { - setParams({ keyword, page: 1 }); + const handleSearch = (newKeyword: string) => { + setSearchParams({ + keyword: newKeyword, + page: "1", + }); }; const handlePageChange = (page: number) => { - setParams((prev) => ({ - ...prev, - page, - })); + setSearchParams({ + keyword, + page: String(page), + }); }; const handleSearchChange = (keyword: string) => { setSearchValue(keyword); }; + // 검색값 유지 + useEffect(() => { + setSearchValue(keyword); + }, [keyword]); + return (
{/* header 섹션 */} @@ -84,7 +91,7 @@ const MatchingListPage = () => { />

"검색 결과가 없습니다"

- {params.keyword + {keyword ? "다른 검색어로 다시 시도해보세요." : "경험 등록하기 버튼을 눌러 경험을 등록해보세요."}

diff --git a/src/shared/api/generate/http-client.ts b/src/shared/api/generate/http-client.ts index c9debb2a..9e83ec38 100644 --- a/src/shared/api/generate/http-client.ts +++ b/src/shared/api/generate/http-client.ts @@ -143,7 +143,7 @@ export interface ExperienceRequestDto { */ title: string; /** @example "INTERNSHIP" */ - type: "INTERNSHIP" | "PROJECT" | "EDUCATION" | "ETC"; + type: string; /** * @format date * @example "2025-12-23" @@ -812,7 +812,7 @@ export class Api< */ getSummaryExperienceList: ( query?: { - type?: "INTERNSHIP" | "PROJECT" | "EDUCATION" | "ETC"; + type?: string; /** * @format int32 * @default 1 diff --git a/src/widgets/experience-filter/experience-filter.tsx b/src/widgets/experience-filter/experience-filter.tsx index 3463965a..e0fcdc63 100644 --- a/src/widgets/experience-filter/experience-filter.tsx +++ b/src/widgets/experience-filter/experience-filter.tsx @@ -1,16 +1,13 @@ import { EXPERIENCE_TYPE } from "@/shared/config/experience"; import { Dropdown } from "@/shared/ui"; -import { - EXPERIENCE_FILTER_OPTIONS, - type ExperienceFilterCode, -} from "./filter-experience-constant"; +import { EXPERIENCE_FILTER_OPTIONS } from "./filter-experience-constant"; import type { ExperienceTypeCode } from "@/shared/config/experience"; interface ExperienceFilterProps { - value: ExperienceTypeCode | null; - onChange: (value: ExperienceTypeCode | null) => void; + value: string | null; + onChange: (value: string) => void; isTouched?: boolean; hasTotal?: boolean; } @@ -22,7 +19,7 @@ const ExperienceFilter = ({ hasTotal = true, }: ExperienceFilterProps) => { let triggerLabel = "경험 유형"; - if (value) triggerLabel = EXPERIENCE_TYPE[value]; + if (value) triggerLabel = EXPERIENCE_TYPE[value as ExperienceTypeCode]; else if (isTouched && hasTotal) triggerLabel = "전체"; const options = hasTotal @@ -37,7 +34,7 @@ const ExperienceFilter = ({ {options.map((option) => ( onChange(option.code as ExperienceFilterCode | null)} + onClick={() => onChange(option.code ?? "")} > {option.label}