feat: rooms API 추가 연동 (방 나가기 / 기록 수정 / 투표 수정)#236
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough기록·투표 수정 API와 방 나가기 API를 추가하고, 기록/투표 작성 페이지에 수정 모드를 도입해 라우트·쿼리 파라미터로 초기화하며 PATCH 기반 업데이트를 호출하도록 변경했다. 관련 컴포넌트에 편집용 props와 자동 포커스/비활성화 로직을 추가했다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as 사용자
participant RW as RecordWrite 페이지
participant API as updateRecord API
participant MEM as 메모리 뷰
rect #E6F0FF
note over RW: 수정 모드: 쿼리 파라미터로 초기화
U->>RW: 저장 클릭
RW->>API: PATCH /rooms/{roomId}/records/{recordId} { content }
API-->>RW: ApiResponse
alt 응답 isSuccess === true
RW-->>MEM: navigate(/rooms/{roomId}/memory)
else 실패/오류
RW-->>U: 오류 스낵바 표시
end
end
sequenceDiagram
participant U as 사용자
participant PW as PollWrite 페이지
participant API as updateVote API
participant MEM as 메모리 뷰
rect #E6FFE6
note over PW: 수정 모드: 쿼리 파라미터로 초기화
U->>PW: 저장 클릭
PW->>API: PATCH /rooms/{roomId}/votes/{voteId} { content }
API-->>PW: ApiResponse
alt 응답 isSuccess === true
PW-->>MEM: navigate(/rooms/{roomId}/memory)
else 실패/오류
PW-->>U: 오류 스낵바 표시
end
end
sequenceDiagram
participant U as 사용자
participant GD as ParticipatedGroupDetail
participant API as leaveRoom API
participant G as 그룹 목록 (/group)
U->>GD: 나가기 확정
GD->>API: DELETE /rooms/{roomId}/leave
API-->>GD: LeaveRoomResponse
alt 응답 isSuccess === true
GD-->>G: navigate(/group, replace=true)
else 실패/오류
GD-->>U: 오류 스낵바(message)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks (3 passed)✅ Passed checks (3 passed)
Poem
✨ 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/components/recordwrite/PageRangeSection.tsx (1)
83-91: isDisabled=true여도 토글이 동작하는 논리 버그.UI는 비활성처럼 보이지만 onClick은 그대로 동작합니다. 핸들러에서 isDisabled 가드를 추가해 주세요.
const handleToggleClick = () => { + if (isDisabled) return; if (canUseOverall) { onOverallToggle(); } else {src/components/memory/RecordItem/RecordItem.tsx (1)
156-166: roomId 미존재 시 '1'로 대체 호출은 위험합니다.다른 방에 잘못된 API 호출이 발생할 수 있습니다. roomId가 없으면 동작을 중단하고 사용자에게 안내해 주세요.
- const handleDelete = useCallback(async () => { - const currentRoomId = roomId || '1'; + const handleDelete = useCallback(async () => { + if (!roomId) { + openSnackbar({ message: '방 정보를 확인할 수 없어요.', variant: 'top', onClose: () => {} }); + return; + } + const currentRoomId = roomId; const recordId = parseInt(record.id);- const handlePinRecord = useCallback(async () => { - const currentRoomId = roomId || '1'; + const handlePinRecord = useCallback(async () => { + if (!roomId) { + openSnackbar({ message: '방 정보를 확인할 수 없어요.', variant: 'top', onClose: () => {} }); + return; + } + const currentRoomId = roomId; const recordId = parseInt(record.id);Also applies to: 216-221
src/pages/pollwrite/PollWrite.tsx (1)
159-174: roomId/voteId 숫자 검증을 선행하세요(방어적 프로그래밍).URL 파라미터가 비정상일 때 조기 종료가 안전합니다.
- if (isSubmitting || !roomId) return; + if (isSubmitting || !roomId) return; + const roomIdNum = Number(roomId); + if (!Number.isInteger(roomIdNum)) { + openSnackbar({ message: '유효하지 않은 방 정보입니다.', variant: 'top', onClose: () => {} }); + return; + } ... - if (!voteId) { + if (!voteId || !Number.isInteger(Number(voteId))) { openSnackbar({ message: '투표 정보를 찾을 수 없습니다.', variant: 'top', onClose: () => {}, }); setIsSubmitting(false); return; }그리고 아래 호출부도 정수형으로 일관 유지:
- const response = await updateVote(parseInt(roomId), parseInt(voteId), updateData); + const response = await updateVote(roomIdNum, Number(voteId), updateData);
🧹 Nitpick comments (14)
src/types/record.ts (1)
59-63: 업데이트 응답 스키마 확인 필요(roomId만 반환).수정 완료 후 UI에서 recordId/voteId나 최종 content 등이 필요하지 않은지 백엔드 스펙을 한 번 더 확인해 주세요. 추후 추가 필드가 필요하면 타입 확장이 필요합니다.
Also applies to: 70-73
src/components/recordwrite/RecordContentSection.tsx (1)
39-52: autoFocus 안정성 보강: DOM autoFocus + rAF로 커서 이동.일부 모바일(iOS Safari)에서 programmatic focus가 불안정할 수 있어 DOM 속성도 함께 사용하는 편이 안전합니다. 커서 이동은 requestAnimationFrame으로 미세 타이밍 이슈를 줄여주세요.
useEffect(() => { adjustHeight(); - - // autoFocus가 true이고 textarea가 있으면 포커스 및 커서를 끝으로 이동 - if (autoFocus && textareaRef.current) { - const textarea = textareaRef.current; - textarea.focus(); - // 커서를 텍스트 끝으로 이동 - const length = textarea.value.length; - textarea.setSelectionRange(length, length); - } + // autoFocus 시 포커스 및 커서를 끝으로 이동 + if (autoFocus && textareaRef.current) { + const el = textareaRef.current; + requestAnimationFrame(() => { + el.focus(); + const length = el.value.length; + el.setSelectionRange(length, length); + }); + } }, [autoFocus]); ... <TextArea ref={textareaRef} placeholder="...한 생각이 들었어요. 🤔" value={content} onChange={handleChange} maxLength={maxLength} rows={1} + autoFocus={autoFocus} />Also applies to: 61-68
src/components/recordwrite/PageRangeSection.tsx (1)
156-173: 토글 비활성 상태 표현 불일치(라벨/슬라이더에도 반영).라벨과 슬라이더는 canUseOverall만 보고 있어 isDisabled를 반영하지 못합니다. 시각/조작 불일치를 해소해 주세요.
- <LeftSection> + <LeftSection> <InfoIcon onClick={handleInfoClick}> <img src={infoIcon} alt="정보" /> </InfoIcon> - <ToggleLabel disabled={!canUseOverall}>총평</ToggleLabel> + <ToggleLabel disabled={!canUseOverall || isDisabled}>총평</ToggleLabel> </LeftSection> <ToggleSwitch active={isOverallEnabled} onClick={handleToggleClick} - disabled={!canUseOverall || isDisabled} + disabled={!canUseOverall || isDisabled} > - <ToggleSlider active={isOverallEnabled} disabled={!canUseOverall} /> + <ToggleSlider active={isOverallEnabled} disabled={!canUseOverall || isDisabled} /> </ToggleSwitch>src/api/rooms/leaveRoom.ts (1)
1-10: API 응답 타입 통일(공통 ApiResponse 사용 제안).다른 모듈과 일관되게 ApiResponse 제네릭을 재사용하면 타입 중복을 줄이고 유지보수가 쉬워집니다.
-import { apiClient } from '../index'; +import { apiClient } from '../index'; +import type { ApiResponse } from '@/types/record'; -// 방 나가기 응답 타입 -export interface LeaveRoomResponse { - isSuccess: boolean; - code: number; - message: string; - data: string; -} +// 방 나가기 응답 타입 +export type LeaveRoomResponse = ApiResponse<string>;src/components/memory/RecordItem/RecordItem.tsx (3)
135-143: 긴 쿼리스트링 전송 대신 상태 전달/재조회 검토.content/옵션 배열을 URL에 담으면 브라우저 URL 길이 제한 및 공유 시 노출 이슈가 있습니다. location.state 또는 전역 스토어로 전달하거나 edit 페이지에서 recordId 기반 재조회하는 설계를 권장합니다.
136-141: poll 옵션 직렬화 시 안전 가드 추가 제안.옵션 객체 스키마가 불완전할 때를 대비해 null-safe 매핑/빈값 필터링이 안전합니다.
- options: JSON.stringify(pollOptions?.map(option => option.text) || []) + options: JSON.stringify((pollOptions ?? []).map(o => o?.text ?? '').filter(Boolean))
68-69: parseInt에 기수(10) 명시.암묵적 10진 해석에 의존하지 않도록 radix를 명시해 주세요.
- const postId = parseInt(id); + const postId = parseInt(id, 10); ... - const recordId = parseInt(record.id); + const recordId = parseInt(record.id, 10); ... - const recordId = parseInt(record.id); + const recordId = parseInt(record.id, 10); ... - openCommentBottomSheet(parseInt(id), type === 'poll' ? 'VOTE' : 'RECORD'); + openCommentBottomSheet(parseInt(id, 10), type === 'poll' ? 'VOTE' : 'RECORD'); ... - postId={parseInt(id)} + postId={parseInt(id, 10)}Also applies to: 157-158, 217-218, 288-289, 371-374
src/pages/groupDetail/ParticipatedGroupDetail.tsx (1)
158-163: Axios 에러 판별을 안전한 타입가드로 교체하세요.
'response' in error체크는 취약합니다.axios.isAxiosError를 사용해 타입을 안전하게 좁히세요.- if (error && typeof error === 'object' && 'response' in error) { - const axiosError = error as { response?: { data?: { message?: string } } }; - if (axiosError.response?.data?.message) { - errorMessage = axiosError.response.data.message; - } - } + if (isAxiosError(error)) { + const msg = error.response?.data?.message; + if (typeof msg === 'string' && msg.trim()) { + errorMessage = msg; + } + }추가 import(파일 상단에 배치):
import { isAxiosError } from 'axios';src/components/pollwrite/PollCreationSection.tsx (1)
131-133: readOnly는 disabled와 중복 — 하나만 두세요.
disabled만으로도 입력 비활성화는 충분합니다. 중복 속성은 제거하는 편이 간결합니다.- disabled={isEditMode} - readOnly={isEditMode} + disabled={isEditMode}src/pages/pollwrite/PollWrite.tsx (3)
164-179: 수정 모드에서도 내용 공백 제출 방지(이중 방어).헤더의 활성화 조건이 있더라도 내부에서도 한 번 더 검증해 서버에 빈 문자열이 가지 않도록 하세요.
if (isEditMode) { // 수정 모드: 내용만 수정 + if (!pollContent.trim()) { + openSnackbar({ message: '내용을 입력해주세요.', variant: 'top', onClose: () => {} }); + setIsSubmitting(false); + return; + }
180-182: 디버그 로그는 제거하거나 환경별로 게이트하세요.콘솔 로그는 노이즈와 잠재적 정보 노출 우려가 있습니다. 개발 환경에서만 출력되도록 제한하세요.
- console.log('투표 수정 API 호출:', updateData); - console.log('roomId:', roomId, 'voteId:', voteId); + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.log('투표 수정 API 호출:', { roomId, voteId, updateData }); + } ... - console.log('투표 수정 성공:', response.data); + if (process.env.NODE_ENV !== 'production') console.log('투표 수정 성공'); ... - console.error('투표 수정 실패:', response.message); + if (process.env.NODE_ENV !== 'production') console.error('투표 수정 실패:', response.message); ... - console.log('투표 생성 API 호출:', voteData); - console.log('roomId:', roomId); + if (process.env.NODE_ENV !== 'production') { + console.log('투표 생성 API 호출:', { roomId, voteData }); + } ... - console.error('투표 생성 실패:', response.message); + if (process.env.NODE_ENV !== 'production') console.error('투표 생성 실패:', response.message);Also applies to: 185-187, 199-206, 256-258, 271-277
139-141: 쿼리 파라미터에 의존하는 초기화라면 의존성 배열에 searchParams 고려.편집 링크에서 쿼리만 바뀌는 내비게이션이 가능하다면,
searchParams를 의존성에 추가해 재동기화하세요. 라우팅 구조상 변경될 일이 없다면 스킵해도 됩니다.가능 시:
- }, [roomId, isEditMode]); + }, [roomId, isEditMode, searchParams]);src/pages/recordwrite/RecordWrite.tsx (2)
130-130: useEffect 의존성 배열 검토 필요
isEditMode는recordId에서 파생된 값이므로 의존성 배열에 포함할 필요가 없습니다.recordId만 포함하면 충분합니다.- }, [roomId, isEditMode]); + }, [roomId, recordId]);
200-212: 페이지 결정 로직 개선 가능페이지 결정 로직을 더 명확하게 리팩토링할 수 있습니다.
- // 페이지 범위 결정 - let finalPage: number; - - if (isOverallEnabled) { - // 총평인 경우: 책의 마지막 페이지 또는 전체 페이지 수 사용 - finalPage = totalPages; - } else { - // 일반 기록인 경우 - if (pageRange.trim() !== '') { - finalPage = parseInt(pageRange.trim()); - } else { - finalPage = lastRecordedPage; - } - } + // 페이지 범위 결정 + const finalPage = isOverallEnabled + ? totalPages + : pageRange.trim() + ? parseInt(pageRange.trim()) + : lastRecordedPage;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (12)
src/api/record/updateRecord.ts(1 hunks)src/api/record/updateVote.ts(1 hunks)src/api/rooms/leaveRoom.ts(1 hunks)src/components/memory/RecordItem/RecordItem.tsx(1 hunks)src/components/pollwrite/PollCreationSection.tsx(3 hunks)src/components/recordwrite/PageRangeSection.tsx(4 hunks)src/components/recordwrite/RecordContentSection.tsx(2 hunks)src/pages/groupDetail/ParticipatedGroupDetail.tsx(2 hunks)src/pages/index.tsx(1 hunks)src/pages/pollwrite/PollWrite.tsx(10 hunks)src/pages/recordwrite/RecordWrite.tsx(9 hunks)src/types/record.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
src/api/record/updateRecord.ts (2)
src/types/record.ts (3)
ApiResponse(75-80)UpdateRecordData(60-62)UpdateRecordRequest(55-57)src/api/index.ts (1)
apiClient(7-14)
src/api/record/updateVote.ts (2)
src/types/record.ts (3)
ApiResponse(75-80)UpdateVoteData(70-72)UpdateVoteRequest(65-67)src/api/index.ts (1)
apiClient(7-14)
src/pages/groupDetail/ParticipatedGroupDetail.tsx (1)
src/api/rooms/leaveRoom.ts (1)
leaveRoom(12-21)
src/components/pollwrite/PollCreationSection.tsx (1)
src/components/pollwrite/PollCreationSection.styled.ts (5)
Section(4-8)PollContentContainer(10-12)PollInput(14-28)DeleteButton(61-81)OptionInputContainer(36-43)
src/api/rooms/leaveRoom.ts (1)
src/api/index.ts (1)
apiClient(7-14)
src/pages/recordwrite/RecordWrite.tsx (5)
src/hooks/usePopupActions.ts (1)
usePopupActions(9-35)src/api/rooms/getBookPage.ts (1)
getBookPage(20-28)src/types/record.ts (2)
UpdateRecordRequest(55-57)CreateRecordRequest(2-6)src/api/record/updateRecord.ts (1)
updateRecord(8-23)src/api/record/createRecord.ts (1)
createRecord(8-14)
src/pages/pollwrite/PollWrite.tsx (3)
src/api/rooms/getBookPage.ts (1)
getBookPage(20-28)src/types/record.ts (2)
UpdateVoteRequest(65-67)CreateVoteRequest(20-25)src/api/record/updateVote.ts (1)
updateVote(8-23)
src/components/recordwrite/PageRangeSection.tsx (1)
src/components/recordwrite/PageRangeSection.styled.ts (9)
PageSuffix(77-88)InputWrapper(35-43)PageInputContainer(23-33)ToggleContainer(127-133)LeftSection(135-139)InfoIcon(141-153)ToggleLabel(155-160)ToggleSwitch(162-174)ToggleSlider(176-187)
🔇 Additional comments (7)
src/pages/index.tsx (1)
65-68: 편집 라우트 추가 👍작성 컴포넌트 재사용/파라미터 기반 모드 전환이 명확합니다. 다른 경로와의 충돌도 없습니다.
src/api/record/updateRecord.ts (1)
7-23: 패턴 일관성/에러 처리 적절 — 승인합니다.PATCH 경로/제네릭 응답/로깅 패턴이 명확합니다. Poll 업데이트와도 일관됩니다.
src/api/record/updateVote.ts (1)
7-23: 투표 수정 API 래퍼 구현 깔끔 — 승인합니다.Record 업데이트와 동일한 구조로 유지보수 용이합니다.
src/pages/recordwrite/RecordWrite.tsx (4)
58-58: URL 인코딩된 콘텐츠 디코딩 처리가 적절합니다쿼리 파라미터로 전달된 콘텐츠를
decodeURIComponent로 디코딩하는 처리가 올바르게 구현되었습니다.
51-77: 수정 모드 초기화 로직이 잘 구현되었습니다쿼리 파라미터에서 기존 데이터를 로드하고 필요한 책 정보를 조회하는 로직이 적절히 구현되었습니다.
166-168: 타입 안정성 향상
UpdateRecordRequest타입이 이미 import되어 있으므로 타입 어노테이션이 적절합니다.
338-344: 수정 모드 UI 제약사항이 적절히 구현되었습니다수정 모드에서 페이지 설정을 비활성화하고 총평 토글을 숨기며, 콘텐츠 입력란에 자동 포커스를 설정하는 UX가 잘 구현되었습니다.
| // autoFocus 처리 | ||
| useEffect(() => { | ||
| if (autoFocus && contentInputRef.current) { | ||
| const input = contentInputRef.current; | ||
| input.focus(); | ||
| // 커서를 텍스트 끝으로 이동 | ||
| const length = input.value.length; | ||
| input.setSelectionRange(length, length); | ||
| } | ||
| }, [autoFocus]); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
수정 모드에서 내용 로드 후 커서가 앞에 머무는 이슈. content 의존성 추가로 커서를 끝으로 이동.
초기 마운트 시점에 content가 아직 비어 있으면 커서가 앞에 위치할 수 있습니다. content를 의존성에 추가해 값이 로드된 뒤에도 커서를 끝으로 보내세요.
- useEffect(() => {
- if (autoFocus && contentInputRef.current) {
- const input = contentInputRef.current;
- input.focus();
- // 커서를 텍스트 끝으로 이동
- const length = input.value.length;
- input.setSelectionRange(length, length);
- }
- }, [autoFocus]);
+ useEffect(() => {
+ if (!autoFocus || !contentInputRef.current) return;
+ const input = contentInputRef.current;
+ // 값 반영 후 커서 이동 보장을 위해 다음 tick에 실행
+ requestAnimationFrame(() => {
+ input.focus();
+ const length = input.value.length;
+ input.setSelectionRange(length, length);
+ });
+ }, [autoFocus, content]);📝 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.
| // autoFocus 처리 | |
| useEffect(() => { | |
| if (autoFocus && contentInputRef.current) { | |
| const input = contentInputRef.current; | |
| input.focus(); | |
| // 커서를 텍스트 끝으로 이동 | |
| const length = input.value.length; | |
| input.setSelectionRange(length, length); | |
| } | |
| }, [autoFocus]); | |
| // autoFocus 처리 | |
| useEffect(() => { | |
| // autoFocus가 꺼져 있거나 ref가 없으면 아무 작업 없이 종료 | |
| if (!autoFocus || !contentInputRef.current) return; | |
| const input = contentInputRef.current; | |
| // 값 반영 후 커서 이동 보장을 위해 다음 tick에 실행 | |
| requestAnimationFrame(() => { | |
| input.focus(); | |
| const length = input.value.length; | |
| input.setSelectionRange(length, length); | |
| }); | |
| }, [autoFocus, content]); |
🤖 Prompt for AI Agents
In src/components/pollwrite/PollCreationSection.tsx around lines 95 to 105, the
useEffect that focuses the input only depends on autoFocus so when content is
loaded later (e.g. in edit mode) the cursor can remain at the start; add content
to the dependency array and ensure the effect runs when content changes so after
content is populated it focuses the input and moves the selection/cursor to the
end (keep the existing null checks for contentInputRef and autoFocus).
| if (!roomId) return; | ||
|
|
||
| try { | ||
| const response = await leaveRoom(parseInt(roomId)); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
roomId 숫자 검증 추가 필요(잘못된 URL 파라미터로 NaN 호출 가능).
parseInt(roomId)가 NaN이면 /rooms/NaN/leave로 호출됩니다. UX 관점에서도 즉시 차단이 낫습니다.
- if (!roomId) return;
+ if (!roomId || Number.isNaN(Number(roomId))) {
+ openSnackbar({
+ message: '유효하지 않은 방 정보입니다.',
+ variant: 'top',
+ isError: true,
+ onClose: () => {},
+ });
+ return;
+ }
- const response = await leaveRoom(parseInt(roomId));
+ const response = await leaveRoom(Number(roomId));📝 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 (!roomId) return; | |
| try { | |
| const response = await leaveRoom(parseInt(roomId)); | |
| if (!roomId || Number.isNaN(Number(roomId))) { | |
| openSnackbar({ | |
| message: '유효하지 않은 방 정보입니다.', | |
| variant: 'top', | |
| isError: true, | |
| onClose: () => {}, | |
| }); | |
| return; | |
| } | |
| try { | |
| const response = await leaveRoom(Number(roomId)); |
🤖 Prompt for AI Agents
In src/pages/groupDetail/ParticipatedGroupDetail.tsx around lines 129 to 133,
parseInt(roomId) can produce NaN (from malformed URL) which causes an invalid
API call; validate roomId first by parsing with parseInt(roomId, 10) and
verifying Number.isFinite(parsedId) and parsedId > 0 (or use a /^\d+$/ check)
before calling leaveRoom; if invalid, return early and surface a user-friendly
error (toast, console.warn, or redirect) so the API is never invoked with NaN.
| // 페이지 범위 결정 | ||
| let finalPage: number; | ||
|
|
||
| if (isOverallEnabled) { | ||
| // 총평인 경우: 책의 마지막 페이지 또는 전체 페이지 수 사용 | ||
| finalPage = totalPages; | ||
| } else { | ||
| finalPage = lastRecordedPage; | ||
| // 일반 투표인 경우 | ||
| 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; | ||
| } | ||
| // 페이지 유효성 검사 | ||
| if (finalPage <= 0 || finalPage > totalPages) { | ||
| openSnackbar({ | ||
| message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`, | ||
| variant: 'top', | ||
| onClose: () => {}, | ||
| }); | ||
| setIsSubmitting(false); | ||
| return; | ||
| } |
There was a problem hiding this comment.
중대 버그: pageRange가 숫자가 아니면 NaN이 생성되어 검증을 통과합니다.
parseInt가 NaN이면 finalPage <= 0/> totalPages 비교가 모두 false가 되어 잘못된 값이 API로 전송됩니다.
- // 페이지 범위 결정
- let finalPage: number;
-
- if (isOverallEnabled) {
- // 총평인 경우: 책의 마지막 페이지 또는 전체 페이지 수 사용
- finalPage = totalPages;
- } else {
- // 일반 투표인 경우
- if (pageRange.trim() !== '') {
- finalPage = parseInt(pageRange.trim());
- } else {
- finalPage = lastRecordedPage;
- }
- }
-
- // 페이지 유효성 검사
- if (finalPage <= 0 || finalPage > totalPages) {
+ // 페이지 범위 결정 + 유효성 검사
+ let finalPage: number;
+ if (isOverallEnabled) {
+ finalPage = totalPages;
+ } else if (pageRange.trim() !== '') {
+ const parsed = Number(pageRange.trim());
+ if (!Number.isInteger(parsed)) {
+ openSnackbar({
+ message: '페이지에는 정수를 입력해주세요.',
+ variant: 'top',
+ onClose: () => {},
+ });
+ setIsSubmitting(false);
+ return;
+ }
+ finalPage = parsed;
+ } else {
+ finalPage = lastRecordedPage;
+ }
+
+ if (!Number.isInteger(finalPage) || finalPage <= 0 || 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.
| // 페이지 범위 결정 | |
| let finalPage: number; | |
| if (isOverallEnabled) { | |
| // 총평인 경우: 책의 마지막 페이지 또는 전체 페이지 수 사용 | |
| finalPage = totalPages; | |
| } else { | |
| finalPage = lastRecordedPage; | |
| // 일반 투표인 경우 | |
| 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; | |
| } | |
| // 페이지 유효성 검사 | |
| if (finalPage <= 0 || finalPage > totalPages) { | |
| openSnackbar({ | |
| message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`, | |
| variant: 'top', | |
| onClose: () => {}, | |
| }); | |
| setIsSubmitting(false); | |
| return; | |
| } | |
| // 페이지 범위 결정 + 유효성 검사 | |
| let finalPage: number; | |
| if (isOverallEnabled) { | |
| finalPage = totalPages; | |
| } else if (pageRange.trim() !== '') { | |
| const parsed = Number(pageRange.trim()); | |
| if (!Number.isInteger(parsed)) { | |
| openSnackbar({ | |
| message: '페이지에는 정수를 입력해주세요.', | |
| variant: 'top', | |
| onClose: () => {}, | |
| }); | |
| setIsSubmitting(false); | |
| return; | |
| } | |
| finalPage = parsed; | |
| } else { | |
| finalPage = lastRecordedPage; | |
| } | |
| if (!Number.isInteger(finalPage) || finalPage <= 0 || finalPage > totalPages) { | |
| openSnackbar({ | |
| message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`, | |
| variant: 'top', | |
| onClose: () => {}, | |
| }); | |
| setIsSubmitting(false); | |
| return; | |
| } |
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
src/pages/recordwrite/RecordWrite.tsx (1)
150-187: 이전 console. 잔존 로그 제거 이행 확인*이전 코멘트에서 지적된 console.log/error가 본 파일에서 제거된 것으로 보입니다. 좋습니다.
🧹 Nitpick comments (1)
src/pages/recordwrite/RecordWrite.tsx (1)
50-67: 콘텐츠를 쿼리스트링으로 전달하지 않는 방향 고려기록 내용이 URL에 노출되면 길이 제한, 공유/이력 노출, 인코딩 문제(이중 디코딩 포함) 위험이 있습니다.
navigate(..., { state })또는 전역 상태/캐시 사용을 권장합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/pages/recordwrite/RecordWrite.tsx(10 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/pages/recordwrite/RecordWrite.tsx (4)
src/api/rooms/getBookPage.ts (1)
getBookPage(20-28)src/types/record.ts (2)
UpdateRecordRequest(55-57)CreateRecordRequest(2-6)src/api/record/updateRecord.ts (1)
updateRecord(8-23)src/api/record/createRecord.ts (1)
createRecord(8-14)
🔇 Additional comments (2)
src/pages/recordwrite/RecordWrite.tsx (2)
126-127: useEffect 의존성 검토 요청initializeData에서
searchParams,openSnackbar,navigate를 사용합니다. 의도적으로 재실행을 제한한 것인지 확인 부탁드립니다. 검색 파라미터만 변경되는 경우 재초기화가 필요하다면 의존성에 포함하세요.
322-329: 수정 모드 UI 연결 LGTMisDisabled/hideToggle, autoFocus 연동이 명확합니다. UX 의도에 부합합니다.
| if (existingContent) { | ||
| setContent(decodeURIComponent(existingContent)); | ||
| } |
There was a problem hiding this comment.
URLSearchParams는 이미 디코드됨 — decodeURIComponent 제거 필요
searchParams.get(...)는 이미 디코딩된 문자열을 반환합니다. decodeURIComponent를 다시 호출하면 예외가 나거나 문자열이 깨질 수 있습니다.
다음과 같이 교체하세요.
- setContent(decodeURIComponent(existingContent));
+ setContent(existingContent);📝 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 (existingContent) { | |
| setContent(decodeURIComponent(existingContent)); | |
| } | |
| if (existingContent) { | |
| setContent(existingContent); | |
| } |
🤖 Prompt for AI Agents
In src/pages/recordwrite/RecordWrite.tsx around lines 56 to 58, the code calls
decodeURIComponent on a value returned by searchParams.get(...), but
URLSearchParams already returns a decoded string; remove the decodeURIComponent
call and directly pass the existingContent to setContent to avoid
double-decoding or exceptions.
| // 수정 모드에서도 전체 페이지 수는 필요하므로 책 정보 조회 | ||
| const response = await getBookPage(parseInt(roomId)); | ||
| if (response.isSuccess) { | ||
| setTotalPages(response.data.totalBookPage); | ||
| } | ||
|
|
||
| setIsLoading(false); | ||
| return; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
roomId 숫자 유효성 검증 추가 및 NaN 호출 방지
parseInt 없이 Number와 정수 검증으로 안전하게 처리하고, 잘못된 ID일 경우 초기화 흐름을 중단하세요. 현재 상태에선 GET /rooms/NaN/book-page가 호출될 수 있습니다.
- // 수정 모드에서도 전체 페이지 수는 필요하므로 책 정보 조회
- const response = await getBookPage(parseInt(roomId));
+ // 수정 모드에서도 전체 페이지 수는 필요하므로 책 정보 조회
+ const roomIdNum = Number(roomId);
+ if (!Number.isInteger(roomIdNum)) {
+ openSnackbar({
+ message: '유효하지 않은 방 정보입니다.',
+ variant: 'top',
+ onClose: () => {},
+ });
+ setIsLoading(false);
+ navigate(-1);
+ return;
+ }
+ const response = await getBookPage(roomIdNum);📝 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 response = await getBookPage(parseInt(roomId)); | |
| if (response.isSuccess) { | |
| setTotalPages(response.data.totalBookPage); | |
| } | |
| setIsLoading(false); | |
| return; | |
| } | |
| // 수정 모드에서도 전체 페이지 수는 필요하므로 책 정보 조회 | |
| const roomIdNum = Number(roomId); | |
| if (!Number.isInteger(roomIdNum)) { | |
| openSnackbar({ | |
| message: '유효하지 않은 방 정보입니다.', | |
| variant: 'top', | |
| onClose: () => {}, | |
| }); | |
| setIsLoading(false); | |
| navigate(-1); | |
| return; | |
| } | |
| const response = await getBookPage(roomIdNum); | |
| if (response.isSuccess) { | |
| setTotalPages(response.data.totalBookPage); | |
| } | |
| setIsLoading(false); | |
| return; | |
| } |
🤖 Prompt for AI Agents
In src/pages/recordwrite/RecordWrite.tsx around lines 68-76, avoid
parseInt(roomId) and prevent calling GET /rooms/NaN/book-page by converting
roomId with Number, validating it with Number.isInteger (and optionally > 0),
and short-circuiting the flow when invalid: compute const id = Number(roomId);
if (!Number.isInteger(id) || id <= 0) { setIsLoading(false); return; } then call
getBookPage(id) and proceed to setTotalPages when response.isSuccess.
| const updateData: UpdateRecordRequest = { | ||
| content: content.trim(), | ||
| }; | ||
|
|
||
| const response = await updateRecord(parseInt(roomId), parseInt(recordId), updateData); | ||
|
|
||
| if (response.isSuccess) { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
update 호출 전 roomId/recordId 정수 검증 및 parseInt 제거
잘못된 파라미터로 /rooms/NaN/records/NaN 호출되는 상황을 차단하세요.
- const response = await updateRecord(parseInt(roomId), parseInt(recordId), updateData);
+ const roomIdNum = Number(roomId);
+ const recordIdNum = Number(recordId);
+ if (!Number.isInteger(roomIdNum) || !Number.isInteger(recordIdNum)) {
+ openSnackbar({
+ message: '유효하지 않은 식별자입니다.',
+ variant: 'top',
+ onClose: () => {},
+ });
+ setIsSubmitting(false);
+ return;
+ }
+ const response = await updateRecord(roomIdNum, recordIdNum, updateData);📝 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 updateData: UpdateRecordRequest = { | |
| content: content.trim(), | |
| }; | |
| const response = await updateRecord(parseInt(roomId), parseInt(recordId), updateData); | |
| if (response.isSuccess) { | |
| const updateData: UpdateRecordRequest = { | |
| content: content.trim(), | |
| }; | |
| const roomIdNum = Number(roomId); | |
| const recordIdNum = Number(recordId); | |
| if (!Number.isInteger(roomIdNum) || !Number.isInteger(recordIdNum)) { | |
| openSnackbar({ | |
| message: '유효하지 않은 식별자입니다.', | |
| variant: 'top', | |
| onClose: () => {}, | |
| }); | |
| setIsSubmitting(false); | |
| return; | |
| } | |
| const response = await updateRecord(roomIdNum, recordIdNum, updateData); | |
| if (response.isSuccess) { |
🤖 Prompt for AI Agents
In src/pages/recordwrite/RecordWrite.tsx around lines 163 to 169, validate
roomId and recordId are valid integers before calling updateRecord and remove
inline parseInt calls: parse both IDs once (using Number or parseInt with radix
10), check that they are not NaN and are integers (e.g., Number.isInteger), and
if invalid, abort the update flow (show an error/toast and return) to avoid
calling `/rooms/NaN/records/NaN`; then call updateRecord with the validated
numeric IDs.
| if (pageRange.trim() !== '') { | ||
| finalPage = parseInt(pageRange.trim()); | ||
| } else { | ||
| finalPage = lastRecordedPage; | ||
| } | ||
| } | ||
|
|
||
| console.log('기록 생성 API 호출:', recordData); | ||
| console.log('roomId:', roomId); | ||
| // 페이지 유효성 검사 | ||
| if (finalPage <= 0 || finalPage > totalPages) { | ||
| openSnackbar({ | ||
| message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`, | ||
| variant: 'top', | ||
| onClose: () => {}, | ||
| }); | ||
| setIsSubmitting(false); | ||
| return; | ||
| } |
There was a problem hiding this comment.
페이지 파싱 시 NaN/비정수 처리 누락
parseInt 사용으로 비정상 입력 시 NaN이 발생해 아래 범위 검증을 통과할 수 있습니다(비교 연산이 모두 false). 정수 여부를 엄격히 확인하세요.
- if (pageRange.trim() !== '') {
- finalPage = parseInt(pageRange.trim());
- } else {
- finalPage = lastRecordedPage;
- }
+ if (pageRange.trim() !== '') {
+ const parsed = Number(pageRange.trim());
+ if (!Number.isInteger(parsed)) {
+ openSnackbar({
+ message: '페이지는 정수로 입력해 주세요.',
+ variant: 'top',
+ onClose: () => {},
+ });
+ setIsSubmitting(false);
+ return;
+ }
+ finalPage = parsed;
+ } else {
+ finalPage = lastRecordedPage;
+ }
- // 페이지 유효성 검사
- if (finalPage <= 0 || finalPage > totalPages) {
+ // 페이지 유효성 검사
+ if (!Number.isInteger(finalPage) || finalPage <= 0 || 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 (pageRange.trim() !== '') { | |
| finalPage = parseInt(pageRange.trim()); | |
| } else { | |
| finalPage = lastRecordedPage; | |
| } | |
| } | |
| console.log('기록 생성 API 호출:', recordData); | |
| console.log('roomId:', roomId); | |
| // 페이지 유효성 검사 | |
| if (finalPage <= 0 || finalPage > totalPages) { | |
| openSnackbar({ | |
| message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`, | |
| variant: 'top', | |
| onClose: () => {}, | |
| }); | |
| setIsSubmitting(false); | |
| return; | |
| } | |
| if (pageRange.trim() !== '') { | |
| const parsed = Number(pageRange.trim()); | |
| if (!Number.isInteger(parsed)) { | |
| openSnackbar({ | |
| message: '페이지는 정수로 입력해 주세요.', | |
| variant: 'top', | |
| onClose: () => {}, | |
| }); | |
| setIsSubmitting(false); | |
| return; | |
| } | |
| finalPage = parsed; | |
| } else { | |
| finalPage = lastRecordedPage; | |
| } | |
| } | |
| // 페이지 유효성 검사 | |
| if (!Number.isInteger(finalPage) || finalPage <= 0 || finalPage > totalPages) { | |
| openSnackbar({ | |
| message: `유효하지 않은 페이지입니다. (1-${totalPages} 사이의 값을 입력해주세요)`, | |
| variant: 'top', | |
| onClose: () => {}, | |
| }); | |
| setIsSubmitting(false); | |
| return; | |
| } |
🤖 Prompt for AI Agents
In src/pages/recordwrite/RecordWrite.tsx around lines 198-214, the code parses
pageRange with parseInt but does not handle NaN or non-integer input; update the
parsing and validation so that after you set finalPage =
parseInt(pageRange.trim()) you explicitly check for Number.isInteger(finalPage)
(and that finalPage is not NaN) before proceeding, and treat any non-integer/NaN
result as invalid by calling openSnackbar and returning (same behavior as
out-of-range values); alternatively, use a stricter parse (e.g., match /^\d+$/)
to ensure only whole numbers are accepted, then convert and validate against
1..totalPages.
| // API 호출 | ||
| const response = await createRecord(parseInt(roomId), recordData); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
create 호출 전 roomId 정수 검증 및 parseInt 제거
생성 플로우에서도 동일하게 방어 코드를 추가하세요.
- const response = await createRecord(parseInt(roomId), recordData);
+ const roomIdNum = Number(roomId);
+ if (!Number.isInteger(roomIdNum)) {
+ openSnackbar({
+ message: '유효하지 않은 방 정보입니다.',
+ variant: 'top',
+ onClose: () => {},
+ });
+ setIsSubmitting(false);
+ return;
+ }
+ const response = await createRecord(roomIdNum, recordData);📝 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.
| // API 호출 | |
| const response = await createRecord(parseInt(roomId), recordData); | |
| // API 호출 | |
| const roomIdNum = Number(roomId); | |
| if (!Number.isInteger(roomIdNum)) { | |
| openSnackbar({ | |
| message: '유효하지 않은 방 정보입니다.', | |
| variant: 'top', | |
| onClose: () => {}, | |
| }); | |
| setIsSubmitting(false); | |
| return; | |
| } | |
| const response = await createRecord(roomIdNum, recordData); |
🤖 Prompt for AI Agents
In src/pages/recordwrite/RecordWrite.tsx around lines 223 to 225, the code calls
createRecord(parseInt(roomId), recordData) without validating roomId; remove the
inline parseInt and instead validate that roomId is a proper integer beforehand
(e.g., attempt to parse once, check Number.isInteger and handle NaN), and then
pass the validated integer to createRecord; if validation fails, return or show
an error/early exit so the API is never called with an invalid id.
#️⃣ 연관된 이슈
#106
📝 작업 내용
1. 방 나가기 API 연동
src/api/rooms/leaveRoom.ts에 DELETE/rooms/{roomId}/leaveAPI 연동 함수 구현/group)로 이동2. 기록 수정 API 연동
src/api/record/updateRecord.ts에 PATCH/rooms/{roomId}/records/{recordId}API 연동 함수 구현UpdateRecordRequest,UpdateRecordData인터페이스 추가/memory/record/edit/:roomId/:recordId경로 추가recordId파라미터를 통한 수정 모드 감지3. 투표 수정 API 연동
src/api/record/updateVote.ts에 PATCH/rooms/{roomId}/votes/{voteId}API 연동 함수 구현UpdateVoteRequest,UpdateVoteData인터페이스 추가/memory/poll/edit/:roomId/:voteId경로 추가voteId파라미터를 통한 수정 모드 감지handleEdit함수 확장4. 수정 페이지 UI/UX 개선
isDisabled,hideToggleprops 추가로 수정 모드에서 페이지 설정은 비활성화하되 정보는 표시isEditModeprop 추가로 투표 옵션 입력창들을disabled/readOnly상태로 변경5. 자동 포커스 및 커서 위치 개선
autoFocusprop 추가 및 수정 모드 진입 시 자동 포커스setSelectionRange를 통해 커서를 기존 텍스트 마지막 위치에 자동 배치autoFocusprop 및contentInputRef추가🕸️ 스크린샷
2025-09-09.2.51.03.mov
Summary by CodeRabbit