Skip to content

feat: 책 페이지 정보 조회 API 연동 구현 및 일부 로직 수정#129

Merged
ljh130334 merged 7 commits intodevelopfrom
feat/api-rooms-page
Aug 17, 2025
Merged

feat: 책 페이지 정보 조회 API 연동 구현 및 일부 로직 수정#129
ljh130334 merged 7 commits intodevelopfrom
feat/api-rooms-page

Conversation

@ljh130334
Copy link
Member

@ljh130334 ljh130334 commented Aug 17, 2025

#️⃣ 연관된 이슈

#106

📝 작업 내용

기록장에서 책 페이지 정보 조회 API를 연동하고 총평 기록이 제대로 표시되지 않는 문제를 해결했습니다.

🕸️ 주요 수정 사항

1. 기록 작성 API 수정 (RecordWrite.tsx, PollWrite.tsx)

  • 총평 작성 시 페이지 값이 0으로 전송되어 서버에서 유효성 검증 오류(40002)가 발생하는 문제 해결
  • 총평인 경우 페이지를 전체 페이지 수(totalPages)로 설정하도록 수정
  • getBookPage API를 추가로 호출하여 책 페이지 정보와 총평 작성 가능 여부를 확인
  • 페이지 유효성 검사 로직 강화 (1~전체페이지 범위 체크)

2. 기록장 조회 API 수정 (Memory.tsx)

  • API 스웨거 명세에 따라 isOverview 파라미터가 필요함을 확인
  • 기존 단일 API 호출로는 일반 기록만 가져와서 총평 기록이 누락되는 문제 발견
  • 일반 기록(isOverview: false)과 총평 기록(isOverview: true)을 병렬로 호출하여 모든 기록을 가져오도록 수정
  • Promise.all을 사용하여 성능 최적화
  • API 응답에서 받은 isOverviewEnabled 값을 readingProgress 계산에 활용하여 총평보기 버튼 활성화 제어

3. 필터링 로직 강화

  • 총평보기 버튼 클릭 시 recordType === 'overall'인 기록만 필터링하여 표시
  • 페이지별보기 필터와 총평보기 필터의 명확한 분리
  • 프론트엔드 레벨에서의 필터링으로 API 호출 최소화

Summary by CodeRabbit

  • New Features
    • 방의 책 페이지 정보 자동 로드(총 페이지·최근 기록·개요 가능 여부) 및 이에 따른 진행률 표시
    • 로딩 화면 추가와 스낵바 기반 안내/오류 메시지
    • 메모리 목록의 전체/페이지 범위 필터 및 업로드 완료 처리
  • Refactor
    • 투표/기록 작성 완료 로직을 유효성 검증·범위 체크 기반으로 재구성
    • 경고창을 스낵바로 대체하고, 작성 후 목록 화면으로 간결하게 이동
    • 로컬 임시 레코드 생성/자동 탭 전환 제거, 목록 병합 흐름 정리

@vercel
Copy link

vercel bot commented Aug 17, 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 17, 2025 3:40am

@coderabbitai
Copy link

coderabbitai bot commented Aug 17, 2025

Walkthrough

새 API 래퍼(getBookPage) 추가와 이를 사용하는 작성 화면(Record/Poll) 초기 로딩·검증 흐름 도입. Memory 페이지는 게시물 로딩/필터링·오버뷰 상태 처리 변경. 두 개의 공용 컴포넌트에 새로운 필수 prop 추가. 경고(alert) → 스낵바로 전환, 로딩 상태 처리 추가.

Changes

Cohort / File(s) Summary
Book page API
src/api/rooms/getBookPage.ts
방의 도서 페이지 정보 조회 API 추가: 타입(BookPageData/Response) 정의와 getBookPage(roomId) 구현(try/catch, GET /rooms/{roomId}/book-page).
Memory page integration
src/pages/memory/Memory.tsx
게시물 로딩 로직 개편: 그룹/내글 병렬 호출 및 오버뷰(isOverview) 데이터 병합, 정렬 매핑, 오류 처리. 필터(전체/페이지 범위) 확장. currentUserPage 전달, 업로드 완료 핸들러 추가.
Record write flow
src/pages/recordwrite/RecordWrite.tsx
마운트 시 getBookPage로 totalPages/recent/isOverviewPossible 로딩, 로딩 UI/스낵바 도입. 완료 시 최종 페이지 계산(전체=totalPages, 아니면 입력/최근), 범위 검증, createRecord 호출 후 이동.
Poll write flow
src/pages/pollwrite/PollWrite.tsx
마운트 시 getBookPage 로딩 및 상태 반영. 오버뷰 불가 시 자동 비활성화. 완료 시 최종 페이지 계산·검증 후 createVote 호출로 전환(로컬 메모리 생성 제거). 로딩 UI/스낵바 적용.
Shared component props
src/components/memory/MemoryContent/MemoryContent.tsx, src/components/recordwrite/PageRangeSection.tsx
MemoryContent에 필수 prop currentUserPage 추가(미사용). PageRangeSection에 필수 prop isOverviewPossible 추가(미사용).

Sequence Diagram(s)

sequenceDiagram
  participant UI as RecordWrite/PollWrite
  participant API as getBookPage
  participant S as Server

  UI->>API: getBookPage(roomId)
  API->>S: GET /rooms/{roomId}/book-page
  S-->>API: { totalBookPage, recentBookPage, isOverviewPossible }
  API-->>UI: 데이터 반환
  UI->>UI: 상태 업데이트(총페이지/최근/오버뷰가능, 로딩 해제)

  UI->>UI: 완료 클릭 시 최종 page 계산(전체 or 입력/최근)
  UI->>UI: 범위 검증(1..totalPages)
  alt Record 작성
    UI->>S: POST /records { page, isOverview, content }
  else Poll 작성
    UI->>S: POST /votes { page, isOverview, content, voteItemList }
  end
  S-->>UI: 성공 응답
  UI->>UI: 메모리 화면으로 이동
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

✨ Feature, 📬 API

Suggested reviewers

  • heeeeyong

Poem

푸른 방에 페이지 바람이 불어와,
총장과 최근, 오버뷰를 살짝 보아라.
토끼는 로딩 바를 톡톡 두드리고,
스낵바로 말하네: 이제 출발이야! 🥕
기록도 투표도 한 걸음에, 메모리로 점프!

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.

✨ 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-rooms-page

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.
    • 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: 5

🧹 Nitpick comments (15)
src/components/recordwrite/PageRangeSection.tsx (3)

31-47: 새 prop(isOverviewPossible) 미사용 — 토글 활성화 기준에 직접 반영 권장

API에서 내려온 총평 가능 여부를 직접 사용하면 상위 컴포넌트와의 “80% 매직 넘버” 결합을 줄이고 의도가 더 명확해집니다.

적용 예시:

 const PageRangeSection = ({
   pageRange,
   onPageRangeChange,
   totalPages,
   lastRecordedPage = 0,
   isOverallEnabled,
   onOverallToggle,
   readingProgress,
+  isOverviewPossible,
 }: PageRangeSectionProps) => {
   const [hasError, setHasError] = useState(false);

-  // 80% 이상일 때만 총평 활성화
-  const canUseOverall = readingProgress >= 80;
+  // 서버 플래그 기반으로 총평 활성화
+  const canUseOverall = isOverviewPossible;

55-61: 페이지 최소값(1 미만) 검증 누락

현재는 전체 페이지 초과만 검사합니다. 0 또는 음수 입력도 즉시 에러로 처리하는 편이 안전합니다.

-      if (value !== '') {
-        const page = parseInt(value);
-        setHasError(page > totalPages);
-      } else {
+      if (value !== '') {
+        const page = parseInt(value, 10);
+        setHasError(page < 1 || page > totalPages);
+      } else {
         setHasError(false);
       }

86-93: placeholder가 0으로 노출될 수 있음 — UX 개선 제안

lastRecordedPage의 기본값이 0이라 placeholder가 "0"으로 보일 수 있습니다. 최소 1 또는 빈 문자열로 처리하는 편이 자연스럽습니다.

-              <PageInput
-                placeholder={lastRecordedPage.toString()}
+              <PageInput
+                placeholder={lastRecordedPage > 0 ? lastRecordedPage.toString() : '1'}
                 value={pageRange}
                 onChange={handleInputChange}
                 inputMode="numeric"
               />
src/components/memory/MemoryContent/MemoryContent.tsx (1)

20-20: 사용되지 않는 필수 prop(currentUserPage) — 선택적으로 전환 권장

현재 컴포넌트 내부에서 사용하지 않으므로 불필요한 호출부 수정이 발생합니다. 가까운 시일 내 사용 계획이 없다면 선택적 prop으로 완화하세요.

-  currentUserPage: number;
+  currentUserPage?: number;
src/pages/memory/Memory.tsx (2)

124-140: 부분 성공 시에도 가능한 데이터는 표시하도록 강건성 향상

두 호출 중 하나만 성공해도 보여줄 수 있는 데이터가 있습니다. 현재는 둘 다 성공해야만 그려지므로 UX가 불필요하게 저하됩니다.

-      if (generalResponse.isSuccess && overviewResponse.isSuccess) {
-        // 일반 기록과 총평 기록을 합치기
-        const allPosts = [...generalResponse.data.postList, ...overviewResponse.data.postList];
-        const convertedRecords = allPosts.map(convertPostToRecord);
-
-        setIsOverviewEnabled(generalResponse.data.isOverviewEnabled);
-
-        if (activeTab === 'group') {
-          setGroupRecords(convertedRecords);
-        } else {
-          setMyRecords(convertedRecords);
-        }
-      } else {
-        setError(
-          generalResponse.message || overviewResponse.message || '기록을 불러오는데 실패했습니다.',
-        );
-      }
+      const postsA = generalResponse.isSuccess ? generalResponse.data.postList : [];
+      const postsB = overviewResponse.isSuccess ? overviewResponse.data.postList : [];
+      const allPosts = [...postsA, ...postsB];
+      const convertedRecords = allPosts.map(convertPostToRecord);
+
+      if (generalResponse.isSuccess) {
+        setIsOverviewEnabled(generalResponse.data.isOverviewEnabled);
+      }
+
+      if (activeTab === 'group') {
+        setGroupRecords(convertedRecords);
+      } else {
+        setMyRecords(convertedRecords);
+      }
+
+      if (!generalResponse.isSuccess && !overviewResponse.isSuccess) {
+        setError(
+          generalResponse.message || overviewResponse.message || '기록을 불러오는데 실패했습니다.',
+        );
+      }

250-252: 하드코딩된 currentUserPage(350) — API 연동으로 치환 권장

신규 API(getBookPage)를 이미 도입한 만큼 여기서도 recentBookPage 등을 사용해 실제 값을 반영하세요. 필요시 전역 상태나 캐싱 고려.

원하시면 getBookPage 연동(useEffect + 상태 추가) 패치를 생성해드릴게요.

src/api/rooms/getBookPage.ts (1)

19-28: 언마운트/경로 전환 시 취소 가능하도록 AbortController 도입 고려

긴 네트워크 지연에서 불필요한 상태 업데이트를 막기 위해 취소 신호를 지원하면 좋습니다. 호출부에서 취소를 전달할 수 있게 옵션 인자로 signal을 받는 형태를 제안합니다.

예시(참고용):

export const getBookPage = async (roomId: number, signal?: AbortSignal): Promise<BookPageResponse> => {
  try {
    const response = await apiClient.get<BookPageResponse>(`/rooms/${roomId}/book-page`, { signal });
    return response.data;
  } catch (error) {
    console.error('책 페이지 정보 조회 API 오류:', error);
    throw error;
  }
};
src/pages/pollwrite/PollWrite.tsx (3)

33-41: roomId 누락 시 조기 반환 전에 로딩 상태 해제

뒤로 이동하더라도 잠깐 로딩 UI가 남을 수 있습니다. navigate 전에 isLoading을 false로 내려 UX 흔들림을 줄이는 것이 깔끔합니다.

       if (!roomId) {
+        setIsLoading(false);
         openSnackbar({
           message: '방 정보를 찾을 수 없습니다.',
           variant: 'top',
           onClose: () => {},
         });
         navigate(-1);
         return;
       }

63-82: 에러 매핑 시 HTTP 상태코드 사용 권장(response.status)

현재는 response.data.code를 상태코드처럼 사용합니다. 실제 HTTP 상태는 response.status에 있습니다. 메시지가 없을 때에만 상태코드로 분기하는 식으로 단순화하세요.

-        if (error && typeof error === 'object' && 'response' in error) {
+        if (error && typeof error === 'object' && 'response' in error) {
           const axiosError = error as {
             response?: {
+              status?: number;
               data?: {
                 message?: string;
-                code?: number;
               };
             };
           };
 
           if (axiosError.response?.data?.message) {
             errorMessage = axiosError.response.data.message;
-          } else if (axiosError.response?.data?.code === 400) {
+          } else if (axiosError.response?.status === 400) {
             errorMessage = '파라미터 값 중 유효하지 않은 값이 있습니다.';
-          } else if (axiosError.response?.data?.code === 403) {
+          } else if (axiosError.response?.status === 403) {
             errorMessage = '방 접근 권한이 없습니다.';
-          } else if (axiosError.response?.data?.code === 404) {
+          } else if (axiosError.response?.status === 404) {
             errorMessage = '존재하지 않는 방입니다.';
           }
         }

262-271: 총평 가능 여부 전달 중복(읽기 진행률 vs 불리언) — 한 가지 소스로 통일 권장

PageRangeSection에 isOverviewPossible를 이미 전달하므로, 읽기 진행률을 “80/70”의 신호로 재사용할 필요가 없습니다. canUseOverall 기준을 isOverviewPossible로 단순화했다면 이곳도 일관되게 유지하세요.

-          readingProgress={isOverviewPossible ? 80 : 70} // 총평 가능하면 80% 이상으로 표시
+          readingProgress={70} // 진행률은 UI 용도에만 사용(총평 토글은 isOverviewPossible로 제어)
           isOverviewPossible={isOverviewPossible}

참고: 상위·하위 컴포넌트 간 총평 활성화 기준이 중복 정의되면 디버깅 난도가 올라갑니다(매직 넘버 제거).

src/pages/recordwrite/RecordWrite.tsx (5)

29-41: roomId가 없을 때 isLoading이 해제되지 않습니다(경미).

현재 분기에서 navigate(-1)로 바로 이탈하여 finallysetIsLoading(false)가 실행되지 않습니다. 즉시 뒤로가기 되지 않거나 히스토리 스택에 따라 화면이 남을 경우 스피너가 유지될 수 있습니다. 진입 직전에 setIsLoading(false)를 호출해 주세요.


57-81: 에러 타입 판정은 axios의 타입가드 사용을 권장합니다.

'response' in error는 범용적이지만 부정확할 수 있습니다. axios.isAxiosError(error) 또는 instanceof AxiosError로 분기하면 안전합니다. 또한 서버가 HTTP status를 함께 주는 경우 response.status도 병행 확인하는 편이 명확합니다.

예시:

import axios, { AxiosError } from 'axios';

} catch (error) {
  let errorMessage = '...';
  if (axios.isAxiosError(error)) {
    const status = error.response?.status;
    const code = (error.response?.data as any)?.code;
    const message = (error.response?.data as any)?.message;
    // message 우선 → status/code 보조
  }
  openSnackbar({ ... });
}

151-158: 디버그 로그는 제거하거나 개발 환경에서만 출력하세요.

민감한 데이터(content 등)가 콘솔에 남을 수 있습니다. 운영 빌드에서는 제거가 안전합니다.

다음 패치를 제안합니다:

-      console.log('기록 생성 API 호출:', recordData);
-      console.log('roomId:', roomId);
+      // TODO: 필요 시 개발 환경에서만 디버그 로그 사용
...
-      if (response.isSuccess) {
-        console.log('기록 생성 성공:', response.data);
+      if (response.isSuccess) {

247-256: readingProgress는 실제 진척도 기반 계산을 권장합니다.

isOverviewPossible ? 80 : 70은 임의 상수라 사용자 경험상 혼동이 있을 수 있습니다. 실제 진척도(예: recent/total * 100)를 사용하고, 총평 가능 여부는 별도 표시로 분리하는 편이 명확합니다.

예시:

const computedProgress = totalPages > 0
  ? Math.min(100, Math.round((lastRecordedPage / totalPages) * 100))
  : 0;

// ...
<PageRangeSection
  // ...
  readingProgress={computedProgress}
  isOverviewPossible={isOverviewPossible}
/>

29-94: 언마운트/roomId 변경 중 응답 도착 시 setState 경합 가능 — 취소 플래그 권장

비동기 호출 완료 시점에 컴포넌트가 언마운트되면 경고가 발생할 수 있습니다. 간단한 취소 플래그 또는 AbortController로 안전하게 처리하세요.

예시:

useEffect(() => {
  let cancelled = false;
  const fetchBookPageInfo = async () => {
    try {
      setIsLoading(true);
      // ...
      const response = await getBookPage(roomNum);
      if (cancelled) return;
      // setState들...
    } catch (e) {
      if (cancelled) return;
      // ...
    } finally {
      if (!cancelled) setIsLoading(false);
    }
  };
  fetchBookPageInfo();
  return () => { cancelled = true; };
}, [roomId]);
📜 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 c35ba06 and 3fddcbd.

📒 Files selected for processing (6)
  • src/api/rooms/getBookPage.ts (1 hunks)
  • src/components/memory/MemoryContent/MemoryContent.tsx (1 hunks)
  • src/components/recordwrite/PageRangeSection.tsx (1 hunks)
  • src/pages/memory/Memory.tsx (6 hunks)
  • src/pages/pollwrite/PollWrite.tsx (4 hunks)
  • src/pages/recordwrite/RecordWrite.tsx (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/api/rooms/getBookPage.ts (1)
src/api/index.ts (1)
  • apiClient (7-14)
src/pages/pollwrite/PollWrite.tsx (3)
src/hooks/usePopupActions.ts (1)
  • usePopupActions (9-35)
src/api/rooms/getBookPage.ts (1)
  • getBookPage (20-28)
src/pages/pollwrite/PollWrite.styled.ts (1)
  • Container (4-15)
src/pages/memory/Memory.tsx (1)
src/api/memory/getMemoryPosts.ts (1)
  • getMemoryPosts (5-51)
src/pages/recordwrite/RecordWrite.tsx (2)
src/hooks/usePopupActions.ts (1)
  • usePopupActions (9-35)
src/api/rooms/getBookPage.ts (1)
  • getBookPage (20-28)
🔇 Additional comments (3)
src/pages/memory/Memory.tsx (1)

118-123: getMemoryPosts 호출에 isOverview 파라미터 명시 권장

Swagger 스펙상 isOverview가 선택(optional)이지만, 백엔드 기본값(false)에 의존하면 추후 기본값 변경 시 의도치 않은 동작을 초래할 수 있습니다. 일반 기록 호출 시에도 명시적으로 isOverview: false를 전달해 안정성을 높여주세요.

• 대상 위치

  • 파일: src/pages/memory/Memory.tsx
  • Lines: 118–123

• 변경 제안

-      const [generalResponse, overviewResponse] = await Promise.all([
-        getMemoryPosts(params), // 일반 기록 (isOverview: false 기본값)
-        getMemoryPosts({ ...params, isOverview: true }), // 총평 기록
-      ]);
+      const [generalResponse, overviewResponse] = await Promise.all([
+        getMemoryPosts({ ...params, isOverview: false }), // 일반 기록
+        getMemoryPosts({ ...params, isOverview: true }),  // 총평 기록
+      ]);

[optional_refactors_recommended]

src/api/rooms/getBookPage.ts (1)

19-28: API 래퍼 구성 적절 — 반환 타입, 예외 처리 모두 무난

요청 경로, 제네릭 응답 타입, 예외 재전파까지 적절합니다. 다른 호출부에서 상태 관리 일관화에 도움이 되겠습니다.

src/pages/recordwrite/RecordWrite.tsx (1)

121-124: 총평 시 마지막 페이지로 전송하는 처리, 방향성 좋습니다

총평을 선택한 경우 finalPage = totalPages로 고정하는 로직은 서버 측 검증(예: 40002) 회피 목적에 부합합니다.

Comment on lines +147 to 156
// 페이지 유효성 검사
if (finalPage <= 0 || finalPage > totalPages) {
openSnackbar({
message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`,
variant: 'top',
onClose: () => {},
});
setIsSubmitting(false);
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

NaN 페이지 값 검증 누락 — API 호출에 NaN 전달 가능

parseInt 실패 시 NaN이 되어 현재 조건(finalPage <= 0 || finalPage > totalPages)을 통과해버립니다. NaN/정수 여부를 먼저 검사하세요.

-      // 페이지 유효성 검사
-      if (finalPage <= 0 || finalPage > totalPages) {
+      // 페이지 유효성 검사
+      if (!Number.isFinite(finalPage) || !Number.isInteger(finalPage) || finalPage < 1 || finalPage > totalPages) {
         openSnackbar({
           message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`,
           variant: 'top',
           onClose: () => {},
         });
         setIsSubmitting(false);
         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 (finalPage <= 0 || finalPage > totalPages) {
openSnackbar({
message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`,
variant: 'top',
onClose: () => {},
});
setIsSubmitting(false);
return;
}
// 페이지 유효성 검사
if (!Number.isFinite(finalPage) || !Number.isInteger(finalPage) || finalPage < 1 || finalPage > totalPages) {
openSnackbar({
message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`,
variant: 'top',
onClose: () => {},
});
setIsSubmitting(false);
return;
}
🤖 Prompt for AI Agents
In src/pages/pollwrite/PollWrite.tsx around lines 147 to 156, the page
validation allows NaN through because parseInt failures make finalPage NaN and
the current comparison (finalPage <= 0 || finalPage > totalPages) won't catch
it; update the validation to first check that finalPage is a finite integer
(e.g., Number.isInteger(finalPage) or !Number.isNaN(finalPage) &&
Number.isFinite(finalPage)) and only then apply the range check, ensuring you
still call openSnackbar and setIsSubmitting(false) and return when the value is
invalid.

Comment on lines +31 to +46
const fetchBookPageInfo = async () => {
if (!roomId) {
openSnackbar({
message: '방 정보를 찾을 수 없습니다.',
variant: 'top',
onClose: () => {},
});
navigate(-1);
return;
}

try {
setIsLoading(true);
const response = await getBookPage(parseInt(roomId));

if (response.isSuccess) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

roomId 숫자 유효성 검증 없이 API를 호출합니다(NaN 경로 호출 위험).

현재 parseInt(roomId)만 사용해 /rooms/NaN/book-page 같은 잘못된 호출이 가능해집니다. 숫자 검증을 선행해 주세요.

다음 패치를 제안합니다:

   useEffect(() => {
     const fetchBookPageInfo = async () => {
       if (!roomId) {
         openSnackbar({
           message: '방 정보를 찾을 수 없습니다.',
           variant: 'top',
           onClose: () => {},
         });
         navigate(-1);
         return;
       }
 
       try {
         setIsLoading(true);
-        const response = await getBookPage(parseInt(roomId));
+        const roomNum = Number(roomId);
+        if (!Number.isFinite(roomNum) || roomNum <= 0) {
+          openSnackbar({
+            message: '유효하지 않은 방 정보입니다.',
+            variant: 'top',
+            onClose: () => {},
+          });
+          navigate(-1);
+          return;
+        }
+        const response = await getBookPage(roomNum);
📝 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 fetchBookPageInfo = async () => {
if (!roomId) {
openSnackbar({
message: '방 정보를 찾을 수 없습니다.',
variant: 'top',
onClose: () => {},
});
navigate(-1);
return;
}
try {
setIsLoading(true);
const response = await getBookPage(parseInt(roomId));
if (response.isSuccess) {
const fetchBookPageInfo = async () => {
if (!roomId) {
openSnackbar({
message: '방 정보를 찾을 수 없습니다.',
variant: 'top',
onClose: () => {},
});
navigate(-1);
return;
}
try {
setIsLoading(true);
const roomNum = Number(roomId);
if (!Number.isFinite(roomNum) || roomNum <= 0) {
openSnackbar({
message: '유효하지 않은 방 정보입니다.',
variant: 'top',
onClose: () => {},
});
navigate(-1);
return;
}
const response = await getBookPage(roomNum);
if (response.isSuccess) {
// …
🤖 Prompt for AI Agents
In src/pages/recordwrite/RecordWrite.tsx around lines 31 to 46, the code calls
getBookPage(parseInt(roomId)) without validating that roomId parses to a valid
integer, risking requests to endpoints like /rooms/NaN/book-page; validate
roomId before calling the API by parsing with parseInt(roomId, 10) (or
Number(roomId)) and checking !isNaN(parsed) and Number.isInteger(parsed) (or
parsed > 0 if IDs are positive), and if invalid show the same snackbar error and
navigate(-1) then return; ensure setIsLoading is only set to true after
validation so you don't enter a loading state for invalid IDs.

Comment on lines +113 to 117
if (isSubmitting || !roomId) return;

setIsSubmitting(true);

try {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

제출 시에도 roomId 숫자 검증을 수행하고, parseInt 사용을 정리해 주세요.

서밋 경로에서도 roomId가 비정상일 수 있으므로 동일한 숫자 검증을 적용하고, 이미 검증한 값을 재사용하는 편이 안전합니다.

다음 패치를 제안합니다:

-    if (isSubmitting || !roomId) return;
+    if (isSubmitting) return;
+    if (!roomId) {
+      openSnackbar({
+        message: '방 정보를 찾을 수 없습니다.',
+        variant: 'top',
+        onClose: () => {},
+      });
+      return;
+    }
+    const roomNum = Number(roomId);
+    if (!Number.isFinite(roomNum) || roomNum <= 0) {
+      openSnackbar({
+        message: '유효하지 않은 방 정보입니다.',
+        variant: 'top',
+        onClose: () => {},
+      });
+      return;
+    }
...
-      const response = await createRecord(parseInt(roomId), recordData);
+      const response = await createRecord(roomNum, recordData);

Also applies to: 155-156

Comment on lines +126 to +142
if (pageRange.trim() !== '') {
finalPage = parseInt(pageRange.trim());
} else {
finalPage = lastRecordedPage;
}
}

// 페이지 유효성 검사
if (finalPage <= 0 || finalPage > totalPages) {
openSnackbar({
message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`,
variant: 'top',
onClose: () => {},
});
setIsSubmitting(false);
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

페이지 입력값 파싱/검증이 불충분합니다(NaN/소수/문자열 통과 가능).

parseInt와 현재 검증식만으로는 다음 케이스가 통과합니다:

  • pageRange = 'abc'NaN이 되어 비교식이 모두 false → NaN이 API로 전송
  • pageRange = '10-20'parseInt가 10으로 잘려 의도와 다른 페이지 전송
  • pageRange = '3.5' → 3으로 내림되어 부정확

정수·유한수 여부를 명시적으로 검사해 주세요.

다음 패치를 제안합니다:

-        if (pageRange.trim() !== '') {
-          finalPage = parseInt(pageRange.trim());
-        } else {
-          finalPage = lastRecordedPage;
-        }
+        if (pageRange.trim() !== '') {
+          const parsed = Number(pageRange.trim());
+          if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
+            openSnackbar({
+              message: '페이지는 1 이상 전체 페이지 이하의 정수여야 합니다.',
+              variant: 'top',
+              onClose: () => {},
+            });
+            setIsSubmitting(false);
+            return;
+          }
+          finalPage = parsed;
+        } else {
+          finalPage = lastRecordedPage;
+        }
 
-      if (finalPage <= 0 || finalPage > totalPages) {
+      if (!Number.isFinite(finalPage) || !Number.isInteger(finalPage) || finalPage <= 0 || finalPage > totalPages) {
         openSnackbar({
           message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`,
           variant: 'top',
           onClose: () => {},
         });
         setIsSubmitting(false);
         return;
       }

Also applies to: 134-140

🤖 Prompt for AI Agents
In src/pages/recordwrite/RecordWrite.tsx around lines 126-142, the current
parseInt+range check allows invalid inputs (NaN, ranges like "10-20", decimals
like "3.5", or strings like "abc") to pass; replace parseInt usage with a strict
integer validation: trim the input, if empty use lastRecordedPage, otherwise
verify the trimmed value matches /^\d+$/ (or convert to Number and assert
Number.isInteger(value) && Number.isFinite(value)), then parse with Number(...)
and only proceed if the resulting integer is within 1..totalPages; on invalid
input show the same snackbar, setIsSubmitting(false) and return.

Comment on lines +241 to 245
rightButton={<div className="complete">완료</div>}
onLeftClick={handleBackClick}
onRightClick={handleCompleteClick}
isNextActive={isFormValid && !isSubmitting}
isNextActive={content.trim().length > 0 && !isSubmitting}
/>
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

TitleHeader의 rightButton/클릭 활성화 규약 확인 필요

rightButton에 div 노드를 직접 넘기고 isNextActive로 활성화 제어하는 패턴이 컴포넌트 시그니처와 합치되는지 확인해 주세요(예: ReactNode 허용 여부, 비활성 시 onRightClick 무시 등).

다음 스크립트로 TitleHeader의 Props 정의와 rightButton 처리부를 확인할 수 있습니다:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# TitleHeader 컴포넌트 파일 후보 찾기
fd -t f -i 'TitleHeader' | tee /tmp/th_files

# Props 정의와 rightButton 처리부 주변 맥락 확인
while read -r f; do
  echo "=== $f ==="
  rg -n -C3 -P '(interface|type)\s+\w*TitleHeader\w*Props|\bright(Button|Text)\b|onRightClick|isNextActive' "$f" || true
done </tmp/th_files

Length of output: 158


tee 명령어가 sandbox에 없어 스크립트가 실패했습니다. 아래처럼 > 리디렉션을 사용해 TitleHeader 파일을 찾고, Props 정의 및 rightButton 처리부를 재확인해주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# TitleHeader 컴포넌트 파일 후보 찾기 (tsx, ts 확장자)
fd -t f -e tsx -e ts -i TitleHeader > /tmp/title_files.txt

echo "=== TitleHeader 파일 목록 ==="
cat /tmp/title_files.txt

# Props 정의와 rightButton 처리부 주변 맥락 확인
while read -r f; do
  echo "=== $f ==="
  rg -n -C3 'interface\s+\w*TitleHeader\w*Props' "$f" || true
  rg -n -C3 'type\s+\w*TitleHeader\w*Props' "$f" || true
  rg -n -C3 '\brightButton\b' "$f" || true
  rg -n -C3 'onRightClick' "$f" || true
  rg -n -C3 'isNextActive' "$f" || true
done < /tmp/title_files.txt

Length of output: 2824


다음으로 NextButton 컴포넌트가 active=false일 때 onClick을 무시하도록 구현되어 있는지 확인해 보겠습니다. 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# NextButton 컴포넌트 파일 후보 찾기 (tsx, ts 확장자)
fd -t f -e tsx -e ts -i NextButton > /tmp/nextbutton_files.txt

echo "=== NextButton 파일 목록 ==="
cat /tmp/nextbutton_files.txt

# Props 정의와 구현부 확인
while read -r f; do
  echo "=== $f ==="
  rg -n -C3 'interface\s+NextButtonProps' "$f" || true
  rg -n -C3 'type\s+NextButtonProps' "$f" || true
  rg -n -C5 'const NextButton' "$f" || true
  rg -n -C5 'styled\.' "$f" || true
done < /tmp/nextbutton_files.txt

Length of output: 159


NextButton의 정의 위치와 구현을 확인하기 위해 아래 스크립트로 TitleHeader.tsx에서 import 경로와 스타일 정의(하단의 styled 코드 포함)를 출력해주세요.

이를 통해 NextButton 컴포넌트가 active=false일 때 클릭 이벤트를 무시하도록 구현되었는지 검증하겠습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# NextButton import 경로 확인
echo "=== NextButton import 경로 ==="
rg -n "^import .*NextButton" src/components/common/TitleHeader.tsx || true

# TitleHeader.tsx 전체 내용(200줄) 출력
echo "=== TitleHeader.tsx (1~200줄) ==="
sed -n '1,200p' src/components/common/TitleHeader.tsx

Length of output: 2476


TitleHeader: isNextActive=false 상태에서 클릭 방지 로직 추가 필요

rightButton<div>를 직접 넘기는 건 React.ReactNode로 정의된 프로퍼티와 일치합니다. 다만 isNextActive는 현재 버튼의 스타일(커서·색상)만 제어하며, 클릭 이벤트(onRightClick)는 항상 바인딩되어 있어 비활성 상태에서도 호출됩니다.
비활성 상태에서 클릭을 무시하려면 TitleHeader 컴포넌트 구현을 수정해야 합니다.

수정 위치

  • 파일: src/components/common/TitleHeader.tsx
  • 위치: InnerHeader 내 NextButton 렌더 부분 (약 76–83줄)

예시 변경안

-      ) : rightButton ? (
-        <NextButton onClick={onRightClick} active={isNextActive}>
-          {rightButton}
-        </NextButton>
+      ) : rightButton ? (
+        <NextButton
+          active={isNextActive}
+          // 비활성 시 onRightClick 호출 방지
+          onClick={isNextActive ? onRightClick : undefined}
+        >
+          {rightButton}
+        </NextButton>
       ) : (
         <div />
       )}

이렇게 하면 isNextActive === false일 때 클릭 핸들러가 제거되어 사용자가 의도치 않게 콜백이 실행되는 문제를 방지할 수 있습니다.

📝 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
rightButton={<div className="complete">완료</div>}
onLeftClick={handleBackClick}
onRightClick={handleCompleteClick}
isNextActive={isFormValid && !isSubmitting}
isNextActive={content.trim().length > 0 && !isSubmitting}
/>
) : rightButton ? (
<NextButton
active={isNextActive}
// 비활성 시 onRightClick 호출 방지
onClick={isNextActive ? onRightClick : undefined}
>
{rightButton}
</NextButton>
) : (
<div />
)}

@heeeeyong heeeeyong assigned heeeeyong, ho0010 and ljh130334 and unassigned heeeeyong and ho0010 Aug 17, 2025
@ljh130334 ljh130334 merged commit a438cfd into develop Aug 17, 2025
3 checks passed
@ljh130334 ljh130334 deleted the feat/api-rooms-page branch August 19, 2025 01:58
@coderabbitai coderabbitai bot mentioned this pull request Aug 20, 2025
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.

3 participants