Skip to content

feat: rooms API 추가 연동 (방 나가기 / 기록 수정 / 투표 수정)#236

Merged
ljh130334 merged 4 commits intodevelopfrom
feat/api-rooms-jh
Sep 9, 2025
Merged

feat: rooms API 추가 연동 (방 나가기 / 기록 수정 / 투표 수정)#236
ljh130334 merged 4 commits intodevelopfrom
feat/api-rooms-jh

Conversation

@ljh130334
Copy link
Member

@ljh130334 ljh130334 commented Sep 9, 2025

#️⃣ 연관된 이슈

#106

📝 작업 내용

1. 방 나가기 API 연동

  • API 함수 생성: src/api/rooms/leaveRoom.ts에 DELETE /rooms/{roomId}/leave API 연동 함수 구현
  • UI 플로우: 확인 모달 → API 호출 → 성공 시 "모임 나가기를 완료했어요." 스낵바 표시 후 모임 메인페이지 (/group)로 이동

2. 기록 수정 API 연동

  • API 함수 생성: src/api/record/updateRecord.ts에 PATCH /rooms/{roomId}/records/{recordId} API 연동 함수 구현
  • 타입 정의: UpdateRecordRequest, UpdateRecordData 인터페이스 추가
  • 라우팅: /memory/record/edit/:roomId/:recordId 경로 추가
  • RecordWrite.tsx 개선:
    • recordId 파라미터를 통한 수정 모드 감지
    • 쿼리 파라미터에서 기존 기록 데이터 로드 (내용, 페이지, 기록 타입)
    • 수정 모드에서는 페이지 설정 비활성화 및 총평 토글 숨김
    • 타이틀을 "기록 수정"으로 변경
  • RecordWrite.tsx 수정: 수정 버튼 클릭 시 기존 데이터를 쿼리 파라미터로 전달하며 수정 페이지로 이동

3. 투표 수정 API 연동

  • API 함수 생성: src/api/record/updateVote.ts에 PATCH /rooms/{roomId}/votes/{voteId} API 연동 함수 구현
  • 타입 정의: UpdateVoteRequest, UpdateVoteData 인터페이스 추가
  • 라우팅: /memory/poll/edit/:roomId/:voteId 경로 추가
  • PollWrite.tsx 개선:
    • voteId 파라미터를 통한 수정 모드 감지
    • 쿼리 파라미터에서 기존 기록 데이터 로드 (내용, 페이지, 기록 타입, 투표 옵션들)
    • 수정 모드에서는 페이지 설정 비활성화 및 총평 토글 숨김
    • 타이틀을 "기록 수정"으로 변경
  • RecordWrite.tsx 수정: 투표와 기록 모두 처리하도록 handleEdit 함수 확장

4. 수정 페이지 UI/UX 개선

  • PageRangeSection.tsx 개선:
    • isDisabled, hideToggle props 추가로 수정 모드에서 페이지 설정은 비활성화하되 정보는 표시
    • 총평 토글 버튼을 수정 모드에서 숨김
  • PollCreationSection.tsx 개선:
    • isEditMode prop 추가로 투표 옵션 입력창들을 disabled/readOnly 상태로 변경
    • 수정 모드에서는 삭제 버튼들과 "항목 추가" 버튼 숨김
    • 투표 내용만 수정 가능하도록 제한

5. 자동 포커스 및 커서 위치 개선

  • RecordContentSection.tsx:
    • autoFocus prop 추가 및 수정 모드 진입 시 자동 포커스
    • setSelectionRange를 통해 커서를 기존 텍스트 마지막 위치에 자동 배치
  • PollCreationSection.tsx:
    • autoFocus prop 및 contentInputRef 추가
    • 수정 모드 진입 시 투표 내용 입력창에 자동 포커스 및 커서 위치 설정

🕸️ 스크린샷

2025-09-09.2.51.03.mov

Summary by CodeRabbit

  • 신기능
    • 기록·투표 수정 기능 추가: 편집 화면 라우트 및 편집 모드에서 수정 API 연동.
    • 편집 모드 UX 개선: 페이지 범위 입력 비활성화, 총평 토글 숨김, 내용·옵션 자동 포커스 등 도입.
    • 기록 항목에서 즉시 편집으로 이동(투표/일반 모두 지원).
    • 모임 나가기 기능 활성화: 성공/오류 스낵바 안내 후 그룹 목록으로 이동.

@ljh130334 ljh130334 self-assigned this Sep 9, 2025
@ljh130334 ljh130334 added ✨ Feature 기능 개발 📬 API 서버 API 통신 labels Sep 9, 2025
@vercel
Copy link

vercel bot commented Sep 9, 2025

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

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Sep 9, 2025 6:11am

@coderabbitai
Copy link

coderabbitai bot commented Sep 9, 2025

Walkthrough

기록·투표 수정 API와 방 나가기 API를 추가하고, 기록/투표 작성 페이지에 수정 모드를 도입해 라우트·쿼리 파라미터로 초기화하며 PATCH 기반 업데이트를 호출하도록 변경했다. 관련 컴포넌트에 편집용 props와 자동 포커스/비활성화 로직을 추가했다.

Changes

Cohort / File(s) Summary
API: Record 업데이트
src/api/record/updateRecord.ts
신규 모듈 추가. PATCH /rooms/{roomId}/records/{recordId} 호출, 응답 타입 UpdateRecordResponse 공개, 오류 로깅 및 재throw.
API: Vote 업데이트
src/api/record/updateVote.ts
신규 모듈 추가. PATCH /rooms/{roomId}/votes/{voteId} 호출, 응답 타입 UpdateVoteResponse 공개, 오류 로깅 및 재throw.
API: 방 나가기
src/api/rooms/leaveRoom.ts
DELETE /rooms/{roomId}/leave 호출 래퍼 추가. LeaveRoomResponse 타입·leaveRoom 함수 공개.
타입 추가
src/types/record.ts
UpdateRecordRequest, UpdateRecordData, UpdateVoteRequest, UpdateVoteData 인터페이스 추가.
라우팅: 수정 경로 추가
src/pages/index.tsx
수정용 라우트 추가: memory/record/edit/:roomId/:recordId, memory/poll/edit/:roomId/:voteId.
페이지: 기록 작성/수정
src/pages/recordwrite/RecordWrite.tsx
recordId 존재로 edit 모드 분기. 쿼리로 초기화, UI 비활성화/포커스 처리, updateRecord 호출 플로우 및 에러/스낵바 처리 추가.
페이지: 투표 작성/수정
src/pages/pollwrite/PollWrite.tsx
voteId 존재로 edit 모드 분기. 쿼리로 초기화, 옵션 로드, updateVote 호출 경로 추가. 생성 모드 검증/최종 페이지 계산 유지.
컴포넌트: 편집용 Props 및 포커스
src/components/pollwrite/PollCreationSection.tsx, src/components/recordwrite/PageRangeSection.tsx, src/components/recordwrite/RecordContentSection.tsx
Poll: isEditMode, autoFocus 추가(옵션 입력 비활성화, 삭제/추가 UI 숨김). PageRange: isDisabled, hideToggle 추가. RecordContent: autoFocus 추가 및 포커스 로직 확장.
메모리 아이템: 편집 네비게이션
src/components/memory/RecordItem/RecordItem.tsx
편집 핸들러 구현. 타입별로 적절한 편집 라우트로 이동하면서 쿼리 파라미터 구성.
그룹 상세: 방 나가기 연동
src/pages/groupDetail/ParticipatedGroupDetail.tsx
leaveRoom 사용해 실제 나가기 처리 추가. 성공 시 그룹 목록으로 이동, 실패/예외 시 메시지 노출.

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
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ho0010

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed 제목은 주요 변경사항인 방 나가기, 기록 수정, 투표 수정 API 연동을 간결하게 요약하고 있어 변경 내용과 완전히 부합합니다.
Description Check ✅ Passed 설명은 각 API 연동과 UI 흐름 개선 사항을 구체적으로 나열하여 변경 내용과 일치하며 충분히 관련성이 있습니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

깡충깡충, 수정과 나가기 척척해요
PATCH로 기억 고치고, DELETE로 방을 떠나요
쿼리로 불러와 포커스 쏙—편집 모드는 준비 완료
토끼가 응원해요, 코드도 깔끔히 정리됐네 🐇✨

✨ 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-jh

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

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 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 의존성 배열 검토 필요

isEditModerecordId에서 파생된 값이므로 의존성 배열에 포함할 필요가 없습니다. 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 03812ff and d05d8ca.

📒 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가 잘 구현되었습니다.

Comment on lines +95 to +105
// autoFocus 처리
useEffect(() => {
if (autoFocus && contentInputRef.current) {
const input = contentInputRef.current;
input.focus();
// 커서를 텍스트 끝으로 이동
const length = input.value.length;
input.setSelectionRange(length, length);
}
}, [autoFocus]);

Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
// 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).

Comment on lines +129 to +133
if (!roomId) return;

try {
const response = await leaveRoom(parseInt(roomId));

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 숫자 검증 추가 필요(잘못된 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.

Suggested change
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.

Comment on lines +222 to +246
// 페이지 범위 결정
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;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

중대 버그: 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.

Suggested change
// 페이지 범위 결정
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;
}

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

♻️ 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.

📥 Commits

Reviewing files that changed from the base of the PR and between d05d8ca and 5ba4424.

📒 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 연결 LGTM

isDisabled/hideToggle, autoFocus 연동이 명확합니다. UX 의도에 부합합니다.

Comment on lines +56 to +58
if (existingContent) {
setContent(decodeURIComponent(existingContent));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +68 to +76
// 수정 모드에서도 전체 페이지 수는 필요하므로 책 정보 조회
const response = await getBookPage(parseInt(roomId));
if (response.isSuccess) {
setTotalPages(response.data.totalBookPage);
}

setIsLoading(false);
return;
}
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 숫자 유효성 검증 추가 및 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.

Suggested change
// 수정 모드에서도 전체 페이지 수는 필요하므로 책 정보 조회
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.

Comment on lines +163 to +169
const updateData: UpdateRecordRequest = {
content: content.trim(),
};

const response = await updateRecord(parseInt(roomId), parseInt(recordId), updateData);

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

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.

Suggested change
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.

Comment on lines +198 to +214
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;
}
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 사용으로 비정상 입력 시 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.

Suggested change
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.

Comment on lines +223 to 225
// API 호출
const response = await createRecord(parseInt(roomId), recordData);

Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
// 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.

@ljh130334 ljh130334 merged commit 126ca23 into develop Sep 9, 2025
3 checks passed
@ljh130334 ljh130334 deleted the feat/api-rooms-jh branch November 17, 2025 00:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신 ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant