Skip to content

[REFACTOR] 필터링 및 검색 v2 로직으로 수정 #304

Merged
seong-hui merged 10 commits intodevelopfrom
feat/#290/filter-ssr
Aug 2, 2025
Merged

[REFACTOR] 필터링 및 검색 v2 로직으로 수정 #304
seong-hui merged 10 commits intodevelopfrom
feat/#290/filter-ssr

Conversation

@seong-hui
Copy link
Member

@seong-hui seong-hui commented Jul 31, 2025

🛰️ 관련 이슈

해결한 이슈 번호를 작성해주세요
close #290

🧑‍💻 작업 내용

작업한 내용을 간략히 작성해주세요

  • 신규 기능

    • 템플스테이 검색 결과를 필터, 정렬, 페이지네이션 기능과 함께 보여주는 새로운 검색 결과 페이지 도입.
    • URL 파라미터를 활용한 검색 및 필터링 기능 개선.
    • 검색 결과 페이지에서 위시리스트(찜) 기능 지원 및 로그인 안내 모달 제공.
  • 기능 개선

    • 필터, 가격, 검색어 등 검색 조건을 URL 파라미터 기반으로 일원화하여 검색 경험을 향상.
    • 필터 모달 내 필터 상태 관리를 로컬 상태로 전환하여 반응성 및 일관성 개선.
    • 지역별 지도 버튼 클릭 시 해당 지역으로 바로 검색 결과 이동 가능.
  • UI 변경

    • 필터 페이지 및 관련 Storybook 스토리 삭제.
    • 검색 결과 페이지 UI를 새로운 컴포넌트로 전면 교체.
    • 버튼 컴포넌트에 링크(href) 속성 추가로 네비게이션 지원.
  • 버그 수정 및 안정성

    • 위시리스트 토글 시 오류 발생 시 UI 롤백 처리.
    • 불필요한 상태 관리 코드 및 전역 atom 제거로 코드 간소화.
  • 정리 및 기타

    • 불필요한 파일 및 코드(필터 페이지, Storybook 등) 삭제.
    • .gitignore에 빌드 캐시 및 문서 파일 추가.

🗯️ PR 포인트

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

🚀 알게된 점

기록하며 개발하기!

📖 참고 자료 (선택)

참고했던 문서들 공유하기!

📸 스크린샷 (선택)

Summary by CodeRabbit

신규 기능

  • 검색 결과 페이지에 필터, 정렬, 페이지네이션, 위시리스트 기능이 포함된 새로운 클라이언트 컴포넌트가 추가되었습니다.
  • URL 쿼리 파라미터를 활용한 검색 및 필터링 기능이 개선되었습니다.
  • URL 쿼리 파라미터를 업데이트하는 커스텀 훅이 도입되었습니다.

기능 개선

  • 필터 및 검색 관련 API가 새로운 버전(v2)으로 교체되어 더욱 정교한 검색이 가능합니다.
  • 필터 상태 관리 방식이 Jotai atom에서 싱글톤 인스턴스 기반으로 변경되어 성능과 일관성이 향상되었습니다.
  • 버튼 컴포넌트에 href 속성이 추가되어 버튼 클릭 시 페이지 이동이 가능해졌습니다.
  • 필터 훅과 관련 컴포넌트들이 내부 상태 관리 및 데이터 페칭 로직을 단순화하였습니다.

버그 수정

  • 필터 및 검색 관련 일부 불필요한 상태 초기화 및 리셋 로직이 제거되어 동작이 간소화되었습니다.

구조 개선

  • 불필요한 스토리북 파일 및 필터 페이지 관련 코드가 삭제되어 코드베이스가 정리되었습니다.
  • 내부 스타일 및 상태 관리 코드가 단순화되었습니다.
  • 검색 결과 페이지 구현이 클라이언트 컴포넌트로 분리되어 구조가 개선되었습니다.

…오던 필터 및 검색 로직을 URL 쿼리 파라미터 기반으로 변경하여 SSR을 지원하도록 개선- useFilter 훅의 역할을 url 생성 및 라우팅 처리로 단순
- Jotai의 `filterListAtom` 대신 `filterListInstance`를 직접 사용하도록 로직을 수정합니다.
- 이로써 Jotai 의존성 없이 필터 상태를 관리할 수 있습니다.
- `useFilter` 훅과 `FilterModalContent` 컴포넌트에서 Jotai 관련 훅(`useAtom`, `useAtomValue`)을 제거하고 `filterListInstance`를 직접 참조하도록 업데이트했습니다.
  - FilterList 모델에 선택된 필터들을 그룹별로 묶어 반환하는 getGroupedSelectedFilters메서드를 새로 추가
@coderabbitai
Copy link

coderabbitai bot commented Jul 31, 2025

Walkthrough

이 변경사항은 필터링 로직을 기존의 내부 상태 및 Jotai atom 기반에서 URL의 query parameter를 활용하는 방식으로 대대적으로 리팩터링합니다. 필터 관련 컴포넌트, 훅, API, 스토리북 파일, 스타일 파일 등이 삭제·수정·추가되었으며, 검색 결과 페이지 렌더링 및 상태 관리 방식이 변경되었습니다.

Changes

Cohort / File(s) Change Summary
필터 API 리팩터링
src/apis/filter/axios.ts, src/apis/filter/index.ts, src/apis/filter/type.ts
필터 API가 v2로 교체되어, POST 기반에서 GET 및 query param 기반으로 변경. 파라미터 변환 함수 추가 및 기존 함수 제거/수정.
필터 상태 및 훅 리팩터링
src/hooks/useFilter.ts, src/store/store.ts, src/model/filter/filterList.ts
Jotai atom 제거, 싱글톤 인스턴스로 필터 상태 관리. useFilter 훅에서 내부 상태/데이터 패칭 제거, query param 기반 검색 및 리셋 지원.
필터 UI 및 페이지 제거
src/app/filter/page.tsx, src/app/filter/filterPage.css.ts, src/stories/filterPage.stories.ts
필터 페이지 및 관련 스타일, 스토리북 파일 삭제.
검색 결과 페이지 리팩터링
src/app/searchResult/SearchResultPageClient.tsx, src/app/searchResult/page.tsx, src/stories/SearchResultPage.stories.ts
검색 결과 페이지 클라이언트 컴포넌트 신설, 기존 페이지 단순화, 스토리북 파일 삭제.
공통 버튼 및 카드 컴포넌트 수정
src/components/common/button/basicBtn/BasicBtn.tsx, src/components/card/mapCard/LocBtn.tsx, src/components/card/mapCard/Map.tsx, src/components/card/lookCard/LookCard.tsx
버튼에 href prop 추가, 지역 버튼/카드에서 필터 훅 제거 및 query param 기반 이동으로 변경.
검색 및 최근 검색 컴포넌트 수정
src/components/search/recentBtn/RecentBtnBox.tsx, src/components/search/searchBar/SearchBar.tsx
handleSearch 호출 방식 string → object로 맞춤. 필터 리셋 로직 제거.
필터 모달 컴포넌트 로컬 상태화
src/components/filter/filterBottomSheetModal/FilterModalContent.tsx
jotai atom 대신 로컬 상태로 필터 관리, 검색시 선택된 필터만 전달.
유틸리티 추가
src/utils/updateSearchParams.ts
query param 기반 URL 업데이트용 커스텀 훅 추가.
기타
.gitignore, src/app/HomeClient.tsx
.gitignore에 파일 추가, HomeClient에서 필터 관련 상태 초기화 제거.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UI_Component
  participant useFilter
  participant Router

  User->>UI_Component: 필터/검색/버튼 클릭
  UI_Component->>useFilter: handleSearch({ ...queryParams })
  useFilter->>Router: /searchResult?{queryParams}로 이동
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
기존 로직을 유지하면서 필터링을 query param으로 전송 ( #290 )

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
.gitignore 파일에 CLAUDE.md, tsconfig.tsbuildinfo 추가 (.gitignore) 이 변경은 필터링 query param 전송과 무관한 개발 환경 관련 변경입니다.

Suggested labels

👩🏻‍🦲FEAT👩🏻‍🦲

Suggested reviewers

  • bykbyk0401
  • maylh

Poem

🐇
필터가 atom에서 벗어나
쿼리 파람 타고 훨훨 날아가네!
버튼엔 href, 지도엔 링크
검색 결과도 새롭게 빛나
코드도 깔끔, 리뷰도 신나
토끼도 춤추는 리팩터링의 봄!
🌸

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 428a065 and 16a02b9.

📒 Files selected for processing (3)
  • src/components/card/lookCard/LookCard.tsx (1 hunks)
  • src/components/card/mapCard/Map.tsx (1 hunks)
  • src/components/search/searchBar/SearchBar.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/search/searchBar/SearchBar.tsx
  • src/components/card/mapCard/Map.tsx
  • src/components/card/lookCard/LookCard.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Chromatic
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#290/filter-ssr

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link

github-actions bot commented Jul 31, 2025

🪷 Storybook 확인 🪷
🔗 https://677fa9af1269762b7858c29f-lzoskfhaxv.chromatic.com/

Copy link

@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

🔭 Outside diff range comments (1)
src/components/common/button/basicBtn/BasicBtn.tsx (1)

31-64: 조건부 렌더링 구현을 확인해주세요.

버튼 컨텐츠를 별도 상수로 추출하고 href 존재 여부에 따라 앵커 태그로 감싸는 로직은 좋지만, 몇 가지 개선이 필요합니다:

  1. 앵커 태그에 접근성 속성이 누락되었습니다
  2. 외부 URL에 대한 보안 고려사항이 없습니다

다음과 같이 개선해주세요:

- return href ? <a href={href}>{buttonContent}</a> : buttonContent;
+ return href ? (
+   <a 
+     href={href}
+     rel={href.startsWith('http') ? 'noopener noreferrer' : undefined}
+     role="button"
+     tabIndex={0}
+   >
+     {buttonContent}
+   </a>
+ ) : buttonContent;
🧹 Nitpick comments (5)
.gitignore (1)

32-33: dist 항목 중복 기재

Line 11에서 이미 dist 디렉터리를 무시하고 있으므로 Line 32의 동일 항목은 불필요한 중복입니다. 유지보수를 위해 한 곳만 남기고 제거하는 편이 깔끔합니다.

src/utils/updateSearchParams.ts (1)

1-19: 잘 구현된 URL 파라미터 업데이트 유틸리티

새로운 useUpdateSearchParams 훅이 깔끔하게 구현되었습니다:

✅ Next.js useRouter 올바른 사용
URLSearchParams를 통한 적절한 인코딩
✅ 모든 값을 문자열로 변환하여 URL 파라미터에 적합
✅ 하드코딩된 /searchResult 경로가 리팩터링 목적과 일치

다음과 같은 개선사항을 고려해보세요:

 const useUpdateSearchParams = () => {
   const router = useRouter();
 
-  const updateSearchParams = (params: Record<string, string | number>) => {
+  const updateSearchParams = (params: Record<string, string | number | undefined>) => {
     const urlParams = new URLSearchParams();
 
     Object.entries(params).forEach(([key, value]) => {
+      if (value !== undefined && value !== '') {
         urlParams.set(key, String(value));
+      }
     });
 
     router.push(`/searchResult?${urlParams.toString()}`);
   };

이렇게 하면 빈 값이나 undefined 값이 URL에 포함되지 않습니다.

src/app/searchResult/SearchResultPageClient.tsx (2)

94-98: 빈 onSuccess 콜백을 제거하거나 의미있는 로직을 추가하세요.

현재 onSuccess 콜백이 비어있어 불필요한 코드입니다.

다음 중 하나를 선택하세요:

  const mutationOptions = {
-   onSuccess: () => {},
    onError: () => {
      optimisticUpdate(!liked);
    },
  };

또는 성공 시 필요한 로직(예: 성공 메시지 표시)을 추가하세요.


144-147: 로그인 리디렉션 로직을 구현하세요.

로그인 버튼 클릭 시 실제 로그인 페이지로 이동하는 로직이 누락되어 있습니다.

  handleSubmit={() => {
-   // 로그인 이동
+   router.push('/login');
+   setIsModalOpen(false);
  }}

이 구현을 위해 useRouter 훅을 import하고 사용해야 합니다. 로그인 페이지 경로가 다르다면 올바른 경로로 수정해주세요.

src/hooks/useFilter.ts (1)

58-63: 불필요한 async 키워드 제거 권장

handleResetFilter 함수가 async로 선언되어 있지만 실제로 await를 사용하지 않고 있습니다.

다음과 같이 수정하는 것을 권장합니다:

-  const handleResetFilter = async () => {
+  const handleResetFilter = () => {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc101a and 81c53a6.

📒 Files selected for processing (22)
  • .gitignore (1 hunks)
  • src/apis/filter/axios.ts (1 hunks)
  • src/apis/filter/index.ts (1 hunks)
  • src/apis/filter/type.ts (1 hunks)
  • src/app/HomeClient.tsx (0 hunks)
  • src/app/filter/filterPage.css.ts (0 hunks)
  • src/app/filter/page.tsx (0 hunks)
  • src/app/searchResult/SearchResultPageClient.tsx (1 hunks)
  • src/app/searchResult/page.tsx (1 hunks)
  • src/components/card/lookCard/LookCard.tsx (1 hunks)
  • src/components/card/mapCard/LocBtn.tsx (1 hunks)
  • src/components/card/mapCard/Map.tsx (1 hunks)
  • src/components/common/button/basicBtn/BasicBtn.tsx (3 hunks)
  • src/components/filter/filterBottomSheetModal/FilterModalContent.tsx (3 hunks)
  • src/components/search/recentBtn/RecentBtnBox.tsx (1 hunks)
  • src/components/search/searchBar/SearchBar.tsx (2 hunks)
  • src/hooks/useFilter.ts (1 hunks)
  • src/model/filter/filterList.ts (1 hunks)
  • src/store/store.ts (1 hunks)
  • src/stories/SearchResultPage.stories.ts (0 hunks)
  • src/stories/filterPage.stories.ts (0 hunks)
  • src/utils/updateSearchParams.ts (1 hunks)
💤 Files with no reviewable changes (5)
  • src/app/filter/filterPage.css.ts
  • src/stories/SearchResultPage.stories.ts
  • src/app/filter/page.tsx
  • src/stories/filterPage.stories.ts
  • src/app/HomeClient.tsx
🧰 Additional context used
🧬 Code Graph Analysis (7)
src/apis/filter/type.ts (1)
src/apis/templeInfo/type.ts (5)
  • TempleDetail (1-17)
  • ReviewsResponse (39-46)
  • Review (29-37)
  • TemplestayImg (19-22)
  • TemplestayImgsResponse (24-27)
src/model/filter/filterList.ts (1)
src/model/filter/filter.ts (1)
  • Filter (1-31)
src/components/filter/filterBottomSheetModal/FilterModalContent.tsx (2)
src/store/store.ts (1)
  • filterListInstance (7-7)
src/components/filter/filterBottomSheetModal/FilterBottomSheetModal.tsx (1)
  • FilterBottomSheetModalProps (8-12)
src/hooks/useFilter.ts (1)
src/store/store.ts (1)
  • filterListInstance (7-7)
src/apis/filter/index.ts (2)
src/apis/filter/type.ts (1)
  • FetchFilteredListProps (14-20)
src/apis/filter/axios.ts (2)
  • convertToV2Params (19-74)
  • fetchFilteredListV2 (7-16)
src/components/card/mapCard/Map.tsx (1)
src/constants/regionInfos.ts (2)
  • REGION_LABEL_MAP (18-33)
  • REGION_INFOS (1-16)
src/store/store.ts (1)
src/model/filter/filter.ts (1)
  • Filter (1-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Chromatic
  • GitHub Check: Build project on pull_request to develop
🔇 Additional comments (24)
.gitignore (1)

34-37: 신규 빌드 산출물 무시 추가 👍

CLAUDE.md와 TypeScript 인크리멘털 빌드 캐시(tsconfig.tsbuildinfo)를 .gitignore에 포함한 것은 적절한 선택입니다. 불필요한 파일이 저장소에 커밋되는 것을 방지할 수 있습니다.

src/apis/filter/type.ts (1)

22-34: 새로운 검색 파라미터 인터페이스의 타입 일관성 확인 필요

TemplestaySearchParamsV2 인터페이스에서 몇 가지 검토가 필요합니다:

  1. userIdsize 속성이 string | number 타입으로 정의되어 있어 타입 일관성 문제가 발생할 수 있습니다. URL 쿼리 파라미터는 항상 문자열이므로 string 타입으로 통일하는 것이 좋겠습니다.

  2. 기존 FilterType 인터페이스에 있던 purpose 속성이 새 인터페이스에서 누락되었습니다. 이것이 의도된 변경인지 확인이 필요합니다.

다음과 같이 수정을 제안합니다:

 export interface TemplestaySearchParamsV2 {
   region?: string;
   type?: string;
   activity?: string;
   etc?: string;
+  purpose?: string;
   min?: number;
   max?: number;
   sort?: string;
   search?: string;
   page?: number;
-  userId?: string;
-  size?: number;
+  userId?: string;
+  size?: string;
 }

Likely an incorrect or invalid review comment.

src/components/search/recentBtn/RecentBtnBox.tsx (1)

30-32: LGTM! 새로운 API 시그니처에 맞게 올바르게 수정됨

handleSearch 함수 호출이 새로운 객체 기반 파라미터 형식에 맞게 올바르게 수정되었습니다. 이는 확장성 측면에서 더 나은 API 설계입니다.

src/components/search/searchBar/SearchBar.tsx (3)

17-17: 필터 리셋 로직 제거 확인

handleResetFilter가 제거된 것이 새로운 URL 기반 필터링 아키텍처와 일치합니다. 내부 상태 관리에서 URL 쿼리 파라미터 기반으로 변경되면서 리셋 로직이 불필요해진 것으로 보입니다.


36-36: LGTM! 새로운 검색 API 시그니처에 맞게 수정됨

handleSearch 호출이 객체 파라미터 형식으로 올바르게 변경되었습니다. 다른 컴포넌트들과 일관성을 유지합니다.


49-54: useEffect 로직 단순화 확인

필터 리셋 로직이 제거되고 pathname 관련 localStorage 설정만 남은 것이 새로운 아키텍처와 일치합니다. URL 기반 필터링에서는 필터 리셋이 URL 파라미터 제거로 처리되므로 적절한 변경입니다.

src/components/card/lookCard/LookCard.tsx (2)

13-15: 네비게이션 로직 단순화 확인

useFilter 사용이 제거되고 프로그래매틱 검색 대신 href prop을 통한 직접 네비게이션으로 변경된 것이 새로운 아키텍처와 일치합니다.


39-39: BasicBtn href prop 지원 확인 완료

  • ButtonProps 인터페이스에 href?: string이 정의되어 있습니다.
  • BasicBtn 컴포넌트에서 href를 구조 분해 할당 후,
  • href 값이 있을 경우 <a href={href}>{buttonContent}</a>로 래핑하여 링크 이동을 지원합니다.

문제없습니다.

src/components/common/button/basicBtn/BasicBtn.tsx (1)

14-14: 인터페이스 확장이 적절합니다.

href 속성을 선택적으로 추가하여 버튼이 링크로도 동작할 수 있도록 한 변경사항이 좋습니다.

src/components/card/mapCard/LocBtn.tsx (2)

12-15: 속성 전달 구현이 올바릅니다.

href 속성을 BasicBtn에 올바르게 전달하고 있습니다.


9-9: href 필수 속성 추가에 문제 없음

rg --glob 검색 결과, LocBtn 컴포넌트의 모든 사용처(Map.tsx)에서 이미 href를 전달하고 있음을 확인했습니다.

  • src/components/card/mapCard/Map.tsx
    <LocBtn … href={/searchResult?${searchParams.toString()}} … />

따라서 href를 필수 속성(required)으로 변경해도 무방합니다.

src/store/store.ts (1)

7-7: 필터 상태 반응성 관리 확인됨

filterListInstance를 사용하는 컴포넌트들은 다음과 같이 수동으로 React 상태를 업데이트하여 UI 리렌더링을 보장하고 있습니다.

  • useFilter 훅 내에서 toggleFilter/handleResetFilter 호출 후 queryClient.invalidateQueries로 데이터 갱신
  • FilterModalContent 컴포넌트에서 useState(() => filterListInstance.getAllStates())를 사용해 초기 상태 설정
  • 필터 토글/리셋 시마다 filterListInstance.getAllStates() 결과로 setFiltersState를 호출

이로써 Jotai atom 없이도 React 상태 및 React Query를 통해 필터 변경 시 UI 업데이트가 정상 동작하므로 추가 조치는 필요하지 않습니다.

src/app/searchResult/page.tsx (1)

1-5: ✅ SearchResultPageClient 구현 확인 완료

src/app/searchResult/SearchResultPageClient.tsx 파일이 존재하며, 기존 검색 결과 로직을 모두 처리하고 있습니다. 서버 컴포넌트(page.tsx)를 최소 래퍼로 두고, 클라이언트 로직을 분리한 구조가 Next.js App Router 패턴에 부합합니다. 변경 사항을 승인합니다.

src/components/card/mapCard/Map.tsx (2)

13-13: 이벤트 로깅만 남긴 것이 적절합니다.

필터 토글과 검색 로직을 제거하고 이벤트 로깅만 유지한 것이 새로운 URL 기반 네비게이션 패턴에 맞습니다.


19-37: URL 파라미터 검증 완료

  • search, page, sort, region 키는 SearchResultPageClient에서 사용하는 이름과 일치합니다.
  • sort: 'recommended' 값은 SORT_OPTIONS.RECOMMEND(='recommended')로 정의된 유효한 옵션입니다.
  • search 파라미터가 빈 문자열로 넘어가도 get('search') ?? '' 기본값('')과 동일하게 동작하므로, 제거해도 기능에는 영향이 없습니다(선택 사항).

변경 불필요합니다.

src/model/filter/filterList.ts (1)

48-63: 새로운 메서드 구현이 올바릅니다.

getGroupedSelectedFilters 메서드가 선택된 필터들을 그룹별로 정확하게 분류하여 반환하고 있습니다. v2 API의 쿼리 파라미터 형식에 맞춰 각 카테고리별로 선택된 필터 이름들을 배열로 제공하는 구조가 적절합니다.

src/apis/filter/index.ts (1)

1-34: v2 API 마이그레이션이 올바르게 구현되었습니다.

새로운 훅 구현이 다음과 같이 적절합니다:

  • sort 매개변수 추가로 정렬 기능 지원
  • convertToV2Params를 통한 매개변수 변환
  • fetchFilteredListV2 함수 호출
  • 쿼리 캐시 키 업데이트 (filteredListV2)

React Query 패턴을 올바르게 따르고 있으며 v2 API 설계와 일관성을 유지하고 있습니다.

src/components/filter/filterBottomSheetModal/FilterModalContent.tsx (1)

22-33: 로컬 상태 관리 패턴이 적절하게 구현되었습니다.

싱글톤 인스턴스와 로컬 상태를 동기화하는 패턴이 올바르게 구현되어 있습니다. 전역 필터 상태 변경 후 로컬 UI 상태를 업데이트하는 래퍼 함수들이 일관성 있게 작성되었습니다.

src/apis/filter/axios.ts (2)

6-16: v2 API 함수가 올바르게 구현되었습니다.

GET 요청으로 변경된 새로운 API 호출 방식이 적절하며, 에러 처리도 기존 패턴을 일관성 있게 따르고 있습니다.


18-74: 매개변수 변환 로직이 효율적이고 정확합니다.

convertToV2Params 함수가 다음과 같이 잘 구현되었습니다:

  • 선택적 매개변수들의 조건부 포함 처리
  • 필터 그룹별 선택된 항목들을 쉼표로 구분된 문자열로 변환
  • 각 필터 타입(region, type, activity, etc)에 대한 일관된 처리
  • 빈 값이나 기본값 필터링으로 불필요한 매개변수 제거

코드가 깔끔하고 유지보수성이 좋습니다.

src/hooks/useFilter.ts (4)

6-17: 타입 정의가 적절합니다!

필터링을 위한 query parameter 타입이 잘 정의되어 있습니다. 배열 타입과 개별 값 타입이 적절하게 구분되어 있습니다.


32-55: 검색 로직이 잘 구현되었습니다!

URLSearchParams를 활용한 query string 생성 로직이 깔끔하게 구현되어 있고, 배열 값 처리와 빈 값 필터링이 적절히 처리되어 있습니다. useCallback을 사용한 메모이제이션도 적절합니다.


21-28: filterListInstance 초기화 상태 검증 완료

src/store/store.ts에서 아래와 같이 즉시 new FilterList(FILTERS)로 인스턴스를 생성하여 export하고 있으므로, 런타임 시점에 초기화되지 않아 발생할 수 있는 에러는 없습니다. 별도의 초기화 확인 로직 추가는 불필요합니다.


65-65: useFilter 훅 반환값 변경 영향 없음 확인됨

아래 4개 컴포넌트에서 모두 제거된 상태 값(price, totalCount 등)이 아닌 함수(toggleFilter, handleResetFilter, handleSearch)만 사용 중임을 확인했습니다. 훅 반환값 변경으로 인한 문제가 없습니다.

  • src/components/search/recentBtn/RecentBtnBox.tsx: const { handleSearch } = useFilter();
  • src/components/search/searchBar/SearchBar.tsx: const { handleSearch } = useFilter();
  • src/components/filter/filterBottomSheetModal/FilterModalContent.tsx: const { toggleFilter, handleResetFilter, handleSearch } = useFilter();
  • src/components/common/empty/wishEmpty/WishEmpty.tsx: const { handleSearch } = useFilter();

<ButtonBar
type="reset"
label={`${totalCount || 0}개의 템플스테이 보기`}
label={`${0}개의 템플스테이 보기`}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

하드코딩된 템플스테이 개수를 실제 값으로 교체해야 합니다.

버튼 라벨의 ${0}개의 템플스테이 보기가 하드코딩되어 있어 사용자에게 잘못된 정보를 제공합니다.

실제 필터링된 결과 개수를 표시하도록 수정이 필요합니다. 다음 방법들을 고려해보세요:

  1. 필터링된 결과 개수를 별도로 조회하는 API 호출
  2. 상위 컴포넌트에서 개수 정보를 전달받아 사용
  3. 로딩 상태일 때는 "검색 중..." 등의 텍스트 표시
- label={`${0}개의 템플스테이 보기`}
+ label={isLoading ? "검색 중..." : `${totalCount}개의 템플스테이 보기`}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/filter/filterBottomSheetModal/FilterModalContent.tsx at line
60, replace the hardcoded 0 in the label `${0}개의 템플스테이 보기` with the actual count
of filtered templestays. To fix this, obtain the filtered results count either
by fetching it from an API, receiving it as a prop from the parent component, or
managing it via state. Also, handle the loading state by displaying a
placeholder text like "검색 중..." until the count is available.

Copy link
Collaborator

@maylh maylh left a comment

Choose a reason for hiding this comment

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

😳 현재 다음 페이지로 이동 시 405 에러가 발생하는데, 해당 부분 확인이 필요해보입니다 !!!

추가로 지금 로딩이 좀 느려서 새로고침 & 페이지 진입 시 ~에 대한 검색 결과가 없어요 문구가 잠깐 노출되는 현상이 있어서, isLoading 넣어도 좋을 것 같습니다 (해줘요)

수고하셨어요이 ~

Comment on lines +36 to +70
// 각 필터 그룹의 선택된 아이템들을 콤마로 구분된 문자열로 변환
if (groupedFilters.region) {
const selectedRegions = Object.entries(groupedFilters.region)
.filter(([, isSelected]) => isSelected)
.map(([region]) => region);
if (selectedRegions.length > 0) {
params.region = selectedRegions.join(',');
}
}

if (groupedFilters.type) {
const selectedTypes = Object.entries(groupedFilters.type)
.filter(([, isSelected]) => isSelected)
.map(([type]) => type);
if (selectedTypes.length > 0) {
params.type = selectedTypes.join(',');
}
}

if (groupedFilters.activity) {
const selectedActivities = Object.entries(groupedFilters.activity)
.filter(([, isSelected]) => isSelected)
.map(([activity]) => activity);
if (selectedActivities.length > 0) {
params.activity = selectedActivities.join(',');
}
}

if (groupedFilters.etc) {
const selectedEtc = Object.entries(groupedFilters.etc)
.filter(([, isSelected]) => isSelected)
.map(([etc]) => etc);
if (selectedEtc.length > 0) {
params.etc = selectedEtc.join(',');
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

각 필터 그룹에 대해 동일한 패턴의 코드가 반복되고 있어, 해당 부분을 별도의 헬퍼 함수로 분리한 후 이를 활용해 각 필터 값을 추출하는 방식으로 리팩토링하면 좋을 것 같습니다 !

Suggested change
// 각 필터 그룹의 선택된 아이템들을 콤마로 구분된 문자열로 변환
if (groupedFilters.region) {
const selectedRegions = Object.entries(groupedFilters.region)
.filter(([, isSelected]) => isSelected)
.map(([region]) => region);
if (selectedRegions.length > 0) {
params.region = selectedRegions.join(',');
}
}
if (groupedFilters.type) {
const selectedTypes = Object.entries(groupedFilters.type)
.filter(([, isSelected]) => isSelected)
.map(([type]) => type);
if (selectedTypes.length > 0) {
params.type = selectedTypes.join(',');
}
}
if (groupedFilters.activity) {
const selectedActivities = Object.entries(groupedFilters.activity)
.filter(([, isSelected]) => isSelected)
.map(([activity]) => activity);
if (selectedActivities.length > 0) {
params.activity = selectedActivities.join(',');
}
}
if (groupedFilters.etc) {
const selectedEtc = Object.entries(groupedFilters.etc)
.filter(([, isSelected]) => isSelected)
.map(([etc]) => etc);
if (selectedEtc.length > 0) {
params.etc = selectedEtc.join(',');
}
const getSelectedItems = (filterGroup?: Record<string, number>): string | undefined => {
if (!filterGroup) return undefined;
const selected = Object.entries(filterGroup)
.filter(([, value]) => value === 1)
.map(([item]) => item);
return selected.length > 0 ? selected.join(',') : undefined;
};
params.region = getSelectedItems(groupedFilters.region);
params.type = getSelectedItems(groupedFilters.type);
params.activity = getSelectedItems(groupedFilters.activity);
params.etc = getSelectedItems(groupedFilters.etc);

import { useEffect, useState } from 'react';
import useEventLogger from 'src/gtm/hooks/useEventLogger';
import { filterListAtom } from 'src/store/store';
const SearchResult = () => <SearchResultPageClient />;
Copy link
Collaborator

Choose a reason for hiding this comment

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

SearchResultPageClient가 페이지의 모든 UI와 로직을 담고 있어서, 현재와 같은 분리 방식도 결국 페이지 전체를 Client Component처럼 동작하게 만든다는 점에서는 기존과 큰 차이가 없을 것 같은데 어떤 이유로 분리하신건지 궁금해요 🥸

Comment on lines +190 to +195
<SearchCardList
data={templestays}
layout="horizontal"
onToggleWishlist={handleToggleWishlist}
onRequireLogin={() => setIsModalOpen(true)}
/>
Copy link
Collaborator

Choose a reason for hiding this comment

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

기존 태그 위치에 사찰명이 위치하는 것으로 변경되어서 이 부분도 컴포넌트 수정이 필요할 것 같습니다 (다음 태스크로 넘겨도 될 것 같긴 함)

import { filterListAtom, priceAtom, contentAtom } from 'src/store/store';
import { filterListInstance } from 'src/store/store';

type FilterQueryParams = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

TemplestaySearchParamsV2를 import해서 쓰면 이 부분은 지워도 될 것 같아요!

Copy link
Collaborator

@Taew00k Taew00k left a comment

Choose a reason for hiding this comment

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

필터링 검색 로직 복잡했는데 SSR이랑 v2로 잘 바꿔주신 것 같아요 가현이가 말한 것 처럼 처음 검색 결과 이동시 ~에 대한 검색 결과가 없어요 문구가 잠깐 노출되는 현상만 수정해주시면 좋을 것 같아요~~

Copy link

@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: 0

♻️ Duplicate comments (1)
src/app/searchResult/SearchResultPageClient.tsx (1)

197-202: 기존 태그 위치 관련 이슈가 남아있습니다.

이전 리뷰에서 언급된 사찰명 위치 변경으로 인한 컴포넌트 수정이 여전히 필요해 보입니다.

🧹 Nitpick comments (4)
src/apis/filter/index.ts (1)

5-12: useQuery 기반 hook으로의 전환이 잘 구현되었습니다.

useMutation에서 useQuery로의 변경은 적절한 선택입니다. 자동 캐싱과 백그라운드 리페칭의 이점을 활용할 수 있습니다. placeholderData를 통한 이전 데이터 유지도 사용자 경험 개선에 도움이 됩니다.

다만 쿼리 stale time을 설정하여 불필요한 리페칭을 방지하는 것을 고려해보세요.

선택적 개선사항:

  return useQuery({
    queryKey: ['filteredListV2', params],
    queryFn: () => fetchFilteredListV2(params),
    enabled: true,
    placeholderData: (previousData) => previousData,
+   staleTime: 1000 * 60 * 5, // 5분간 fresh 상태 유지
  });
src/app/searchResult/SearchResultPageClient.tsx (3)

43-54: 쿼리 파라미터 구성이 체계적으로 잘 되어 있습니다.

URL 파라미터를 적절히 파싱하고 기본값을 제공하고 있습니다. 다만 하드코딩된 기본값들을 상수로 분리하는 것을 고려해보세요.

선택적 개선사항:

+ const DEFAULT_PRICE_MIN = 0;
+ const DEFAULT_PRICE_MAX = 30;
+ const DEFAULT_PAGE_SIZE = 5;

  const queryParams: TemplestaySearchParamsV2 = {
    region: getJoinedArrayParam('region'),
    type: getJoinedArrayParam('type'),
    activity: getJoinedArrayParam('activity'),
    etc: getJoinedArrayParam('etc'),
-   min: Number(searchParams.get('min') ?? '0'),
-   max: Number(searchParams.get('max') ?? '30'),
+   min: Number(searchParams.get('min') ?? DEFAULT_PRICE_MIN),
+   max: Number(searchParams.get('max') ?? DEFAULT_PRICE_MAX),
    sort: searchParams.get('sort') ?? SORT_OPTIONS.RECOMMEND,
    search: searchParams.get('search') ?? '',
    page: Number(searchParams.get('page') ?? '1'),
-   size: Number(searchParams.get('size') ?? '5'),
+   size: Number(searchParams.get('size') ?? DEFAULT_PAGE_SIZE),
  };

72-110: 찜하기 토글 로직이 잘 구현되었지만 타입 안전성을 개선할 수 있습니다.

optimistic update와 rollback 로직이 잘 구현되어 있고, 로그인 상태 확인도 적절합니다. 다만 타입 캐스팅 부분에서 더 안전한 타입 가드를 사용하는 것을 권장합니다.

타입 안전성 개선:

    const optimisticUpdate = (newLiked: boolean) => {
      queryClient.setQueryData(['filteredListV2', queryParams], (oldData: unknown) => {
-       if (!oldData || typeof oldData !== 'object') return oldData;
-       const data = oldData as { content: TempleStay[] };
+       if (!oldData || typeof oldData !== 'object' || !('content' in oldData)) return oldData;
+       const data = oldData as { content: TempleStay[]; totalPages: number; currentPage: number };
        return {
          ...data,
          content: data.content.map((item: TempleStay) =>
            item.templestayId === templestayId ? { ...item, liked: newLiked } : item,
          ),
        };
      });
    };

151-153: 로그인 이동 로직이 구현되지 않았습니다.

모달의 로그인 버튼 클릭 시 실제 로그인 페이지로 이동하는 로직이 누락되어 있습니다.

다음 코드로 로그인 이동을 구현할 수 있습니다:

  handleSubmit={() => {
-   // 로그인 이동
+   setIsModalOpen(false);
+   // 로그인 페이지로 이동 (실제 라우팅 경로에 맞게 수정)
+   window.location.href = '/login';
  }}

실제 로그인 페이지 경로에 맞게 수정해야 합니다. Next.js router를 사용하고 싶다면 해당 이슈를 생성해드릴까요?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a475aef and d68d131.

📒 Files selected for processing (2)
  • src/apis/filter/index.ts (1 hunks)
  • src/app/searchResult/SearchResultPageClient.tsx (1 hunks)
🔇 Additional comments (9)
src/apis/filter/index.ts (2)

1-3: LGTM! 적절한 import 구문입니다.

새로운 V2 API와 타입을 정확하게 가져오고 있습니다.


14-14: LGTM! default export 변경이 적절합니다.

새로운 V2 hook의 default export가 올바르게 설정되었습니다.

src/app/searchResult/SearchResultPageClient.tsx (7)

1-22: 적절한 import 구문들입니다.

필요한 모든 의존성이 올바르게 가져와졌고, 새로운 V2 API를 사용하고 있습니다.


24-33: TempleStay 인터페이스 정의가 적절합니다.

필요한 모든 필드가 포함되어 있고 타입이 올바르게 정의되었습니다.


38-41: 유틸리티 함수가 잘 구현되었습니다.

배열 형태의 쿼리 파라미터를 콤마로 조인하는 로직이 깔끔하게 구현되었습니다.


64-68: useQuery 기반 데이터 페칭으로 개선되었습니다.

이전 리뷰에서 지적된 useEffect 내부의 비동기 작업 에러 처리 문제가 useQuery를 통해 해결되었습니다. React Query가 에러 처리와 로딩 상태를 자동으로 관리합니다.


115-127: 필터 활성 상태 판단 로직이 효율적입니다.

가격 변경 여부와 활성 필터들을 체계적으로 관리하고 있습니다.


128-135: 페이지 변경과 정렬 변경 핸들러가 적절합니다.

URL 파라미터 업데이트를 통한 상태 관리가 일관되게 구현되었습니다. 정렬 변경 시 페이지를 1로 리셋하는 것도 좋은 UX입니다.


143-216: 전체적인 UI 렌더링 구조가 잘 설계되었습니다.

조건부 렌더링, 모달 처리, 빈 상태 처리가 모두 적절하게 구현되어 있습니다. 컴포넌트 구조도 깔끔하고 유지보수하기 좋습니다.

Copy link

@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

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d68d131 and 428a065.

📒 Files selected for processing (1)
  • src/components/filter/filterBottomSheetModal/FilterModalContent.tsx (3 hunks)
🔇 Additional comments (3)
src/components/filter/filterBottomSheetModal/FilterModalContent.tsx (3)

6-9: import 변경사항이 리팩터링 목표와 일치합니다.

useState와 useAtom import 추가, 그리고 filterListInstance 사용으로의 전환이 적절하게 이루어졌습니다.


38-48: 검색 함수 리팩터링이 잘 구현되었습니다.

filterListInstance에서 선택된 필터를 가져와 price와 결합하는 방식이 명확하고 이해하기 쉽습니다. URL query parameter 방식으로의 전환 목표와 잘 일치합니다.


60-70: UI 속성 변경사항이 적절하지만 확인이 필요합니다.

핸들러 함수들이 로컬 상태 관리에 맞게 잘 업데이트되었습니다. Line 68에서 버튼 라벨이 동적 개수 표시에서 정적 텍스트로 변경되었는데, 이것이 의도된 최종 상태인지 확인이 필요합니다.

이전 리뷰에서 하드코딩된 개수 문제가 제기되었는데, 현재는 개수 표시를 완전히 제거했습니다. 이것이 의도된 디자인 결정인지, 아니면 향후 동적 개수 표시가 다시 구현될 예정인지 확인해 주세요.

Comment on lines +20 to +36
const { toggleFilter, handleResetFilter, handleSearch } = useFilter();
const { logClickEvent } = useEventLogger('filter_tag');
const [price, setPrice] = useAtom(priceAtom);

const [filtersState, setFiltersState] = useState(() => filterListInstance.getAllStates());

const handleToggleFilter = (filterName: string) => {
toggleFilter(filterName);
const updatedState = filterListInstance.getAllStates();
setFiltersState(updatedState);
};

const handleReset = async () => {
await handleResetFilter();
setFiltersState(filterListInstance.getAllStates());
setPrice({ minPrice: 0, maxPrice: 30 });
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

상태 관리 리팩터링이 적절하지만 개선사항이 있습니다.

로컬 상태와 singleton 인스턴스 간의 동기화 패턴이 올바르게 구현되었습니다. 하지만 Line 35에서 price 기본값이 하드코딩되어 있습니다.

price 기본값을 constants나 설정 파일에서 가져오도록 수정하는 것을 권장합니다:

- setPrice({ minPrice: 0, maxPrice: 30 });
+ setPrice(DEFAULT_PRICE_RANGE);
📝 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
const { toggleFilter, handleResetFilter, handleSearch } = useFilter();
const { logClickEvent } = useEventLogger('filter_tag');
const [price, setPrice] = useAtom(priceAtom);
const [filtersState, setFiltersState] = useState(() => filterListInstance.getAllStates());
const handleToggleFilter = (filterName: string) => {
toggleFilter(filterName);
const updatedState = filterListInstance.getAllStates();
setFiltersState(updatedState);
};
const handleReset = async () => {
await handleResetFilter();
setFiltersState(filterListInstance.getAllStates());
setPrice({ minPrice: 0, maxPrice: 30 });
};
const { toggleFilter, handleResetFilter, handleSearch } = useFilter();
const { logClickEvent } = useEventLogger('filter_tag');
const [price, setPrice] = useAtom(priceAtom);
const [filtersState, setFiltersState] = useState(() => filterListInstance.getAllStates());
const handleToggleFilter = (filterName: string) => {
toggleFilter(filterName);
const updatedState = filterListInstance.getAllStates();
setFiltersState(updatedState);
};
const handleReset = async () => {
await handleResetFilter();
setFiltersState(filterListInstance.getAllStates());
setPrice(DEFAULT_PRICE_RANGE);
};
🤖 Prompt for AI Agents
In src/components/filter/filterBottomSheetModal/FilterModalContent.tsx around
lines 20 to 36, the price default values in the handleReset function are
hardcoded as { minPrice: 0, maxPrice: 30 }. To improve maintainability, replace
these hardcoded values by importing and using constants or configuration values
from a dedicated constants or config file. This ensures the default price range
is centralized and easier to update.

@seong-hui seong-hui merged commit 9bb06c7 into develop Aug 2, 2025
7 checks passed
@seong-hui seong-hui deleted the feat/#290/filter-ssr branch August 2, 2025 13:34
@coderabbitai coderabbitai bot mentioned this pull request Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 필터링 query param으로 수정

3 participants