Skip to content

[Refactor] pagination 상태 유지 개선(searchParams)#158

Merged
hummingbbird merged 9 commits intodevfrom
feat/#152/pagination-state-persistence
Mar 17, 2026
Merged

[Refactor] pagination 상태 유지 개선(searchParams)#158
hummingbbird merged 9 commits intodevfrom
feat/#152/pagination-state-persistence

Conversation

@hummingbbird
Copy link
Copy Markdown
Contributor

@hummingbbird hummingbbird commented Mar 10, 2026

✏️ Summary

📑 Tasks

페이지네이션의 상태가 유지되도록 리팩토링을 진행했습니다. 상세 페이지에서 '뒤로가기' 버튼을 통해 목록 페이지로 넘어왔을 때 이전 상태를 유지하려면 url에 목록에 관련된 정보들을 저장해야 하는데요, 이 때 페이지와 필터, 검색어 등 상태가 유지되어야 하는 정보들을 쿼리 파라미터로 넘겨 페이지를 옮겨다니더라도 상태가 유지되도록 구현해주었습니다.

// matching-list-page.tsx
const [searchParams, setSearchParams] = useSearchParams();
const currentPage = Number(searchParams.get("page")) || 1;
const keyword = searchParams.get("keyword") || "";

구현 방식은 위와 같습니다. 페이지 렌더링 시 searchParams를 통해 쿼리 파라미터의 값을 가져오고, 존재하지 않을 경우 디폴트 값으로 설정되도록 수정해주었습니다. 추가적으로 인라인 함수로 구현되어 있던 부분도 컨벤션에 맞게 'handle-' 형태의 함수로 분리해주었습니다.

현재 페이지네이션이 사용되는 페이지는 홈, 경험 목록, 매칭 경험 목록 이렇게 3가지인데요, 이중 홈과 매칭 경험 목록의 경우 검색 기능이 포함되어 있는데 검색값이 날아가는 것을 방지하기 위해 useEffect를 통해 검색값이 유지되도록 해주었습니다~

  // 검색값 유지
  useEffect(() => {
    setSearchValue(keyword);
  }, [keyword]);

👀 To Reviewer

리팩토링 과정에서 ExperienceFilter 컴포넌트의 value의 type을 string | null로 확장해주었습니다. 그 이유는 .. 제가 안정성을 위해 범위를 너무 좁혀놨더니 오히려 결합도가 지나치게 올라가서 하나 수정하려고 130913개의 파일을 건너야 하는 불상사가 발생하여서 리팩토링 하는 김에 같이 바꿔주었습니다!

이 부분 수정하면서 http-client의 getRequestDto와, ExperienceRequestDto의 type과 getSummaryExperienceList 메소드의 type의 타입을 string으로 수정해주었습니다. 또한 유진이가 작업한 파일에서

  • src/features/experience-detail/types/experience-detail.types.ts의 type의 타입
  • src/features/experience/api/use-experience-list.query.ts의 type의 타입
    이렇게 수정되었습니다! 타입이랑, label의 기본값 정도만 수정되어서 큰 문제는 없겠지만 그래도 알아두어야 할 거 같아 전달합니다 ~

📸 Screenshot

추추의 작업 이슈로 화면녹화를 못하네요 쩝 .. 테스트 직접 해보기 ~~~~ >_<

🔔 ETC

Summary by CodeRabbit

  • 버그 수정

    • 경험 상세 뷰의 타입 표시 로직을 강화해 누락된 유형은 "미지정"으로 안전하게 표시
  • 리팩토링

    • 경험 목록, 경험 필터, 검색, 매칭 리스트, 홈 검색의 필터·페이지네이션 상태를 URL 검색 파라미터로 전환해 공유·북마크·뒤로가기 동작 개선
    • 일부 컴포넌트의 내부 타입·파라미터 처리를 문자열 기반으로 정리
  • 새 기능

    • 목록 로딩 시 중앙 스피너 표시 및 관련 스타일 추가
  • 변경

    • 경험 타입 관련 입력과 API/컴포넌트 인터페이스가 문자열 기반으로 통일됨

@hummingbbird hummingbbird self-assigned this Mar 10, 2026
@hummingbbird hummingbbird linked an issue Mar 10, 2026 that may be closed by this pull request
2 tasks
@github-actions github-actions bot added 🔗API api 연동 🛠️REFACTOR 코드 리팩토링 채영🥦 labels Mar 10, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Experience 타입을 유니온에서 문자열로 완화하고 관련 API/컴포넌트 타입을 문자열 기반으로 업데이트했으며, 여러 페이지 컴포넌트의 필터·페이징 상태 관리를 로컬 state에서 React Router의 URL 검색 파라미터(useSearchParams)로 마이그레이션했습니다.

Changes

Cohort / File(s) Summary
타입 변경 (백엔드/DTO)
src/shared/api/generate/http-client.ts
ExperienceRequestDto.typegetSummaryExperienceList 쿼리 파라미터 타입을 유니온 리터럴 → string으로 완화.
도메인 타입 정의 변경
src/features/experience-detail/types/experience-detail.types.ts
ExperienceUpsertBody.type을 `ExperienceType
컴포넌트 적응
src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx, src/widgets/experience-filter/experience-filter.tsx
Experience 타입을 문자열로 받아들이도록 prop/내부 로직 조정(널 체크·안전 캐스트 포함). ExperienceFilterPropsvalue/onChange 시그니처를 문자열 기반으로 변경.
API 훅/쿼리 시그니처
src/features/experience/api/use-experience-list.query.ts
getExperienceListuseGetExperienceListtype 파라미터 타입을 string으로 변경하고 관련 타입 임포트 제거 및 호출 인자 조정.
페이지 URL 상태 마이그레이션
src/pages/experience/experience-page.tsx, src/pages/home/search-section/search-section.tsx, src/pages/matching-list/matching-list-page.tsx
로컬 필터/페이징 상태를 useSearchParams 기반 URL 쿼리로 전환, URL 유효성 검사/보정 로직 및 핸들러들이 searchParams를 갱신하도록 변경. ExperiencePage 명시적 export 추가.
스타일 추가
src/pages/experience/experience-page.css.ts
로딩 스피너 및 리스트 컨테이너 관련 스타일(listContainer, spinner, spinnerText) 신규 추가.

Sequence Diagram(s)

sequenceDiagram
    participant 사용자 as 사용자
    participant 페이지 as 페이지 컴포넌트
    participant 라우터 as React Router (useSearchParams)
    participant 훅 as API 훅 (useGetExperienceList / useGetAiReportList)
    participant 서버 as 서버/API

    사용자->>페이지: 방문 또는 필터/페이징/검색 조작
    페이지->>라우터: searchParams 읽기/갱신 (type, page, keyword ...)
    라우터-->>페이지: 현재 쿼리파라미터 반환
    페이지->>훅: 데이터 요청 (type: string, page: number, ...)
    훅->>서버: HTTP 요청 (쿼리 파라미터 포함)
    서버-->>훅: 응답 데이터
    훅-->>페이지: 데이터 반환
    페이지->>사용자: UI 렌더링 (목록, 페이징, 필터 상태)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

수빈🍋

Suggested reviewers

  • u-zzn
  • qowjdals23
  • odukong
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive 일부 범위 외 변경사항이 감지됩니다. ExperienceFilter 컴포넌트의 타입 확장(ExperienceTypeCode | null → string | null)과 이에 따른 http-client, experience-detail.types, use-experience-list.query의 타입 변경은 원래 #152 범위를 벗어나며, PR 저자도 '결합도 감소'를 위한 '김에 같이 바꿔주었다'고 언급했습니다. 이는 의도적이지만 명확한 설명은 필요합니다. 타입 확장 변경사항(ExperienceTypeCode → string)이 #152의 직접적인 요구사항이 아니라는 점을 명확히 했으므로, 이들이 목표 달성을 위해 필수적이었는지 또는 별도 리팩토링으로 분리할 수 있었는지 확인 권고.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 반영하고 있습니다. 'pagination 상태 유지 개선(searchParams)'은 쿼리 파라미터를 통한 페이지네이션 상태 관리 개선이라는 핵심 변경사항을 간결하게 표현하고 있습니다.
Description check ✅ Passed PR 설명이 제공된 템플릿의 주요 섹션을 모두 포함하고 있습니다. Summary, Tasks, To Reviewer, Screenshot, ETC 섹션이 모두 채워져 있으며, 구체적인 구현 방식(useSearchParams 사용), 변경된 타입들, 그리고 리뷰어에게 필요한 정보(ExperienceFilter 타입 확장 이유)가 상세히 설명되어 있습니다.
Linked Issues check ✅ Passed PR은 #152의 요구사항(페이지네이션 상태 유지)을 완전히 충족합니다. useSearchParams를 통해 URL 쿼리 파라미터로 페이지/필터/검색어 상태를 저장하고, 3개 페이지(홈, 경험 목록, 매칭 목록)에 URL 기반 상태 관리를 구현했으며, 검색값 유지를 위한 useEffect도 추가했습니다. 리뷰어 u-zzn의 평가도 긍정적이며 블로킹 이슈는 없습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 10, 2026

🚀 빌드 결과

린트 검사 완료
빌드 성공

로그 확인하기
Actions 탭에서 자세히 보기

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/experience/api/use-experience-list.query.ts (1)

29-32: ⚠️ Potential issue | 🟠 Major

queryKeyqueryFn에서 falsy 값 처리가 불일치해요.

queryKey: experienceQueryKey.list(type ?? "", page),  // "" 으로 정규화
queryFn: () => getExperienceList({ type: type ?? undefined, page }),  // undefined로 정규화

이로 인해 캐시 동작에 문제가 발생할 수 있어요:

  • type=null, type="", type=undefined가 모두 같은 캐시 키("")를 공유하지만
  • 실제 API 호출은 다르게 동작해요 ("" vs undefined 쿼리 파라미터)

캐시 키와 API 호출이 일관되게 동작하도록 통일해주세요.

🔧 일관된 처리를 위한 제안
 export const useGetExperienceList = ({
   type,
   page,
 }: {
   type?: string | null;
   page: number;
 }) => {
+  const normalizedType = type || undefined;
   return useQuery({
-    queryKey: experienceQueryKey.list(type ?? "", page),
-    queryFn: () => getExperienceList({ type: type ?? undefined, page }),
+    queryKey: experienceQueryKey.list(normalizedType ?? "", page),
+    queryFn: () => getExperienceList({ type: normalizedType, page }),
   });
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/experience/api/use-experience-list.query.ts` around lines 29 -
32, Normalize the `type` value the same way in both the cache key and the API
call: choose a single canonical form (e.g., undefined) and apply it to both
`experienceQueryKey.list(...)` and the call to `getExperienceList(...)` so the
cache key matches the actual request parameters; specifically, update the
`queryKey` invocation (experienceQueryKey.list) to accept the same normalized
`type` as the `queryFn` that calls `getExperienceList` (both using type ??
undefined or both using type ?? ""), ensuring consistency between the cache key
and the API request.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/experience/experience-page.tsx`:
- Around line 34-39: handlePageChange currently uses an unnecessary nullish
coalescing (type ?? "") even though type is already derived from
searchParams.get("type") || "" and thus always a string; update the
handlePageChange function to pass type directly to setSearchParams (remove the
"?? \"\"" usage) so setSearchParams({ type: type, page: String(page) }) uses the
existing type variable from the searchParams.get("type") || "" initialization.

In `@src/widgets/experience-filter/experience-filter.tsx`:
- Line 22: Guard the cast of value to ExperienceTypeCode before indexing
EXPERIENCE_TYPE: replace the unvalidated cast in the experience-filter component
by checking membership first (e.g., if (value &&
Object.prototype.hasOwnProperty.call(EXPERIENCE_TYPE, value)) triggerLabel =
EXPERIENCE_TYPE[value as ExperienceTypeCode];) so that triggerLabel cannot
become undefined when an invalid string is passed; reference the symbols value,
EXPERIENCE_TYPE, ExperienceTypeCode, and triggerLabel in your change.

---

Outside diff comments:
In `@src/features/experience/api/use-experience-list.query.ts`:
- Around line 29-32: Normalize the `type` value the same way in both the cache
key and the API call: choose a single canonical form (e.g., undefined) and apply
it to both `experienceQueryKey.list(...)` and the call to
`getExperienceList(...)` so the cache key matches the actual request parameters;
specifically, update the `queryKey` invocation (experienceQueryKey.list) to
accept the same normalized `type` as the `queryFn` that calls
`getExperienceList` (both using type ?? undefined or both using type ?? ""),
ensuring consistency between the cache key and the API request.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0cdcf287-49f4-4da2-a5be-0ec5d6d95d1c

📥 Commits

Reviewing files that changed from the base of the PR and between cf791e2 and f513221.

📒 Files selected for processing (8)
  • src/features/experience-detail/types/experience-detail.types.ts
  • src/features/experience-detail/ui/experience-viewer/experience-viewer.tsx
  • src/features/experience/api/use-experience-list.query.ts
  • src/pages/experience/experience-page.tsx
  • src/pages/home/search-section/search-section.tsx
  • src/pages/matching-list/matching-list-page.tsx
  • src/shared/api/generate/http-client.ts
  • src/widgets/experience-filter/experience-filter.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/experience/experience-page.tsx`:
- Line 17: The code forces searchParams.get("type") to an empty string which
breaks the "no-filter selected" state; revert to letting const type =
searchParams.get("type") return null when absent and update any places that read
or set this param (the var name type and uses of searchParams.get) to treat null
as "unset" — when updating the URL/query (e.g., via your setSearchParams or
related hooks referenced around lines 22-23, 28-31, 35-37) remove the key
entirely instead of writing type=""; ensure any consumer hooks accept
null/undefined as the unset state rather than an empty string.
- Line 16: The page query isn't being normalized so values like "-1" or "1.5"
slip through; read the raw value from searchParams.get("page"), parse it with
parseInt(raw, 10) (or Math.floor(Number(raw))), validate that the parsed value
is an integer >= 1, and if it's NaN or < 1 fall back to 1; replace the current
currentPage assignment (which uses Number(searchParams.get("page")) || 1) with
this defensive parsing/validation logic so currentPage is always a positive
integer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 31c762cb-a7b0-44b0-b011-746c1ef767a1

📥 Commits

Reviewing files that changed from the base of the PR and between f513221 and 8a8d1cd.

📒 Files selected for processing (1)
  • src/pages/experience/experience-page.tsx

Copy link
Copy Markdown
Collaborator

@odukong odukong left a comment

Choose a reason for hiding this comment

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

추추의 QA .. 완벽하게 완료해주셨네요..!
특히 searchParams 업데이트 로직을 아래처럼 updateSearchParams로 추상화해서 url 관련 상태들을 업데이트 할 때 하나의 함수로 재사용이 가능해져서 전체 코드 흐름도 볼 때도 깔끔해지게 된 것 같아요🥸💕

Object.entries(patch).forEach(([key, value]) => {
    if (value === undefined || value === "") {
      newParams.delete(key);
    } else {
      newParams.set(key, String(value));
    }
  });

수고하셨뜹니다 ~~~ o(^@^)o

@u-zzn
Copy link
Copy Markdown
Collaborator

u-zzn commented Mar 16, 2026

상태를 URL 쿼리 파라미터로 올려서 상세 페이지에서 뒤로 돌아왔을 때도 기존 목록 상태가 유지되도록 정리해주신 방향 너무 좋네요!
목록 화면의 상태를 컴포넌트 내부 state가 아니라 URL을 기준으로 읽도록 맞춰서, 뒤로가기·새로고침·공유 가능한 상태까지 함께 가져갈 수 있게 된 점이 좋은 것 같습니다 :)

제가 작업한 파일 내에서 type 변경되었다고 전달해주신 내용도 확인했습니다 🙂

몇 군데 코드 상으로 조금 더 명확하게 정리되면 좋을 것 같은 부분만 아래 코멘트로 남겨두겠습니다. 수고 많으셨습니다! 👍

Comment on lines +54 to +56
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.

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

Copy link
Copy Markdown
Collaborator

@qowjdals23 qowjdals23 left a comment

Choose a reason for hiding this comment

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

페이지네이션 상태 유지 !!! 뒤로가기 했을 때도 목록 상태가 유지되도록 searchParams 기준으로 잘 정리해주신 것 같고, 흐름도 한눈에 보기 더 좋아진 것 같네요 !!! 👏

여러 페이지 리팩토링하시느라 고민도 많으셨을텐데 너무너무 수고많으셨습니다 ~~ 많이 배워가용 🔥

const currentPage = Number(searchParams.get("page")) || 1;
const type = searchParams.get("type") || "";

const [isExpTouched, setIsExpTouched] = useState(false);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

사소한 부분인데요..!
필터 값은 URL 에서 잘 복원되지만 isExpTouched는 로컬 state라 뒤로가기하면 다시 false로 초기화 될 것 같은데 혹시 의도한 동작인건지 궁금해서 리뷰 남깁니다 ! 👀

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.

말씀주신 대로 현재는 isExpTouched가 state이기 때문에, 페이지 리렌더링 시 false로 초기화 되고 있습니다. 하지만 현재 로직상 type 파라미터가 비어있을 때 '전체' 데이터를 보여주고 있어서 필터 라벨이 기본값(경험 유형)으로 돌아가더라도 UX 측면에서 큰 지장이 없다고 생각해 지금과 같은 방식으로 구현했습니다 😀

허나 ‘전체’와 ‘경험 유형’은 엄연히 다른 상태이기 때문에 구분하는 것이 더 좋을 것이라고 생각해요! 우선 우선순위가 더 높은 쿼리스트링값의 유지를 우선적으로 구현하고 해당 부분은 추후 스프린트에 적용할까 하는데 정민님 생각은 어떠신가요??

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

넵 이해했습니다!

말씀해주신 것처럼 현재 PR에서는 쿼리스트링 기반으로 목록 상태를 유지하는 쪽이 우선순위가 더 높아 보여서 ! 지금 방향으로 가도 괜찮을 것 같아요 👍

전체 / 경험 유형 구분 부분은 추후 스프린트에서 보완되면 더 좋을 것 같습니다!! 설명 감사합니다 😄

@hummingbbird hummingbbird removed the 🔗API api 연동 label Mar 17, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/experience/experience-page.tsx`:
- Around line 107-111: The loading message "기업 정보를 불러오고 있어요" is wrong for the
Experience page; update the JSX inside the isLoading branch (the <img
src={CAT_SPINNER} ... /> and <p className={styles.spinnerText}>...</p> block) to
use a page-appropriate message such as "경험 목록을 불러오고 있어요" (or "경험 정보를 불러오고 있어요")
by replacing the string in the <p className={styles.spinnerText}> element; keep
the rest (isLoading check, CAT_SPINNER, styles.spinner and styles.spinnerText)
unchanged.
- Around line 52-73: The page-correction useEffect (useEffect block referencing
currentPage, totalPage, isInvalidNumber, isValidType, typeParam, searchParams,
setSearchParams) runs before data loads because totalPage defaults to 1; update
the effect to only perform validation when data loading is complete (e.g., check
an isLoading or isDataLoaded flag) so the isExceeding check uses a real
totalPage and avoid resetting page prematurely; ensure the dependency array
includes that loading flag and the conditional early-returns if loading.
- Around line 22-25: The current page validation (variables isInvalidNumber and
currentPage using pageParam) allows fractional values like "1.5"; update the
isInvalidNumber expression to also reject non-integer numbers by adding a
Number.isInteger(Number(pageParam)) check (e.g., mark invalid when pageParam !==
null && (!Number.isInteger(Number(pageParam)) || isNaN(Number(pageParam)) ||
Number(pageParam) < 1)), then compute currentPage the same way so fractional
pageParam values resolve to 1.
- Around line 27-28: The URL retains an empty `type` query because you set
`type` to "" but the useEffect only removes `type` when `typeParam` exists and
is invalid; update the useEffect condition to also delete the query when the
resolved `type` is an empty string so `newParams.delete("type")` runs for both
invalid params and cleared filters—locate the useEffect that references
`typeParam`, `isValidType`, and `type` and change the conditional to include
`type === ""` (e.g., if ((typeParam && !isValidType) || type === "") then call
newParams.delete("type")).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4c8109fc-89a4-44f5-a439-1ceeca9a00b5

📥 Commits

Reviewing files that changed from the base of the PR and between 8a8d1cd and 8f4199e.

📒 Files selected for processing (2)
  • src/pages/experience/experience-page.css.ts
  • src/pages/experience/experience-page.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/home/search-section/search-section.tsx (1)

167-190: 🧹 Nitpick | 🔵 Trivial

필터/토글 이벤트 핸들러를 handle* 네이밍의 명시 함수로 분리해 주세요

현재 Line [167], Line [176], Line [188]의 인라인 핸들러는 추적/재사용/테스트 측면에서 일관성이 떨어집니다. handleIndustryChange, handleScaleChange, handleRecruitedChange로 분리하면 의도가 더 명확해집니다.

As per coding guidelines, "Use handle prefix for event handler functions and on prefix for event handler props (e.g., handleButtonClick, onAlertClick)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/home/search-section/search-section.tsx` around lines 167 - 190,
Extract the inline handlers into named functions: create
handleIndustryChange(newIndustry) that calls setIsIndustryTouched(true) and
updateSearchParams({ industry: newIndustry }); create
handleScaleChange(newScale) that calls setIsScaleTouched(true) and
updateSearchParams({ scale: newScale }); and create
handleRecruitedChange(checked) that calls updateSearchParams({ isRecruited:
checked }); then replace the inline onChange/onCheckedChange props on the
Industry filter, ScaleFilter, and Toggle with these handle* functions
(referencing setIsIndustryTouched, setIsScaleTouched, updateSearchParams and
isRecruited to locate state/props). Ensure the handlers use the same parameter
names used by the components and are defined in the same component scope so they
can be tested and reused.
♻️ Duplicate comments (1)
src/pages/home/search-section/search-section.tsx (1)

30-43: ⚠️ Potential issue | 🟠 Major

필터 코드 검증 로직이 실제 코드셋과 불일치하고, null 단언으로 잘못된 파라미터가 전파됩니다

Line [32] / Line [41]의 하드코딩 목록이 실제 IndustryCode/ScaleCode 정의와 달라서 유효한 URL 값이 삭제될 수 있습니다. 또한 Line [31], Line [36], Line [42] 흐름에서는 nullas IndustryCode/as ScaleCode로 통과되어 런타임에서 params로 전달될 수 있습니다. 단언 대신 “실제 코드셋 기반 파싱 + undefined 반환”으로 바꾸는 게 안전합니다.

수정 예시
+import { FILTER_INDUSTRY, FILTER_SCALE } from "@/shared/config/company";
 import type { IndustryCode, ScaleCode } from "@/shared/config";
@@
-  const isValidIndustry =
-    industryParam === null ||
-    ["IT_SERVICE", "COMMERCE", "FINANCE", "CONSUMER_GOODS"].includes(
-      industryParam
-    );
-  const industry = isValidIndustry
-    ? (industryParam as IndustryCode)
-    : undefined;
+  const INDUSTRY_CODES = new Set(
+    FILTER_INDUSTRY.map(({ code }) => code)
+  );
+  const industry =
+    industryParam && INDUSTRY_CODES.has(industryParam as IndustryCode)
+      ? (industryParam as IndustryCode)
+      : undefined;
@@
-  const isValidScale =
-    scaleParam === null ||
-    ["STARTUP", "SMALL", "MID_LARGE", "LARGE"].includes(scaleParam);
-  const scale = isValidScale ? (scaleParam as ScaleCode) : undefined;
+  const SCALE_CODES = new Set(FILTER_SCALE.map(({ code }) => code));
+  const scale =
+    scaleParam && SCALE_CODES.has(scaleParam as ScaleCode)
+      ? (scaleParam as ScaleCode)
+      : undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/home/search-section/search-section.tsx` around lines 30 - 43, The
hardcoded validation lists in isValidIndustry/isValidScale must be replaced by
checking against the real code sets (use the canonical IndustryCode and
ScaleCode enums/arrays) and remove the null/assert casts; change the logic so
industryParam and scaleParam are parsed against those canonical sets and if not
matched return undefined (do not cast null to IndustryCode/ScaleCode). Update
the variables industry and scale to be derived from the safe-parse result (e.g.,
parseIndustry(industryParam) and parseScale(scaleParam)) so invalid or null
inputs yield undefined rather than being asserted through.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/home/search-section/search-section.tsx`:
- Around line 63-99: The effect that enforces URL normalization can run too
early because placeholderData keeps a stale totalPage; update the guard in the
useEffect that computes isExceedingPage so it also returns when placeholder data
is present—i.e., add isPlaceholderData alongside isLoading in the early
return—and leave the rest of the logic (isExceedingPage, isInvalidRecruited,
constructing newParams, and calling setSearchParams with replace) unchanged;
this ensures currentPage/totalPage corrections only run on real data rather than
while previousData is being used.

---

Outside diff comments:
In `@src/pages/home/search-section/search-section.tsx`:
- Around line 167-190: Extract the inline handlers into named functions: create
handleIndustryChange(newIndustry) that calls setIsIndustryTouched(true) and
updateSearchParams({ industry: newIndustry }); create
handleScaleChange(newScale) that calls setIsScaleTouched(true) and
updateSearchParams({ scale: newScale }); and create
handleRecruitedChange(checked) that calls updateSearchParams({ isRecruited:
checked }); then replace the inline onChange/onCheckedChange props on the
Industry filter, ScaleFilter, and Toggle with these handle* functions
(referencing setIsIndustryTouched, setIsScaleTouched, updateSearchParams and
isRecruited to locate state/props). Ensure the handlers use the same parameter
names used by the components and are defined in the same component scope so they
can be tested and reused.

---

Duplicate comments:
In `@src/pages/home/search-section/search-section.tsx`:
- Around line 30-43: The hardcoded validation lists in
isValidIndustry/isValidScale must be replaced by checking against the real code
sets (use the canonical IndustryCode and ScaleCode enums/arrays) and remove the
null/assert casts; change the logic so industryParam and scaleParam are parsed
against those canonical sets and if not matched return undefined (do not cast
null to IndustryCode/ScaleCode). Update the variables industry and scale to be
derived from the safe-parse result (e.g., parseIndustry(industryParam) and
parseScale(scaleParam)) so invalid or null inputs yield undefined rather than
being asserted through.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 31e8e2d2-2af7-4851-b915-70b9c9bd33f4

📥 Commits

Reviewing files that changed from the base of the PR and between 8f4199e and 73cfe54.

📒 Files selected for processing (1)
  • src/pages/home/search-section/search-section.tsx

Comment on lines +63 to +99
// URL 강제 교정
useEffect(() => {
if (isLoading) return;

const isExceedingPage = currentPage > totalPage && totalPage > 0;
const isInvalidRecruited =
recruitedParam !== null &&
recruitedParam !== "true" &&
recruitedParam !== "false";

if (
isInvalidPage ||
isExceedingPage ||
!isValidIndustry ||
!isValidScale ||
isInvalidRecruited
) {
const newParams = new URLSearchParams(searchParams);

if (isInvalidPage || isExceedingPage) newParams.set("page", "1");
if (!isValidIndustry) newParams.delete("industry");
if (!isValidScale) newParams.delete("scale");
if (isInvalidRecruited) newParams.set("isRecruited", "true");

setSearchParams(newParams, { replace: true });
}
}, [
currentPage,
totalPage,
isInvalidPage,
isValidIndustry,
isValidScale,
recruitedParam,
isLoading,
searchParams,
setSearchParams,
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

placeholderData 상태에서 페이지 강제 교정이 조기 실행될 수 있습니다

useGetCompaniesplaceholderData: previousData를 사용하므로, 새 쿼리 전환 직후에는 이전 totalPage가 잠시 유지됩니다. 현재 Line [65]는 isLoading만 가드해서, placeholder 구간에 Line [67]의 isExceedingPage가 참이 되면 정상 페이지도 1로 리셋될 수 있습니다. isPlaceholderData도 함께 가드해 주세요.

수정 예시
-  useEffect(() => {
-    if (isLoading) return;
+  useEffect(() => {
+    if (isLoading || isPlaceholderData) return;
@@
   }, [
@@
     isLoading,
+    isPlaceholderData,
     searchParams,
     setSearchParams,
   ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// URL 강제 교정
useEffect(() => {
if (isLoading) return;
const isExceedingPage = currentPage > totalPage && totalPage > 0;
const isInvalidRecruited =
recruitedParam !== null &&
recruitedParam !== "true" &&
recruitedParam !== "false";
if (
isInvalidPage ||
isExceedingPage ||
!isValidIndustry ||
!isValidScale ||
isInvalidRecruited
) {
const newParams = new URLSearchParams(searchParams);
if (isInvalidPage || isExceedingPage) newParams.set("page", "1");
if (!isValidIndustry) newParams.delete("industry");
if (!isValidScale) newParams.delete("scale");
if (isInvalidRecruited) newParams.set("isRecruited", "true");
setSearchParams(newParams, { replace: true });
}
}, [
currentPage,
totalPage,
isInvalidPage,
isValidIndustry,
isValidScale,
recruitedParam,
isLoading,
searchParams,
setSearchParams,
]);
// URL 강제 교정
useEffect(() => {
if (isLoading || isPlaceholderData) return;
const isExceedingPage = currentPage > totalPage && totalPage > 0;
const isInvalidRecruited =
recruitedParam !== null &&
recruitedParam !== "true" &&
recruitedParam !== "false";
if (
isInvalidPage ||
isExceedingPage ||
!isValidIndustry ||
!isValidScale ||
isInvalidRecruited
) {
const newParams = new URLSearchParams(searchParams);
if (isInvalidPage || isExceedingPage) newParams.set("page", "1");
if (!isValidIndustry) newParams.delete("industry");
if (!isValidScale) newParams.delete("scale");
if (isInvalidRecruited) newParams.set("isRecruited", "true");
setSearchParams(newParams, { replace: true });
}
}, [
currentPage,
totalPage,
isInvalidPage,
isValidIndustry,
isValidScale,
recruitedParam,
isLoading,
isPlaceholderData,
searchParams,
setSearchParams,
]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/home/search-section/search-section.tsx` around lines 63 - 99, The
effect that enforces URL normalization can run too early because placeholderData
keeps a stale totalPage; update the guard in the useEffect that computes
isExceedingPage so it also returns when placeholder data is present—i.e., add
isPlaceholderData alongside isLoading in the early return—and leave the rest of
the logic (isExceedingPage, isInvalidRecruited, constructing newParams, and
calling setSearchParams with replace) unchanged; this ensures
currentPage/totalPage corrections only run on real data rather than while
previousData is being used.

@TEAM-COMFIT TEAM-COMFIT deleted a comment from coderabbitai bot Mar 17, 2026
@TEAM-COMFIT TEAM-COMFIT deleted a comment from coderabbitai bot Mar 17, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/matching-list/matching-list-page.tsx`:
- Around line 56-67: handleSearch and handlePageChange overwrite the entire URL
search params and drop any existing filters/sort; change both handlers to merge
updates into the current search params instead of replacing them: read the
current params (via the searchParams object or the setter callback), copy them,
update only keyword or page (use the existing keyword variable for page updates
and newKeyword for search), then call setSearchParams with the merged params so
other query keys are preserved; update references in handleSearch,
handlePageChange, and any call sites using setSearchParams.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f027d835-b173-4ead-bf2f-13f2d7838932

📥 Commits

Reviewing files that changed from the base of the PR and between 73cfe54 and c84f628.

📒 Files selected for processing (1)
  • src/pages/matching-list/matching-list-page.tsx

@hummingbbird
Copy link
Copy Markdown
Contributor Author

@u-zzn 유진님의 꼼꼼한 코드리뷰 감사합니다! 사실 해당 부분 관련해서는 다음 스프린트에서 리팩토링을 고려했어요. 현재 queryString을 이용해 페이지를 파싱하는 방식은 아래와 같은 문제점이 있어요.

  1. 강제 교정 코드와 ui 로직이 섞여 책임 분리가 명확하지 않음
  2. 페이지네이션이 포함된 3군데 페이지에 중복된 코드가 존재

그리하여 toss의 queryString 라이브러리 공식 문서를 참고하여 쿼리파라미터값을 제어할 수 있는 훅을 따로 분리하는 작업을 고민중에 있습니다.

다만, 1차 스프린트 내에 진행해야 하는, 우선순위가 더 높은 작업이 존재하기 때문에 이번 스프린트에서는 다음과 같이 개선한 후, 추후에 리팩토링을 진행하고자 합니다.


⚡️ 존재하지 않는, 유효하지 않은 페이지 혹은 타입으로 접근 시 유효한 값으로 강제 교정
쿼리 스트링에 직접적으로 접근하여 유효하지 않은 값에 접근 시도를 할 경우, 유효한 값에 접근하도록 강제 교정하도록 코드를 개선해주었습니다. 그 과정에서 파일 자체의 길이는 길어졌지만, 사용자 경험을 고려하였을 때 사용자가 404에러를 직면하게 하는 것보다는 훨씬 나은 방식이라고 판단했습니다. 그리하여

  • page 음수 혹은 문자 접근 시 -> page=1으로 강제 교정
  • 필터값 타입 외의 값으로 접근 시 -> 전체로 강제 교정
  • 그 외의 검색(keyword) -> 기존 방식 그대로 유지(searchParameter가 검색어로 사용됨)

위와 같은 방식으로 개선 후, 다음 스프린트에서 관심사 분리를 진행하는 방식으로 개선해나가려고 하는데 여러분 생각은 어떠신가요?? 😊😊

@u-zzn
Copy link
Copy Markdown
Collaborator

u-zzn commented Mar 17, 2026

상세하게 공유해주셔서 감사합니다! 😊

말씀해주신 것처럼 현재 구조에서는 쿼리 파라미터 파싱과 UI 로직이 일부 섞여 있고, 페이지네이션 로직도 여러 페이지에 분산되어 있어 이후 유지보수나 확장 시 부담이 될 수 있는 부분이라고 생각이 드네요.

이번 스프린트에서는 말씀해주신 방향대로 유효하지 않은 값에 대해 안전하게 보정하는 것에 집중하고, 다음 단계에서 queryString 기반으로 파싱/검증 로직을 분리하면서 관심사 분리와 중복 제거까지 함께 가져가는 흐름이 자연스럽고 좋은 접근인 것 같습니다 👍

특히 페이지 값이나 필터값을 다루는 부분은 공통 유틸이나 레이어로 분리된다면 각 페이지에서는 “사용”에만 집중할 수 있어서 구조적으로도 더 안정적일 것 같네요 🙂

이후 리팩토링 과정에서 해당 부분들이 어떻게 정리될지 기대됩니다! 💪

@odukong
Copy link
Copy Markdown
Collaborator

odukong commented Mar 17, 2026

사용자가 올바른 쿼리 URL을 입력하지 않았을 상황을 고려하여 클라 단에서 쿼리를 강제시킴으로서 사용자는 일관된 페이지네이션 결과를 볼 수 있게 하는 것이 적절한 방향이라고 생각하고, 말씀해주신 대로 홈, 경험리스트, 매칭리스트 페이지에서 중복 코드가 작성되고 있는 상황이기 때문에 해당 이슈 역시 생각해주신 훅 분리로 리팩토링 진행해주시면 좋을 것 같아요.
세부적인 사항들은 채영님께서 리팩토링 진행하면서 챙겨주실 부분 같아 따로 언급하진 않겠습니다! (toss querystring 적용.. 기대하겠습니다) ( ̄︶ ̄)↗ 

@qowjdals23
Copy link
Copy Markdown
Collaborator

자세한 설명 남겨주신 덕분에 이번에 어떤 기준으로 우선순위를 잡으셨는지 잘 이해됐습니다 !

우선 이번 스프린트에서는 유효하지 않은 쿼리값을 안전하게 교정해서 사용자 경험을 우선 보완하고, 말씀주신 것처럼 공통으로 들어가는 로직들은 다음 스프린트에서 한 번 정리되면 더 좋아질 것 같아요 !!

추후에 훅으로 분리되면 구조도 더 깔끔해질 것 같아서 기대됩니다 🤩

@hummingbbird hummingbbird merged commit 55858a8 into dev Mar 17, 2026
3 checks passed
@hummingbbird hummingbbird deleted the feat/#152/pagination-state-persistence branch March 17, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 페이지네이션 상태 유지되도록 개선

4 participants