Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type ExperienceType = ExperienceTypeCode;
export interface ExperienceUpsertBody {
title: string;

type: ExperienceType | null;
type: string | null;

startAt: string | null;

Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 (
<main className={s.page}>
Expand Down
5 changes: 2 additions & 3 deletions src/features/experience/api/use-experience-list.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -24,7 +23,7 @@ export const useGetExperienceList = ({
type,
page,
}: {
type: ExperienceTypeCode | null;
type?: string | null;
page: number;
}) => {
return useQuery({
Expand Down
23 changes: 23 additions & 0 deletions src/pages/experience/experience-page.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,26 @@ export const icon = style({
width: "6.4rem",
height: "6.4rem",
});

export const listContainer = style({
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
maxWidth: "106rem",
minHeight: "50rem",
margin: "4rem auto 0",
});

export const spinner = style({
width: "20rem",
aspectRatio: "1 / 1",
objectFit: "contain",
});

export const spinnerText = style({
marginTop: "0.2rem",
...themeVars.fontStyles.hline_m_18,
color: themeVars.color.gray600,
});
80 changes: 65 additions & 15 deletions src/pages/experience/experience-page.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,77 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useState, useEffect } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";

import { ROUTES } from "@/app/routes/paths";
import { useGetExperienceList } from "@/features/experience/api/use-experience-list.query";
import { CAT_SPINNER } from "@/shared/assets/gifs";
import { IconExp } from "@/shared/assets/icons";
import { EXPERIENCE_TYPE } from "@/shared/config/experience";
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<ExperienceTypeCode | null>(null);

const [isExpTouched, setIsExpTouched] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [isExpTouched, setIsExpTouched] = useState(false);

const pageParam = searchParams.get("page");
const typeParam = searchParams.get("type");

// url 파라미터 값(page, type) 유효성 검사
const isInvalidNumber =
pageParam !== null && (isNaN(Number(pageParam)) || Number(pageParam) < 1);
const currentPage = isInvalidNumber ? 1 : Number(pageParam) || 1;

const isValidType = typeParam && typeParam in EXPERIENCE_TYPE;
const type = isValidType ? typeParam : "";

const { data } = useGetExperienceList({
type: filter,
const { data, isLoading } = useGetExperienceList({
type,
page: currentPage,
});

const handleFilterChange = (value: ExperienceTypeCode | null) => {
const { totalPage = 1 } = data ?? {};

const handleFilterChange = (value: string) => {
setIsExpTouched(true);
setFilter(value);
setCurrentPage(1);
setSearchParams({
type: value,
page: "1",
});
};

const handlePageChange = (page: number) => {
setSearchParams({
type,
page: String(page),
});
};

// 페이지 쿼리스트링 강제 교정
useEffect(() => {
const isExceeding = currentPage > totalPage && totalPage > 0;

// 페이지가 이상하거나 'type'에 정의되지 않은 유형(abc)이 들어온 경우 강제 교정
if (isInvalidNumber || isExceeding || (typeParam && !isValidType)) {
const newParams = new URLSearchParams(searchParams);

if (isInvalidNumber || isExceeding) newParams.set("page", "1");
if (typeParam && !isValidType) newParams.delete("type");

setSearchParams(newParams, { replace: true });
}
}, [
currentPage,
totalPage,
isInvalidNumber,
isValidType,
typeParam,
searchParams,
setSearchParams,
]);

return (
<div className={styles.page}>
<section className={styles.header}>
Expand All @@ -53,15 +96,22 @@ const ExperiencePage = () => {
</button>

<ExperienceFilter
value={filter}
value={type}
onChange={handleFilterChange}
isTouched={isExpTouched}
hasTotal={true}
/>
</div>
</section>

<ExperienceListContainer data={data} onPageChange={setCurrentPage} />
{isLoading ? (
<section className={styles.listContainer}>
<img src={CAT_SPINNER} className={styles.spinner} alt="로딩중" />
<p className={styles.spinnerText}>기업 정보를 불러오고 있어요</p>
</section>
) : (
<ExperienceListContainer data={data} onPageChange={handlePageChange} />
)}
</div>
);
};
Expand Down
91 changes: 58 additions & 33 deletions src/pages/home/search-section/search-section.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<CompanySearchParamsType>({
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<CompanySearchParamsType>) => {
setParams((prev) => ({
...prev,
...patch,
}));
const updateSearchParams = (
patch: Record<string, string | number | boolean | undefined>
) => {
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");
}
Copy link
Copy Markdown
Collaborator

@u-zzn u-zzn Mar 16, 2026

Choose a reason for hiding this comment

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

패치된 값 기준으로 page를 자동으로 1로 돌려주는 방식 좋은 것 같습니다 👍
다만 현재는 !patch.page 조건이라 page: 0 같은 값도 동일하게 reset 대상으로 들어가게 되어 의미가 조금 모호해질 수 있다고 생각하는데, “이번 patch에 page가 포함되지 않은 경우”라는 의도를 더 명확히 드러내기 위해 !("page" in patch)처럼 표현하는 방식은 어떨까요? 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

좋은 지적 감사합니다! 유진님 말씀대로 조건문의 목적을 고려했을 때 제안해주신 방향으로 수정하는 게 더 적합한 것 같습니다! 반영하겠습니다 😀


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 (
<>
<section
Expand All @@ -68,7 +93,7 @@ const SearchSection = () => {
placeholder="지원하고 싶은 기업을 검색해보세요"
value={searchValue}
onChange={setSearchValue}
onSearch={(keyword) => updateParams({ keyword, page: 1 })}
onSearch={handleSearch}
/>
</div>
</div>
Expand All @@ -78,20 +103,20 @@ const SearchSection = () => {
<div className={styles.container}>
<div className={styles.filterWrapper}>
<IndustryFilter
value={params.industry ?? null}
value={industry ?? null}
isTouched={isIndustryTouched}
onChange={(industry) => {
onChange={(newIndustry) => {
setIsIndustryTouched(true);
updateParams({ industry, page: 1 });
updateSearchParams({ industry: newIndustry });
}}
/>

<ScaleFilter
value={params.scale}
value={scale}
isTouched={isScaleTouched}
onChange={(scale) => {
onChange={(newScale) => {
setIsScaleTouched(true);
updateParams({ scale, page: 1 });
updateSearchParams({ scale: newScale });
}}
/>

Expand All @@ -100,9 +125,9 @@ const SearchSection = () => {
</p>

<Toggle
checked={params.isRecruited ?? true}
onCheckedChange={(isRecruited) =>
updateParams({ isRecruited, page: 1 })
checked={isRecruited}
onCheckedChange={(checked) =>
updateSearchParams({ isRecruited: checked })
}
/>
</div>
Expand All @@ -122,7 +147,7 @@ const SearchSection = () => {
))}
</div>
<Pagination
currentPage={currentPage}
currentPage={page}
totalPage={data?.totalPage ?? 1}
onPageChange={handlePageChange}
/>
Expand Down
Loading
Loading