Skip to content

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

Merged
ljh130334 merged 43 commits into
mainfrom
develop
Aug 20, 2025
Merged

develp branch 작업내용 머지 : develop -> main#180
ljh130334 merged 43 commits into
mainfrom
develop

Conversation

@ljh130334
Copy link
Copy Markdown
Member

@ljh130334 ljh130334 commented Aug 20, 2025

이하동일

Summary by CodeRabbit

  • New Features

    • 스낵바에 오류 상태(isError) 표시 추가, 페이지 입력 너비 자동 조절 및 총평 안내 툴팁(빨강/초록) 도입.
    • 기록 잠금(isLocked) 반영으로 블러·비활성화 처리 및 투표/버튼 비활성화.
    • 피드 저장 토글 통지(onSaveToggle) 경로 추가.
  • Bug Fixes

    • 비활성 버튼 클릭 차단, 페이지 범위 유효성 강화, 오버레이 바깥 클릭 시 취소 동작 개선.
    • 오늘의 한마디 날짜 인식 개선 및 자동 스크롤 보강.
  • Style

    • 여러 레이아웃에 min/max(320–767px) 제약 적용, 섹션 너비를 calc로 정렬·패딩·플레이스홀더 미세조정, 모달/오버레이 정렬 수정.
  • Refactor

    • 진행도 표시 정수화, 개발용 UI 제거, 메뉴에서 고정(Pin) UI 제거 및 롱프레스 메뉴 전환.

ljh130334 and others added 30 commits August 20, 2025 12:27
@vercel
Copy link
Copy Markdown

vercel Bot commented Aug 20, 2025

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

Project Deployment Preview Comments Updated (UTC)
thip Ready Ready Preview Comment Aug 20, 2025 0:49am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 20, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

여러 컴포넌트의 스타일·레이아웃 조정과 메모리(기록) 도메인 API 연동 및 블러(잠금) 기반 상호작용 도입, 모달/스낵바·팝업 관련 API/props 변화, 페이지범위 입력·툴팁 및 일부 라우팅·텍스트 수정을 포함합니다.

Changes

Cohort / File(s) Summary
Popup/Modal 변경
src/components/common/Modal/MoreMenu.tsx, src/components/common/Modal/Snackbar.tsx, src/stores/usePopupStore.ts
MoreMenu에서 onPin 제거·오버레이/레이아웃 변경. Snackbar에 isError prop 추가 및 스타일 반영. Popup store의 SnackbarPropsisError?: boolean 추가.
메모리 도메인: 타입·진행도·블러
src/pages/memory/Memory.tsx, src/types/memory.ts, src/components/memory/MemoryContent/MemoryContent.tsx, .../RecordList.tsx, .../RecordList.styled.ts, .../RecordItem/PollRecord.tsx, .../RecordItem/RecordItem.tsx, src/components/memory/PageRangeModal*.ts*
Record.isLocked 추가, Memory가 API의 totalPages/currentUserPagereadingProgress 계산. RecordList/MemoryContent에서 readingProgress/dev 토글 삭제, blur 판정은 isLocked·shouldBlur로 전환. PageInput inputLength 도입.
RecordFilters 및 PageInput 변경
src/components/memory/RecordFilters/RecordFilters.tsx, .../FilterButtons*.ts*, .../PageInputMode*.ts*
숫자 입력 검증 강화(빈값 또는 숫자만 반영), 취소 동작(handleCancel) 및 오버레이 클릭으로 초기화, 비활성 버튼은 클릭·포인터 이벤트 금지, type="number"inputMode="numeric".
RecordWrite: 페이지범위·툴팁 UI 키트
src/components/recordwrite/PageRangeSection.styled.ts, src/components/recordwrite/PageRangeSection.tsx
PageInput 가변 폭(inputLength)·애니메이션, Info 아이콘과 빨강/초록 툴팁 컴포넌트 추가·연동(진행도 조건에 따라 red/green 표시).
Post 작성/수정·텍스트에디터 스타일
src/pages/post/CreatePost.tsx, src/pages/post/UpdatePost.tsx, src/components/createpost/PostContentSection.styled.ts, src/components/createpost/PostContentSection.tsx
헤더 우측 버튼 라벨을 상수 "완료"로 고정. TextArea의 readOnly 스타일을 styled로 이전; CreatePost에서 readOnly 해제. UpdatePost는 cover 초기값 설정.
그룹/레이아웃·표기 조정
src/pages/groupDetail/GroupDetail.styled.ts, src/pages/groupDetail/ParticipatedGroupDetail.tsx, src/pages/group/CreateGroup.tsx, src/components/group/*Section*.ts*, src/components/group/MyGroupCard.tsx
TagGenre에 genre prop으로 장르 색맵 적용, ParticipatedWrapper min-height:100vh, 도서 경로 /search/book/{isbn}로 변경, 섹션 폭을 calc(100% - 40px)로 통일, 저자 표기에 " 저", 진행도 텍스트 Math.floor 적용, 생성 버튼 라벨 고정.
ActivityPeriod / RoomInfo 미세조정
src/components/creategroup/ActivityPeriodSection/*.ts*, src/components/creategroup/RoomInfoSection*.ts*
DateGroup에 alignItems? prop, DateUnitText에 isLast? prop. TextArea 폰트·플레이스홀더 문구·컬러 조정.
공통 스타일·로딩·모달 폭 제약
src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.styled.ts, src/components/common/LoadingSpinner.tsx, src/components/group/...Section.styled.ts, src/components/group/RecordSection.styled.ts
BottomSheet/LoadingSpinner 등에 max-width:767px/min-width:320px 제약 추가 및 여러 섹션 폭을 calc(100% - 40px)로 통일.
Feed/Post 저장 토글 전파
src/components/common/Post/PostFooter.tsx, src/components/feed/FeedPost.tsx, src/types/post.ts
PostFooter에 onSaveToggle?(feedId,newSaveState) 추가, FeedPost에 prop 전달 및 타입 확장.
UserProfileItem / FollowerList 변경
src/components/feed/UserProfileItem.tsx, src/pages/feed/FollowerListPage.tsx, src/types/follow.ts, src/types/user.ts
isMyself prop 추가로 자기 프로필 네비게이션/팔로우 버튼 숨김 처리; FollowData/UserProfileItemProps에 isMyself 필드(선택적) 추가.
RecordItem 팝업/핀·상호작용 개선
src/components/memory/RecordItem/RecordItem.tsx, src/hooks/usePopupActions.ts, src/stores/usePopupStore.ts
롱프레스 기반 메뉴(작성자/비작성자 분기), closePopup() public 추가, openConfirmonClose 콜백 수용, 핀 플로우에서 팝업 닫기 + 네비게이션 또는 에러 스낵바(isError) 처리.
TodayWords 개선
src/pages/today-words/TodayWords.tsx
오늘 날짜 비교를 다중 포맷으로 변경, 초기/작성 후 자동 하단 스크롤, 일일 제한 스낵바에 isError:true 적용 및 메시지 조건 조정.

Sequence Diagram(s)

sequenceDiagram
  actor U as 사용자
  participant M as Memory 페이지
  participant API as 서버 API
  participant MC as MemoryContent
  participant RL as RecordList
  participant RI as RecordItem/PollRecord

  U->>M: 페이지 진입
  M->>API: posts + (totalPages,currentUserPage) 요청
  API-->>M: posts, totalPages, currentUserPage
  M->>M: readingProgress = currentUserPage/totalPages
  M->>MC: records(with isLocked), readingProgress
  MC->>RL: records
  RL->>RI: 각 record, shouldBlur = record.isLocked
  U-->>RI: 클릭/투표 시도
  alt shouldBlur == true
    RI-->>U: 상호작용 차단
  else
    RI->>API: 투표/행동 수행
  end
Loading
sequenceDiagram
  actor U as 사용자
  participant RF as RecordFilters
  participant O as Overlay
  participant S as State

  U->>RF: 필터 열기
  RF->>O: 투명 오버레이 표시
  U->>RF: 숫자 입력 (빈값 또는 숫자만 반영)
  RF->>RF: 입력 검증(isValid)
  alt 확인
    U->>RF: 확인 클릭
    RF->>S: 필터 적용
  else 취소/오버레이 클릭
    U->>O: 클릭
    O->>RF: handleCancel()
    RF->>S: 입력/모드 초기화
  end
Loading
sequenceDiagram
  actor U as 사용자
  participant PRS as PageRangeSection
  participant TT as Tooltip(red/green)

  U->>PRS: 총평 토글 클릭
  alt 진행도 < 80%
    PRS->>TT: red 툴팁 표시("80% 필요")
  else
    PRS->>PRS: 토글 상태 변경
  end
  U->>PRS: Info 아이콘 클릭
  PRS->>TT: green 툴팁 표시(안내)
  U->>TT: 닫기 클릭
  TT->>PRS: 툴팁 숨김
Loading
sequenceDiagram
  actor U as 사용자(롱프레스)
  participant RI as RecordItem
  participant P as usePopupActions
  participant N as 네비게이션
  participant SB as Snackbar

  U->>RI: 롱프레스
  RI->>P: openMoreMenu(작성자/비작성자 분기)
  U->>RI: 핀 선택
  RI->>P: openConfirm(onClose 제공)
  alt 성공
    P->>RI: closePopup()
    RI->>N: /feed/write 이동(pinData)
  else 실패
    P->>RI: closePopup()
    RI->>SB: openSnackbar({ message, isError: true })
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

✨ Feature, 🎨 Html&css, 📬 API

Suggested reviewers

  • ho0010

Poem

(\_/) 깡총—
(•ᴥ•) 새 툴팁과 잠금, 레이아웃 정리했어요.
(🍃) 블러는 잠기고 토스트는 붉게 울려요.
(•‿•) 버튼은 “완료”, 길은 새 경로로—
(✧ㅅ✧) 토끼가 폴짝, 배포 준비 끝!

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5077cea and 5f021f3.

📒 Files selected for processing (12)
  • src/components/common/LoadingSpinner.tsx (1 hunks)
  • src/components/common/Post/PostBody.tsx (2 hunks)
  • src/components/common/Post/PostFooter.tsx (3 hunks)
  • src/components/feed/FeedPost.tsx (1 hunks)
  • src/components/feed/UserProfileItem.tsx (2 hunks)
  • src/components/group/Modal.styles.ts (1 hunks)
  • src/pages/feed/FollowerListPage.tsx (2 hunks)
  • src/pages/mypage/SavePage.tsx (7 hunks)
  • src/pages/searchBook/SearchBook.tsx (1 hunks)
  • src/types/follow.ts (1 hunks)
  • src/types/post.ts (1 hunks)
  • src/types/user.ts (1 hunks)
✨ 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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@ljh130334 ljh130334 added the 🌏 Deploy 배포 관련 label Aug 20, 2025
@ljh130334 ljh130334 merged commit 5ca4638 into main Aug 20, 2025
1 check passed
Copy link
Copy Markdown

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
src/pages/post/UpdatePost.tsx (1)

107-113: 빈 배열 미전송으로 “전체 삭제(태그/이미지)”가 서버에 반영되지 않을 수 있음

현재 tagList와 remainImageUrls는 length > 0일 때만 요청 본문에 포함됩니다. 사용자가 모든 태그/이미지를 제거한 경우, 필드가 아예 누락되어 서버가 “변경 없음”으로 해석할 가능성이 큽니다. 비워진 상태를 명시하려면 빈 배열도 항상 전송하세요.

다음처럼 조건부 확장을 제거해 항상 포함하도록 수정 제안드립니다.

-  const body: UpdateFeedBody = {
-    contentBody: postContent.trim(),
-    isPublic: !isPrivate,
-    ...(selectedTags.length ? { tagList: selectedTags } : {}),
-    ...(remainImageUrls.length ? { remainImageUrls } : {}),
-  };
+  const body: UpdateFeedBody = {
+    contentBody: postContent.trim(),
+    isPublic: !isPrivate,
+    tagList: selectedTags,        // 빈 배열도 전송
+    remainImageUrls,              // 빈 배열도 전송
+  };
src/pages/post/CreatePost.tsx (1)

23-34: ensureIsbn13에 정규화 전 원본(raw)을 전달 — 변환 실패 가능성 높음

Line 26에서 ensureIsbn13에 raw를 넘기고 있습니다. 하이픈/공백 제거 및 대문자 X 정규화된 normalized를 넘기는 것이 의도와 맞습니다.

- const isbn13 = ensureIsbn13(raw); // 13으로 변환 성공 시
+ const isbn13 = ensureIsbn13(normalized); // 정규화된 값으로 변환 시도
src/pages/group/CreateGroup.tsx (2)

51-53: 한 달 뒤 기본 종료일 계산이 ‘내일’과 동일하게 설정되는 버그

oneMonthLater가 today+1일로 설정되어 tomorrow와 동일합니다. 말 그대로 “한 달 뒤”가 되어야 합니다.

- const oneMonthLater = new Date(today);
- oneMonthLater.setDate(today.getDate() + 1);
+ const oneMonthLater = new Date(tomorrow);
+ oneMonthLater.setMonth(tomorrow.getMonth() + 1);

월말/윤달 경계 처리를 더 안전하게 하려면 date-fns(addMonths) 같은 유틸 사용도 고려해 주세요.


150-151: 불필요한 중복 OR: 항상 동일한 피연산자

response.isSuccess || response.isSuccess는 의미가 없습니다. 의도에 맞게 단일 체크 또는 보조 필드와의 OR를 사용하세요.

- const isSuccessful = response.isSuccess || response.isSuccess;
+ const isSuccessful = !!response.isSuccess;
+ // 또는 다양한 응답 케이스를 고려:
+ // const isSuccessful = !!(response.isSuccess ?? response.data?.isSuccess);
src/components/memory/RecordItem/PollRecord.tsx (1)

112-124: 중복된 에러 코드(140011) 분기 제거 필요

140011(방 접근 권한 없음) 분기가 두 번 등장합니다. 후행 분기는 절대 도달하지 않으므로 제거하세요. 유지보수성과 가독성에 불리합니다.

다음과 같이 정리하세요:

       } else {
         // 에러 처리
         let errorMessage = '투표 처리 중 오류가 발생했습니다.';
         
-        if (response.code === 120001) {
+        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 = '방 접근 권한이 없습니다.';
         }
src/stores/usePopupStore.ts (1)

20-27: PR 요약과 코드 간 onPin 불일치 확인 필요

PR 설명에는 “onPin 제거”라고 되어 있는데, 실제로는 타입 정의와 호출부에 onPin이 남아 있습니다. 아래 위치를 확인하고, onPin 제거가 의도된 것이라면 타입, 호출부, 컴포넌트 사용처를 일괄 정리하거나, 의도대로 유지한다면 PR 요약을 수정해주세요.

  • src/stores/usePopupStore.ts: MoreMenuProps에서 onPin 정의 (라인 24)
  • src/components/memory/RecordItem/RecordItem.tsx: openMoreMenu 호출 시 onPin 전달부 (라인 290)
src/components/memory/RecordItem/RecordItem.tsx (2)

137-140: roomId 미존재 시 '1'로 대체 호출은 데이터 오염/권한 오류를 유발할 수 있습니다. 안전하게 중단하세요.

하드코딩된 기본값(‘1’)으로 삭제 API를 호출하면 엉뚱한 방/레코드에 영향을 줄 위험이 큽니다.

-    const currentRoomId = roomId || '1';
-    const recordId = parseInt(record.id);
+    if (!roomId) {
+      openSnackbar({
+        message: '방 정보를 찾을 수 없습니다.',
+        variant: 'top',
+        onClose: () => {},
+      });
+      return;
+    }
+    const currentRoomId = roomId;
+    const recordId = parseInt(record.id, 10);

197-201: 핀하기도 동일하게 roomId 기본값 하드코딩 제거 필요

삭제 케이스와 같은 리스크입니다. 미존재 시 사용자에게 안내하고 중단하세요.

-    const currentRoomId = roomId || '1';
-    const recordId = parseInt(record.id);
+    if (!roomId) {
+      openSnackbar({
+        message: '방 정보를 찾을 수 없습니다.',
+        variant: 'top',
+        onClose: () => {},
+      });
+      return;
+    }
+    const currentRoomId = roomId;
+    const recordId = parseInt(record.id, 10);
🧹 Nitpick comments (53)
src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.styled.ts (2)

21-23: 모바일 기준 폭 상한을 767px로 상향 — 의도는 적절. 다만 토큰화와 CSS clamp로 단순화 제안

  • 767px은 흔한 md 브레이크포인트(768px) 바로 아래로 합리적인 선택입니다.
  • “매직 넘버” 대신 프로젝트의 브레이크포인트 토큰(있다면)을 사용하면 일관성이 올라갑니다.
  • width, min/max-width 조합을 CSS clamp로 단일 선언으로 단순화하면 가독성과 유지보수성이 좋아집니다.

아래처럼 교체를 제안합니다.

-  width: 100%;
-  max-width: 767px;
-  min-width: 320px;
+  width: clamp(320px, 100%, 767px);

검증 요청:

  • 320/360/375/390/414/768/834/1024px 뷰포트에서 수평 중앙정렬과 오버플로우 유무를 확인해 주세요.
  • iPad 세로(768px)에서 1px 차이로 인한 서브픽셀 렌더링/잔여 여백이 디자인 의도와 맞는지 확인이 필요합니다.

30-31: transform 애니메이션 렌더링 최적화 제안

transform/transition을 사용 중이므로 will-change 힌트를 추가하면 스크롤/열림 애니메이션이 더 부드러워질 수 있습니다.

   transform: translateY(${props => (props.isOpen ? '0' : '100%')});
+  will-change: transform;
   transition: transform 0.3s ease;
src/pages/post/UpdatePost.tsx (3)

121-127: PhotoSection 핸들러가 비어 있고 시그니처도 맞지 않을 수 있음

수정 화면에서 사진 추가/삭제를 의도적으로 막으려는 것이라면, 현재처럼 빈 함수로 두는 대신 명시적으로 비활성화를 알리거나(스낵바), 컴포넌트 측에 “추가 비활성화” 플래그(예: hideAddButton/allowAddNew=false 등)가 있다면 그걸 사용하는 편이 명확합니다. 또한 onPhotoAdd/onPhotoRemove의 파라미터 시그니처가 맞지 않으면 TS에서 타입 불일치가 날 수 있습니다.

예시(시그니처 정합 + 의도 노출):

- const handlePhotoAdd = () => {
-   return;
- };
+ const handlePhotoAdd = (_files: File[]) => {
+   openSnackbar({ message: '수정 화면에서는 새 사진 추가를 지원하지 않습니다.', variant: 'top', onClose: closePopup });
+ };

- const handlePhotoRemove = () => {
-   return;
- };
+ const handlePhotoRemove = (_index: number) => {
+   // 기존 이미지 삭제는 onExistingImageRemove로만 허용
+ }

또는 PhotoSection에 “추가 버튼 숨김/비활성”을 지원한다면, 그 prop을 사용하는 방향을 권장합니다.


67-69: bookImageUrl null/undefined 대비 필요

백엔드에서 이미지 URL이 누락될 수 있다면, cover에 안전한 기본값을 넣어 이미지 컴포넌트 오류를 피하세요.

- cover: data.bookImageUrl,
+ cover: data.bookImageUrl ?? '',

150-159: 로딩 중 버튼 라벨 고정으로 피드백 약화(선택사항)

rightButton 라벨을 고정 "완료"로 바꾸셨는데, isNextActive로 비활성화는 되더라도 사용자가 “진행 중”임을 명확히 인지하기 어렵습니다. 로딩 시 스피너(또는 “수정 중…” 라벨) 표기는 유지하는 것을 권장드립니다.

예시: TitleHeader가 로딩 인디케이터를 지원한다면 해당 prop 사용, 없으면 rightButton 라벨을 updateLoading에 따라 토글.

src/components/createpost/PostContentSection.styled.ts (2)

13-16: readOnly 배경을 밝게 바꾼 경우 텍스트/플레이스홀더 대비도 함께 조정 필요

배경을 #f5f5f5(밝음)로 바꾸면 현 텍스트 색(semanticColors.text.secondary: 다크 테마용)이 흐릿하게 보일 수 있습니다. readOnly일 때 텍스트/플레이스홀더 색상을 onlight 계열로 전환하는 것을 권장합니다.

- background-color: ${props => props.readOnly ? '#f5f5f5' : semanticColors.background.primary};
- color: ${semanticColors.text.secondary};
+ background-color: ${props => props.readOnly ? '#f5f5f5' : semanticColors.background.primary};
+ color: ${props => props.readOnly ? semanticColors.text.onlightPrimary : semanticColors.text.secondary};

  &::placeholder {
-   color: ${semanticColors.text.ghost};
+   color: ${props => props.readOnly ? semanticColors.text.onlightSecondary : semanticColors.text.ghost};
  }

Also applies to: 24-26


21-23: overflow: hidden은 긴 텍스트 편집 시 스크롤 불가 문제를 유발할 수 있음

컴포넌트가 자동 높이 증가를 안정적으로 보장하지 않는다면, 최소한 세로 스크롤은 허용하는 게 안전합니다.

- overflow: hidden;
+ overflow-x: hidden;
+ overflow-y: auto;
src/pages/post/CreatePost.tsx (2)

174-178: 로딩 중 버튼 라벨 고정으로 피드백 약화(선택사항)

작성 중에도 버튼 라벨이 “완료”로 고정되어 있어 즉각적인 진행 피드백이 부족할 수 있습니다. 스피너/라벨 토글 등 보완을 권장드립니다.


189-193: readOnly={false}는 불필요한 명시입니다(선택사항)

기본값이 false라면 prop 제거로 간결하게 유지하는 것을 권장합니다.

- <PostContentSection 
-   content={postContent} 
-   onContentChange={setPostContent}
-   readOnly={false}
- />
+ <PostContentSection 
+   content={postContent} 
+   onContentChange={setPostContent}
+ />
src/pages/group/CreateGroup.tsx (2)

10-10: 임포트 경로의 이중 슬래시 정리(사소하지만 혼동 소지)

경로에 이중 슬래시가 포함되어 있습니다. 일부 환경에서 문제가 되지 않더라도 통일성을 위해 정리하는 게 좋습니다.

- import PrivacySettingSection from '../../components/creategroup//PrivacySettingSection/PrivacySettingSection';
+ import PrivacySettingSection from '../../components/creategroup/PrivacySettingSection/PrivacySettingSection';

241-245: 로딩 중 버튼 라벨 고정으로 피드백 약화(선택사항)

isSubmitting과 유효성으로 버튼 활성은 제어되지만, 라벨 고정 “완료”는 진행 피드백이 부족할 수 있습니다. 스피너/라벨 토글을 고려해 주세요.

src/components/creategroup/RoomInfoSection.styled.ts (1)

24-26: placeholder 색상은 팔레트(colors) 직접 참조 대신 시맨틱 토큰 사용을 권장합니다.
현재 colors.grey[300]는 semanticColors.text.ghost와 동일한 값을 가리킵니다. 시맨틱 토큰으로 전환하면 테마 변경·가독성 검증에 유리합니다. import도 함께 정리하세요.

적용 diff:

-import { typography, semanticColors, colors } from '../../styles/global/global';
+import { typography, semanticColors } from '../../styles/global/global';
…
   &::placeholder {
-    color: ${colors.grey[300]};
+    color: ${semanticColors.text.ghost};
     font-size: ${typography.fontSize.sm};
   }

또한 다크/라이트 배경에서의 대비(특히 14px 텍스트) 충족을 위해 토큰 사용 시 자동으로 관리되므로 유지보수성이 올라갑니다.

Also applies to: 2-2

src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx (1)

192-192: DateGroup의 prop 이름이 CSS 속성 align-items와 혼동될 수 있습니다.
실제 적용은 justify-content를 바꾸므로 prop을 명확히 하는 편이 좋습니다. 이와 함께 DOM 속성 포워딩을 방지하기 위해 transient prop 사용을 권장합니다.

제안:

  • styled: alignItems → $justify
  • 사용처: alignItems="end|start" → $justify="end|start"

해당 라인 적용 diff:

-          <DateGroup alignItems="end">
+          <DateGroup $justify="end">-          <DateGroup alignItems="start">
+          <DateGroup $justify="start">

참고: styled 정의 변경은 해당 styled 파일 코멘트에 별도 diff로 제안합니다.

Also applies to: 221-221

src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.styled.ts (2)

17-26: prop 명 확장과 DOM 포워딩 방지를 위해 transient prop($justify) 사용을 권장합니다.
현재 prop 이름 alignItems는 CSS align-items와 혼동되며, 실제로는 justify-content를 제어합니다. 명확한 의도 표현과 안전한 prop 관리를 위해 다음과 같이 변경해 보세요.

적용 diff:

-export const DateGroup = styled.div<{ alignItems?: 'start' | 'end' | 'center' }>`
+export const DateGroup = styled.div<{ $justify?: 'start' | 'end' | 'center' }>`
   display: flex;
   align-items: center;
   gap: 2px;
   flex: 1;
-  justify-content: ${({ alignItems = 'center' }) => {
-    if (alignItems === 'start') return 'flex-start';
-    if (alignItems === 'end') return 'flex-end';
+  justify-content: ${({ $justify = 'center' }) => {
+    if ($justify === 'start') return 'flex-start';
+    if ($justify === 'end') return 'flex-end';
     return 'center';
   }};

이 변경과 함께 TSX 사용처도 $justify로 교체해야 합니다(해당 파일 코멘트에 diff 포함).


96-101: 불필요한 prop 포워딩 최소화를 위해 transient prop와 논리 방향성 대응을 제안합니다.
현재 isLast는 의미가 명확하지만, DOM으로 흘러갈 가능성을 줄이기 위해 $ 접두를 권장합니다. 또한 국제화(RTL) 가능성을 열어두려면 margin-inline-end가 더 탄탄합니다.

적용 diff:

-export const DateUnitText = styled.div<{ isLast?: boolean }>`
+export const DateUnitText = styled.div<{ $last?: boolean }>`
   color: ${semanticColors.text.primary};
   font-size: ${typography.fontSize.xs};
   font-weight: ${typography.fontWeight.regular};
-  margin-right: ${({ isLast }) => (isLast ? '0px' : '6px')};
+  margin-inline-end: ${({ $last }) => ($last ? '0px' : '6px')};

TSX 사용처는 <DateUnitText isLast><DateUnitText $last>로 함께 갱신해야 합니다(해당 TSX 코멘트 참조).

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

10-10: 문구 변경 좋습니다. 다만 ‘블러 처리’는 띄어 쓰는 것이 자연스럽습니다.

가독성 측면에서 아래처럼 띄어 쓰기를 권장합니다.

-      <InfoText>내 진행도에 따라 일부 기록은 블러처리됩니다.</InfoText>
+      <InfoText>내 진행도에 따라 일부 기록은 블러 처리됩니다.</InfoText>

추가 제안: 정보 아이콘이 장식용이라면 스크린리더 중복 낭독을 피하려고 alt를 빈 문자열로 두거나 aria-hidden을 고려해 주세요. (참고: 같은 코드베이스의 Participants 아이콘은 alt="")

src/components/group/CommentSection.styled.ts (1)

7-7: box-sizing: border-box 적용 제안 (수평 오버플로우 방지)
width: calc(100% − 40px)와 padding 조합은 내부 여백이 width 계산에 포함되지 않아 컨테이너가 의도 너비를 초과할 수 있습니다. 다음 컴포넌트들에 box-sizing: border-box;를 추가해 내부 패딩까지 너비에 포함되도록 하는 것을 권장합니다.

– src/components/group/CommentSection.styled.ts
– src/components/group/RecordSection.styled.ts
– src/components/group/HotTopicSection.styled.ts
– src/components/group/GroupBookSection.styled.ts
– src/components/common/Modal/Snackbar.tsx

※ src/components/search/SearchBar.tsx에는 이미 box-sizing: border-box;가 적용되어 있습니다.

// 예시: src/components/group/CommentSection.styled.ts
 export const CommentSection = styled.section`
   display: flex;
   flex-direction: column;
   width: calc(100% - 40px);
+  box-sizing: border-box;
   gap: 12px;
   background: ${colors.darkgrey.dark};
   margin: 20px 20px 0 20px;
   padding: 16px 12px;
   border-radius: 12px;
 `;
src/components/group/MyGroupCard.tsx (1)

28-31: 표시 퍼센트와 진행 바 로직을 일치시키고, 값 범위를 0~100으로 클램핑하는 것을 권장합니다.

현재 텍스트는 Math.floor로 정수화하지만, 막대는 원본 값을 그대로 사용해 미세한 불일치가 생길 수 있고, 0 미만/100 초과 값이 들어오면 UI가 깨질 수 있습니다. 한 번 계산한 안전한 progress 값을 텍스트/막대 모두에서 재사용하면 견고합니다.

-            <Percent>{Math.floor(group.progress || 0)}%</Percent>
+            <Percent>{progress}%</Percent>
...
-            <Fill width={group.progress || 0} />
+            <Fill width={progress} />

컴포넌트 내부에 아래 변수를 추가해 주세요(라인 14 인근):

const progress = Math.max(0, Math.min(100, Math.floor(group.progress ?? 0)));
src/pages/groupDetail/ParticipatedGroupDetail.styled.ts (1)

12-12: 100vh는 모바일(특히 iOS)에서 주소창 영역 포함 문제로 레이아웃 점프가 생길 수 있습니다. 100dvh/100svh 폴백을 제안합니다.

동적 뷰포트 단위를 우선 적용하고, 미지원 환경을 위해 100vh를 폴백으로 두면 안정적입니다.

-  min-height: 100vh;
+  /* fallback */
+  min-height: 100vh;
+  /* modern browsers */
+  min-height: 100svh;

또는 @supports로 감싸는 패턴도 가능합니다:

min-height: 100vh;
@supports (height: 100dvh) {
  min-height: 100dvh;
}
src/components/group/RecordSection.tsx (1)

31-36: 표시값 내림은 OK. 진행도는 0~100 범위로 클램프하여 표시/바 너비를 일치시키는 것을 권장

현재 텍스트는 floor 처리만 하고, 바 너비는 원본 progress를 그대로 사용합니다. 100% 초과/음수 값이 들어오면 UI가 깨질 수 있어 표시값과 바 너비 모두를 0~100으로 클램프하는 편이 안전합니다.

다음과 같이 inline 클램프를 적용해보세요:

-          <ProgressText>{Math.floor(progress)}</ProgressText>
+          <ProgressText>{Math.floor(Math.max(0, Math.min(progress, 100)))}</ProgressText>
...
-          <ProgressBarFill progress={progress} />
+          <ProgressBarFill progress={Math.max(0, Math.min(progress, 100))} />
src/components/memory/PageRangeModal.styled.ts (1)

156-166: PageInput 가변 폭 도입으로 UX 향상. 계산식·타이포그래피 일관성 소폭 제안

inputLength 기반 가변 폭과 width transition은 입력 시 레이아웃 점프를 줄이는 좋은 개선입니다. 다만 동일 패턴을 쓰는 다른 컴포넌트(PageRangeSection.styled.ts)가 최소폭/계수 값이 다릅니다(거기서는 30px, factor 8). 제품 전반의 일관성을 위해 최소폭/계수 통일을 검토해 주세요. 또한 폭 계산의 시각적 일관성을 위해 font-family를 명시하는 것을 권장합니다.

아래처럼 폰트 패밀리를 지정해 폭 계산 오차를 줄일 수 있습니다:

 export const PageInput = styled.input<{ active?: boolean; inputLength?: number }>`
   background: none;
   border: none;
   outline: none;
   color: ${semanticColors.text.primary};
   font-size: ${typography.fontSize.base};
+  font-family: ${typography.fontFamily.primary};
   width: ${props => props.inputLength ? `${Math.max(40, props.inputLength * 12 + 10)}px` : '40px'};
   text-align: center;
   caret-color: ${colors.neongreen};
   transition: width 0.2s ease;
src/pages/groupDetail/GroupDetail.styled.ts (1)

138-153: 중앙화된 장르 매핑 및 정규화 도입 권장
현재 getGenreColor(src/pages/groupDetail/GroupDetail.styled.ts)과 getBackgroundImageByGenre(src/utils/getBackgroundImageByGenre.ts) 등이 각자 switch–case로 장르별 매핑을 처리하고 있습니다. 특히 ‘과학·IT’(U+00B7)와 ‘과학•IT’(U+2022)가 혼용되어 기본값으로 빠질 위험이 있습니다. 운영 중 API나 공용 JSON(public/genres.json)의 값이 미세하게 달라지면 의도한 컬러·이미지가 적용되지 않습니다.

• 매핑 항목

  • src/pages/groupDetail/GroupDetail.styled.ts → getGenreColor
  • src/utils/getBackgroundImageByGenre.ts → getBackgroundImageByGenre
  • 그 외 RecruitingGroupBox.tsx, GenreSelectionSection.tsx 등에서 하드코딩된 장르 배열

• 개선 제안

  1. GENRE_COLOR_MAP, GENRE_BACKGROUND_MAP 같은 객체로 매핑을 분리
  2. API·JSON 기준의 title 값을 불러오거나, 공용 util에서
    • trim()
    • toLowerCase()
    • 특수문자(· vs •) 통일
      같은 정규화를 거쳐 조회
  3. 공용 타입 (public/genres.json → TypeScript 타입)으로 자동 생성/사용

위 리팩터를 통해 중복 제거, 오타·심볼 불일치 위험 완화, 재사용성을 높일 수 있습니다.

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

23-28: 잠금(blur) 상태 투표 차단 흐름 적절 — 접근성 속성 추가 제안

shouldBlur로 API 호출과 클릭을 모두 차단한 점, 커서/포인터 이벤트 처리 모두 적절합니다. 접근성 향상을 위해 aria-disabled를 함께 표기하면 좋습니다.

아래처럼 aria-disabled를 추가하세요.

           <PollOptionStyled 
             key={option.id} 
             isHighest={option.isHighest}
             onClick={shouldBlur ? undefined : () => handleOptionClick(option)}
+            aria-disabled={shouldBlur || isVoting}
             style={{ 
               cursor: shouldBlur ? 'default' : (isVoting ? 'not-allowed' : 'pointer'),
               opacity: shouldBlur ? 1 : (isVoting ? 0.7 : 1),
               pointerEvents: shouldBlur ? 'none' : 'auto'
             }}
           >

Also applies to: 71-72, 152-157


85-96: 미세 최적화: 최댓값 계산을 1회로 줄여 비용 감소

isHighest 판별을 위해 Math.max(...)를 항목마다 재계산합니다. 한 번만 계산해 재사용하세요. 데이터가 커질수록 차이가 납니다.

-        const updatedOptions = currentOptions.map(opt => {
-          const updatedItem = response.data.voteItems.find(
-            item => item.voteItemId === opt.voteItemId
-          );
+        const maxPercentage = Math.max(...response.data.voteItems.map(item => item.percentage));
+        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))
+              isHighest: updatedItem.percentage === maxPercentage
             };
           }
           return opt;
         });
src/components/common/Modal/MoreMenu.tsx (2)

81-93: 핀 관련 잔여 코드 정리로 혼란 방지

UI에서 핀 버튼을 제거했으므로 주석과 Button variant의 'pin' 분기/타입을 함께 정리하세요. 유지보수에 유리합니다.

-// 3개 버튼을 위한 컨테이너 (핀하기 포함)
+// 작성자용 버튼 컨테이너 (수정/삭제)
 const RecordContainer = styled.div`
   position: relative;
   display: flex;
   flex-direction: column;
   min-width: 320px;
   max-width: 767px;
   width: 100%;
   padding: 20px;
   border-radius: 12px 12px 0px 0px;
   background-color: ${colors.darkgrey.main};
 `;
 
-const Button = styled.div<{ variant: 'edit' | 'delete' | 'report' | 'pin' }>`
+const Button = styled.div<{ variant: 'edit' | 'delete' | 'report' }>`
   display: flex;
   height: 50px;
   align-items: center;
   color: ${({ variant }) => {
     if (variant === 'edit') return colors.white;
     if (variant === 'delete') return colors.red;
     if (variant === 'report') return colors.red;
-    if (variant === 'pin') return colors.white;
     return colors.white;
   }};
   font-size: ${typography.fontSize.base};
   font-weight: ${typography.fontWeight.semibold};
   line-height: 24px;
   border-bottom: 1px solid ${colors.grey[400]};
   cursor: pointer;
 
   ${({ variant }) => {
     if (variant === 'report') {
       return `
         padding: 0;
         border-bottom: none;
       `;
     }
-    if (variant === 'pin') {
-      return `
-        &:first-of-type {
-          padding: 13px 12px 21px 12px;
-        }
-        
-        &:nth-of-type(2) {
-          padding: 13px 12px 21px 12px;
-        }
-
-        &:last-of-type {
-          padding: 21px 12px 13px 12px;
-          border-bottom: none;
-        }
-      `;
-    }
     return `
       &:first-of-type {
         padding: 13px 12px 21px 12px;
       }
       
       &:nth-of-type(2) {
         padding: 13px 12px 21px 12px;
       }
 
       &:last-of-type {
         padding: 21px 12px 13px 12px;
         border-bottom: none;
       }
     `;
   }}
 `;

Also applies to: 94-149


52-66: 모바일 100vh 이슈 대비: dvh 적용 권장

모바일 브라우저(특히 iOS Safari)에서 100vh가 화면 하단에 겹치는 문제를 방지하기 위해 100dvh를 우선 적용하고 100vh를 폴백으로 두는 것을 권장합니다.

   display: flex;
   justify-content: center;
   align-items: flex-end;
-  width: 100vw;
-  height: 100vh;
+  width: 100vw;
+  height: 100vh; /* fallback */
+  height: 100dvh;
   background-color: rgba(18, 18, 18, 0.1);
   backdrop-filter: blur(2.5px);
   z-index: 1200;
src/components/recordwrite/PageRangeSection.styled.ts (2)

186-199: 하드코딩된 색상 토큰화 및 중복 조건 단순화 제안

'#3d3d3d'와 '#FF9496'은 글로벌 토큰으로 대체하면 일관성과 유지보수가 좋아집니다. 또한 Tooltip 배경색은 red/green 모두 동일하므로 조건식을 단순화하세요.

 export const Tooltip = styled.div<{ variant: 'red' | 'green' }>`
   position: absolute;
   top: 33px;
   right: 0;
   left: 0;
-  background-color: ${props => (props.variant === 'red' ? '#3d3d3d' : '#3d3d3d')};
+  background-color: ${colors.darkgrey.main};
   border-radius: 12px;
   padding: 21px 12px;
   display: flex;
   align-items: center;
   justify-content: space-between;
   z-index: 10;
   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
 `;
 
 export const TooltipText = styled.span<{ variant: 'red' | 'green' }>`
-  color: ${props => (props.variant === 'red' ? '#FF9496' : semanticColors.text.point.green)};
+  color: ${props => (props.variant === 'red' ? semanticColors.text.warning : semanticColors.text.point.green)};
   font-size: ${typography.fontSize.xs};
   font-weight: ${typography.fontWeight.medium};
   flex: 1;
 `;
 
 export const TooltipArrow = styled.div<{ variant: 'red' | 'green' }>`
   position: absolute;
   bottom: 60px;
   right: 61px;
   width: 12px;
   height: 12px;
-  background-color: #3d3d3d;
+  background-color: ${colors.darkgrey.main};
   transform: rotate(45deg);
   z-index: 9;
 `;

Also applies to: 201-206, 219-228


219-228: 툴팁 위치 하드코딩 개선 제안

bottom/right 픽셀 고정값은 반응형 레이아웃에서 빗나갈 가능성이 큽니다. 트리거 요소(InfoIcon 또는 토글) 기준으로 계산하거나, TooltipContainer 내에서 중앙 정렬/transform으로 배치하는 방식으로 개선을 권장합니다.

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

79-88: 총평 토글 가드(80% 미만 시 툴팁) 흐름 적절 — 닫힘 UX와 접근성 보완 제안

조건 미충족 시 빨간 툴팁 노출하고 토글을 막는 흐름이 명확합니다. 다음 개선을 제안합니다.

  • 외부 클릭/ESC로 툴팁 닫기 지원
  • 툴팁에 role="tooltip" aria-live="polite" 추가
  • 토글에 aria-disabled 추가

적용 예시(발췌):

-        {showRedTooltip && (
-          <Tooltip variant="red">
+        {showRedTooltip && (
+          <Tooltip variant="red" role="tooltip" aria-live="polite">
             ...
           </Tooltip>
         )}
-        {showGreenTooltip && (
-          <Tooltip variant="green">
+        {showGreenTooltip && (
+          <Tooltip variant="green" role="tooltip" aria-live="polite">
             ...
           </Tooltip>
         )}
         <ToggleContainer>
           <LeftSection>
             <InfoIcon onClick={handleInfoClick}>
               <img src={infoIcon} alt="정보" />
             </InfoIcon>
             <ToggleLabel disabled={!canUseOverall}>총평</ToggleLabel>
           </LeftSection>
           <ToggleSwitch
             active={isOverallEnabled}
             onClick={handleToggleClick}
             disabled={!canUseOverall}
+            aria-disabled={!canUseOverall}
           >
             <ToggleSlider active={isOverallEnabled} disabled={!canUseOverall} />
           </ToggleSwitch>
         </ToggleContainer>

외부 클릭/ESC 닫기는 다음과 같이 추가할 수 있습니다(참고용):

// 컴포넌트 내
useEffect(() => {
  const onKey = (e: KeyboardEvent) => {
    if (e.key === 'Escape') {
      setShowRedTooltip(false);
      setShowGreenTooltip(false);
    }
  };
  const onDocClick = (e: MouseEvent) => {
    // TooltipContainer 영역 외부 클릭 감지 로직 필요 시 ref로 보완
    setShowRedTooltip(false);
    setShowGreenTooltip(false);
  };
  document.addEventListener('keydown', onKey);
  document.addEventListener('click', onDocClick);
  return () => {
    document.removeEventListener('keydown', onKey);
    document.removeEventListener('click', onDocClick);
  };
}, []);

Also applies to: 128-166


29-38: 사용되지 않는 prop 정리 제안: isOverviewPossible

isOverviewPossible가 선언만 되고 사용되지 않습니다. 혼선을 줄이기 위해 제거하거나 실제 조건로직에 반영하세요.

src/components/memory/RecordFilters/PageInputMode.tsx (1)

39-39: inputMode 전환은 적절합니다. 모바일 키패드 힌트를 더 확실히 하려면 pattern/aria를 추가하세요.

inputMode="numeric"만으로는 브라우저가 비숫자 입력을 막지는 않습니다. iOS/안드로이드에서 숫자 키패드 일관성을 높이고 접근성(a11y)을 개선하려면 pattern과 명확한 aria-label을 함께 제공하는 것을 권장합니다. enterKeyHint도 UX를 개선합니다.

아래처럼 속성을 추가할 수 있습니다.

         <PageInput
           value={startPage}
           placeholder=""
           onChange={e => onInputChange('start', e.target.value)}
           inputMode="numeric"
+          pattern="[0-9]*"
+          aria-label="시작 페이지"
+          enterKeyHint="done"
           autoFocus
           style={{
             width: startPage.length > 0 ? `${Math.max(36, startPage.length * 12)}px` : '36px',
           }}
         />
...
         <PageInput
           value={endPage}
           placeholder=""
           onChange={e => onInputChange('end', e.target.value)}
           inputMode="numeric"
+          pattern="[0-9]*"
+          aria-label="끝 페이지"
+          enterKeyHint="done"
           style={{
             width: endPage.length > 0 ? `${Math.max(36, endPage.length * 12)}px` : '36px',
           }}
         />

Also applies to: 52-52

src/components/memory/RecordFilters/FilterButtons.tsx (1)

60-63: onClick 제거만으로는 접근성 보장이 부족합니다. disabled 속성도 함께 지정하세요.

현재 시각적/포인터 인터랙션은 막히지만, 키보드 포커스/스크린리더 관점에서는 여전히 활성 버튼처럼 보일 수 있습니다. 네이티브 disabled 속성을 추가해 의미론과 a11y를 확보하세요.

         <FilterButton
           active={activeFilter === 'overall'}
           $disabled={!isOverallEnabled}
-          onClick={isOverallEnabled ? onOverallFilterClick : undefined}
+          onClick={isOverallEnabled ? onOverallFilterClick : undefined}
+          disabled={!isOverallEnabled}
         >
src/components/memory/RecordFilters/PageInputMode.styled.ts (2)

41-46: WebKit 스피너 제거 규칙은 현재 타입(text)에서는 무의미합니다. 유지/제거 중 선택하세요.

inputMode를 사용하고 실제 input 타입이 number가 아니라면 해당 규칙은 매치되지 않아 no-op입니다. 코드를 간결하게 하려면 제거, 혹시 타입을 되돌릴 가능성을 고려하면 유지가 합리적입니다.


48-50: 폭 애니메이션을 의도하셨다면 PageInput 자체에 transition을 두는 편이 맞습니다.

현재 width 변경은 TSX에서 PageInput의 inline style로 이뤄지고, transition은 InputWrapper에 선언되어 있어 애니메이션이 적용되지 않습니다. PageInput에 transition을 추가하거나, width 제어를 wrapper로 옮기는 것을 권장합니다.

 export const PageInput = styled.input`
   background: none;
   border: none;
   outline: none;
   color: ${semanticColors.text.primary};
   font-size: ${typography.fontSize.sm};
   font-weight: ${typography.fontWeight.regular};
   width: 100%;
   min-width: 36px;
+  transition: width 0.2s ease;
   text-align: center;
   caret-color: ${colors.neongreen};
src/components/common/Modal/Snackbar.tsx (3)

9-19: 중첩 setTimeout 정리 누락 — 언마운트 시 onClose 타이머가 남습니다

외부 타이머만 clear하고, 내부 onClose 지연 타이머는 정리되지 않습니다. 컴포넌트 언마운트/재마운트 시 예기치 않은 onClose 호출이 발생할 수 있습니다.

아래처럼 두 타이머 모두를 관리/정리해 주세요.

   useEffect(() => {
     setVisible(true);

     const showDuration = variant === 'bottom' ? 5000 : 2000;
-    const timer = setTimeout(() => {
-      setVisible(false);
-      setTimeout(() => onClose(), 2000);
-    }, showDuration);
+    let closeTimer: number | undefined;
+    const hideTimer = window.setTimeout(() => {
+      setVisible(false);
+      closeTimer = window.setTimeout(() => onClose(), 2000);
+    }, showDuration);

-    return () => clearTimeout(timer);
+    return () => {
+      window.clearTimeout(hideTimer);
+      if (closeTimer) window.clearTimeout(closeTimer);
+    };
   }, [variant, onClose]);

21-26: Snackbar 접근성(ARIA) 개선 제안

스크린 리더에 알림임을 명시하면 UX가 좋아집니다. 오류 시 assertive, 일반 메시지는 polite가 적합합니다.

-    <Container variant={variant} visible={visible}>
+    <Container
+      variant={variant}
+      visible={visible}
+      role="status"
+      aria-live={isError ? 'assertive' : 'polite'}
+    >

55-59: 스타일드 컴포넌트 커스텀 prop DOM 전달 차단

Emotion에서 커스텀 prop(isError)이 DOM에 전달될 수 있습니다. shouldForwardProp으로 차단하세요.

-const Discription = styled.div<{ isError?: boolean }>`
+const Discription = styled('div', {
+  shouldForwardProp: (prop) => prop !== 'isError',
+})<{ isError?: boolean }>`
   font-size: ${typography.fontSize.xs};
   font-weight: ${typography.fontWeight.medium};
-  color: ${({ isError }) => isError ? colors.red : colors.white};
+  color: ${({ isError }) => (isError ? colors.red : colors.white)};
 `;

또한 컴포넌트 이름 오탈자(Discription → Description) 정리하면 가독성이 좋아집니다.

가능하면 아래처럼 리네이밍하세요.

-const Discription = styled('div', { ... })<{ isError?: boolean }>`
+const Description = styled('div', { ... })<{ isError?: boolean }>`

그리고 사용처도 함께 수정:

-  <Discription isError={isError}>{message}</Discription>
+  <Description isError={isError}>{message}</Description>
src/components/memory/RecordFilters/RecordFilters.tsx (3)

64-79: parseInt 사용 시 기수 명시 및 중복 파싱 제거

여러 번 파싱 대신 한 번만 파싱하고, 기수(10)를 명시해 명확성을 높이세요.

-  const handleConfirm = () => {
-    const start = parseInt(startPage);
-    const end = parseInt(endPage);
+  const handleConfirm = () => {
+    const start = parseInt(startPage, 10);
+    const end = parseInt(endPage, 10);
@@
-    if (!isNaN(start) && !isNaN(end) && start > 0 && end > 0 && start <= end) {
+    if (!isNaN(start) && !isNaN(end) && start > 0 && end > 0 && start <= end) {
       if (onPageRangeSet) {
         onPageRangeSet({ start, end });
       }
       setShowInputMode(false);
       setStartPage('');
       setEndPage('');
     }
   };

103-111: isValid 평가 최적화(가독성/중복 파싱 제거)

동일 값을 여러 번 parseInt 하는 대신, 미리 숫자로 변환해 검증하면 가독성과 미세 성능 향상됩니다.

-  const isValid = Boolean(
-    startPage && 
-    endPage && 
-    !isNaN(parseInt(startPage)) && 
-    !isNaN(parseInt(endPage)) && 
-    parseInt(startPage) > 0 && 
-    parseInt(endPage) > 0 && 
-    parseInt(startPage) <= parseInt(endPage)
-  );
+  const isValid = (() => {
+    const start = parseInt(startPage, 10);
+    const end = parseInt(endPage, 10);
+    return Boolean(
+      startPage &&
+      endPage &&
+      !isNaN(start) &&
+      !isNaN(end) &&
+      start > 0 &&
+      end > 0 &&
+      start <= end
+    );
+  })();

116-129: 오버레이 인라인 스타일 → 스타일드 컴포넌트 전환 제안

인라인 스타일은 유지보수에 불리합니다. 오버레이/컨테이너 z-index 규칙을 스타일드로 고정하면 일관성이 좋아집니다.

예시:

// RecordFilters.styled.ts
export const FullscreenOverlay = styled.div`
  position: fixed;
  inset: 0;
  background-color: transparent;
  z-index: 999;
`;

export const ContainerWrapper = styled.div`
  position: relative;
  z-index: 1000;
`;

적용:

- {isPageInputMode && (<div style={{ ... }} onClick={handleCancel} />)}
+ {isPageInputMode && <FullscreenOverlay onClick={handleCancel} />}

- <Container style={{ position: 'relative', zIndex: 1000 }}>
+ <ContainerWrapper>
+   <Container>
      ...
-   </Container>
+   </Container>
+ </ContainerWrapper>

Also applies to: 131-156

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

120-126: 페이지 메타데이터 부재 시 이전 값 잔존 가능성 — 기본값으로 리셋 권장

API 응답에 totalPages/currentUserPage가 없으면 이전 상태가 남아 잘못된 진행률을 표시할 수 있습니다. 응답에 없을 때 0으로 리셋하세요.

-        if (response.data.totalPages !== undefined) {
-          setTotalPages(response.data.totalPages);
-        }
-        if (response.data.currentUserPage !== undefined) {
-          setCurrentUserPage(response.data.currentUserPage);
-        }
+        if (response.data.totalPages !== undefined) {
+          setTotalPages(response.data.totalPages);
+        } else {
+          setTotalPages(0);
+        }
+        if (response.data.currentUserPage !== undefined) {
+          setCurrentUserPage(response.data.currentUserPage);
+        } else {
+          setCurrentUserPage(0);
+        }

242-247: 진행률 계산 가드(클램핑) 및 로그 정리 제안

예외적으로 currentUserPage > totalPages가 될 수 있으므로 0~100으로 클램핑하세요. 콘솔 로그는 개발 환경에서만 출력하도록 조정하면 좋습니다.

-  const readingProgress = totalPages > 0 ? Math.round((currentUserPage / totalPages) * 100) : 0;
-  
-  const overviewStatus = isOverviewEnabled ? '총평 활성화' : '총평 비활성화';
-  console.log('📊 현재 상태:', overviewStatus, `진행률: ${readingProgress}%`);
+  const rawProgress = totalPages > 0 ? (currentUserPage / totalPages) * 100 : 0;
+  const readingProgress = Math.max(0, Math.min(100, Math.round(rawProgress)));
+
+  if (process.env.NODE_ENV !== 'production') {
+    const overviewStatus = isOverviewEnabled ? '총평 활성화' : '총평 비활성화';
+    // eslint-disable-next-line no-console
+    console.log('📊 현재 상태:', overviewStatus, `진행률: ${readingProgress}%`);
+  }
src/pages/today-words/TodayWords.tsx (7)

50-58: getTodayMyMessageCount는 useMemo로 값 자체를 메모이즈하면 간결하고 안전합니다.

현재는 함수를 useCallback으로 감싼 뒤 즉시 호출하고 있어 불필요한 간접 호출이 있습니다. useMemo로 숫자 값을 직접 계산하도록 정리하면 의도가 더 명확해집니다.

제안 diff:

-  const getTodayMyMessageCount = useCallback(() => {
-    const todayFormats = getTodayDateStrings();
-    
-    return messages.filter(message => {
-      if (!message.isWriter) return false;
-      
-      // 여러 날짜 포맷과 비교
-      return todayFormats.includes(message.timestamp);
-    }).length;
-  }, [messages, getTodayDateStrings]);
-
-  const todayMyMessageCount = getTodayMyMessageCount();
+  const todayMyMessageCount = useMemo(() => {
+    const todayFormats = getTodayDateStrings();
+    return messages.filter((message) => {
+      if (!message.isWriter) return false;
+      return todayFormats.includes(message.timestamp);
+    }).length;
+  }, [messages, getTodayDateStrings]);

추가: 파일 상단 import에 useMemo 추가가 필요합니다.

import { useState, useRef, useCallback, useEffect, useMemo } from 'react';

Also applies to: 60-60


69-69: 주석과 실제 사용 필드가 어긋납니다.

timeAgo에는 postDate를, timestamp에는 item.date를 사용하고 있으므로 주석을 아래처럼 명확히 해 주세요.

-    // 서버에서 받아오는 postDate를 그대로 사용 (이미 날짜 기반으로 계산된 값)
+    // 서버에서 내려오는 postDate는 상대 시간 문자열(예: '1시간 전')이므로 timeAgo로 사용

116-121: 초기 로딩 스크롤: body 대신 documentElement 사용 권장

일부 브라우저에서 scrollHeight 참조 대상이 달라 예외가 있을 수 있습니다. document.documentElement로 통일을 권장합니다.

-          setTimeout(() => {
-            window.scrollTo({ top: document.body.scrollHeight, behavior: 'auto' });
-          }, 100);
+          setTimeout(() => {
+            window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'auto' });
+          }, 100);

233-243: 5번째 작성 성공 시 ‘제한 알림’만 뜨면 혼동 소지가 있습니다. 성공 알림/제한 도달 안내를 분리해 주세요.

현재 로직은 5번째 작성 성공 후 “제한” 토스트만 isError로 노출됩니다. 사용자는 실패로 오해할 수 있습니다. “작성 완료”와 “오늘 한도 도달”을 함께/순차로 알리거나, 카피를 성공/안내 톤으로 조정하는 것을 권장합니다. 예: “작성되었습니다. 오늘은 여기까지!” 혹은 “오늘의 한마디 5개를 모두 작성했어요.”

필요하면 UX 카피/로직 수정안(토스트 1회 노출로 성공+한도 도달을 자연스럽게 전달) 제안 드리겠습니다.


250-254: 작성 후 스크롤도 documentElement 사용으로 통일

초기 로딩과 동일 사유로 documentElement 사용을 권장합니다. 그리고 콘텐츠 렌더가 안정된 뒤 스크롤하려면 requestAnimationFrame을 한 번 더 감싸는 것도 유효합니다.

-        setTimeout(() => {
-          window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
-        }, 100);
+        setTimeout(() => {
+          window.requestAnimationFrame(() => {
+            window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' });
+          });
+        }, 100);

221-221: parseInt 대신 Number 사용(또는 radix 명시)으로 가독성과 안전성 향상

문자 숫자 변환은 Number(...)가 의도를 더 잘 드러냅니다. parseInt를 유지한다면 10진수 radix를 명시해 주세요.

-      const response = await createDailyGreeting(parseInt(roomId), inputValue.trim());
+      const response = await createDailyGreeting(Number(roomId), inputValue.trim());

33-46: 날짜 비교를 문자열 포맷 대신 Date 객체로 전환 권장

메시지의 timestamp(string)와 getTodayDateStrings()로 생성한 포맷을 나열 비교하면
– 0패딩, 로케일, 자정 전후 타임존 차이 등으로 오늘 카운트가 빗나갈 수 있습니다.
대신 메시지의 createdAt: Date를 직접 비교하도록 리팩터링할 것을 추천드립니다.

예시 제안:

// 오늘 기준 Date 객체
const today = new Date();
today.setHours(0, 0, 0, 0);

const getTodayMyMessageCount = useCallback(() => {
  return messages.filter(m => {
    if (!m.isWriter) return false;
    const msgDate = new Date(m.createdAt);
    msgDate.setHours(0, 0, 0, 0);
    return msgDate.getTime() === today.getTime();
  }).length;
}, [messages]);

– 이 방식으로 getTodayDateStrings와 문자열 비교 로직은 제거하거나(불필요 시)
포맷 출력 용도로만 분리하세요.

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

63-63: 브라우저 환경에서 NodeJS.Timeout 타입은 부정확할 수 있습니다. ReturnType으로 교체를 권장합니다.

CRA/Vite DOM 타깃에서는 setTimeout의 반환이 number인 경우가 많습니다.

-  const longPressTimer = useRef<NodeJS.Timeout | null>(null);
+  const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

131-134: ‘수정’ 기능은 현재 비활성인데, 메뉴에서는 노출됩니다. UX 혼선을 줄이려면 숨기거나 비활성 표기로 전환을 권장합니다.

onEdit을 더보기 메뉴에 넘기지 않거나, 메뉴에서 disabled 상태를 지원해 사용자 기대를 관리하세요.

원하시면 메뉴 아이템 비활성/숨김 처리에 맞는 MoreMenu props 설계를 같이 정리하겠습니다.


177-185: 삭제 Confirm 모달에도 onClose 전달을 통일하면 좋습니다.

핀 Confirm에서는 onClose를 넘겼지만 삭제 Confirm에는 없습니다. 취소/외부 클릭 시 동작을 통일하려면 onClose: closePopup을 함께 넘겨주세요.

   openConfirm({
     title: `${recordTypeName}을 삭제하시겠어요?`,
     disc: `삭제된 ${recordTypeName}은 복구할 수 없습니다.`,
     onConfirm: handleDelete,
+    onClose: closePopup,
   });

286-301: MoreMenu 호출에서 onPin 파라미터 제거 권장

MoreMenu 컴포넌트가 onPin을 실제로 사용하지 않으므로, RecordItem 에서 불필요한 prop 전달을 제거하세요.

• 위치: src/components/memory/RecordItem/RecordItem.tsx, 286–301행

-          openMoreMenu({
-            onEdit: handleEdit,
-            onDelete: handleDeleteConfirm,
-            onPin: handlePinConfirm,
-            onClose: closePopup,
-            type: 'post',
-            isWriter: true,
-          });
+          openMoreMenu({
+            onEdit: handleEdit,
+            onDelete: handleDeleteConfirm,
+            onClose: closePopup,
+            type: 'post',
+            isWriter: true,
+          });

참고: 추후 pin 기능이 필요하다면 src/components/common/Modal/MoreMenu.tsx 에 onPin 렌더링 로직을 추가하세요.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 427a707 and 5077cea.

⛔ Files ignored due to path filters (1)
  • src/assets/memory/record-x.svg is excluded by !**/*.svg
📒 Files selected for processing (42)
  • src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.styled.ts (1 hunks)
  • src/components/common/Modal/MoreMenu.tsx (4 hunks)
  • src/components/common/Modal/Snackbar.tsx (3 hunks)
  • src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.styled.ts (2 hunks)
  • src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx (3 hunks)
  • src/components/creategroup/RoomInfoSection.styled.ts (3 hunks)
  • src/components/creategroup/RoomInfoSection.tsx (2 hunks)
  • src/components/createpost/PostContentSection.styled.ts (1 hunks)
  • src/components/createpost/PostContentSection.tsx (0 hunks)
  • src/components/group/CommentSection.styled.ts (1 hunks)
  • src/components/group/GroupBookSection.styled.ts (1 hunks)
  • src/components/group/GroupBookSection.tsx (1 hunks)
  • src/components/group/HotTopicSection.styled.ts (1 hunks)
  • src/components/group/MyGroupCard.tsx (1 hunks)
  • src/components/group/RecordSection.styled.ts (1 hunks)
  • src/components/group/RecordSection.tsx (1 hunks)
  • src/components/memory/MemoryContent/MemoryContent.styled.ts (1 hunks)
  • src/components/memory/MemoryContent/MemoryContent.tsx (2 hunks)
  • src/components/memory/MemoryContent/RecordList.styled.ts (1 hunks)
  • src/components/memory/MemoryContent/RecordList.tsx (1 hunks)
  • src/components/memory/PageRangeModal.styled.ts (1 hunks)
  • src/components/memory/PageRangeModal.tsx (2 hunks)
  • src/components/memory/RecordFilters/FilterButtons.styled.ts (1 hunks)
  • src/components/memory/RecordFilters/FilterButtons.tsx (1 hunks)
  • src/components/memory/RecordFilters/PageInputMode.styled.ts (1 hunks)
  • src/components/memory/RecordFilters/PageInputMode.tsx (2 hunks)
  • src/components/memory/RecordFilters/RecordFilters.tsx (4 hunks)
  • src/components/memory/RecordInfoMessage.tsx (1 hunks)
  • src/components/memory/RecordItem/PollRecord.tsx (3 hunks)
  • src/components/memory/RecordItem/RecordItem.tsx (11 hunks)
  • src/components/recordwrite/PageRangeSection.styled.ts (4 hunks)
  • src/components/recordwrite/PageRangeSection.tsx (5 hunks)
  • src/pages/group/CreateGroup.tsx (1 hunks)
  • src/pages/groupDetail/GroupDetail.styled.ts (1 hunks)
  • src/pages/groupDetail/ParticipatedGroupDetail.styled.ts (1 hunks)
  • src/pages/groupDetail/ParticipatedGroupDetail.tsx (2 hunks)
  • src/pages/memory/Memory.tsx (6 hunks)
  • src/pages/post/CreatePost.tsx (2 hunks)
  • src/pages/post/UpdatePost.tsx (2 hunks)
  • src/pages/today-words/TodayWords.tsx (6 hunks)
  • src/stores/usePopupStore.ts (1 hunks)
  • src/types/memory.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • src/components/createpost/PostContentSection.tsx
🧰 Additional context used
🧬 Code Graph Analysis (19)
src/components/createpost/PostContentSection.styled.ts (1)
src/styles/global/global.ts (2)
  • semanticColors (80-153)
  • typography (56-77)
src/components/memory/RecordInfoMessage.tsx (1)
src/components/memory/RecordInfoMessage.styled.ts (1)
  • InfoText (22-26)
src/components/group/RecordSection.tsx (1)
src/components/group/RecordSection.styled.ts (1)
  • ProgressText (71-75)
src/components/creategroup/RoomInfoSection.styled.ts (1)
src/styles/global/global.ts (2)
  • typography (56-77)
  • colors (4-53)
src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.styled.ts (2)
src/components/today-words/MessageList/MessageList.styled.ts (1)
  • DateGroup (9-14)
src/styles/global/global.ts (2)
  • semanticColors (80-153)
  • typography (56-77)
src/components/group/GroupBookSection.tsx (1)
src/components/group/GroupBookSection.styled.ts (1)
  • BookAuthor (24-28)
src/pages/groupDetail/GroupDetail.styled.ts (1)
src/styles/global/global.ts (1)
  • colors (4-53)
src/pages/groupDetail/ParticipatedGroupDetail.tsx (1)
src/pages/groupDetail/GroupDetail.styled.ts (1)
  • TagGenre (155-157)
src/components/memory/PageRangeModal.styled.ts (2)
src/components/recordwrite/PageRangeSection.styled.ts (1)
  • PageInput (44-71)
src/styles/global/global.ts (3)
  • semanticColors (80-153)
  • typography (56-77)
  • colors (4-53)
src/components/group/GroupBookSection.styled.ts (1)
src/styles/global/global.ts (1)
  • colors (4-53)
src/components/recordwrite/PageRangeSection.styled.ts (3)
src/components/memory/RecordFilters/PageInputMode.styled.ts (1)
  • PageInput (25-50)
src/components/memory/PageRangeModal.styled.ts (1)
  • PageInput (156-173)
src/styles/global/global.ts (3)
  • semanticColors (80-153)
  • typography (56-77)
  • colors (4-53)
src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx (1)
src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.styled.ts (3)
  • DateGroup (17-27)
  • DateUnitText (96-101)
  • SeparatorText (103-110)
src/components/common/Modal/Snackbar.tsx (2)
src/stores/usePopupStore.ts (1)
  • SnackbarProps (29-36)
src/styles/global/global.ts (2)
  • typography (56-77)
  • colors (4-53)
src/components/common/Modal/MoreMenu.tsx (1)
src/stores/usePopupStore.ts (1)
  • MoreMenuProps (19-27)
src/components/memory/RecordItem/PollRecord.tsx (2)
src/types/memory.ts (1)
  • PollOption (80-87)
src/components/memory/RecordItem/PollRecord.styled.ts (1)
  • PollOption (23-32)
src/components/memory/RecordFilters/RecordFilters.tsx (1)
src/components/memory/RecordFilters/RecordFilters.styled.ts (1)
  • Container (3-8)
src/components/memory/MemoryContent/RecordList.tsx (1)
src/components/memory/MemoryContent/RecordList.styled.ts (1)
  • RecordListContainer (3-7)
src/components/recordwrite/PageRangeSection.tsx (1)
src/components/recordwrite/PageRangeSection.styled.ts (11)
  • TooltipContainer (181-184)
  • Tooltip (186-199)
  • TooltipText (201-206)
  • TooltipCloseButton (208-217)
  • TooltipArrow (219-228)
  • ToggleContainer (119-125)
  • LeftSection (127-131)
  • InfoIcon (133-145)
  • ToggleLabel (147-152)
  • ToggleSwitch (154-166)
  • ToggleSlider (168-179)
src/components/memory/RecordItem/RecordItem.tsx (2)
src/hooks/usePopupActions.ts (1)
  • usePopupActions (9-35)
src/components/memory/RecordItem/RecordItem.styled.ts (1)
  • ActionButton (60-79)
🔇 Additional comments (34)
src/components/createpost/PostContentSection.styled.ts (1)

10-10: LGTM: readOnly prop 추가 및 타입 명시 적절

DOM의 textarea가 readOnly를 지원하므로 스타일과 동작을 동시에 제어할 수 있어 일관성이 좋아졌습니다.

src/components/creategroup/RoomInfoSection.tsx (2)

23-28: 플레이스홀더 문구 개선 LGTM

사용자 톤 앤 매너에 맞게 자연스럽고 명확해졌습니다.


39-45: 소개 플레이스홀더 문구 개선 LGTM

짧고 구체적인 안내로 입력 의도를 잘 전달합니다.

src/components/creategroup/RoomInfoSection.styled.ts (1)

16-16: 본문 폰트 사이즈를 sm(14px)로 낮춘 변경, 가독성과 토큰 일관성 측면에서 적절합니다.
타이포그래피 스케일(14/16/18)에 맞춰 입력 본문을 통일하는 방향과도 일치합니다.

src/components/creategroup/ActivityPeriodSection/ActivityPeriodSection.tsx (1)

215-215: 마지막 단위 간격 제어를 prop로 넘기는 방식은 OK이나, transient prop로 바꾸면 DOM 반출 위험이 줄어듭니다.
[ suggest_optional_refactor ]

DateUnitText의 isLast → $last로 변경 권장. 적용 diff:

-            <DateUnitText isLast>일</DateUnitText>
+            <DateUnitText $last>일</DateUnitText>-            <DateUnitText isLast>일</DateUnitText>
+            <DateUnitText $last>일</DateUnitText>

추가로 국제화(잠재적 RTL)까지 고려한다면 margin-right 대신 margin-inline-end 사용을 검토해도 좋습니다. 해당 styled 정의 변경은 아래 파일 코멘트를 참고하세요.

Also applies to: 244-244

src/components/memory/MemoryContent/RecordList.styled.ts (1)

6-6: 간격 40px 조정 LGTM.

리스트 아이템 간 시각적 여백 확대가 의도(가독성 향상)와 맞아 보입니다.

src/components/memory/RecordFilters/FilterButtons.styled.ts (1)

21-22: 비활성화 상태에서의 사용자 경험이 개선되었습니다.

비활성화된 버튼에 대해 cursor: not-allowedpointer-events: none을 적용하여 사용자가 클릭할 수 없는 상태임을 명확히 표시했습니다. 이는 UX 관점에서 좋은 개선사항입니다.

src/components/group/HotTopicSection.styled.ts (1)

7-7: 일관된 레이아웃 정렬을 위한 width 조정이 적절합니다.

width: calc(100% - 40px)로 변경하여 좌우 20px 마진을 고려한 정확한 너비 계산이 이루어졌습니다. 다른 그룹 컴포넌트들과 동일한 패턴으로 적용되어 레이아웃 일관성이 유지됩니다.

src/components/group/GroupBookSection.styled.ts (2)

8-8: 다른 그룹 컴포넌트와 일관된 width 계산입니다.

calc(100% - 40px)로 변경하여 좌우 마진(20px씩)을 정확히 반영한 너비 계산이 적용되었습니다.


10-10: margin 단축 속성 사용이 적절합니다.

margin: 0 20px 0 20px에서 margin: 0 20px로 간소화되어 코드가 더 간결해졌습니다. 동일한 결과를 더 효율적으로 표현합니다.

src/components/group/GroupBookSection.tsx (1)

21-21: 한국어 표기법에 맞는 저자명 표시입니다.

저자명 뒤에 " 저"를 추가하여 한국어 표기법에 맞게 개선되었습니다. 사용자 경험 측면에서 더 자연스러운 표현입니다.

src/types/memory.ts (2)

48-49: 페이지네이션 메타데이터 추가가 적절합니다.

totalPagescurrentUserPage 필드 추가로 읽기 진도율 계산을 위한 API 기반 데이터를 제공할 수 있게 되었습니다. 선택적 필드로 설정되어 기존 코드와의 호환성도 유지됩니다.


75-75: 기록 잠금 상태 관리를 위한 필수 필드 추가입니다.

isLocked: boolean 필드 추가로 개별 기록의 블러 처리 여부를 관리할 수 있게 되었습니다. 기존의 readingProgress 기반 방식에서 더 정확한 잠금 상태 관리로 개선된 것으로 보입니다.

src/components/group/RecordSection.styled.ts (1)

7-7: calc(100% - 40px)로 마진 합산폭 보정 — 수평 오버플로 방지, 좋습니다

좌우 마진(20px + 20px)을 고려해 콘텐츠 영역 폭을 정확히 맞췄습니다. 컨테이너가 padding 없이 100% 폭일 때 가로 스크롤이 생기지 않는 안전한 선택입니다.

src/pages/groupDetail/GroupDetail.styled.ts (1)

155-156: TagGenre에 genre prop 추가 — 스타일 분기 깔끔, 문제 없습니다

styled에서 함수형 color 계산으로 장르 컬러 반영이 명확합니다. 기본값도 존재해 회피 경로가 확보돼 있습니다.

src/pages/groupDetail/ParticipatedGroupDetail.tsx (2)

236-238: 장르 태그 prop 추가 적용 좋습니다

TagGenre에 genre={data.category} 전달로 컬러 매핑이 적용됩니다. GroupDetail.styled.ts의 getGenreColor가 미정의/미매칭 장르에 대한 안전한 기본값을 제공한다면 런타임 에러 우려는 낮습니다. 현 변경은 의도와 부합해 보입니다.


142-146: /search/book/:isbn 라우트 정의 및 호출부 확인 완료

  • src/pages/index.tsx에 <Route path="search/book/:isbn" element={<SearchBook />} /> 경로 정의 확인
  • /book/:isbn 구 경로 호출부는 더 이상 존재하지 않음

문제 없습니다.

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

44-57: 입력 필드 PageInput 정의 비교 및 디자인 의도 확인 요청

PageRangeSection.styled.ts vs PageRangeModal.styled.ts의 PageInput를 비교해 보면 다음과 같은 차이가 있습니다:

  • PageRangeSection.styled.ts (lines 44–57)
    • font-size: typography.fontSize.sm
    • font-weight: typography.fontWeight.regular
    • font-family: typography.fontFamily.primary
    • width: props.inputLength ? Math.max(30, inputLength × 8 + 10)px : '30px'
    • caret-color: colors.white
    • padding & margin: 0

  • PageRangeModal.styled.ts (lines 153–160)
    • font-size: typography.fontSize.base
    • (font-weight·font-family 미명시)
    • width: props.inputLength ? Math.max(40, inputLength × 12 + 10)px : '40px'
    • text-align: center
    • caret-color: colors.neongreen
    • padding·margin 없음

계수(8 vs 12), 최소 너비(30px vs 40px), 글꼴 크기·색상·정렬 등 주요 스펙이 상이한데, 모두 의도된 디자인 차이인지 확인 부탁드립니다.

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

56-71: 숫자 입력 검증 로직 안전하게 잘 처리했습니다

빈 문자열 허용, 숫자만 허용, NaN 및 범위(>0, ≤ totalPages) 검증이 명확합니다. UX 측면에서 충분합니다.


110-116: 확인 완료: placeholder와 lastRecordedPage 동기화 보장

API 응답의 response.data.recentBookPagesetLastRecordedPage가 호출되므로, 백엔드에서 반환된 값이 변경될 때마다 lastRecordedPage와 그로부터 계산된 placeholder·inputLength가 항상 최신 상태로 유지됩니다.

src/stores/usePopupStore.ts (1)

33-33: SnackbarProps에 isError 추가는 합리적입니다.

스타일링/표현 분리에 유용한 확장입니다. isError가 전달되지 않는 기존 호출부에서도 안전하게 동작하도록 Snackbar 쪽에서 기본값(false) 처리만 확인해 주세요.

기존 호출부가 isError를 생략해도 문제가 없도록, Snackbar 컴포넌트에서 다음과 같이 기본값을 보장하는지 확인해 주세요:

  • props.isError ?? false로 처리
  • 또는 defaultProps/파라미터 기본값 사용
src/components/memory/MemoryContent/MemoryContent.styled.ts (1)

2-2: 불필요한 typography import 제거는 깔끔합니다.

DevButton 제거와 일관되며 잔여 의존성도 없습니다. LGTM.

src/components/common/Modal/Snackbar.tsx (1)

6-6: isError 플래그 도입 적절 — API/스토어 타입과 일치합니다

SnackbarProps의 isError를 전달해 색상 제어하는 방향 문제 없습니다.

src/components/memory/RecordFilters/RecordFilters.tsx (2)

49-56: 숫자 입력 검증 강화 적절

빈 문자열 또는 순수 숫자만 허용하는 변경으로 UX와 데이터 일관성이 좋아졌습니다.


81-88: 취소 동작 명확 — 입력모드 종료/초기화 일관성 확보

오버레이 클릭 시 입력모드 종료 및 범위 초기화(onPageRangeClear 호출) 흐름이 명확합니다.

src/components/memory/MemoryContent/RecordList.tsx (1)

9-14: 레코드 isLocked 필드 정의·매핑 확인 및 신규 생성 경로 검토 요청

src/types/memory.tsRecord 인터페이스에 isLocked: boolean 정의 확인
src/pages/memory/Memory.tsxconvertPostToRecord에서 isLocked: post.isLocked 매핑 확인
– 단, 작성 완료 후 location.state.newRecord 등 신규 레코드 생성 경로에서도 isLocked 프로퍼티가 빠지지 않았는지 수동으로 검토해주세요

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

76-76: RecordList prop 정리 반영 OK

readingProgress 제거에 맞춰 로 수정된 부분 문제 없습니다.

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

32-33: API isLocked 매핑 추가 적절

UI 블러 제어에 필요한 필드 매핑이 명확합니다.


198-200: 라우터 경로 수동 확인 필요

스크립트 실행 중 인용부호 오류로 자동 확인이 실패했습니다.
라우터 설정(예: src/App.tsx, src/routes.tsx 등)에서 /group/detail/joined/:roomId 경로가 실제로 정의되어 있는지 수동으로 점검해 주세요.
정의가 없다면 해당 경로를 추가하거나, 변경된 의도를 공유해 주시기 바랍니다.

src/pages/today-words/TodayWords.tsx (2)

162-163: 훅 의존성 정비, LGTM

의존성 정비로 stale 클로저 리스크를 줄였습니다.

Also applies to: 169-170, 176-176


211-213: 하루 제한 사전 차단 토스트에 isError: true 적용, LGTM

오류 스타일 일관성에 도움이 됩니다.

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

37-37: closePopup 도입 및 사용, LGTM

상태 일관성(네비게이션/에러 시 팝업 정리)에 도움이 됩니다.


200-221: 핀하기 플로우에서 ‘먼저 닫고 후처리’ 패턴, LGTM

  • 성공/실패/네트워크 오류 경로 모두에서 closePopup 호출이 일관적입니다.
  • 네비게이션에 앞서 팝업을 닫는 순서도 적절합니다.

Also applies to: 234-241, 246-254


385-398: shouldBlur에 따른 액션 가드(이벤트 차단/커서 변경), LGTM

모든 버튼에 동일한 패턴으로 적용되어 일관적입니다. PollRecord에도 shouldBlur 전달이 되어 컨텍스트가 맞습니다.

Also applies to: 405-418, 423-435, 371-380

Comment on lines +5 to 8
const MoreMenu = ({ onEdit, onDelete, onClose, onReport, isWriter, type }: MoreMenuProps) => {
return (
<Overlay onClick={onClose}>
<Overlay onClick={() => onClose?.()}>
{type === 'post' ? (
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

onPin 제거에 따른 타입/사용처 정합성 점검 필요

컴포넌트 시그니처에서 onPin을 제거했습니다. 그러나 usePopupStore의 MoreMenuProps에는 여전히 onPin이 정의되어 있습니다. 또한 기존 호출부가 onPin을 기대하고 있을 수 있으니 검색/정리 필요합니다.

다음 스크립트로 onPin 사용처와 타입 정의를 확인하세요.

MoreMenuProps는 아래처럼 정리하는 것을 권장합니다(별도 파일 변경):

// src/stores/usePopupStore.ts
export interface MoreMenuProps {
  onEdit?: () => void;
  onDelete?: () => void;
  onClose?: () => void;
  onReport?: () => void;
  // onPin?: () => void; // 제거
  isWriter?: boolean;
  type?: 'post' | 'reply';
}

🏁 Script executed:

#!/bin/bash
# onPin 타입/사용처 탐색
rg -n --type=ts --type=tsx -C2 "\bonPin\b"

# MoreMenu 호출부에서 onPin을 넘기는 곳이 있는지 확인
rg -nP --type=ts --type=tsx -C2 "MoreMenu\s*\(\{[^\}]*onPin"

Length of output: 163


아래 스크립트로 onPin 사용처와 타입 정의, MoreMenu 호출부를 재검증해 주세요. (--type 대신 -g 옵션으로 파일 확장자 필터링)


🏁 Script executed:

#!/bin/bash
# 1) onPin 키워드 사용처 검색 (TS/TSX)
rg -n -g "*.{ts,tsx}" -C2 "\bonPin\b"

# 2) MoreMenuProps 정의 위치 확인
rg -n -C5 "interface MoreMenuProps" -g "src/stores/usePopupStore.ts"

# 3) MoreMenu 컴포넌트 시그니처 확인
rg -n -C5 "const MoreMenu" -g "src/components/common/Modal/MoreMenu.tsx"

# 4) MoreMenu 호출부(사용처) 검색
rg -n -C3 -g "*.{ts,tsx}" "MoreMenu"

Length of output: 13205


MoreMenuProps 및 사용처에서 onPin 제거 필요

MoreMenu 컴포넌트에서 onPin을 제거하셨으므로, 타입 정의와 호출부에서도 완전히 정리해야 합니다:

  • src/stores/usePopupStore.ts
    • export interface MoreMenuProps 에서 onPin?: () => void; 삭제
  • src/components/memory/RecordItem/RecordItem.tsx
    • openMoreMenu({ …, onPin: handlePinConfirm, … }) 호출부에서 onPin: handlePinConfirm 제거

현재 이 두 곳 외에는 onPin 사용처가 없습니다. 위 수정 후 컴파일 및 동작을 한번 더 확인해 주세요.

🤖 Prompt for AI Agents
In src/stores/usePopupStore.ts (where MoreMenuProps is declared) and
src/components/memory/RecordItem/RecordItem.tsx (call site opening the more
menu), remove the now-unused onPin property: delete the onPin?: () => void; from
the MoreMenuProps interface and remove the onPin: handlePinConfirm (or
equivalent) entry from the openMoreMenu(...) call; then rebuild to ensure no
other references remain and verify runtime behavior.

Comment on lines +101 to 102
inputLength={startPage.length || 1}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

입력 길이 기반 가변 폭 전달은 LGTM. 다만 확인 조건 불일치로 onSelect가 호출되지 않는 경우가 있음

현재 isValid는 start/end가 '0'일 때도 true가 될 수 있지만(handleConfirm에서는 0이 falsy라 onSelect가 호출되지 않음), 이로 인해 버튼이 활성화되는데 동작하지 않는 상태가 됩니다. 유효성 판단과 confirm 조건을 일치시키고, 1페이지 이상만 허용한다면 해당 조건을 명시하세요. parseInt에는 radix(10)도 지정하는 것을 권장합니다.

다음처럼 정규화하면 일관됩니다(참고용 코드 스니펫 — 변경 위치는 isValid, handleConfirm):

const toInt = (s: string) => Number.parseInt(s, 10);
const startNum = toInt(startPage);
const endNum = toInt(endPage);

const isValid =
  startPage !== '' &&
  endPage !== '' &&
  Number.isFinite(startNum) &&
  Number.isFinite(endNum) &&
  startNum >= 1 &&
  endNum >= 1 &&
  startNum <= endNum;

const handleConfirm = () => {
  if (isValid) {
    onSelect(startNum, endNum);
  }
};

Also applies to: 110-111

🤖 Prompt for AI Agents
In src/components/memory/PageRangeModal.tsx around lines 101-102 (also apply
same fix at 110-111), the current validity check allows '0' to be considered
valid while handleConfirm treats 0 as falsy and won't call onSelect; normalize
parsing and validation: parse startPage/endPage with Number.parseInt(..., 10)
(or Number) into startNum/endNum, make isValid require non-empty inputs, numeric
finite values, startNum >= 1, endNum >= 1 and startNum <= endNum, and change
handleConfirm to call onSelect(startNum, endNum) only when isValid is true so
the button state and action are consistent.

⚠️ Potential issue

사용자 노출 문구 오탈자

안내 문구(라인 90–91): “내 진행도에 따라 읽을 곳을 불러져립니다.” → “불러와 드립니다.” 등으로 수정 필요합니다. 사용자에게 직접 노출되는 문구라 우선 수정 권장합니다.

참고 수정안:

<InfoText>내 진행도에 따라 읽을 곳을 불러와 드립니다.</InfoText>

Also applies to: 110-111

🤖 Prompt for AI Agents
In src/components/memory/PageRangeModal.tsx around lines 90–91 and 110–111,
there is a typo in the user-facing informational text ("내 진행도에 따라 읽을 곳을
불러져립니다."); replace those strings with the corrected phrase ("내 진행도에 따라 읽을 곳을 불러와
드립니다.") in both locations so the displayed InfoText shows the proper wording to
users.

⚠️ Potential issue

'이동' 버튼이 handleReset을 호출함 — 레이블과 기능이 상충

'이동'은 확인/적용에 가까운 레이블인데 현재는 handleReset을 호출합니다(입력값을 지우는 동작). 오동작으로 오해할 소지가 큽니다. 또한 '-' 키는 클릭 핸들러가 없어 무의미합니다.

다음처럼 매핑을 정리해 주세요(참고용 코드 스니펫):

// '이동' → 확인
<KeyButton onClick={handleConfirm}>이동</KeyButton>

// 백스페이스 기능 추가 예시
const handleBackspace = () => {
  if (activeInput === 'start') setStartPage(prev => prev.slice(0, -1));
  else if (activeInput === 'end') setEndPage(prev => prev.slice(0, -1));
};

// '-' 키를 백스페이스로 활용
<KeyButton onClick={handleBackspace}></KeyButton>

// 모달 닫기 '✕'는 유지
<KeyButton onClick={onClose}></KeyButton>

Also applies to: 110-111

🤖 Prompt for AI Agents
In src/components/memory/PageRangeModal.tsx around lines 101-102 and 110-111,
the "이동" button currently calls handleReset (clearing input) and the '-' button
has no handler; change mappings so "이동" calls handleConfirm instead of
handleReset, implement a handleBackspace function that removes the last char
from the active input (if activeInput === 'start' slice startPage, else if 'end'
slice endPage) and wire the '-' KeyButton to call handleBackspace (use a
backspace label like '⌫'), and ensure the modal close button still calls
onClose; update any imports/props accordingly and keep handlers consistent with
existing state setters.

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.

2 participants