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
Loading
Loading