Conversation
Merge pull request #176 from THIP-TextHip/develop
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. Walkthrough여러 컴포넌트의 스타일·레이아웃 조정과 메모리(기록) 도메인 API 연동 및 블러(잠금) 기반 상호작용 도입, 모달/스낵바·팝업 관련 API/props 변화, 페이지범위 입력·툴팁 및 일부 라우팅·텍스트 수정을 포함합니다. Changes
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
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
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: 툴팁 숨김
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (12)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 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 등에서 하드코딩된 장르 배열
• 개선 제안
GENRE_COLOR_MAP,GENRE_BACKGROUND_MAP같은 객체로 매핑을 분리- API·JSON 기준의 title 값을 불러오거나, 공용 util에서
- trim()
- toLowerCase()
- 특수문자(· vs •) 통일
같은 정규화를 거쳐 조회- 공용 타입 (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 정리 제안: isOverviewPossibleisOverviewPossible가 선언만 되고 사용되지 않습니다. 혼선을 줄이기 위해 제거하거나 실제 조건로직에 반영하세요.
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.
⛔ Files ignored due to path filters (1)
src/assets/memory/record-x.svgis 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-allowed와pointer-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: 페이지네이션 메타데이터 추가가 적절합니다.
totalPages와currentUserPage필드 추가로 읽기 진도율 계산을 위한 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:0PageRangeModal.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.recentBookPage로setLastRecordedPage가 호출되므로, 백엔드에서 반환된 값이 변경될 때마다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.ts의Record인터페이스에isLocked: boolean정의 확인
–src/pages/memory/Memory.tsx내convertPostToRecord에서isLocked: post.isLocked매핑 확인
– 단, 작성 완료 후location.state.newRecord등 신규 레코드 생성 경로에서도isLocked프로퍼티가 빠지지 않았는지 수동으로 검토해주세요src/components/memory/MemoryContent/MemoryContent.tsx (1)
76-76: RecordList prop 정리 반영 OKreadingProgress 제거에 맞춰 로 수정된 부분 문제 없습니다.
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
| const MoreMenu = ({ onEdit, onDelete, onClose, onReport, isWriter, type }: MoreMenuProps) => { | ||
| return ( | ||
| <Overlay onClick={onClose}> | ||
| <Overlay onClick={() => onClose?.()}> | ||
| {type === 'post' ? ( |
There was a problem hiding this comment.
💡 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.tsexport interface MoreMenuProps에서onPin?: () => void;삭제
src/components/memory/RecordItem/RecordItem.tsxopenMoreMenu({ …, 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.
| inputLength={startPage.length || 1} | ||
| /> |
There was a problem hiding this comment.
입력 길이 기반 가변 폭 전달은 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.
사용자 노출 문구 오탈자
안내 문구(라인 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.
'이동' 버튼이 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.
이하동일
Summary by CodeRabbit
New Features
Bug Fixes
Style
Refactor