Skip to content

develp branch 작업내용 머지 : develop -> main#238

Merged
heeeeyong merged 33 commits intomainfrom
develop
Sep 11, 2025
Merged

develp branch 작업내용 머지 : develop -> main#238
heeeeyong merged 33 commits intomainfrom
develop

Conversation

@heeeeyong
Copy link
Collaborator

@heeeeyong heeeeyong commented Sep 11, 2025

ver 1.2.0 작업내용 반영

Summary by CodeRabbit

  • 신기능
    • 기록/투표 수정 모드 추가: 전용 경로로 진입, 기존 내용 자동 채움, 입력 비활성화/자동 포커스 지원
    • 피드 이미지 업로드: 사전 서명 URL로 S3 업로드 후 imageUrls로 게시
    • 오늘의 한마디 삭제 기능 추가 및 목록 실시간 반영, 스낵바 안내
    • 그룹 나가기 기능 추가 및 성공/오류 안내
    • 동적 뷰포트 높이/세이프에어리어 지원, 전체 화면 표시 강화
  • 버그 수정
    • 투표 옵션 클릭 시 이벤트 전파 차단으로 오동작 방지
  • 스타일
    • 모달/오버레이 안전영역 패딩, 이미지 아바타 테두리, 빈 상태 모서리 12px, 북 검색 결과 제목 2줄 말줄임, 여러 화면 레이아웃 미세 조정
  • 작업
    • 로컬 네트워크 접속용 dev:lan 스크립트 추가

heeeeyong and others added 30 commits September 1, 2025 11:34
Main -> develop 히스토리 merge
책 제목과 저자와 출판사 부분 디자인 요구사항 변경에 따른 수정
post가 없을 때와 동일하게 min-height를 부여
fix: QA 9월 1주차 추가 이슈 해결
feat: 오늘의 한마디 삭제 API 연결
feat: rooms API 추가 연동 (방 나가기 / 기록 수정 / 투표 수정)
feat: presigned URL API 연동 및 이미지 업로드 방식 수정
@vercel
Copy link

vercel bot commented Sep 11, 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 11, 2025 1:38am

@coderabbitai
Copy link

coderabbitai bot commented Sep 11, 2025

Walkthrough

피드 이미지 업로드를 사전 서명 URL 기반으로 전환하고, 기록/투표 편집 라우트 및 편집 플로우를 추가했습니다. 오늘의 한마디 삭제 및 방 나가기 API를 연동했으며, 사용자 프로필 이미지 필드명을 profileImageUrl로 일괄 변경했습니다. 뷰포트/세이프 에어리어 대응 UI 조정과 일부 스타일/토큰 처리 정리가 포함됩니다.

Changes

Cohort / File(s) Summary
Feed 이미지 업로드 Presigned URL 전환
src/api/feeds/createFeed.ts, src/api/feeds/getPresignedUrl.ts, src/api/feeds/uploadToS3.ts, src/hooks/useCreateFeed.ts, src/pages/post/CreatePost.tsx
createFeed가 JSON 본문과 imageUrls를 사용하도록 변경. Presigned URL 요청/응답 타입과 S3 업로더 추가. useCreateFeed에서 URL 발급→병렬 업로드→fileUrl 수집→createFeed 호출 플로우로 교체. CreatePost는 주석만 수정.
기록/투표 편집 플로우 & 타입/API 추가
src/pages/index.tsx, src/pages/recordwrite/RecordWrite.tsx, src/pages/pollwrite/PollWrite.tsx, src/components/pollwrite/PollCreationSection.tsx, src/components/recordwrite/PageRangeSection.tsx, src/components/recordwrite/PageRangeSection.styled.ts, src/components/recordwrite/RecordContentSection.tsx, src/api/record/updateRecord.ts, src/api/record/updateVote.ts, src/types/record.ts, src/components/memory/RecordItem/RecordItem.tsx, src/components/memory/RecordItem/PollRecord.tsx
편집 라우트 추가(/memory/.../edit/:id). 편집 모드에서 쿼리 파라미터로 프리필, UpdateRecord/UpdateVote API 호출. 편집 시 비활성화/오토포커스 지원. PollRecord 클릭 이벤트 전파 차단. PageInput 폭 계산 미세 조정.
오늘의 한마디 삭제 연동
src/api/rooms/deleteDailyGreeting.ts, src/components/today-words/MessageList/MessageList.tsx, src/pages/today-words/TodayWords.tsx
삭제 API 추가 및 MessageList에서 실제 삭제 호출/스낵바 처리. 상위 TodayWords가 onMessageDelete로 상태 동기화. roomId prop 전파.
방 나가기 연동
src/api/rooms/leaveRoom.ts, src/pages/groupDetail/ParticipatedGroupDetail.tsx
leaveRoom API 추가 및 확인 시 호출, 성공 시 /group 이동, 실패 시 메시지 노출.
사용자 프로필 필드/프롭 명 변경
src/types/user.ts, src/components/feed/UserProfileItem.tsx, src/pages/feed/FollowerListPage.tsx, src/components/feed/FollowList.tsx, src/data/userData.ts
profileImgUrl → profileImageUrl로 명 변경. 관련 컴포넌트/데이터 모두 반영.
뷰포트/세이프에어리어 대응
index.html, src/components/common/Modal/MoreMenu.tsx
--vvh CSS 변수 계산 로직과 viewport-fit=cover 메타 추가. 모달 오버레이가 dvh/세이프영역 반영. 터치/제스처 리스너 포맷 정리(동작 동일).
토큰/요청 처리 정리
src/api/index.ts, src/pages/feed/Feed.tsx
Axios withCredentials 제거. 토큰 미존재 시 불필요 로그/코드 정리. Feed에서 토큰 부재 조기 반환 제거.
메모리/레이아웃 소소한 변경
src/pages/memory/Memory.tsx, src/components/memory/RecordItem/RecordItem.styled.ts, src/components/search/BookSearchResult.tsx, src/pages/mypage/SavePage.tsx, src/pages/groupDetail/ParticipatedGroupDetail.styled.ts, src/components/members/MemberList.styled.ts, src/components/members/MemberList.tsx
isOverviewEnabled 갱신 제거. 아바타 보더 추가 및 블러 시 인터랙션 처리 방식 변경(오버레이). 텍스트 줄바꿈/트렁케이션/패딩/호버 스타일 등 UI 조정. MemberRole에 roleType 기반 색상 로직 추가 및 prop 전달.
검색 결과 피드 컨테이너 추가
src/pages/searchBook/SearchBook.styled.ts, src/pages/searchBook/SearchBook.tsx
FeedPostContainer 추가 및 피드 리스트 래퍼 교체.
개발 편의 스크립트
package.json
dev:lan 스크립트 추가(vite --host 0.0.0.0 --port 5173).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as useCreateFeed (Client)
  participant API as Backend API
  participant S3 as S3 (Presigned URL)

  User->>UI: 이미지 선택 후 게시 요청
  UI->>API: POST /feeds/images/presigned-url [{extension,size}]
  API-->>UI: presignedUrls[{presignedUrl,fileUrl}]
  par 병렬 업로드
    UI->>S3: PUT file (Content-Type)
    S3-->>UI: 200 OK
  and
    UI->>S3: PUT file ...
    S3-->>UI: 200 OK
  end
  UI->>API: POST /feeds { content, tags, imageUrls:[fileUrl...] }
  API-->>UI: createFeed 응답
  UI-->>User: 성공/실패 스낵바
Loading
sequenceDiagram
  autonumber
  actor User
  participant Item as RecordItem/PollRecord
  participant Router as Router
  participant RW as RecordWrite
  participant PW as PollWrite
  participant API as Backend API

  User->>Item: 더보기 > 수정
  Item->>Router: navigate(/memory/(record|poll)/edit/:roomId/:id?query)
  alt 기록 편집
    Router->>RW: mount with params/query
    RW->>API: (필요 시) 책 페이지 정보 조회
    User->>RW: 완료 클릭
    RW->>API: PATCH /rooms/{roomId}/records/{recordId} {content}
  else 투표 편집
    Router->>PW: mount with params/query
    PW->>API: (필요 시) 책 페이지 정보 조회
    User->>PW: 완료 클릭
    PW->>API: PATCH /rooms/{roomId}/votes/{voteId} {content}
  end
  API-->>Router: 성공 응답
  Router-->>User: 메모리 화면으로 이동
Loading
sequenceDiagram
  autonumber
  actor User
  participant List as MessageList
  participant API as deleteDailyGreeting
  participant Page as TodayWords

  User->>List: 메시지 삭제 선택
  alt roomId 없음
    List-->>User: 에러 스낵바(방 정보 없음)
  else roomId 있음
    List->>API: DELETE /rooms/{roomId}/daily-greeting/{id}
    API-->>List: 응답(isSuccess/ message)
    alt 성공
      List->>Page: onMessageDelete(id)
      Page-->>User: 성공 스낵바
    else 실패
      List-->>User: 에러 스낵바(message)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

✨ Feature, 📬 API

Suggested reviewers

  • ho0010
  • ljh130334

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목 "develp branch 작업내용 머지 : develop -> main"은 PR이 develop 브랜치에서 main으로 병합된다는 점을 나타내므로 변경사항과 관련은 있지만, 오타("develp"), 불필요한 브랜치 표기 및 핵심 변경사항(예: v1.2.0 릴리스, 주요 기능 추가/수정)에 대한 설명이 없어 한눈에 무엇이 바뀌었는지 파악하기 어렵습니다. 따라서 형식과 가독성 측면에서 개선이 필요합니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

깡충깡충, 새 길을 연다
URL을 품고 구름에 척!
기록은 살짝, 투표는 톡—수정의 문이 열리고,
한마디는 바람처럼 사라져도
토끼 마음은 포근히 저장된다.
화면 끝까지, vv(h)~ 풍경 넓혀 뛰어가자! 🐇✨

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

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.

@heeeeyong heeeeyong merged commit c0adbd5 into main Sep 11, 2025
2 of 3 checks passed
@heeeeyong heeeeyong self-assigned this Sep 11, 2025
@heeeeyong heeeeyong added the 🌏 Deploy 배포 관련 label Sep 11, 2025
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 (8)
src/components/today-words/MessageList/MessageList.tsx (1)

81-81: State 배열을 직접 sort하여 원본 변형 발생(불안정 렌더링 가능)

messages.sort는 상태 원본을 변형합니다. 복사 후 정렬하세요.

-    const sortedMessages = messages.sort((a, b) => parseInt(a.id) - parseInt(b.id));
+    const sortedMessages = [...messages].sort((a, b) => {
+      const ai = Number(a.id);
+      const bi = Number(b.id);
+      if (Number.isFinite(ai) && Number.isFinite(bi)) return ai - bi;
+      // 숫자가 아닐 경우 createdAt 기준 보정
+      return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
+    });
index.html (1)

128-135: viewport 메타 태그 중복 정의 정리 필요

두 개가 동시 존재합니다. 마지막 것이 우선하지만 혼선 위험이 있어 하나로 합치세요.

-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <!-- merged -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
@@
-    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
src/pages/feed/FollowerListPage.tsx (2)

30-37: stale 클로저로 로딩 가드 무력화 가능성

loadUserListloading을 캡처하지 않아 병렬 호출이 발생할 수 있습니다. deps에 loading을 추가하세요.

-  const loadUserList = useCallback(
+  const loadUserList = useCallback(
     async (cursor?: string) => {
       if (loading) return;
@@
-    [type, userId],
+    [type, userId, loading],
   );

95-115: 스크롤 핸들러에서도 loading이 deps에 누락

이펙트가 loading 변화에 반응하지 않아 stale 상태를 참조합니다.

-  }, [isLast, error, retryCount, nextCursor, loadUserList]);
+  }, [loading, isLast, error, retryCount, nextCursor, loadUserList]);
src/hooks/useCreateFeed.ts (1)

65-73: 파일 MIME/사이즈 검증 추가 권장

확장자만으로는 위·변조 가능성이 있어 MIME 검사와 사이즈 제한을 함께 권장합니다(예: 파일당 10MB).

다음 변경으로 보완할 수 있습니다.

-        // 확장자 제한
-        const extOk = (name: string) => /\.(jpe?g|png|gif)$/i.test(name);
-        if (images.some(f => !extOk(f.name))) {
+        // 확장자 제한
+        const extOk = (name: string) => /\.(jpe?g|png|gif)$/i.test(name);
+        if (images.some(f => !extOk(f.name))) {
           openSnackbar({
             message: '파일 형식은 jpg, jpeg, png, gif만 가능해요.',
             variant: 'top',
             onClose: closePopup,
           });
           return { success: false as const };
         }
+        // MIME 타입 검증
+        if (images.some(f => !/^image\/(jpe?g|png|gif)$/i.test(f.type))) {
+          openSnackbar({
+            message: '이미지 MIME 타입이 올바르지 않아요.',
+            variant: 'top',
+            onClose: closePopup,
+          });
+          return { success: false as const };
+        }
+        // 사이즈 제한 (예: 10MB)
+        const MAX_SIZE = 10 * 1024 * 1024;
+        if (images.some(f => f.size > MAX_SIZE)) {
+          openSnackbar({
+            message: '이미지는 파일당 10MB 이하만 업로드할 수 있어요.',
+            variant: 'top',
+            onClose: closePopup,
+          });
+          return { success: false as const };
+        }
src/components/recordwrite/PageRangeSection.tsx (1)

83-91: [버그] 비활성(isDisabled)이어도 토글 클릭 시 onOverallToggle가 실행됩니다.

스타일만 비활성일 뿐, 핸들러에서 isDisabled를 가드하지 않아 상태가 변경됩니다.

다음 가드를 추가해 주세요.

-  const handleToggleClick = () => {
-    if (canUseOverall) {
+  const handleToggleClick = () => {
+    if (isDisabled) return;
+    if (canUseOverall) {
       onOverallToggle();
     } else {
       // 80% 미만이면 빨간 툴팁 표시
       setShowRedTooltip(true);
       setShowGreenTooltip(false);
     }
   };
src/components/memory/RecordItem/RecordItem.tsx (2)

156-166: [위험] roomId 없음에 '1'로 대체 후 삭제 호출 — 오삭제 위험.

파괴적 동작(삭제)에 기본값을 사용하면 다른 방의 리소스를 건드릴 수 있습니다. roomId 미존재 시 조용히 종료하거나 에러 안내가 필요합니다.

-    const currentRoomId = roomId || '1';
-    const recordId = parseInt(record.id);
+    if (!roomId) {
+      openSnackbar({
+        message: '방 정보가 유효하지 않습니다.',
+        variant: 'top',
+        onClose: () => {},
+      });
+      return;
+    }
+    const currentRoomId = roomId;
+    const recordId = Number(record.id);

214-221: [위험] roomId 없음에 '1'로 대체 후 핀하기 호출 — 오동작 위험.

핀하기 역시 부수효과이므로 동일하게 가드해 주세요.

-    const currentRoomId = roomId || '1';
-    const recordId = parseInt(record.id);
+    if (!roomId) {
+      openSnackbar({
+        message: '방 정보가 유효하지 않습니다.',
+        variant: 'top',
+        onClose: () => {},
+      });
+      return;
+    }
+    const currentRoomId = roomId;
+    const recordId = Number(record.id);
🧹 Nitpick comments (49)
package.json (1)

13-13: LAN 공개 개발 서버: 기본 안전장치 몇 가지 추가 권장

현재 vite --host 0.0.0.0 --port 5173는 같은 네트워크의 누구나 접근 가능합니다. 실수로 공용/회사망에서 띄우는 사고를 줄이기 위해 옵션을 조금 강화하는 것을 제안드립니다.

다음과 같이 보완을 고려해 주세요:

  • 포트 고정 실패 방지: --strictPort 추가(의도치 않은 포트 변경 방지)
  • 동시 실행 충돌 회피: 기본 dev와 포트 분리(예: 5174)
  • 미리보기도 노출 필요 시 별도 스크립트 추가

예시 diff:

-    "dev:lan": "vite --host 0.0.0.0 --port 5173"
+    "dev:lan": "vite --host 0.0.0.0 --port 5174 --strictPort"
+    "preview:lan": "vite preview --host 0.0.0.0 --port 4173 --strictPort"

추가로, 사내/공용망에서는 민감한 개발용 API 엔드포인트나 토큰이 콘솔/네트워크 탭을 통해 노출되지 않도록 환경변수 설정(.env)와 기능 게이트를 점검해 주세요.

src/pages/searchBook/SearchBook.styled.ts (1)

178-180: 모바일 뷰포트 단위 대응을 위해 dvh 병행 사용 제안

iOS/안드로이드 주소창 수축에 따른 vh 오차를 줄이기 위해 dvh를 함께 선언해 두면 안전합니다.

 export const FeedPostContainer = styled.div`
-  min-height: 50vh;
+  min-height: 50vh;  /* fallback */
+  min-height: 50dvh; /* dynamic viewport for mobile UAs */
 `;
src/pages/searchBook/SearchBook.tsx (1)

307-310: ref 콜백 inline 생성 최소화

불필요한 화살표 함수를 없애 미세한 렌더 비용을 줄일 수 있습니다. 타입 호환에 문제 없다면 아래처럼 간소화하세요.

-              <div
-                key={post.feedId}
-                ref={idx === feeds.length - 1 ? el => lastFeedElementCallback(el) : undefined}
-              >
+              <div
+                key={post.feedId}
+                ref={idx === feeds.length - 1 ? lastFeedElementCallback : undefined}
+              >
src/components/search/BookSearchResult.tsx (1)

118-129: 멀티라인 말줄임 보강 및 비표준 속성 대체 제안

표준 line-clamp(지원 브라우저에서만 적용)와 overflow-wrap으로 안전성을 높일 수 있습니다.

 const Title = styled.h3`
   font-size: ${typography.fontSize.base};
   font-weight: ${typography.fontWeight.semibold};
   color: ${colors.white};
   line-height: 20px;
   display: -webkit-box;
   -webkit-line-clamp: 2;
+  line-clamp: 2; /* progressive enhancement */
   -webkit-box-orient: vertical;
   overflow: hidden;
   text-overflow: ellipsis;
-  word-break: break-word;
+  overflow-wrap: anywhere; /* 긴 단어/URL 대비 */
 `;
src/components/members/MemberList.styled.ts (3)

65-65: DOM에 roleType 속성 누수 방지 (Emotion shouldForwardProp 적용 제안)

현재 styled.div에 선언된 roleType이 기본값으로 DOM까지 전달될 수 있어 React 콘솔 경고 및 불필요한 속성 노출 위험이 있습니다. Emotion의 shouldForwardProp으로 필터링을 권장합니다.

아래와 같이 교체하면 DOM 전달이 차단됩니다:

-export const MemberRole = styled.div<{ roleType?: string }>`
+export const MemberRole = styled('div', { shouldForwardProp: (prop) => prop !== 'roleType' })<{ roleType?: string }>`

66-88: 부분 문자열 오탐 줄이기: 영문 역할 매칭을 토큰/정규식 기반으로 개선

includes('art')"smart" 같은 케이스에 오탐이 발생합니다. 영문은 단어 토큰 기준으로 비교하도록 간단히 보완하는 것을 권장합니다. (한글은 현행 includes 유지)

-  color: ${({ roleType }) => {
-    if (!roleType) return semanticColors.text.point.green;
-    
-    const role = roleType.toLowerCase();
-    
-    if (role.includes('예술') || role.includes('art')) {
-      return semanticColors.text.character.pink;
-    }
-    if (role.includes('문학') || role.includes('literature')) {
-      return semanticColors.text.character.mint;
-    }
-    if (role.includes('사회') || role.includes('sociology')) {
-      return semanticColors.text.character.orange;
-    }
-    if (role.includes('인문') || role.includes('humanities')) {
-      return semanticColors.text.character.skyblue;
-    }
-    if (role.includes('과학') || role.includes('science')) {
-      return semanticColors.text.character.lavender;
-    }
-    
-    return semanticColors.text.point.green;
-  }};
+  color: ({ roleType }) => {
+    const value = roleType?.toString().toLowerCase() ?? '';
+    if (!value) return semanticColors.text.point.green;
+    // 영어 단어는 토큰 단위로 비교해 부분 문자열 오탐(예: "smart") 방지
+    const enTokens = value.match(/[a-z]+/g) ?? [];
+    const has = (ko: string, ...en: string[]) =>
+      value.includes(ko) || en.some(t => enTokens.includes(t));
+
+    if (has('예술', 'art', 'arts')) return semanticColors.text.character.pink;
+    if (has('문학', 'literature')) return semanticColors.text.character.mint;
+    if (has('사회', 'sociology')) return semanticColors.text.character.orange;
+    if (has('인문', 'humanities')) return semanticColors.text.character.skyblue;
+    if (has('과학', 'science', 'sciences')) return semanticColors.text.character.lavender;
+    return semanticColors.text.point.green;
+  }};

93-97: 디자인 토큰 일관성 유지

하드코딩된 white 대신 글로벌 토큰 사용을 권장합니다. 현재 테마에서도 semanticColors.text.primary가 white라 의미 동일하며, 테마 변경 시 유연합니다.

-  color: white;
+  color: ${semanticColors.text.primary};
src/components/members/MemberList.tsx (3)

62-62: MemberRole에 roleType 전달 OK — DOM 속성 경고가 없는지 확인

roleType 전달 자체는 의도에 부합합니다. 다만 스타일 컴포넌트 쪽에서 shouldForwardProp로 필터링하지 않으면 DOM에 roleType이 노출될 수 있으니 함께 적용해 주세요(동 PR 제안 참조). 적용 후 개발 모드에서 콘솔 경고가 없는지 확인 바랍니다.


47-54: 키보드 접근성: Space는 keyup에서 트리거하도록 분리

네이티브 버튼과 동작 일치(Enter=keydown, Space=keyup)를 권장합니다. 현재 구현은 keydown 기준으로 Space 처리되어 스크롤/반복 호출 이슈 여지가 있습니다.

-          onKeyDown={(e: KeyboardEvent) => {
-            if (e.key === 'Enter' || e.key === ' ') {
-              e.preventDefault();
-              handleMemberClick(member);
-            }
-          }}
+          onKeyDown={(e: KeyboardEvent) => {
+            if (e.key === 'Enter') {
+              e.preventDefault();
+              handleMemberClick(member);
+            }
+          }}
+          onKeyUp={(e: KeyboardEvent) => {
+            if (e.key === ' ') {
+              e.preventDefault();
+              handleMemberClick(member);
+            }
+          }}

76-83: 스타일 토큰 사용 일관화(선택)

ProfileImageWithSrc의 배경색이 CSS 변수로 하드코딩되어 있습니다. 다른 구성요소(ProfileImage)와 동일하게 글로벌 토큰을 사용하면 테마 변경 대응이 좋아집니다. (이 파일 상단에 colors 미사용이므로, 토큰 사용 시 import 추가 필요)

예시:

+import { colors } from '../../styles/global/global';
 ...
   background-color: var(--color-grey-400);
+  background-color: ${colors.grey['400']};
src/api/rooms/leaveRoom.ts (2)

12-21: roomId 런타임 검증 추가로 잘못된 호출 방지

라우트 파라미터 파싱 실패 시 NaN이 전달되어 /rooms/NaN/leave 호출 위험이 있습니다. 간단한 가드를 추가하세요.

 export const leaveRoom = async (roomId: number): Promise<LeaveRoomResponse> => {
-  try {
+  if (!Number.isInteger(roomId) || roomId <= 0) {
+    throw new Error(`유효하지 않은 roomId: ${roomId}`);
+  }
+  try {
     const response = await apiClient.delete<LeaveRoomResponse>(`/rooms/${roomId}/leave`);
 
     return response.data;
   } catch (error) {
     console.error('방 나가기 API 오류:', error);
     throw error;
   }
 };

17-20: 에러 표준화(선택): Axios 에러 구분 로깅

UI 단에서 Axios 에러 형태로 분기하고 있으므로, API 레이어에서도 Axios 에러를 구분해 응답 바디를 함께 로깅하면 디버깅이 쉬워집니다.

-  } catch (error) {
-    console.error('방 나가기 API 오류:', error);
+  } catch (error) {
+    // 선택: axios의 isAxiosError 활용
+    // import { isAxiosError } from 'axios';
+    // console.error('방 나가기 API 오류:', isAxiosError(error) ? error.response?.data ?? error : error);
+    console.error('방 나가기 API 오류:', error);
     throw error;
   }
src/pages/groupDetail/ParticipatedGroupDetail.tsx (3)

128-131: roomId 파싱/검증 보강 및 radix 지정

parseInt(roomId)NaN 가능성이 있고 radix 미지정입니다. 숫자 검증 후 진행하세요.

-      onConfirm: async () => {
-        if (!roomId) return;
+      onConfirm: async () => {
+        if (!roomId) return;
+        const id = Number(roomId);
+        if (!Number.isInteger(id) || id <= 0) {
+          openSnackbar({
+            message: '유효하지 않은 방 ID입니다.',
+            variant: 'top',
+            isError: true,
+            onClose: () => {},
+          });
+          return;
+        }

132-143: 중복 제출 방지(선택): 이중 탭으로 인한 다중 호출 차단

확인 버튼 연타 시 중복 호출 가능성이 있습니다. isLeaving 상태로 가드하세요.

-        try {
-          const response = await leaveRoom(parseInt(roomId));
+        try {
+          if (isLeaving) return;
+          setIsLeaving(true);
+          const response = await leaveRoom(id);
           ...
-            navigate('/group', { replace: true });
+            // 필요 시 바텀시트 닫기 후 네비게이션
+            // setIsBottomSheetOpen(false);
+            navigate('/group', { replace: true });
           ...
-        } catch (error: unknown) {
+        } catch (error: unknown) {
           ...
-        }
+        } finally {
+          setIsLeaving(false);
+        }

추가 상태 정의(파일 상단 상태 영역):

const [isLeaving, setIsLeaving] = useState(false);

158-163: Axios 에러 판별은 isAxiosError 사용 권장

직접 프로퍼티 검사 대신 axios 헬퍼를 사용하면 타입 안전합니다.

-          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;
-            }
-          }
+          // import { isAxiosError } from 'axios';
+          if (isAxiosError(error) && error.response?.data?.message) {
+            errorMessage = error.response.data.message as string;
+          }

상단 임포트 추가:

import { isAxiosError } from 'axios';
src/pages/today-words/TodayWords.tsx (2)

299-303: 부모/자식 동시 상태 갱신: 중복 제거로 인한 깜빡임 가능성 점검

MessageList도 내부에서 제거하고, 부모도 handleMessageDelete로 다시 필터링합니다. 비동기 타이밍에 따라 순간적으로 UI 깜빡임이 생길 수 있어 한쪽만 소스로 삼는 편이 안전합니다(예: MessageList를 완전 제어형으로 두고 내부 상태 제거).

필요하면 MessageList를 완전 제어형으로 정리하는 패치 제안 드리겠습니다.


325-327: roomId 파싱 일관화(Number 권장) 및 중복 파싱 제거

parseInt 대신 Number를 사용해 의도 명확히 하고, 렌더 전 상수화하세요.

적용 예:

-                roomId={roomId ? parseInt(roomId) : undefined}
+                roomId={numericRoomId}

추가(컴포넌트 상단, return 이전):

const numericRoomId = roomId ? Number(roomId) : undefined;
src/components/today-words/MessageList/MessageList.tsx (1)

46-52: MessageList 내부 상태/동기화 단순화 제안

initialMessages를 로컬 상태로 복제해 관리하고 있어 상위/하위가 이중 소스로 작동합니다. 삭제·페이지네이션 동작 시 동기화 비용이 커집니다. MessageList를 제어형(읽기 전용 props + 콜백)으로 단순화하는 것을 고려해 주세요.

src/components/feed/FollowList.tsx (4)

123-127: 웹킷 스크롤바 숨김 셀렉터가 비효율적입니다: 중첩 내에서 & 누락

현재 중첩된 .followerList 블록 안에서 .followerList::-webkit-scrollbar를 다시 지정해 중복 선택자가 생성됩니다. 아래처럼 &를 사용해 현재 셀렉터를 참조하세요.

-    .followerList::-webkit-scrollbar {
+    &::-webkit-scrollbar {
       display: none; /* Chrome, Safari */
     }

58-60: 아이콘 이미지 대체 텍스트 누락

스크린 리더 접근성을 위해 장식용이면 alt="", 의미가 있으면 적절한 alt를 추가하세요.

-        <img src={people} />
+        <img src={people} alt="" aria-hidden="true" />

144-145: CSS 변수 오타 가능성

var(--string-weight-regular, 400)var(--font-weight-regular, 400)로 보입니다. 토큰 네이밍 일관성 확인 부탁드립니다.

-        font-weight: var(--string-weight-regular, 400);
+        font-weight: var(--font-weight-regular, 400);

78-79: 캐릭터 이미지는 장식용으로 보입니다

alt="더보기"는 의미가 부정확합니다. 장식용이면 alt=""로 비활성화하세요.

-          <img src={character} alt="더보기" />
+          <img src={character} alt="" aria-hidden="true" />
src/pages/mypage/SavePage.tsx (2)

361-361: BookItem 패딩 12px로 축소 OK

모바일 터치 타겟이 너무 작아지지 않는지만 확인하면 됩니다(아이콘 주변 최소 24px 권장).


104-119: IntersectionObserver 관리 개선(중복/누수 가능)

  • 콜백 ref마다 새로운 Observer를 생성하고 해제하지 않아 누수가 날 수 있습니다.
  • 책 탭에서 콜백 ref와 sentinel ref를 동시에 사용해 중복 트리거가 발생할 수 있습니다. 하나로 통일을 권장합니다.

아래처럼 이전 Observer를 해제하는 패턴을 권장합니다.

const lastBookObserverRef = useRef<IntersectionObserver | null>(null);

const lastBookElementCallback = useCallback((node: HTMLDivElement | null) => {
  if (bookLoading || bookIsLast) return;

  // 이전 observer 정리
  if (lastBookObserverRef.current) {
    lastBookObserverRef.current.disconnect();
    lastBookObserverRef.current = null;
  }

  if (!node) return;

  const observer = new IntersectionObserver(entries => {
    if (entries[0].isIntersecting && !bookLoading && !bookIsLast) {
      loadMoreBooks();
    }
  });
  observer.observe(node);
  lastBookObserverRef.current = observer;
}, [bookLoading, bookIsLast, loadMoreBooks]);

또는 feed와 동일하게 sentinel 한 방식으로 단일 Observer만 유지하세요.

Also applies to: 277-281

src/components/recordwrite/PageRangeSection.styled.ts (1)

55-55: 입력 폭 계산 상수 조정 OK — 단, 가변폭 폰트 고려

inputLength * 9는 글자폭 가정치입니다. 폰트/숫자 조합에 따라 잘림이 생길 수 있어 ch 단위를 활용하면 안정적입니다.

-    return `${Math.max(9, props.inputLength * 9)}px`;
+    const px = Math.max(9, (props.inputLength ?? 1) * 0.6);
+    return `max(12px, ${px}ch)`;
src/pages/memory/Memory.tsx (1)

244-247: 로그는 배포 시 억제 권장

진행률/상태 로그는 개발 환경에서만 출력되도록 가드하세요.

-  console.log('📊 현재 상태:', overviewStatus, `진행률: ${readingProgress}%`);
+  if (process.env.NODE_ENV !== 'production') {
+    console.log('📊 현재 상태:', overviewStatus, `진행률: ${readingProgress}%`);
+  }
src/components/memory/RecordItem/PollRecord.tsx (4)

70-70: 이벤트 타입을 구체화해 주세요

HTMLElement가 명확하므로 이벤트 제네릭을 지정하면 타입 안정성이 올라갑니다.

아래처럼 수정 권장:

-  const handleOptionClick = async (e: React.MouseEvent, option: PollOption) => {
+  const handleOptionClick = async (e: React.MouseEvent<HTMLDivElement>, option: PollOption) => {

115-125: 에러 코드 분기 중복(140011) 제거

140011 분기가 두 번 존재합니다. 후행 분기는 절대 도달하지 않아 유지보수성에 불리합니다.

다음처럼 중복을 제거하세요:

         if (response.code === 120001) {
           errorMessage = '이미 투표한 투표항목입니다.';
         } else if (response.code === 120002) {
           errorMessage = '투표하지 않은 투표항목은 취소할 수 없습니다.';
         } else if (response.code === 140011) {
           errorMessage = '방 접근 권한이 없습니다.';
         } else if (response.code === 120000) {
           errorMessage = '투표는 존재하지만 투표항목이 비어있습니다.';
-        } else if (response.code === 140011) {
-          errorMessage = '방 접근 권한이 없습니다.';
         }

86-103: 상태 갱신은 함수형 업데이트로 경합/구버전 참조 방지

비동기 투표 중 외부에서 옵션이 갱신되면 현재 클로저의 currentOptions가 구버전일 수 있습니다. 함수형 업데이트로 안전하게 갱신하세요.

예시 수정:

-        const updatedOptions = currentOptions.map(opt => {
-          const updatedItem = response.data.voteItems.find(
-            item => item.voteItemId === opt.voteItemId
-          );
-          if (updatedItem) {
-            return {
-              ...opt,
-              percentage: updatedItem.percentage,
-              isVoted: updatedItem.isVoted,
-              isHighest: updatedItem.percentage === Math.max(...response.data.voteItems.map(item => item.percentage))
-            };
-          }
-          return opt;
-        });
-
-        setCurrentOptions(updatedOptions);
-        onVoteUpdate?.(updatedOptions);
+        setCurrentOptions(prev => {
+          const max = Math.max(...response.data.voteItems.map(item => item.percentage));
+          const next = prev.map(opt => {
+            const updatedItem = response.data.voteItems.find(item => item.voteItemId === opt.voteItemId);
+            return updatedItem
+              ? {
+                  ...opt,
+                  percentage: updatedItem.percentage,
+                  isVoted: updatedItem.isVoted,
+                  isHighest: updatedItem.percentage === max,
+                }
+              : opt;
+          });
+          onVoteUpdate?.(next);
+          return next;
+        });

Also applies to: 101-103


157-161: 투표 중 클릭 완전 차단

스타일만으로는 빠른 연타 시 핸들러 진입 시도가 발생할 수 있습니다. 투표 중에는 포인터 이벤트도 차단하세요(핸들러에서도 이미 가드하고 있으나 UI 일관성 차원).

-              pointerEvents: shouldBlur ? 'none' : 'auto'
+              pointerEvents: shouldBlur ? 'none' : (isVoting ? 'none' : 'auto')
src/api/record/updateRecord.ts (1)

25-44: 사용 예시는 JSDoc/문서로 이동 권장

소스에 대형 예시 블록이 있으면 가독성이 떨어집니다. 함수 상단 JSDoc 또는 README로 이전해 주세요.

src/components/common/Modal/MoreMenu.tsx (2)

70-71: inset: 0 사용 시 width: 100vw는 중복/잠재 오버플로우

고정 위치에서 inset: 0이면 전체 뷰포트가 커버됩니다. 100vw는 iOS에서 스크롤바 폭 포함 이슈를 유발할 수 있어 제거 권장.

-  width: 100vw;

91-101: RecordContainer에도 안전영역 보정 적용

ReportContainer에는 bottom safe-area가 반영되지만 RecordContainer에는 없어 홈바와 겹칠 수 있습니다. 동일 패딩을 부여하세요.

 const RecordContainer = styled.div`
   position: relative;
   display: flex;
   flex-direction: column;
   min-width: 320px;
   max-width: 767px;
   width: 100%;
   padding: 20px;
+  padding-bottom: calc(20px + env(safe-area-inset-bottom, 0));
   border-radius: 12px 12px 0px 0px;
   background-color: ${colors.darkgrey.main};
 `;
src/components/recordwrite/RecordContentSection.tsx (1)

39-51: 포커스는 rAF로 래핑해 스크롤 점프 최소화

모바일(특히 iOS)에서 즉시 focus 시 레이아웃 점프가 발생할 수 있어 requestAnimationFrame으로 한 프레임 늦추는 것을 권장합니다.

-    if (autoFocus && textareaRef.current) {
-      const textarea = textareaRef.current;
-      textarea.focus();
-      // 커서를 텍스트 끝으로 이동
-      const length = textarea.value.length;
-      textarea.setSelectionRange(length, length);
-    }
+    if (autoFocus && textareaRef.current) {
+      const textarea = textareaRef.current;
+      requestAnimationFrame(() => {
+        textarea.focus();
+        const length = textarea.value.length;
+        textarea.setSelectionRange(length, length);
+      });
+    }
src/pages/feed/FollowerListPage.tsx (1)

60-62: 디버그 로그 제거/가드

콘솔 로그는 노이즈가 될 수 있습니다. 개발 환경에서만 출력하거나 제거하세요.

-        console.log('API 응답:', response);
-        console.log('추출된 사용자 데이터:', userData);
+        if (import.meta?.env?.MODE === 'development') {
+          console.log('API 응답:', response);
+          console.log('추출된 사용자 데이터:', userData);
+        }
src/components/feed/UserProfileItem.tsx (1)

62-62: 프로필 이미지에 접근성/퍼포먼스 속성 추가

alt/지연로딩(loading)/디코딩(decoding)을 추가해 접근성과 초기 렌더 성능을 개선하세요.

-          <img src={profileImageUrl} />
+          <img
+            src={profileImageUrl}
+            alt={`${nickname}의 프로필 이미지`}
+            loading="lazy"
+            decoding="async"
+          />

참고: 동일 파일의 Line 78 아이콘에도 alt 또는 aria-hidden을 고려해 주세요.

src/types/record.ts (1)

54-73: 업데이트 응답 스키마 확인 요청 (roomId만 반환)

UpdateRecordData/UpdateVoteData가 roomId만 포함합니다. 프런트에서 수정 대상 식별(예: recordId, voteId)이나 후속 라우팅이 필요 없다면 OK입니다. 백엔드 계약이 roomId만 반환하도록 확정됐는지 확인해 주세요. 필요 시 다음 형태를 고려할 수 있습니다: { roomId, recordId } / { roomId, voteId }.

검증 포인트:

  • PATCH /rooms/{roomId}/records/{recordId} 응답 필드 목록
  • PATCH /rooms/{roomId}/votes/{voteId} 응답 필드 목록
src/components/memory/RecordItem/RecordItem.styled.ts (1)

17-27: 아바타 외곽선 추가 및 background-image 가독성, 좋습니다

얇은 보더로 다양한 배경에서 대비가 확보됩니다. 선택사항: 레티나에서 반픽셀 보더가 흐릿하면 1px로 조정하거나 theme 토큰으로 스냅핑을 고려하세요.

src/api/record/updateVote.ts (1)

14-21: 에러 처리 정책 일원화 제안

API 레이어에서 console.error는 최소화하고(또는 로거 사용), 전역 axios 인터셉터에서 공통 처리하는 편이 유지보수에 좋습니다. 또한 AxiosError 타입 내 status/message 표준화도 고려하세요.

예: 인터셉터에서 status별 메시지 매핑 후 여기서는 throw만 수행.

src/hooks/useCreateFeed.ts (3)

43-44: 불변 패턴 제안 (선택)

uploadedImageUrls를 재할당하기보다 업로드 블록 내부에서 const로 선언하고, 최종 body 생성 시 바로 사용하면 가독성이 약간 좋아집니다.

-      let uploadedImageUrls: string[] = [];
+      // 업로드 성공 시에만 지역 변수로 생성하여 아래 body에 인라인로 사용

75-80: 확장자 산출 로직 보강(파일명 확장자 부재 케이스)

파일명에 점이 없는 케이스 대비로 MIME에서 확장자를 보완해 주세요.

-        const presignedRequests: PresignedUrlRequest[] = images.map(file => ({
-          extension: file.name.split('.').pop()?.toLowerCase() || 'jpg',
+        const presignedRequests: PresignedUrlRequest[] = images.map(file => ({
+          extension:
+            (file.name.includes('.') ? file.name.split('.').pop() : file.type.split('/')[1])?.toLowerCase() || 'jpg',
           size: file.size,
         }));

93-114: 업로드 실패 시 잔여 객체 처리에 대한 운영적 가이드

부분 실패 시 이미 업로드된 객체가 참조되지 않고 남을 수 있습니다. S3 버킷에 TTL(Lifecycle Rule)로 미참조 객체 정리를 두거나, 피드 생성 실패 시 키 접두사로 정리하는 백엔드 잡을 고려하세요.

src/components/recordwrite/PageRangeSection.tsx (1)

37-37: 미사용 prop 제거 제안: isOverviewPossible 선언 및 전달 삭제

PageRangeSection 컴포넌트에서 인터페이스에만 선언되어 실제로 사용되지 않습니다. 호출부에서만 전달하고 있으니 컴포넌트 인터페이스와 호출부를 함께 제거하세요.

수정 위치:

  • src/components/recordwrite/PageRangeSection.tsx — 인터페이스 선언(약 37행): isOverviewPossible 제거
  • src/pages/recordwrite/RecordWrite.tsx — PageRangeSection 사용부(약 318–321행): isOverviewPossible={isOverviewPossible} 제거
  • src/pages/pollwrite/PollWrite.tsx — PageRangeSection 사용부(약 358–361행): isOverviewPossible={isOverviewPossible} 제거

적용 diff:

-  isOverviewPossible: boolean;

변경 후 TypeScript 타입 검사 및 빌드 확인.

src/api/feeds/getPresignedUrl.ts (2)

20-26: 빈 요청 방지 가드 추가 제안.

빈 배열로 호출될 경우 불필요한 네트워크 왕복이 발생합니다. 초기에 검증하고 명시적으로 실패시키는 편이 안전합니다.

 export const getPresignedUrl = async (
   requests: PresignedUrlRequest[]
 ): Promise<PresignedUrlResponse> => {
+  if (!Array.isArray(requests) || requests.length === 0) {
+    throw new Error('requests must be a non-empty array');
+  }
   const { data } = await apiClient.post<PresignedUrlResponse>(
     '/feeds/images/presigned-url',
     requests
   );
   
   return data;
 };

20-22: 함수명 복수형 권장(getPresignedUrls).

응답이 여러 URL을 반환(presignedUrls)하므로 복수형 네이밍이 더 직관적입니다. 추후 여유 있을 때 리팩터 고려 부탁드립니다.

src/components/memory/RecordItem/RecordItem.tsx (3)

68-68: 숫자 변환 일관화(Number 사용).

parseInt 대신 Number(...) 사용을 권장합니다(10진 강제 필요 시 parseInt(x, 10)). 가독성과 실수 방지에 유리합니다.

-      const postId = parseInt(id);
+      const postId = Number(id);
...
-    const recordId = parseInt(record.id);
+    const recordId = Number(record.id);
...
-        response = await deleteVote(parseInt(currentRoomId), recordId);
+        response = await deleteVote(parseInt(currentRoomId, 10), recordId);
-        response = await deleteRecord(parseInt(currentRoomId), recordId);
+        response = await deleteRecord(parseInt(currentRoomId, 10), recordId);
...
-    const recordId = parseInt(record.id);
+    const recordId = Number(record.id);
-      const response = await pinRecordToFeed(parseInt(currentRoomId), recordId);
+      const response = await pinRecordToFeed(parseInt(currentRoomId, 10), recordId);
...
-    openCommentBottomSheet(parseInt(id), type === 'poll' ? 'VOTE' : 'RECORD');
+    openCommentBottomSheet(Number(id), type === 'poll' ? 'VOTE' : 'RECORD');

Also applies to: 157-157, 163-166, 217-221, 288-289


80-81: 콘솔 로그 제거 또는 개발 모드 한정.

프로덕션 로그 노이즈를 줄이기 위해 정리 권장합니다.

-        console.log('좋아요 상태 변경 성공:', response.data.isLiked);
...
-              console.log('투표 결과 업데이트:', updatedOptions);

Also applies to: 376-378


135-142: 긴 콘텐츠의 쿼리스트링 전달은 URL 길이 제한/인코딩 이슈가 있습니다.

가능하면 navigate state로 전달하는 방식을 고려해 주세요(편집 페이지에서 location.state 사용).

예시:

navigate(`/memory/poll/edit/${roomId}/${record.id}`, {
  state: { content, pageRange: pageRange || '', recordType: recordType || 'normal', options: (pollOptions||[]).map(o=>o.text) }
});

Also applies to: 145-151

src/pages/pollwrite/PollWrite.tsx (1)

70-77: 투표 옵션 파싱 에러 처리가 적절합니다

JSON 파싱 실패 시 try-catch로 에러를 처리하는 것이 좋습니다. 다만, 파싱 실패 시 사용자에게 알림을 제공하는 것을 고려해보세요.

파싱 실패 시 사용자에게 알림을 추가하는 것을 제안합니다:

 if (existingOptions) {
   try {
     const options = JSON.parse(decodeURIComponent(existingOptions));
     setPollOptions(options);
   } catch (e) {
     console.error('투표 옵션 파싱 오류:', e);
+    openSnackbar({
+      message: '기존 투표 옵션을 불러오는데 실패했습니다.',
+      variant: 'top',
+      onClose: () => {},
+    });
   }
 }
📜 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 1e120e3 and 8881134.

📒 Files selected for processing (42)
  • index.html (4 hunks)
  • package.json (1 hunks)
  • src/api/feeds/createFeed.ts (2 hunks)
  • src/api/feeds/getPresignedUrl.ts (1 hunks)
  • src/api/feeds/uploadToS3.ts (1 hunks)
  • src/api/index.ts (0 hunks)
  • src/api/record/updateRecord.ts (1 hunks)
  • src/api/record/updateVote.ts (1 hunks)
  • src/api/rooms/deleteDailyGreeting.ts (1 hunks)
  • src/api/rooms/leaveRoom.ts (1 hunks)
  • src/api/users/getUsers.ts (1 hunks)
  • src/components/common/Modal/MoreMenu.tsx (2 hunks)
  • src/components/feed/FollowList.tsx (1 hunks)
  • src/components/feed/UserProfileItem.tsx (2 hunks)
  • src/components/members/MemberList.styled.ts (1 hunks)
  • src/components/members/MemberList.tsx (1 hunks)
  • src/components/memory/RecordItem/PollRecord.tsx (2 hunks)
  • src/components/memory/RecordItem/RecordItem.styled.ts (2 hunks)
  • src/components/memory/RecordItem/RecordItem.tsx (3 hunks)
  • src/components/pollwrite/PollCreationSection.tsx (3 hunks)
  • src/components/recordwrite/PageRangeSection.styled.ts (1 hunks)
  • src/components/recordwrite/PageRangeSection.tsx (4 hunks)
  • src/components/recordwrite/RecordContentSection.tsx (2 hunks)
  • src/components/search/BookSearchResult.tsx (1 hunks)
  • src/components/today-words/MessageList/MessageList.tsx (4 hunks)
  • src/data/userData.ts (4 hunks)
  • src/hooks/useCreateFeed.ts (3 hunks)
  • src/pages/feed/Feed.tsx (0 hunks)
  • src/pages/feed/FollowerListPage.tsx (1 hunks)
  • src/pages/groupDetail/ParticipatedGroupDetail.styled.ts (0 hunks)
  • src/pages/groupDetail/ParticipatedGroupDetail.tsx (2 hunks)
  • src/pages/index.tsx (1 hunks)
  • src/pages/memory/Memory.tsx (3 hunks)
  • src/pages/mypage/SavePage.tsx (1 hunks)
  • src/pages/pollwrite/PollWrite.tsx (10 hunks)
  • src/pages/post/CreatePost.tsx (1 hunks)
  • src/pages/recordwrite/RecordWrite.tsx (10 hunks)
  • src/pages/searchBook/SearchBook.styled.ts (1 hunks)
  • src/pages/searchBook/SearchBook.tsx (3 hunks)
  • src/pages/today-words/TodayWords.tsx (2 hunks)
  • src/types/record.ts (1 hunks)
  • src/types/user.ts (1 hunks)
💤 Files with no reviewable changes (3)
  • src/pages/groupDetail/ParticipatedGroupDetail.styled.ts
  • src/pages/feed/Feed.tsx
  • src/api/index.ts
🧰 Additional context used
🧬 Code graph analysis (19)
src/api/rooms/deleteDailyGreeting.ts (1)
src/api/index.ts (1)
  • apiClient (7-13)
src/components/memory/RecordItem/PollRecord.tsx (2)
src/types/memory.ts (1)
  • PollOption (81-88)
src/components/memory/RecordItem/PollRecord.styled.ts (1)
  • PollOption (23-32)
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-13)
src/pages/searchBook/SearchBook.tsx (1)
src/pages/searchBook/SearchBook.styled.ts (1)
  • FeedPostContainer (178-180)
src/api/rooms/leaveRoom.ts (1)
src/api/index.ts (1)
  • apiClient (7-13)
src/components/members/MemberList.styled.ts (1)
src/styles/global/global.ts (1)
  • semanticColors (80-153)
src/components/search/BookSearchResult.tsx (1)
src/styles/global/global.ts (2)
  • typography (56-77)
  • colors (4-53)
src/api/feeds/createFeed.ts (1)
src/api/index.ts (1)
  • apiClient (7-13)
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/components/members/MemberList.tsx (1)
src/components/members/MemberList.styled.ts (1)
  • MemberRole (65-91)
src/components/memory/RecordItem/RecordItem.styled.ts (1)
src/styles/global/global.ts (1)
  • colors (4-53)
src/api/feeds/getPresignedUrl.ts (1)
src/api/index.ts (1)
  • apiClient (7-13)
src/pages/groupDetail/ParticipatedGroupDetail.tsx (1)
src/api/rooms/leaveRoom.ts (1)
  • leaveRoom (12-21)
src/components/today-words/MessageList/MessageList.tsx (1)
src/api/rooms/deleteDailyGreeting.ts (1)
  • deleteDailyGreeting (14-28)
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)
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-13)
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)
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/hooks/useCreateFeed.ts (3)
src/api/feeds/getPresignedUrl.ts (2)
  • PresignedUrlRequest (3-6)
  • getPresignedUrl (20-29)
src/api/feeds/uploadToS3.ts (1)
  • uploadFileToS3 (1-19)
src/api/feeds/createFeed.ts (3)
  • CreateFeedBody (4-10)
  • CreateFeedResponse (29-29)
  • createFeed (36-41)
🔇 Additional comments (38)
package.json (1)

12-12: LGTM: format:check 뒤 쉼표 추가 적절

새 스크립트 추가에 따른 JSON 문법 유지가 잘 되어 있습니다.

src/pages/searchBook/SearchBook.styled.ts (1)

178-180: 피드 리스트 컨테이너 추가 적절 — 레이아웃 안정성 향상

스크롤 영역이 최소 높이를 가지게 되어 빈·초기 로딩 시 UI 안정성이 좋아집니다.

src/pages/searchBook/SearchBook.tsx (1)

305-331: FeedPostContainer로 래핑 변경 OK — 리스트 영역 시각적 일관성 확보

DOM 래핑 변경이 로딩/빈 상태 UI와 자연스럽게 맞물립니다.

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

104-109: flex-shrink: 0, min-width: 0, line-height 보완 모두 적절

  • Cover의 flex-shrink: 0으로 이미지 축소 방지 OK
  • BookInfo의 min-width: 0으로 텍스트 줄바꿈/말줄임 동작 안정화 OK
  • Subtitle line-height 정렬 개선 OK

Also applies to: 111-116, 131-137

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

4-9: ```shell
#!/bin/bash
set -euo pipefail

echo "1) 검색: ApiResponse 키워드(공용 타입 존재 여부)"
rg -n --hidden -S 'ApiResponse' -C3 || true

echo
echo "2) 검색: LeaveRoomResponse 및 leaveRoom 관련 파일/심볼"
rg -n --hidden -S 'LeaveRoomResponse' -C3 || true
rg -n --hidden -S 'leaveRoom' -C2 || true

echo
echo "3) 검색: 공용 응답 필드 패턴(isSuccess / code / message / data)"
rg -n --hidden -S -C3 'isSuccess' || true
rg -n --hidden -S -C3 'code:' || true
rg -n --hidden -S -C3 'message:' || true
rg -n --hidden -S -C3 'data:' || true

echo
echo "4) 파일 직접 출력: src/api/rooms/leaveRoom.ts (존재하면 상위 200줄)"
if [ -f src/api/rooms/leaveRoom.ts ]; then
sed -n '1,200p' src/api/rooms/leaveRoom.ts
else
echo "src/api/rooms/leaveRoom.ts 파일을 찾을 수 없습니다."
fi


</blockquote></details>
<details>
<summary>src/pages/groupDetail/ParticipatedGroupDetail.tsx (1)</summary><blockquote>

`37-37`: **API 의존성 분리 👍**

페이지에서 API 레이어를 분리해 임포트하는 방향은 좋습니다. 추후 목킹/테스트와 교체 가능성이 높아집니다.

</blockquote></details>
<details>
<summary>src/components/feed/FollowList.tsx (1)</summary><blockquote>

`167-167`: **빈 상태 카드 라운드값 8→12px 변경 OK**

디자인 톤 정리에 부합합니다. 다른 유사 카드 컴포넌트들과 반경 일관성만 확인하면 됩니다.

</blockquote></details>
<details>
<summary>src/types/user.ts (1)</summary><blockquote>

`4-4`: **profileImgUrl → profileImageUrl 리네임: 레거시 참조 존재 여부 재검증 필요**

rg 검사에서 "No files were searched" 오류로 자동 검증에 실패했습니다. 레포 루트에서 아래 명령을 실행해 잔존 참조 출력(또는 오류)을 코멘트로 붙이세요.

```shell
rg -nPI -g '!dist/**' -g '!build/**' '\bprofileImgUrl\b|profileImageUrl'
src/pages/post/CreatePost.tsx (1)

104-106: 주석 갱신 승인 — undefined 전달 안전

createNewFeed는 시그니처가 images?: File[]이며 런타임에서 if (images && images.length > 0)로 검사하므로 undefined를 안전하게 처리합니다 (참조: src/hooks/useCreateFeed.ts:15).

src/api/users/getUsers.ts (1)

6-6: 해결: 레거시 키(profileImgUrl) 미검출 — 매핑 불필요
rg 검색 결과 프로젝트 내에 'profileImgUrl'이 존재하지 않고 사용 중인 키는 'profileImageUrl'로 통일되어 있어 제안한 하위호환 매핑은 적용할 필요가 없습니다.

src/pages/memory/Memory.tsx (2)

119-119: 공백/형식 변경만 발생

기능 영향 없음. 코멘트 생략합니다.


302-304: 형식 변경만 발생

기능 영향 없음. 추가 코멘트 없습니다.

src/components/memory/RecordItem/PollRecord.tsx (1)

70-71: 클릭 이벤트 버블링 차단 추가 적절

옵션 클릭 시 상위 카드 클릭과 충돌을 막아 UX가 개선됩니다.

src/api/record/updateRecord.ts (1)

8-23: API 모듈 구성 깔끔합니다

요청/응답 타입 명시, 예외 로깅·재던지기까지 패턴이 일관적입니다.

src/components/recordwrite/RecordContentSection.tsx (1)

16-20: autoFocus prop 추가 좋아요

편집 플로우에서의 UX가 자연스러워졌습니다.

src/data/userData.ts (2)

5-40: 프로필 이미지 키 일괄 변경 LGTM

목업 데이터가 새로운 프로퍼티에 맞춰 정규화되었습니다.


1-40: 레거시 키(profileImgUrl) 잔존 여부 확인 필요

rg가 파일을 건너뛰어 자동 검사가 실패했습니다 — 리포지토리 루트에서 아래 명령어로 잔존 참조를 확인하세요.

# 모든 파일(숨김/무시된 파일 포함)에서 레거시 키 및 현재 키 검색
rg -nIP --hidden --no-ignore -S '(profile[_\- ]?img[_\- ]?url|profileimgurl|profileImageUrl)' -C3 || true

# 대안 (git 리포지토리인 경우)
git grep -n --untracked -e 'profileImgUrl' || true
src/pages/index.tsx (2)

65-68: 편집 라우트 추가 적절

작성 컴포넌트 재활용으로 복잡도 증가 없이 플로우 확장되었습니다.


31-68: 편집 모드 파라미터 처리 검증 완료 — 수정 모드 분기 정상

RecordWrite와 PollWrite가 URL 파라미터(recordId / voteId)를 useParams로 받아 isEditMode = Boolean(...)으로 분기하며, 편집 시 updateRecord/updateVote를 호출하고 파라미터 누락 시 openSnackbar로 처리되는 것을 확인했습니다.

확인 위치:

  • src/pages/recordwrite/RecordWrite.tsx — useParams(라인 16), isEditMode 선언(라인 20), update 호출(라인 167-168)
  • src/pages/pollwrite/PollWrite.tsx — useParams(라인 16), isEditMode 선언(라인 20), update 호출(라인 183-184)
src/components/feed/UserProfileItem.tsx (1)

11-21: props 리네임 적용 범위 확인 — 완료 (profileImgUrl → profileImageUrl)
레포 전역 검색 결과 'profileImgUrl' 잔존 참조가 없고, 타입 정의 및 모든 호출부가 'profileImageUrl'로 일관되어 있습니다.

src/components/memory/RecordItem/RecordItem.styled.ts (1)

4-9: 오버레이 스택을 위한 position: relative 추가, 좋습니다

블러 상태에서 오버레이를 절대배치로 얹을 수 있게 되어 상호작용 차단을 컴포넌트 밖에서 제어하기 용이합니다.

src/api/feeds/createFeed.ts (2)

31-35: 문서화 업데이트(L7 presigned URL 플로우) 좋습니다

application/json + imageUrls 설명이 명확합니다.


9-10: imageUrls 필드 전환 — 호출부·백엔드 계약 즉시 확인

  • 프런트: createFeed 호출 전역에서 더 이상 File[] 또는 FormData('images')를 두번째 인자로 전달하지 않는지 확인 (참고: src/api/feeds/createFeed.ts — lines 9-10, 39-41).
  • 백엔드: API가 정확히 "imageUrls"라는 string[] 필드를 JSON 바디로 기대하는지(필드명·대소문자·Content-Type 일치) 확인.
src/api/record/updateVote.ts (1)

8-23: PATCH 구현 및 타입 바인딩 적절함

엔드포인트와 제네릭 응답 타입 연결이 명확합니다. 호출부에서 ApiResponse 분기 처리만 일관되게 해주면 됩니다.

src/hooks/useCreateFeed.ts (2)

3-4: presigned URL / S3 업로드 분리, 의존성 주입 구조 깔끔합니다


126-133: imageUrls 조건부 병합 및 createFeed 호출 흐름 적절

불필요한 필드 전송을 피하면서도 업로드 성공 케이스만 포함합니다. LGTM.

src/components/recordwrite/PageRangeSection.tsx (2)

119-120: 입력 비활성 처리 LGTM.

편집 불가 상태에서의 UX가 명확해졌습니다.


156-173: 토글 숨김(hideToggle) 처리 LGTM.

편집 모드/권한 시나리오에서 토글 UI 노출 제어가 명확합니다.

src/components/memory/RecordItem/RecordItem.tsx (1)

292-300: 블라인드 상태 클릭 차단 로직 LGTM.

이벤트 중단 + 오버레이로 상호작용을 명확히 제한합니다.

Also applies to: 334-356

src/components/pollwrite/PollCreationSection.tsx (3)

95-104: 자동 포커스 로직이 올바르게 구현되었습니다

수정 모드에서 콘텐츠 입력 필드에 자동으로 포커스를 설정하고 커서를 텍스트 끝으로 이동시키는 로직이 적절합니다.


131-132: 수정 모드에서 옵션 입력 비활성화가 적절합니다

수정 모드에서는 투표 옵션을 변경할 수 없도록 disabledreadOnly 속성을 모두 설정한 것이 좋습니다.


134-150: 수정 모드에서 삭제 버튼 숨김 처리가 잘 되었습니다

수정 모드일 때 옵션 삭제 버튼들을 숨기는 조건부 렌더링이 적절하게 구현되었습니다.

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

50-76: 수정 모드 초기화 로직이 잘 구현되었습니다

URL 쿼리 파라미터에서 기존 데이터를 로드하고 디코딩하는 처리가 적절합니다. 수정 모드에서도 전체 페이지 수를 조회하는 것은 좋은 접근입니다.


93-114: 에러 메시지 처리가 개선되었습니다

HTTP 상태 코드별로 사용자 친화적인 에러 메시지를 제공하는 것이 좋습니다.


163-186: 기록 수정 API 호출이 올바르게 구현되었습니다

수정 모드에서 UpdateRecordRequest 타입을 사용하여 콘텐츠만 업데이트하는 로직이 적절합니다.


189-239: 생성 모드의 페이지 검증 로직이 견고합니다

페이지 범위 유효성 검사와 총평/일반 기록 처리 분기가 명확하게 구현되었습니다.

src/pages/pollwrite/PollWrite.tsx (2)

210-221: 투표 옵션 유효성 검사가 잘 구현되었습니다

빈 옵션을 필터링하고 최소 2개 이상의 옵션을 요구하는 검증 로직이 적절합니다.


249-254: 투표 생성 요청 데이터 구조가 올바릅니다

CreateVoteRequest 타입에 맞게 투표 옵션을 voteItemList로 변환하는 로직이 적절합니다.

Comment on lines +1 to +19
export const uploadFileToS3 = async (
presignedUrl: string,
file: File
): Promise<boolean> => {
try {
const response = await fetch(presignedUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
});

return response.ok;
} catch (error) {
console.error('S3 업로드 실패:', error);
return false;
}
}; No newline at end of file
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

대외 호출: 타임아웃/재시도/에러 메시지 보강 필요

현재 무한 대기 가능성과 일시적 5xx에 취약합니다. 업로드 UX/복구력을 위해 최소 타임아웃과 제한적 재시도를 권장합니다.

 export const uploadFileToS3 = async (
   presignedUrl: string,
   file: File
 ): Promise<boolean> => {
   try {
-    const response = await fetch(presignedUrl, {
-      method: 'PUT',
-      body: file,
-      headers: {
-        'Content-Type': file.type,
-      },
-    });
+    // 15s 타임아웃 + 최대 2회 재시도(총 3회)
+    const maxAttempts = 3;
+    const timeoutMs = 15000;
+    let lastError: unknown = null;
+
+    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+      const controller = new AbortController();
+      const timer = setTimeout(() => controller.abort(), timeoutMs);
+      try {
+        const response = await fetch(presignedUrl, {
+          method: 'PUT',
+          body: file,
+          headers: { 'Content-Type': file.type },
+          signal: controller.signal,
+        });
+        clearTimeout(timer);
+        if (response.ok) return true;
+        // 5xx만 재시도, 그 외는 즉시 실패
+        if (response.status < 500 || response.status >= 600) {
+          const msg = await response.text().catch(() => '');
+          console.error(`S3 업로드 실패: ${response.status} ${msg}`); 
+          return false;
+        }
+      } catch (e) {
+        clearTimeout(timer);
+        lastError = e;
+        // AbortError 등은 재시도
+      }
+      // 지수 백오프(최대 ~1s)
+      await new Promise(r => setTimeout(r, Math.min(1000, 100 * 2 ** (attempt - 1))));
+    }
+    console.error('S3 업로드 반복 실패:', lastError);
+    return false;
-    return response.ok;
   } catch (error) {
     console.error('S3 업로드 실패:', error);
     return false;
   }
 };
🤖 Prompt for AI Agents
In src/api/feeds/uploadToS3.ts around lines 1 to 19, the current uploadFileToS3
can hang indefinitely and has no retry for transient 5xx errors; add an
AbortController-based timeout (e.g., configurable ms) to ensure fetch cancels
after a max wait, implement a small bounded retry loop (e.g., 2-3 attempts) with
exponential backoff for network errors and 5xx responses only, and surface
richer error logs (include attempt number, http status, and response text or
error message) before returning false so callers can observe failure reasons.

Comment on lines +18 to +24
try {
const response = await apiClient.delete<DeleteDailyGreetingResponse>(
`/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
);

return response.data;
} catch (error) {
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

DELETE 204(No Content) 응답 시 타입-런타임 불일치로 실패 처리될 위험

서버가 204를 반환하면 response.data가 비거나 없음으로 내려올 수 있어, 호출부에서 result.isSuccess 검사 시 실패로 오인됩니다. 바디에 의존하지 말고 HTTP status로 성공을 판정하도록 정규화하세요.

한 가지 완화안(호출부 변경 없이 204 허용):

-    const response = await apiClient.delete<DeleteDailyGreetingResponse>(
-      `/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
-    );
-
-    return response.data;
+    const response = await apiClient.delete(
+      `/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
+    );
+    // 2xx는 성공으로 정규화
+    if (response.status >= 200 && response.status < 300) {
+      return (response.data as DeleteDailyGreetingResponse) ?? {
+        isSuccess: true,
+        code: response.status,
+        message: '',
+        data: { roomId },
+      };
+    }
+    return response.data as DeleteDailyGreetingResponse;

🏁 Script executed:

#!/bin/bash
# 삭제 API 응답 바디 유무/스키마 사용처 점검
rg -n -C2 --type ts "DeleteDailyGreetingResponse|deleteDailyGreeting|daily-greeting"

Length of output: 2800


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== src/api/rooms/deleteDailyGreeting.ts ====="
sed -n '1,200p' src/api/rooms/deleteDailyGreeting.ts || true

echo
echo "===== src/components/today-words/MessageList/MessageList.tsx (lines ~90-130) ====="
sed -n '90,130p' src/components/today-words/MessageList/MessageList.tsx || true

Length of output: 2020


DELETE 204(No Content) 대응 — deleteDailyGreeting에서 2xx 응답을 성공으로 정규화하세요.

현재 deleteDailyGreeting는 response.data를 바로 반환하므로 서버가 204를 반환하면 호출부의 result.isSuccess 검사에서 실패로 처리됩니다. API 함수에서 2xx 응답을 성공으로 정규화하도록 수정하세요.
수정 파일: src/api/rooms/deleteDailyGreeting.ts (호출부 참고: src/components/today-words/MessageList/MessageList.tsx, result.isSuccess 검사).

-    const response = await apiClient.delete<DeleteDailyGreetingResponse>(
-      `/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
-    );
-
-    return response.data;
+    const response = await apiClient.delete(
+      `/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
+    );
+    // 2xx는 성공으로 정규화
+    if (response.status >= 200 && response.status < 300) {
+      return (response.data as DeleteDailyGreetingResponse) ?? {
+        isSuccess: true,
+        code: response.status,
+        message: '',
+        data: { roomId },
+      };
+    }
+    return response.data as DeleteDailyGreetingResponse;
📝 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
try {
const response = await apiClient.delete<DeleteDailyGreetingResponse>(
`/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
);
return response.data;
} catch (error) {
try {
const response = await apiClient.delete(
`/rooms/${roomId}/daily-greeting/${attendanceCheckId}`,
);
// 2xx는 성공으로 정규화
if (response.status >= 200 && response.status < 300) {
return (response.data as DeleteDailyGreetingResponse) ?? {
isSuccess: true,
code: response.status,
message: '',
data: { roomId },
};
}
return response.data as DeleteDailyGreetingResponse;
} catch (error) {
🤖 Prompt for AI Agents
In src/api/rooms/deleteDailyGreeting.ts around lines 18-24, the function returns
response.data directly which fails when the server responds with 204 No Content;
change the function to treat any 2xx status as success and return a normalized
value. Update the try block to check response.status (200 <= status < 300) and
then return response.data if present, otherwise return a normalized empty value
matching DeleteDailyGreetingResponse (e.g., {} cast to
DeleteDailyGreetingResponse or a small success object) so callers relying on
result.isSuccess treat 204 as success.

Comment on lines +162 to +170
<ToggleLabel disabled={!canUseOverall}>총평</ToggleLabel>
</LeftSection>
<ToggleSwitch
active={isOverallEnabled}
onClick={handleToggleClick}
disabled={!canUseOverall || isDisabled}
>
<ToggleSlider active={isOverallEnabled} disabled={!canUseOverall} />
</ToggleSwitch>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

토글의 비활성 상태 정합성 확보(라벨/슬라이더 동기화).

Switch는 isDisabled를 반영하지만 Label과 Slider는 반영하지 않아 시각·행동 불일치가 납니다.

아래처럼 disabled 계산을 동일하게 맞춰 주세요.

-              <ToggleLabel disabled={!canUseOverall}>총평</ToggleLabel>
+              <ToggleLabel disabled={!canUseOverall || isDisabled}>총평</ToggleLabel>
...
-              <ToggleSlider active={isOverallEnabled} disabled={!canUseOverall} />
+              <ToggleSlider active={isOverallEnabled} disabled={!canUseOverall || isDisabled} />

추가로, 실제 클릭 차단을 위해 styled 컴포넌트에도 pointer-events를 권장합니다(별도 파일 수정 예시).

// src/components/recordwrite/PageRangeSection.styled.ts
 export const ToggleSwitch = styled.div<{ active: boolean; disabled?: boolean }>`
   width: 48px;
   height: 28px;
   background-color: ${({ active, disabled }) => {
     if (disabled) return semanticColors.text.ghost;
     return active ? semanticColors.button.fill.primary : semanticColors.background.card;
   }};
   border-radius: 14px;
   position: relative;
   cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
+  pointer-events: ${props => (props.disabled ? 'none' : 'auto')};
   transition: background-color 0.3s;
   opacity: ${props => (props.disabled ? 0.5 : 1)};
 `;
📝 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
<ToggleLabel disabled={!canUseOverall}>총평</ToggleLabel>
</LeftSection>
<ToggleSwitch
active={isOverallEnabled}
onClick={handleToggleClick}
disabled={!canUseOverall || isDisabled}
>
<ToggleSlider active={isOverallEnabled} disabled={!canUseOverall} />
</ToggleSwitch>
<ToggleLabel disabled={!canUseOverall || isDisabled}>총평</ToggleLabel>
</LeftSection>
<ToggleSwitch
active={isOverallEnabled}
onClick={handleToggleClick}
disabled={!canUseOverall || isDisabled}
>
<ToggleSlider active={isOverallEnabled} disabled={!canUseOverall || isDisabled} />
</ToggleSwitch>
Suggested change
<ToggleLabel disabled={!canUseOverall}>총평</ToggleLabel>
</LeftSection>
<ToggleSwitch
active={isOverallEnabled}
onClick={handleToggleClick}
disabled={!canUseOverall || isDisabled}
>
<ToggleSlider active={isOverallEnabled} disabled={!canUseOverall} />
</ToggleSwitch>
// src/components/recordwrite/PageRangeSection.styled.ts
export const ToggleSwitch = styled.div<{ active: boolean; disabled?: boolean }>`
width: 48px;
height: 28px;
background-color: ${({ active, disabled }) => {
if (disabled) return semanticColors.text.ghost;
return active ? semanticColors.button.fill.primary : semanticColors.background.card;
}};
border-radius: 14px;
position: relative;
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
pointer-events: ${props => (props.disabled ? 'none' : 'auto')};
transition: background-color 0.3s;
opacity: ${props => (props.disabled ? 0.5 : 1)};
`;
🤖 Prompt for AI Agents
In src/components/recordwrite/PageRangeSection.tsx around lines 162 to 170, the
ToggleLabel and ToggleSlider aren’t using the same disabled logic as the
ToggleSwitch, causing visual/behavior mismatch; compute a single disabled flag
(e.g., const overallDisabled = isDisabled || !canUseOverall) and pass
overallDisabled to ToggleLabel, ToggleSwitch (already using similar props but
unify), and ToggleSlider so label and slider reflect the same disabled state;
additionally, update the styled components (where ToggleSlider/Label are
defined) to apply pointer-events: none when the disabled prop is true to prevent
clicks.

Comment on lines +107 to +149
const handleDelete = async () => {
if (selectedMessageId && roomId) {
try {
// API에서는 attendanceCheckId가 필요하므로 selectedMessageId를 사용
const attendanceCheckId = parseInt(selectedMessageId);

const result = await deleteDailyGreeting(roomId, attendanceCheckId);

if (result.isSuccess) {
// 로컬 상태에서 메시지 제거
setMessages(prevMessages =>
prevMessages.filter(msg => msg.id !== selectedMessageId)
);

// 부모 컴포넌트에 삭제 알림
if (onMessageDelete) {
onMessageDelete(selectedMessageId);
}

openSnackbar({
message: '오늘의 한마디가 삭제되었습니다.',
variant: 'top',
isError: false,
onClose: () => {},
});
} else {
openSnackbar({
message: result.message || '삭제에 실패했습니다.',
variant: 'top',
isError: true,
onClose: () => {},
});
}
} catch (error) {
console.error('삭제 오류:', error);
openSnackbar({
message: '삭제 중 오류가 발생했습니다.',
variant: 'top',
isError: true,
onClose: () => {},
});
}
} else if (!roomId) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

삭제 API 응답 바디 의존성 제거(204 대응) + 중복 클릭 방지

일부 서버는 DELETE에 204(No Content)를 반환합니다. 현재는 result.isSuccess에 의존해 성공 판단하므로 204에서 실패 토스트가 뜰 수 있습니다. 또한 연타시 중복 호출을 막아주세요.

@@
-    const handleDelete = async () => {
-      if (selectedMessageId && roomId) {
-        try {
-          // API에서는 attendanceCheckId가 필요하므로 selectedMessageId를 사용
-          const attendanceCheckId = parseInt(selectedMessageId);
-          
-          const result = await deleteDailyGreeting(roomId, attendanceCheckId);
-          
-          if (result.isSuccess) {
-            // 로컬 상태에서 메시지 제거
-            setMessages(prevMessages => 
-              prevMessages.filter(msg => msg.id !== selectedMessageId)
-            );
-            
-            // 부모 컴포넌트에 삭제 알림
-            if (onMessageDelete) {
-              onMessageDelete(selectedMessageId);
-            }
-            
-            openSnackbar({
-              message: '오늘의 한마디가 삭제되었습니다.',
-              variant: 'top',
-              isError: false,
-              onClose: () => {},
-            });
-          } else {
-            openSnackbar({
-              message: result.message || '삭제에 실패했습니다.',
-              variant: 'top',
-              isError: true,
-              onClose: () => {},
-            });
-          }
-        } catch (error) {
-          console.error('삭제 오류:', error);
-          openSnackbar({
-            message: '삭제 중 오류가 발생했습니다.',
-            variant: 'top',
-            isError: true,
-            onClose: () => {},
-          });
-        }
-      } else if (!roomId) {
+    const handleDelete = async () => {
+      if (selectedMessageId && roomId) {
+        if (isDeleting) return;
+        setIsDeleting(true);
+        try {
+          const attendanceCheckId = Number(selectedMessageId);
+          if (!Number.isInteger(attendanceCheckId)) {
+            openSnackbar({ message: '잘못된 메시지 ID입니다.', variant: 'top', isError: true, onClose: () => {} });
+            return;
+          }
+          const result = await deleteDailyGreeting(roomId, attendanceCheckId);
+          // 204 대응: 바디가 없으면 성공으로 처리
+          const ok = result?.isSuccess !== false;
+          if (!ok) {
+            openSnackbar({ message: result?.message || '삭제에 실패했습니다.', variant: 'top', isError: true, onClose: () => {} });
+            return;
+          }
+          setMessages(prev => prev.filter(msg => msg.id !== selectedMessageId));
+          onMessageDelete?.(selectedMessageId);
+          openSnackbar({ message: '오늘의 한마디가 삭제되었습니다.', variant: 'top', isError: false, onClose: () => {} });
+        } catch (error) {
+          console.error('삭제 오류:', error);
+          openSnackbar({ message: '삭제 중 오류가 발생했습니다.', variant: 'top', isError: true, onClose: () => {} });
+        } finally {
+          setIsDeleting(false);
+        }
+      } else if (!roomId) {
         openSnackbar({
           message: '방 정보가 없습니다.',
           variant: 'top',
           isError: true,
           onClose: () => {},
         });
       }
       setSelectedMessageId(null);
     };

또한 상단 상태 정의에 다음을 추가하세요:

-    const [messages, setMessages] = useState(initialMessages);
+    const [messages, setMessages] = useState(initialMessages);
+    const [isDeleting, setIsDeleting] = useState(false);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleDelete = async () => {
if (selectedMessageId && roomId) {
try {
// API에서는 attendanceCheckId가 필요하므로 selectedMessageId를 사용
const attendanceCheckId = parseInt(selectedMessageId);
const result = await deleteDailyGreeting(roomId, attendanceCheckId);
if (result.isSuccess) {
// 로컬 상태에서 메시지 제거
setMessages(prevMessages =>
prevMessages.filter(msg => msg.id !== selectedMessageId)
);
// 부모 컴포넌트에 삭제 알림
if (onMessageDelete) {
onMessageDelete(selectedMessageId);
}
openSnackbar({
message: '오늘의 한마디가 삭제되었습니다.',
variant: 'top',
isError: false,
onClose: () => {},
});
} else {
openSnackbar({
message: result.message || '삭제에 실패했습니다.',
variant: 'top',
isError: true,
onClose: () => {},
});
}
} catch (error) {
console.error('삭제 오류:', error);
openSnackbar({
message: '삭제 중 오류가 발생했습니다.',
variant: 'top',
isError: true,
onClose: () => {},
});
}
} else if (!roomId) {
const [messages, setMessages] = useState(initialMessages);
const [isDeleting, setIsDeleting] = useState(false);
const handleDelete = async () => {
if (selectedMessageId && roomId) {
if (isDeleting) return;
setIsDeleting(true);
try {
const attendanceCheckId = Number(selectedMessageId);
if (!Number.isInteger(attendanceCheckId)) {
openSnackbar({ message: '잘못된 메시지 ID입니다.', variant: 'top', isError: true, onClose: () => {} });
return;
}
const result = await deleteDailyGreeting(roomId, attendanceCheckId);
// 204 대응: 바디가 없으면 성공으로 처리
const ok = result?.isSuccess !== false;
if (!ok) {
openSnackbar({ message: result?.message || '삭제에 실패했습니다.', variant: 'top', isError: true, onClose: () => {} });
return;
}
setMessages(prev => prev.filter(msg => msg.id !== selectedMessageId));
onMessageDelete?.(selectedMessageId);
openSnackbar({ message: '오늘의 한마디가 삭제되었습니다.', variant: 'top', isError: false, onClose: () => {} });
} catch (error) {
console.error('삭제 오류:', error);
openSnackbar({ message: '삭제 중 오류가 발생했습니다.', variant: 'top', isError: true, onClose: () => {} });
} finally {
setIsDeleting(false);
}
} else if (!roomId) {
openSnackbar({
message: '방 정보가 없습니다.',
variant: 'top',
isError: true,
onClose: () => {},
});
}
setSelectedMessageId(null);
};

Comment on lines +81 to +92
try {
const presignedResponse = await getPresignedUrl(presignedRequests);

if (!presignedResponse.isSuccess || !presignedResponse.data) {
openSnackbar({
message: presignedResponse.message || 'Presigned URL 발급에 실패했습니다.',
variant: 'top',
onClose: closePopup,
});
return { success: false as const };
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

응답-요청 매핑 길이 검증 추가

presignedUrls 개수와 images 개수가 불일치하면 잘못된 매핑으로 업로드될 수 있습니다. 사전 길이 검증이 필요합니다.

-          if (!presignedResponse.isSuccess || !presignedResponse.data) {
+          if (!presignedResponse.isSuccess || !presignedResponse.data) {
             openSnackbar({
               message: presignedResponse.message || 'Presigned URL 발급에 실패했습니다.',
               variant: 'top',
               onClose: closePopup,
             });
             return { success: false as const };
           }
+          if (presignedResponse.data.presignedUrls.length !== images.length) {
+            openSnackbar({
+              message: '발급 URL 수가 이미지 수와 일치하지 않아요.',
+              variant: 'top',
+              onClose: closePopup,
+            });
+            return { success: false as const };
+          }
📝 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
try {
const presignedResponse = await getPresignedUrl(presignedRequests);
if (!presignedResponse.isSuccess || !presignedResponse.data) {
openSnackbar({
message: presignedResponse.message || 'Presigned URL 발급에 실패했습니다.',
variant: 'top',
onClose: closePopup,
});
return { success: false as const };
}
try {
const presignedResponse = await getPresignedUrl(presignedRequests);
if (!presignedResponse.isSuccess || !presignedResponse.data) {
openSnackbar({
message: presignedResponse.message || 'Presigned URL 발급에 실패했습니다.',
variant: 'top',
onClose: closePopup,
});
return { success: false as const };
}
if (presignedResponse.data.presignedUrls.length !== images.length) {
openSnackbar({
message: '발급 URL 수가 이미지 수와 일치하지 않아요.',
variant: 'top',
onClose: closePopup,
});
return { success: false as const };
}
🤖 Prompt for AI Agents
In src/hooks/useCreateFeed.ts around lines 81 to 92, after confirming
presignedResponse.isSuccess and presignedResponse.data, add a defensive length
check to ensure the number of returned presigned URLs matches the number of
images/presignedRequests; if the lengths differ, call openSnackbar with an
appropriate error message, call closePopup, and return { success: false } to
abort upload. Make sure to safely access the array (guard against undefined) and
include a clear log or message so the mismatch is visible for debugging.

Comment on lines +180 to +182
console.log('투표 수정 API 호출:', updateData);
console.log('roomId:', roomId, 'voteId:', voteId);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

프로덕션 환경에서 console.log 제거 필요

디버깅용 console.log 문들이 남아있습니다. 프로덕션 배포 전에 제거하거나 개발 환경에서만 실행되도록 조건을 추가해주세요.

-console.log('투표 수정 API 호출:', updateData);
-console.log('roomId:', roomId, 'voteId:', voteId);

-console.log('투표 생성 API 호출:', voteData);
-console.log('roomId:', roomId);

Also applies to: 256-257

🤖 Prompt for AI Agents
In src/pages/pollwrite/PollWrite.tsx around lines 180-182 and 256-257, there are
leftover console.log statements used for debugging; remove these console.log
calls or guard them so they only run in non-production (e.g., wrap with if
(process.env.NODE_ENV !== 'production') { ... } or use a logger that is no-op in
production), and ensure no console.log remains in the production build.

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

Labels

🌏 Deploy 배포 관련

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants