feat: 책 페이지 정보 조회 API 연동 구현 및 일부 로직 수정#129
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough새 API 래퍼(getBookPage) 추가와 이를 사용하는 작성 화면(Record/Poll) 초기 로딩·검증 흐름 도입. Memory 페이지는 게시물 로딩/필터링·오버뷰 상태 처리 변경. 두 개의 공용 컴포넌트에 새로운 필수 prop 추가. 경고(alert) → 스낵바로 전환, 로딩 상태 처리 추가. Changes
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: 메모리 화면으로 이동
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
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 unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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)로 바로 이탈하여finally의setIsLoading(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.
📒 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) 회피 목적에 부합합니다.
| // 페이지 유효성 검사 | ||
| if (finalPage <= 0 || finalPage > totalPages) { | ||
| openSnackbar({ | ||
| message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`, | ||
| variant: 'top', | ||
| onClose: () => {}, | ||
| }); | ||
| setIsSubmitting(false); | ||
| return; | ||
| } |
There was a problem hiding this comment.
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.
| // 페이지 유효성 검사 | |
| 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.
| 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) { |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| if (isSubmitting || !roomId) return; | ||
|
|
||
| setIsSubmitting(true); | ||
|
|
||
| try { |
There was a problem hiding this comment.
🛠️ 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
| 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; | ||
| } |
There was a problem hiding this comment.
페이지 입력값 파싱/검증이 불충분합니다(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.
| rightButton={<div className="complete">완료</div>} | ||
| onLeftClick={handleBackClick} | ||
| onRightClick={handleCompleteClick} | ||
| isNextActive={isFormValid && !isSubmitting} | ||
| isNextActive={content.trim().length > 0 && !isSubmitting} | ||
| /> |
There was a problem hiding this comment.
💡 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_filesLength 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.txtLength 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.txtLength 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.tsxLength 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.
| 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 /> | |
| )} |
#️⃣ 연관된 이슈
#106
📝 작업 내용
기록장에서 책 페이지 정보 조회 API를 연동하고 총평 기록이 제대로 표시되지 않는 문제를 해결했습니다.
🕸️ 주요 수정 사항
1. 기록 작성 API 수정 (RecordWrite.tsx, PollWrite.tsx)
getBookPageAPI를 추가로 호출하여 책 페이지 정보와 총평 작성 가능 여부를 확인2. 기록장 조회 API 수정 (Memory.tsx)
isOverview파라미터가 필요함을 확인isOverview: false)과 총평 기록(isOverview: true)을 병렬로 호출하여 모든 기록을 가져오도록 수정Promise.all을 사용하여 성능 최적화isOverviewEnabled값을readingProgress계산에 활용하여 총평보기 버튼 활성화 제어3. 필터링 로직 강화
recordType === 'overall'인 기록만 필터링하여 표시Summary by CodeRabbit