Skip to content

fix: 회원가입 튜토리얼 페이지 추가#125

Merged
heeeeyong merged 6 commits intodevelopfrom
feat/api-auth
Aug 16, 2025
Merged

fix: 회원가입 튜토리얼 페이지 추가#125
heeeeyong merged 6 commits intodevelopfrom
feat/api-auth

Conversation

@heeeeyong
Copy link
Collaborator

@heeeeyong heeeeyong commented Aug 16, 2025

#️⃣연관된 이슈

없음

📝작업 내용

  • 회원가입 튜토리얼 페이지 추가

image
- 띱취소, 띱하기 snackbar 동작 추가
- bookinfocard 네비게이션 추가
- 책 검색 무한스크롤 구현
- 모달 배경 색 수정

💬리뷰 요구사항

없음

Summary by CodeRabbit

  • New Features

    • 검색 결과에 무한 스크롤과 마지막 항목 관찰자 지원을 추가해 추가 결과를 자동 로드합니다.
    • 6단계 온보딩 가이드를 새로 추가하고 가입 흐름에 통합(가이드 및 완료 라우트 추가).
    • 도서 카드 클릭 시 검색 기반 도서 상세 화면으로 이동 경로를 통일했습니다.
    • 팔로우/언팔로우 시 상단 스낵바로 즉각적인 피드백을 제공합니다.
  • Style

    • 모달 배경 오버레이 불투명도 및 답글 모달 배경을 조정해 시각적 일관성 개선.
  • Chores

    • 가입 관련 라우트·뒤로가기 경로 및 일부 이동 흐름을 업데이트했으며 가입 완료 컴포넌트의 사전 검사 로직을 제거했습니다.

@vercel
Copy link

vercel bot commented Aug 16, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Aug 16, 2025 1:36pm

@coderabbitai
Copy link

coderabbitai bot commented Aug 16, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

모달 오버레이 불투명도 조정, BookInfoCard 클릭 경로를 /search/book/{isbn}로 변경, 검색 결과에 IntersectionObserver 기반 무한 스크롤 추가, Profile/UserProfileItem에 팔로우/언팔로우 시 상단 스낵바 피드백 도입, Guide 페이지 및 signup 라우팅 추가/수정이 포함됩니다.

Changes

Cohort / File(s) Summary
Modal 오버레이 스타일 조정
src/components/common/Modal/PopupContainer.tsx, src/components/common/Modal/ReplyModal.tsx
모달 오버레이/배경색을 rgba(18,18,18,0.3)로 변경하여 투명도 상향. 로직 변경 없음.
검색 무한 스크롤 도입
src/components/search/BookSearchResult.tsx, src/pages/search/Search.tsx
BookSearchResult에 hasMore/isLoading/lastBookElementCallback props 추가 및 마지막 아이템에 ref 연결. Search 페이지에 IntersectionObserver 기반 페이징·로딩·클린업 로직 추가.
피드 팔로우 스낵바 피드백
src/components/feed/Profile.tsx, src/components/feed/UserProfileItem.tsx
usePopupStore 연동으로 팔로우/언팔로우 성공 시 top 스낵바 호출. ProfilePropsisFollowing?: boolean 추가. 기존 API/에러 로직 유지.
책 카드 네비게이션 경로 변경
src/components/feed/BookInfoCard.tsx
클릭 시 경로를 /search/book/${isbn}로 변경하고 로컬 props 인터페이스 추가. 레이아웃 변동 없음.
온보딩 가이드 추가 및 라우팅 변경
src/pages/Guide.tsx, src/pages/index.tsx, src/pages/signup/SignupDone.tsx, src/pages/signup/SignupGenre.tsx
신규 Guide 컴포넌트 추가 및 signup 라우트 분리(/signup/guide, /signup/done). SignupDone의 백버튼 경로와 SignupGenre의 post-signup 네비게이션 대상 변경. SignupDone에서 사전 렌더 가드 삭제.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SearchPage
  participant IntersectionObserver
  participant API

  User->>SearchPage: 검색 실행
  SearchPage->>API: getSearchBooks(page=1)
  API-->>SearchPage: 결과(최대 페이지 크기)
  SearchPage->>IntersectionObserver: 마지막 아이템 observe
  IntersectionObserver-->>SearchPage: 교차 이벤트(보임)
  SearchPage->>API: getSearchBooks(page=next)
  API-->>SearchPage: 추가 결과
  SearchPage->>IntersectionObserver: 새 마지막 아이템 재-observe
Loading
sequenceDiagram
  participant User
  participant ProfileItem
  participant FollowAPI
  participant PopupStore

  User->>ProfileItem: 팔로우/언팔로우 클릭
  ProfileItem->>FollowAPI: 요청 전송
  FollowAPI-->>ProfileItem: 성공 응답(상태 토글)
  ProfileItem->>PopupStore: openPopup('snackbar', {message, variant:'top'})
  PopupStore-->>User: 스낵바 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

🐞 BugFix

Suggested reviewers

  • ho0010
  • ljh130334

Poem

토끼가 깡충대며 전하네, 색을 짙게 바꿨다네 🐇
책 한 장 클릭하면 /search 로 달려가고 📚
마지막 책은 옵저버가 살짝 건드리네 👀
팔로우하면 스낵바가 톡! 소곤대고
가이드를 건너뛰면 다음 길이 반짝인다 ✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 35004c0 and 523b74d.

📒 Files selected for processing (1)
  • src/pages/Guide.tsx (1 hunks)
✨ 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/api-auth

🪧 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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

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

🔭 Outside diff range comments (1)
src/components/common/Modal/ReplyModal.tsx (1)

79-87: 자기 댓글/타인 댓글 분기 조건이 반대로 적용됨 (삭제/신고가 뒤바뀜)

isMyReply가 true일 때는 "삭제하기"가, false일 때는 "신고하기"가 보여야 합니다. 현재 반대로 되어 있어 다른 사람 댓글을 삭제할 수 있는 UI가 노출됩니다. 아래와 같이 분기 및 클래스/클릭 핸들러를 교정해 주세요.

-          {isMyReply ? (
-            <OptionItem className="report">
-              <div className="option-text">신고하기</div>
-            </OptionItem>
-          ) : (
-            <OptionItem onClick={handleDelete} className="delete">
-              <div className="option-text">삭제하기</div>
-            </OptionItem>
-          )}
+          {isMyReply ? (
+            <OptionItem onClick={handleDelete} className="delete">
+              <div className="option-text">삭제하기</div>
+            </OptionItem>
+          ) : (
+            <OptionItem className="report">
+              <div className="option-text">신고하기</div>
+            </OptionItem>
+          )}
🧹 Nitpick comments (12)
src/components/common/Modal/ReplyModal.tsx (1)

114-116: 반투명 컨테이너 배경은 대비 저하 가능성 — 실색 배경 유지 권장

컨텍스트 메뉴(드롭다운/팝오버) 성격의 컨테이너 배경을 반투명으로 변경하면 텍스트 대비가 떨어질 수 있습니다. 특히 다양한 페이지 배경색 위에서 가독성 저하가 발생할 수 있어, 실색 배경(colors.black.main) 유지가 안전합니다.

-  background-color: rgba(18, 18, 18, 0.3);
+  background-color: ${colors.black.main};
src/components/feed/UserProfileItem.tsx (1)

22-36: 중복 클릭/레이스 가드 및 실패 시 스낵바 추가 권장

빠른 연타 시 중복 요청/레이스가 발생할 수 있습니다. 요청 중 가드를 두고 실패 시에도 스낵바로 안내하면 UX가 좋아집니다.

   const [followed, setFollowed] = useState(isFollowing);
+  const [isPending, setIsPending] = useState(false);
   const { openPopup } = usePopupStore();

   const toggleFollow = async (e: React.MouseEvent) => {
     e.stopPropagation();
 
     try {
+      if (isPending) return;
+      setIsPending(true);
       const response = await postFollow(userId, !followed);
       // API 응답으로 팔로우 상태 업데이트
       setFollowed(response.data.isFollowing);
       console.log(`${nickname} - ${response.data.isFollowing ? '띱 완료' : '띱 취소'}`);
 
       // Snackbar 표시
       const message = response.data.isFollowing 
         ? `${nickname}님을 띱 했어요.` 
         : `${nickname}님을 띱 취소했어요.`;
       
       openPopup('snackbar', {
         message,
         variant: 'top',
         onClose: () => {}
       });
     } catch (error) {
       console.error('팔로우/언팔로우 실패:', error);
+      openPopup('snackbar', {
+        message: '팔로우/언팔로우에 실패했어요. 잠시 후 다시 시도해주세요.',
+        variant: 'top',
+        onClose: () => {}
+      });
+    } finally {
+      setIsPending(false);
     }
   };

Also applies to: 38-49, 66-68

src/components/feed/Profile.tsx (1)

30-35: 초깃값 불리언 캐스팅, 중복 클릭 가드, 실패 스낵바 권장

isFollowing이 undefined일 수 있어 불리언 캐스팅으로 일관성 확보가 필요합니다. 또한 요청 중 중복 클릭 방지와 실패 시 스낵바 안내를 추가하면 안정성이 높아집니다.

-  const [followed, setFollowed] = useState(isFollowing);
+  const [followed, setFollowed] = useState<boolean>(!!isFollowing);
+  const [isPending, setIsPending] = useState(false);
   const { openPopup } = usePopupStore();

   useEffect(() => {
-    setFollowed(isFollowing);
+    setFollowed(!!isFollowing);
   }, [isFollowing]);

   const toggleFollow = async () => {
     if (!userId) {
       console.error('userId가 없습니다.');
       return;
     }

     try {
+      if (isPending) return;
+      setIsPending(true);
       console.log('현재 팔로우 상태:', followed);
       console.log('요청할 타입:', !followed);

       // 현재 팔로우 상태의 반대값으로 API 호출
       const response = await postFollow(userId, !followed);

       console.log('API 응답:', response);

       // API 응답으로 팔로우 상태 업데이트
       setFollowed(response.data.isFollowing);
       console.log(`${nickname} - ${response.data.isFollowing ? '띱 완료' : '띱 취소'}`);

       // Snackbar 표시
       const message = response.data.isFollowing 
         ? `${nickname}님을 띱 했어요.` 
         : `${nickname}님을 띱 취소했어요.`;
       
       openPopup('snackbar', {
         message,
         variant: 'top',
         onClose: () => {}
       });
     } catch (error) {
       console.error('팔로우/언팔로우 실패:', error);
       // 에러 발생 시 상태 변경하지 않음
+      openPopup('snackbar', {
+        message: '팔로우/언팔로우에 실패했어요. 잠시 후 다시 시도해주세요.',
+        variant: 'top',
+        onClose: () => {}
+      });
+    } finally {
+      setIsPending(false);
     }
   };

Also applies to: 37-69, 85-88

src/components/feed/BookInfoCard.tsx (1)

24-25: 장식용 아이콘 접근성 개선: alt 빈값/ARIA 숨김 추가

장식용 화살표 이미지는 보조기기에 노이즈가 되므로 alt=""와 aria-hidden을 권장합니다.

-        <img src={rightArrow} />
+        <img src={rightArrow} alt="" aria-hidden="true" />
src/components/search/BookSearchResult.tsx (4)

49-52: 작은 최적화: 더 불러올 항목이 없을 때는 ref 부착 생략

last 아이템 ref 부착 전에 hasMore를 함께 체크하면, 더 이상 로드할 데이터가 없을 때 불필요한 IntersectionObserver 관찰을 방지할 수 있습니다. 현재 콜백 내부에서도 가드가 있지만, 불필요한 observe 자체를 줄이는 편이 낫습니다.

-              ref={
-                index === searchedBookList.length - 1 && lastBookElementCallback
-                  ? lastBookElementCallback
-                  : undefined
-              }
+              ref={
+                index === searchedBookList.length - 1 &&
+                hasMore &&
+                lastBookElementCallback
+                  ? lastBookElementCallback
+                  : undefined
+              }

65-69: 로딩/마지막 상태가 빈 Fragment로 표시됨 — 사용자 피드백 UI 추가 권장

하단 로딩 스피너/메시지와 “더 이상 결과 없음” 같은 피드백을 제공하면 UX가 개선됩니다.

-        {isLoading && searchedBookList.length > 0 && <></>}
+        {isLoading && searchedBookList.length > 0 && <StatusRow>로딩 중...</StatusRow>}
 
-        {!hasMore && searchedBookList.length > 0 && <></>}
+        {!hasMore && searchedBookList.length > 0 && <StatusRow>마지막 결과입니다</StatusRow>}

컴포넌트 하단에 추가(지원 코드):

const StatusRow = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 12px 0;
  color: ${colors.grey[200]};
  font-size: ${typography.fontSize.sm};
`;

45-53: 접근성: 클릭 가능한 div에 키보드 접근성 보강

div에 onClick만 있으면 키보드 사용자가 접근하기 어렵습니다. role, tabIndex, Enter 키 핸들링을 추가해 주세요. (a/Link 사용이 더 이상적이지만, 최소한의 보강 방안 제안)

             <BookItem
               key={book.isbn}
               onClick={() => navigate(`/search/book/${book.isbn}`)}
+              role="button"
+              tabIndex={0}
+              onKeyDown={e => {
+                if (e.key === 'Enter') {
+                  navigate(`/search/book/${book.isbn}`);
+                }
+              }}

95-100: 카운트 라벨 문구 개선 제안

“전체 N”은 전체 검색 결과 수로 오해될 수 있습니다. 현재는 ‘로딩된 항목 수’이므로 문구를 명확히 하면 좋겠습니다.

-      {type === 'searching' ? <></> : <ResultHeader>전체 {searchedBookList.length}</ResultHeader>}
+      {type === 'searching' ? <></> : <ResultHeader>검색 결과 {searchedBookList.length}건</ResultHeader>}
src/pages/search/Search.tsx (4)

42-45: 미사용 ref 제거로 간소화

lastBookElementRef는 선언만 되고 실사용이 없습니다(할당만 1회). 제거해도 동작에 영향이 없습니다.

-  const lastBookElementRef = useRef<HTMLDivElement | null>(null);
...
-      lastBookElementRef.current = node;

Also applies to: 79-80


66-83: 무한 스크롤 콜백의 의존성/클로저 안정화 및 관찰 타이밍 개선

  • lastBookElementCallback이 loadMore에 의존하지만 deps에 loadMore가 없어 ESLint(exhaustive-deps) 경고 및 잠재적 스테일 클로저 이슈가 발생할 수 있습니다.
  • IntersectionObserver에 rootMargin/threshold를 지정해 ‘근접 사전 로드’가 가능하도록 하면 UX가 부드러워집니다.
  • loadMore를 useCallback으로 래핑하고, append 시 isbn 기준으로 중복 제거를 권장합니다(서버 페이지 경계 중복 방지).

Observer 옵션 및 deps 보강:

   observerRef.current = new IntersectionObserver(entries => {
     if (entries[0].isIntersecting && hasMore && !isLoadingMore) {
       loadMore();
     }
-  });
+  }, { root: null, rootMargin: '200px 0px', threshold: 0.01 });
 ...
-  [isLoadingMore, hasMore],
+  [isLoadingMore, hasMore, loadMore],

loadMore를 useCallback으로 전환 + 중복 제거:

-  const loadMore = async () => {
+  const loadMore = useCallback(async () => {
     if (!searchTerm.trim() || isLoadingMore || !hasMore) return;

     try {
       setIsLoadingMore(true);
       const nextPage = page + 1;

       const response = await getSearchBooks(searchTerm, nextPage, isFinalized);

       if (response.isSuccess) {
         const newResults = convertToSearchedBooks(response.data.searchResult);

         if (newResults.length > 0) {
-          setSearchResults(prev => [...prev, ...newResults]);
+          // isbn 기준으로 중복 제거
+          setSearchResults(prev => {
+            const seen = new Set(prev.map(b => b.isbn));
+            const merged = [...prev];
+            for (const b of newResults) {
+              if (!seen.has(b.isbn)) {
+                merged.push(b);
+                seen.add(b.isbn);
+              }
+            }
+            return merged;
+          });
           setPage(nextPage);
-          // 더 이상 데이터가 없으면 hasMore를 false로 설정
-          setHasMore(newResults.length === 10); // size가 10이므로
+          // 더 이상 데이터가 없으면 hasMore를 false로 설정
+          setHasMore(newResults.length === PAGE_SIZE);
         } else {
           setHasMore(false);
         }
       } else {
         console.error('추가 데이터 로드 실패:', response.message);
         setHasMore(false);
       }
     } catch (error) {
       console.error('추가 데이터 로드 중 오류 발생:', error);
       setHasMore(false);
     } finally {
       setIsLoadingMore(false);
     }
-  };
+  }, [searchTerm, page, isFinalized, isLoadingMore, hasMore]);

상단에 PAGE_SIZE 상수 정의는 아래 코멘트 참고.

Also applies to: 85-116


101-102: 매직 넘버(10) 상수화

페이지 크기 10을 여러 곳에서 직접 사용하고 있습니다. 상수로 추출해 가독성/일관성을 높여주세요.

-          setHasMore(newResults.length === 10); // size가 10이므로
+          setHasMore(newResults.length === PAGE_SIZE); // size가 PAGE_SIZE이므로
-        setHasMore(convertedResults.length === 10); // size가 10이므로
+        setHasMore(convertedResults.length === PAGE_SIZE); // size가 PAGE_SIZE이므로

지원 코드(파일 상단 import 아래 적절한 위치에 추가):

const PAGE_SIZE = 10;

Also applies to: 170-171


150-190: 검색 경쟁 상태(race condition) 방지 가드 권장

빠르게 검색어가 바뀌거나 수동검색/자동검색이 뒤섞이면, 늦게 도착한 이전 요청의 응답이 최신 결과를 덮어쓸 수 있습니다. 요청 ID(useRef) 또는 AbortController로 “구식 응답 무시” 가드를 두는 것을 권장합니다.

원리 예시(지원 코드):

// 상단에 추가
const latestRequestIdRef = useRef(0);

// handleSearch 내부
const requestId = ++latestRequestIdRef.current;
try {
  const response = await getSearchBooks(term, 1, isManualSearch);
  if (latestRequestIdRef.current !== requestId) return; // 구식 응답 무시
  ...
} catch (e) {
  if (latestRequestIdRef.current !== requestId) return;
  ...
}

원하시면 AbortController 기반으로 apiClient 요청 취소 패턴으로도 변환해 드릴게요.

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 534c0c0 and 95755b0.

📒 Files selected for processing (7)
  • src/components/common/Modal/PopupContainer.tsx (1 hunks)
  • src/components/common/Modal/ReplyModal.tsx (1 hunks)
  • src/components/feed/BookInfoCard.tsx (1 hunks)
  • src/components/feed/Profile.tsx (3 hunks)
  • src/components/feed/UserProfileItem.tsx (3 hunks)
  • src/components/search/BookSearchResult.tsx (4 hunks)
  • src/pages/search/Search.tsx (9 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/components/feed/UserProfileItem.tsx (1)
src/stores/usePopupStore.ts (1)
  • usePopupStore (51-57)
src/components/feed/Profile.tsx (1)
src/stores/usePopupStore.ts (1)
  • usePopupStore (51-57)
src/pages/search/Search.tsx (3)
src/api/recentsearch/getRecentSearch.ts (1)
  • RecentSearchData (7-10)
src/api/books/getSearchBooks.ts (2)
  • getSearchBooks (41-59)
  • convertToSearchedBooks (61-70)
src/components/search/BookSearchResult.tsx (1)
  • BookSearchResult (14-73)
src/components/search/BookSearchResult.tsx (1)
src/components/common/BookSearchBottomSheet/BookSearchBottomSheet.styled.ts (1)
  • BookItem (165-177)
🔇 Additional comments (6)
src/components/common/Modal/PopupContainer.tsx (1)

72-75: 오버레이 투명도 상향 일관성 확보, OK

모달 오버레이를 rgba(18, 18, 18, 0.3)로 올려 시각적 집중도를 높인 점 좋습니다. ReplyModal과의 일관성도 확보됩니다.

src/components/feed/UserProfileItem.tsx (1)

38-47: 팔로우/언팔로우 후 스낵바 피드백 도입, UX 향상 👍

API 응답에 따라 메시지를 분기해 상단 스낵바로 즉시 피드백을 주는 흐름이 명확하고 일관적입니다.

src/components/feed/Profile.tsx (1)

56-65: 팔로우/언팔로우 스낵바 피드백 추가, OK

상태 업데이트 직후 사용자에게 명확한 피드백을 제공해 상호작용성이 좋아졌습니다.

src/components/feed/BookInfoCard.tsx (1)

14-16: 루트 설정에 /search/book/:isbn 경로 정의 확인 필요

자동 스크립트 검색 결과 해당 경로가 발견되지 않아, 아래 파일들에서 수동으로 검증 부탁드립니다:

  • src/routes.ts 또는 routes.tsx (객체 스타일 경로 정의)
  • src/App.tsx<Route path="/search/book/:isbn" …> JSX 사용 위치
  • 기타 커스텀 라우터 설정 파일

경로가 없다면 검색 상세 페이지로 네비게이션이 동작하지 않으니, 정의를 추가하거나 올바른 파일 위치로 이동시켜 주세요.

src/components/search/BookSearchResult.tsx (1)

47-47: 라우팅 경로 확인 필요

/search/book/${book.isbn}로 이동하는 링크(/search/book/:isbn)가 실제 라우터 설정에 등록되어 있는지 확인해주세요. 등록되지 않으면 클릭 시 빈 화면 또는 404가 발생할 수 있습니다.

확인 방법 예시:

rg -nF '/search/book/:isbn' --glob '*.tsx' --glob '*.ts' --glob '*.jsx' --glob '*.js'

결과가 없다면 src/routes.tsx, App.tsx(또는 라우트 정의 파일)에 다음과 같이 경로를 추가해주세요:

<Route path="/search/book/:isbn" element={<BookDetail />} />
src/pages/search/Search.tsx (1)

287-296: 무한 스크롤 연동 전반적으로 잘 구성되었습니다

초기 로딩 분기, 하위 컴포넌트에 hasMore/isLoading/lastBookElementCallback 전달 흐름 모두 자연스럽습니다. 상단 개선 사항만 보완하면 안정적으로 동작할 것으로 보입니다.

Comment on lines +44 to +53
searchedBookList.map((book, index) => (
<BookItem
key={book.id}
onClick={() => navigate(`/search/book/${book.isbn}`)}
ref={
index === searchedBookList.length - 1 && lastBookElementCallback
? lastBookElementCallback
: undefined
}
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

key 충돌 가능성: 페이지 append 시 동일 id 재사용 → key를 isbn으로 전환 권장

convertToSearchedBooks가 각 페이지 내 index 기반 id를 부여하고 있습니다. 무한 스크롤에서 페이지 단위로 결과를 append하면 id가 중복되어 React key 충돌이 발생할 수 있어 리스트 업데이트/관찰자(ref) 부착이 오작동할 수 있습니다. 안정적인 고유키인 isbn을 key로 사용해 주세요.

아래처럼 변경을 제안드립니다:

-            <BookItem
-              key={book.id}
+            <BookItem
+              key={book.isbn}
               onClick={() => navigate(`/search/book/${book.isbn}`)}
📝 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
searchedBookList.map((book, index) => (
<BookItem
key={book.id}
onClick={() => navigate(`/search/book/${book.isbn}`)}
ref={
index === searchedBookList.length - 1 && lastBookElementCallback
? lastBookElementCallback
: undefined
}
>
searchedBookList.map((book, index) => (
<BookItem
key={book.isbn}
onClick={() => navigate(`/search/book/${book.isbn}`)}
ref={
index === searchedBookList.length - 1 && lastBookElementCallback
? lastBookElementCallback
: undefined
}
>
🤖 Prompt for AI Agents
In src/components/search/BookSearchResult.tsx around lines 44-53, the current
key uses book.id (which convertToSearchedBooks assigns per-page/index and can
duplicate when appending pages); change the key prop to use the stable unique
identifier book.isbn instead (i.e. key={book.isbn}) so React sees unique items
across appended pages; if some items may lack isbn, fallback to a deterministic
unique string that includes page/index (e.g. `${book.isbn ??
`${book.id}-${index}-${page}`}`) to avoid collisions; keep the ref and onClick
logic unchanged.

@heeeeyong heeeeyong changed the title fix: 오류 수정 및 예외처리 추가 fix: 회원가입 튜토리얼 페이지 추가 Aug 16, 2025
@heeeeyong heeeeyong self-assigned this Aug 16, 2025
@heeeeyong heeeeyong added ✨ Feature 기능 개발 🎨 Html&css 마크업 & 스타일링 labels Aug 16, 2025
@heeeeyong heeeeyong merged commit c35ba06 into develop Aug 16, 2025
2 checks passed
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

🧹 Nitpick comments (9)
src/pages/signup/SignupGenre.tsx (3)

46-51: fetch 취소 처리 및 에러 로깅 보완 제안

컴포넌트 언마운트 시 fetch를 취소하지 않으면 불필요한 setState 경고/리소스 낭비가 발생할 수 있습니다. AbortController로 보완을 권장합니다.

아래처럼 개선할 수 있습니다:

 useEffect(() => {
-    fetch('/genres.json')
-      .then(res => res.json())
-      .then(data => setGenres(data))
-      .catch(console.error);
+    const controller = new AbortController();
+    fetch('/genres.json', { signal: controller.signal })
+      .then(res => res.json())
+      .then(data => setGenres(data))
+      .catch(err => {
+        if (err?.name !== 'AbortError') {
+          console.error(err);
+        }
+      });
+    return () => controller.abort();
   }, []);

29-44: 디버그 로그 정리 또는 환경 분기 권장

console.log가 다수 존재합니다. 디버깅 시 유용하지만, 배포 빌드에서는 노이즈가 될 수 있습니다. NODE_ENV 분기나 로거 유틸로 관리하거나 제거하는 것을 권장합니다.

Also applies to: 60-66


72-86: 회원가입 실패 시 사용자 피드백 보완 제안

현재 실패/예외는 console로만 기록됩니다. 본 PR에서 스낵바 피드백이 도입된 만큼, 여기서도 동일한 패턴으로 안내(스낵바/토스트)를 제공하면 UX 일관성이 좋아집니다.

원하시면 스낵바 훅/컴포넌트에 맞춘 에러 핸들링 코드도 함께 제안드리겠습니다.

src/pages/index.tsx (1)

47-52: 레거시 경로 리디렉션(하위 호환) 고려

기존 북마크/외부 링크가 '/signupdone'를 참조할 수 있습니다. 부드러운 마이그레이션을 위해 리디렉션 라우트를 추가하는 것을 권장합니다.

아래와 같이 Navigate를 사용한 리디렉션을 추가할 수 있습니다:

 import {
   createBrowserRouter,
   createRoutesFromElements,
   Route,
   RouterProvider,
+  Navigate,
 } from 'react-router-dom';
@@
-        <Route path="signup/guide" element={<Guide />} />
-        <Route path="signup/done" element={<SignupDone />} />
+        <Route path="signup/guide" element={<Guide />} />
+        <Route path="signup/done" element={<SignupDone />} />
+        {/* Legacy route redirect */}
+        <Route path="signupdone" element={<Navigate to="/signup/done" replace />} />
src/pages/Guide.tsx (5)

183-185: dangerouslySetInnerHTML 제거 또는 최소한의 안전장치 적용

현재는 하드코딩된 문자열이라 실질적 XSS 위험은 낮지만, lint/security 경고를 피하고 향후 동적 콘텐츠로 전환될 가능성을 대비해 React 노드로 표현하는 방식을 권장합니다.

아래처럼 description 타입을 React.ReactNode로 바꾸고
를 JSX로 표기하면 dangerouslySetInnerHTML 없이 동일한 UI를 구현할 수 있습니다.

-interface GuideStep {
-  id: number;
-  title: string;
-  description: string;
-  image: string;
-}
+interface GuideStep {
+  id: number;
+  title: string;
+  description: React.ReactNode;
+  image: string;
+}
-  const guideSteps: GuideStep[] = [
+  const guideSteps: GuideStep[] = [
     {
       id: 1,
       title: '피드',
-      description: '피드에서 책과 독서에 대한 생각을<br/>자유롭게 나누어보세요!',
+      description: <>피드에서 책과 독서에 대한 생각을<br />자유롭게 나누어보세요!</>,
       image: guide1,
     },
     {
       id: 2,
       title: '피드',
-      description:
-        "칭호를 통해 내 독서 취향을 드러내고,<br/>마음에 드는 유저를 '띱'하고 감상을 공유해보세요!",
+      description: <>칭호를 통해 내 독서 취향을 드러내고,<br />마음에 드는 유저를 '띱'하고 감상을 공유해보세요!</>,
       image: guide2,
     },
     {
       id: 3,
       title: '모임',
-      description: '모임방에서는 글은 물론 투표 기능을 통해<br/>감상과 의견을 나눌 수 있어요.',
+      description: <>모임방에서는 글은 물론 투표 기능을 통해<br />감상과 의견을 나눌 수 있어요.</>,
       image: guide3,
     },
     {
       id: 4,
       title: '모임',
-      description:
-        '읽고 싶은 책으로 나만의 독서 모임을 만들고,<br/>독서메이트와 함께 기록을 나눌 수 있어요. ',
+      description: <>읽고 싶은 책으로 나만의 독서 모임을 만들고,<br />독서메이트와 함께 기록을 나눌 수 있어요. </>,
       image: guide4,
     },
     {
       id: 5,
       title: 'Thip+',
-      description:
-        '기록은 자유롭게, 감상은 방해없이.<br/>읽지 않은 페이지에 대한 기록은<br/>블라인드되어 스포일러 걱정없이 몰입할 수 있어요.',
+      description: <>기록은 자유롭게, 감상은 방해없이.<br />읽지 않은 페이지에 대한 기록은<br />블라인드되어 스포일러 걱정없이 몰입할 수 있어요.</>,
       image: guide5,
     },
     {
       id: 6,
       title: 'Thip+',
-      description: "모임방의 인상깊은 기록을<br/>'핀하기'로 피드에 다시 공유해보세요.",
+      description: <>모임방의 인상깊은 기록을<br />'핀하기'로 피드에 다시 공유해보세요.</>,
       image: guide6,
     },
   ];
-          <Description dangerouslySetInnerHTML={{ __html: guideSteps[currentStep].description }} />
+          <Description>{guideSteps[currentStep].description}</Description>

대안: 만약 문자열 유지가 필요하면 DOMPurify로 sanitize 후 주입하는 방식도 가능합니다.

Also applies to: 14-19, 94-134


176-179: 마지막 단계에서는 버튼 라벨을 ‘완료’로 표기

현재 모든 단계에서 "다음"으로 노출되며, 마지막 단계에서도 "다음"입니다. 마지막 단계에서 "완료"로 표기하면 사용성이 좋아집니다.

-        <div className="next-button" onClick={handleNext}>
-          다음
-        </div>
+        <div className="next-button" onClick={handleNext}>
+          {currentStep === guideSteps.length - 1 ? '완료' : '다음'}
+        </div>

27-28: containerRef 미사용

ref를 선언하고 넘기지만 실제로 사용하지 않습니다. 제거하거나, 추후 키보드 포커스/스크롤 제어 등 용도로 활용 예정이라면 TODO를 남겨주세요.

-  const containerRef = useRef<HTMLDivElement>(null);
@@
-    <Container
-      ref={containerRef}
+    <Container
       isDragging={isDragging}

Also applies to: 165-174


137-139: 주석/인터랙션 불일치 정리

주석은 “active 상태일 때만 다음 단계로 이동”이라 돼 있으나, 실제로는 어떤 단계에서도 클릭 시 진행됩니다. 주석을 정정하거나, active가 아닌 경우 클릭을 무시하도록 로직/스타일을 통일해 주세요.

Also applies to: 239-253


214-219: 스타일 상수(폭) 일관성 제안

max-width가 766/767로 혼재되어 있습니다. 하나의 상수로 통일하면 유지보수성이 좋아집니다.

Also applies to: 234-236, 302-305

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 95755b0 and 35004c0.

⛔ Files ignored due to path filters (6)
  • src/assets/signup/guide1.svg is excluded by !**/*.svg
  • src/assets/signup/guide2.svg is excluded by !**/*.svg
  • src/assets/signup/guide3.svg is excluded by !**/*.svg
  • src/assets/signup/guide4.svg is excluded by !**/*.svg
  • src/assets/signup/guide5.svg is excluded by !**/*.svg
  • src/assets/signup/guide6.svg is excluded by !**/*.svg
📒 Files selected for processing (4)
  • src/pages/Guide.tsx (1 hunks)
  • src/pages/index.tsx (2 hunks)
  • src/pages/signup/SignupDone.tsx (1 hunks)
  • src/pages/signup/SignupGenre.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/pages/Guide.tsx (1)
src/styles/global/global.ts (2)
  • colors (4-53)
  • typography (56-77)
🪛 Biome (2.1.2)
src/pages/Guide.tsx

[error] 208-209: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🪛 ast-grep (0.38.6)
src/pages/Guide.tsx

[warning] 183-183: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🔇 Additional comments (5)
src/pages/signup/SignupGenre.tsx (2)

74-79: 온보딩 플로우에 맞춘 라우팅 변경: LGTM

회원가입 성공 시 '/signup/guide'로 네비게이션하는 변경이 신규 가이드 플로우(/signup/guide → /signup/done)와 일치합니다. 상태로 aliasName, nickname을 넘기는 부분도 Guide에서 기대하는 키와 일치합니다.


90-97: 헤더 단계 표기 검토

현재 타이틀이 "설정 2/2"로 고정되어 있으나, 온보딩에 Guide(여러 단계)가 추가되었습니다. 사용자 여정과의 일관성을 위해 단계 표기 업데이트가 필요한지 확인 부탁드립니다.

src/pages/signup/SignupDone.tsx (2)

15-16: 뒤로가기 경로 변경: LGTM

신규 온보딩 흐름에 맞춰 '/signup/guide'로 이동하도록 한 변경은 타 파일(Guide, Router)과 일관됩니다.


22-27: 상태 미존재 시 라우팅 방침 확인 요청

완료 화면은 공유/외부 진입 가능성이 있습니다. 상태가 없는 경우에도 기본값으로 표시하는 현재 접근이 의도된 제품 결정인지, 아니면 '/signup/guide'로 안내하는 것이 맞는지 확인 부탁드립니다.

Also applies to: 38-41

src/pages/index.tsx (1)

41-41: Guide 및 Done 라우트 추가: LGTM

신규 플로우에 맞춰 '/signup/guide', '/signup/done' 라우트를 추가한 변경이 다른 파일들과 잘 정합됩니다.

Also applies to: 50-51

Comment on lines +44 to +45
if (!touchStart || !touchEnd) return;

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일 때 스와이프가 동작하지 않는 버그

0은 falsy라 좌측 가장자리(0px)에서 스와이프하면 조건문이 조기 return 됩니다. null 비교로 바꿔주세요.

다음과 같이 수정하면 됩니다:

-    if (!touchStart || !touchEnd) return;
+    if (touchStart === null || touchEnd === null) return;
📝 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
if (!touchStart || !touchEnd) return;
if (touchStart === null || touchEnd === null) return;
🤖 Prompt for AI Agents
In src/pages/Guide.tsx around lines 44-45, the current guard uses falsy checks
(if (!touchStart || !touchEnd) return;) which treats 0 as absent and prevents
edge swipes; change the condition to explicitly check for null/undefined (e.g.
if (touchStart == null || touchEnd == null) return;) so zero coordinates are
allowed while still guarding against missing values.

@@ -12,19 +12,13 @@ const SignupDone = () => {
const { nickName, aliasName } = location.state || {};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

직접 진입 시 undefined 노출 가능성 해결 및 접근성 보완

pre-render 가드 제거로 상태 없이 접근하면 닉네임/칭호가 undefined로 노출될 수 있습니다. 기본값을 부여하거나 가벼운 가드를 유지하는 것을 권장합니다. 또한 프로필 이미지에 alt가 없습니다.

아래처럼 최소한의 기본값과 alt를 추가하면 UX/접근성이 개선됩니다.

-  const { nickName, aliasName } = location.state || {};
+  const { nickName = '사용자', aliasName = '독서가' } = location.state || {};
-            <img src={art} />
+            <img src={art} alt="가입 완료 일러스트" />

Also applies to: 28-37

🤖 Prompt for AI Agents
In src/pages/signup/SignupDone.tsx around line 12 (and also lines 28-37),
location.state may be undefined causing nickName/aliasName to render as
undefined and the profile image lacks an alt attribute; change the destructure
to provide safe defaults (e.g., const { nickName = '회원', aliasName = '' } =
location.state || {}) or add a lightweight guard that redirects to the signup
start if state is missing, and add a meaningful alt prop to the profile image
(e.g., alt={`프로필 이미지 of ${nickName}` or alt="프로필 이미지") to fix UX/accessibility
issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발 🎨 Html&css 마크업 & 스타일링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant