feat: 기록 삭제 API 연동 및 더보기 메뉴 기능 추가#120
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough기록/투표 삭제 API 클라이언트를 신규 추가하고, Memory 페이지의 Record 타입에 isWriter 필드를 도입했습니다. RecordItem 컴포넌트에 롱프레스 제스처를 추가하여 소유자에겐 편집/삭제 메뉴를, 타인에겐 신고 동작을 트리거합니다. 삭제 시 확인 다이얼로그 후 API 호출 및 스낵바 처리와 페이지 갱신을 수행합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as User
participant RI as RecordItem
participant PA as PopupActions
participant API as apiClient
participant N as Router
U->>RI: Long press on record
RI->>RI: isWriter? 판단
alt isWriter = true
RI->>PA: 더보기 메뉴 오픈 (편집/삭제)
U->>PA: 삭제 선택
PA->>RI: 삭제 확인 요청
else isWriter = false
RI->>PA: 신고 트리거
end
RI->>PA: Confirm(삭제)
U-->>PA: 확인
alt Poll Record
RI->>API: DELETE /rooms/{roomId}/vote/{voteId}
else Text Record
RI->>API: DELETE /rooms/{roomId}/record/{recordId}
end
API-->>RI: response.data
RI->>PA: 스낵바(성공)
RI->>N: 페이지 리로드
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested labels
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. ✨ 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: 1
🔭 Outside diff range comments (2)
src/components/memory/RecordItem/RecordItem.tsx (2)
247-253: 상호작용 영역에서 롱프레스 오탐 방지 + ARIA 보강좋아요 버튼을 길게 누를 때도 상위 Container의 롱프레스가 발화할 수 있습니다. 자식 버튼에서 이벤트 전파를 막아 의도치 않은 메뉴 오픈을 방지하세요. 또한 ARIA 상태를 추가하면 접근성이 향상됩니다.
- <ActionButton onClick={handleLikeClick}> + <ActionButton + onMouseDown={(e) => e.stopPropagation()} + onTouchStart={(e) => e.stopPropagation()} + onClick={handleLikeClick} + aria-pressed={isLiked} + aria-label={isLiked ? '좋아요 취소' : '좋아요 추가'} + > <img src={isLiked ? heartFilledIcon : heartIcon} alt={isLiked ? '좋아요 취소' : '좋아요'} /> <span>{currentLikeCount}</span> </ActionButton>
254-257: 댓글 버튼도 롱프레스 오탐 방지 적용동일한 이유로 전파 차단 및 접근성 라벨을 부여해 주세요.
- <ActionButton> + <ActionButton + onMouseDown={(e) => e.stopPropagation()} + onTouchStart={(e) => e.stopPropagation()} + aria-label="댓글 보기" + > <img src={commentIcon} alt="댓글" /> <span>{commentCount}</span> </ActionButton>
🧹 Nitpick comments (12)
src/pages/memory/Memory.tsx (1)
52-52: API 불일치 대비: isWriter 기본값 보강 권장백엔드가
post.isWriter를 누락하거나 null을 반환할 경우를 대비해 boolean으로 강제 변환해두면 방어적입니다.아래처럼 기본값을 강제하면 런타임 가드가 됩니다.
- isWriter: post.isWriter, + isWriter: Boolean(post.isWriter),추가로, 실제 API 스키마에서
isWriter가 항상 내려오는지 확인 부탁드립니다. 누락 가능성이 있다면 위 보강을 권장합니다.src/api/record/deleteVote.ts (2)
13-21: DELETE 204 No Content 대응 필요 가능성일부 서버는 DELETE 성공 시 204(본문 없음)를 반환합니다. 이 경우
response.data가 빈 문자열/undefined가 되어, 호출부(response.isSuccess접근)에서 런타임 오류가 날 수 있습니다. 상태코드 기반 성공 판단과 폴백을 두는 편이 안전합니다.아래처럼 204/빈 바디를 허용하는 폴백을 두는 방식을 제안드립니다:
-export const deleteVote = async (roomId: number, voteId: number): Promise<DeleteVoteResponse> => { - try { - const response = await apiClient.delete<DeleteVoteResponse>(`/rooms/${roomId}/vote/${voteId}`); - return response.data; - } catch (error) { - console.error('투표 삭제 API 오류:', error); - throw error; - } -}; +export const deleteVote = async (roomId: number, voteId: number): Promise<DeleteVoteResponse> => { + try { + const response = await apiClient.delete(`/rooms/${roomId}/vote/${voteId}`); + // 일부 백엔드는 204 No Content를 반환하므로 data가 비어 있을 수 있음 + if (response.status === 204 || response.data == null || response.data === '') { + return { isSuccess: true, message: '', data: { roomId } }; + } + return response.data as DeleteVoteResponse; + } catch (error) { + console.error('투표 삭제 API 오류:', error); + throw error; + } +};확인 요청:
- 백엔드가 항상
ApiResponse<T>바디를 반환하는지, 아니면 204를 반환하는지 확인 부탁드립니다.
5-11: 응답 데이터 스키마 재검토 제안현재
DeleteVoteData는roomId만 포함합니다. 호출부에서 추가 정보(예: 삭제된 리소스 ID 또는 성공 메시지)가 필요할 가능성이 있다면 스키마를 확장하는 것도 고려할 수 있습니다. 다만 현 요구사항상 필수는 아닙니다.백엔드 응답에 삭제된
voteId가 포함되는지 확인해, 포함된다면 타입 반영을 권장드립니다.src/api/record/deleteRecord.ts (2)
13-26: DELETE 204 No Content 대응 필요 가능성
deleteVote와 동일하게 204(본문 없음)일 때의 런타임 안전성이 필요합니다. 아래와 같이 상태코드 기반 폴백을 두면 안전합니다.-export const deleteRecord = async ( - roomId: number, - recordId: number, -): Promise<DeleteRecordResponse> => { - try { - const response = await apiClient.delete<DeleteRecordResponse>( - `/rooms/${roomId}/record/${recordId}`, - ); - return response.data; - } catch (error) { - console.error('기록 삭제 API 오류:', error); - throw error; - } -}; +export const deleteRecord = async ( + roomId: number, + recordId: number, +): Promise<DeleteRecordResponse> => { + try { + const response = await apiClient.delete(`/rooms/${roomId}/record/${recordId}`); + if (response.status === 204 || response.data == null || response.data === '') { + return { isSuccess: true, message: '', data: { roomId } }; + } + return response.data as DeleteRecordResponse; + } catch (error) { + console.error('기록 삭제 API 오류:', error); + throw error; + } +};백엔드의 실제 응답 형태(항상 바디 반환 vs. 204)를 확인해 주시면, 위 처리를 적용할지 결정할 수 있습니다.
12-21: 중복 로직 공통화 제안
deleteRecord와deleteVote가 거의 동일한 패턴입니다. 리소스 타입만 다른 공통 함수를 만들어 중복을 제거하면 유지보수성이 좋아집니다.예: 별도 유틸
export async function deleteRoomResource<T extends 'record' | 'vote'>( roomId: number, resource: T, id: number, ): Promise<ApiResponse<{ roomId: number }>> { const res = await apiClient.delete(`/rooms/${roomId}/${resource}/${id}`); if (res.status === 204 || res.data == null || res.data === '') { return { isSuccess: true, message: '', data: { roomId } }; } return res.data; }그리고 각 모듈에서 thin wrapper로 노출.
src/components/memory/RecordItem/RecordItem.tsx (7)
54-55: 브라우저 환경에서 setTimeout 타입 정정
NodeJS.Timeout은 브라우저 빌드에서 타입 불일치가 날 수 있습니다. DOM 환경에 맞춰ReturnType<typeof setTimeout>로 교체하세요.- const longPressTimer = useRef<NodeJS.Timeout | null>(null); + const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
168-171: 모바일 스크롤 차단 우려: touchstart의 preventDefault 제거 권장
touchstart에서e.preventDefault()를 호출하면 리스트 스크롤이 차단될 수 있습니다. 현재touchAction: 'manipulation'을 적용하고 있고, 별도touchmove취소 로직도 갖고 있으므로 기본 동작을 막지 않는 편이 더 안전합니다.- if ('touches' in e) { - e.preventDefault(); - } + // 주의: touchstart에서 기본 동작을 막으면 스크롤이 차단될 수 있음
114-117: window.location.reload() 대신 목록 상태 갱신으로 대체현재는 임시 처리라고 주석이 있지만, 전체 리로드는 UX/성능 모두 불리합니다. 상위에서 내려주는 콜백(예: onDeleted(id))으로 리스트에서 제거하거나, 전역 스토어/캐시 무효화로 갱신하는 구조로 전환을 권장합니다.
원하시면 상위(예: MemoryContent/Memory 페이지)로 콜백을 끌어올리는 형태의 수정안까지 포함해 패치 제안 드릴 수 있습니다.
83-91: roomId 기본값 '1' 사용 여부 확인URL 파라미터에
roomId가 없을 때 '1'로 대체하고 있습니다. 잘못된 방에서 편집 화면으로 진입할 수 있으니, 이 기본값 전략이 의도된 것인지 확인이 필요합니다. 미의도라면 가드(에러/리디렉션)를 두는 편이 안전합니다.
189-196: 타이머 정리: 언마운트 시 클린업 추가 권장컴포넌트 언마운트 순간 타이머가 살아있으면 의도치 않게 핸들러가 뒤늦게 실행될 수 있습니다.
useEffect클린업으로 타이머를 정리해 주세요.예시(컴포넌트 내 추가):
useEffect(() => { return () => { if (longPressTimer.current) { clearTimeout(longPressTimer.current); longPressTimer.current = null; } }; }, []);
197-212: 마우스 드래그 시 롱프레스 취소 로직도 필요터치에만 이동 임계값(10px) 취소가 있고, 마우스에는 없습니다. 데스크톱에서 드래그 스크롤 중 롱프레스 오작동이 발생할 수 있습니다. 마우스 이동도 동일한 임계값 적용을 권장합니다.
참고:
onMouseMove에서 delta를 계산해handleLongPressEnd()를 호출하는 방식으로 정렬할 수 있습니다.
152-165: 접근성 UX 제안: “더보기” 명시적 트리거 제공롱프레스만으로 메뉴를 여는 패턴은 발견 가능성이 낮습니다. 우측 상단 등에 “⋯” 아이콘 버튼(aria-label: "더보기")을 추가해 동일한
openMoreMenu/handleDeleteConfirm을 트리거하게 하면 키보드/스크린리더 사용자 경험이 크게 개선됩니다.원하시면 아이콘 버튼 추가와 핸들러 연결 패치도 제안드릴게요.
📜 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 selected for processing (4)
src/api/record/deleteRecord.ts(1 hunks)src/api/record/deleteVote.ts(1 hunks)src/components/memory/RecordItem/RecordItem.tsx(4 hunks)src/pages/memory/Memory.tsx(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/api/record/deleteVote.ts (1)
src/api/index.ts (1)
apiClient(7-14)
src/api/record/deleteRecord.ts (1)
src/api/index.ts (1)
apiClient(7-14)
src/components/memory/RecordItem/RecordItem.tsx (5)
src/pages/memory/Memory.tsx (1)
Record(15-29)src/hooks/usePopupActions.ts (1)
usePopupActions(9-35)src/api/record/deleteVote.ts (1)
deleteVote(13-21)src/api/record/deleteRecord.ts (1)
deleteRecord(13-26)src/components/memory/RecordItem/RecordItem.styled.ts (1)
Container(4-9)
🔇 Additional comments (2)
src/pages/memory/Memory.tsx (1)
28-28: isWriter 필드 추가 방향성 적합합니다소유권 분기를 위한 옵셔널 필드 도입 적절합니다. RecordItem 쪽에서
isWriter ?? false로 안전하게 처리하는 것도 일관적입니다.src/components/memory/RecordItem/RecordItem.tsx (1)
100-104: poll 삭제 시 ID 매핑 확인 필요 (record.id ↔ voteId)현재
type === 'poll'인 경우에도record.id를 정수 변환하여deleteVote에 넘기고 있습니다. 백엔드가 투표 리소스의 식별자로postId를 그대로 사용한다는 보장이 없다면, 삭제가 실패하거나 다른 리소스를 지목할 수 있습니다.확인 요청:
- 응답의
post모델에voteId가 별도로 존재하는지?- 만약 분리되어 있다면,
Record에voteId?: string등의 필드를 추가하고,convertPostToRecord에서 매핑한 뒤 여기서 사용하도록 수정하는 것을 권장드립니다.
| return ( | ||
| <Container shouldBlur={shouldBlur}> | ||
| <Container | ||
| shouldBlur={shouldBlur} | ||
| onMouseDown={handleLongPressStart} | ||
| onMouseUp={handleLongPressEnd} | ||
| onMouseLeave={handleLongPressEnd} | ||
| onTouchStart={handleLongPressStart} | ||
| onTouchEnd={handleLongPressEnd} | ||
| onTouchMove={handleTouchMove} | ||
| style={{ | ||
| transform: isPressed ? 'scale(0.98)' : 'scale(1)', | ||
| transition: 'transform 0.1s ease', | ||
| touchAction: 'manipulation', | ||
| }} | ||
| > |
There was a problem hiding this comment.
🛠️ Refactor suggestion
접근성: 키보드/마우스 대체 입력(Enter/Space/오른쪽 클릭) 지원 추가
롱프레스는 키보드 사용자에게 닿기 어렵습니다. Container에 포커스/키보드/컨텍스트 메뉴 진입점을 추가해 주세요.
<Container
shouldBlur={shouldBlur}
onMouseDown={handleLongPressStart}
onMouseUp={handleLongPressEnd}
onMouseLeave={handleLongPressEnd}
onTouchStart={handleLongPressStart}
onTouchEnd={handleLongPressEnd}
onTouchMove={handleTouchMove}
+ role="button"
+ tabIndex={0}
+ aria-label={isMyRecord ? '더보기 메뉴 열기' : '신고 열기'}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handleLongPress();
+ }
+ }}
+ onContextMenu={(e) => {
+ e.preventDefault();
+ handleLongPress();
+ }}
style={{
transform: isPressed ? 'scale(0.98)' : 'scale(1)',
transition: 'transform 0.1s ease',
touchAction: 'manipulation',
}}
>가능하면 별도의 “더보기” 아이콘 버튼을 제공하면 스크린리더 사용자에게 더 명확한 affordance를 줄 수 있습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return ( | |
| <Container shouldBlur={shouldBlur}> | |
| <Container | |
| shouldBlur={shouldBlur} | |
| onMouseDown={handleLongPressStart} | |
| onMouseUp={handleLongPressEnd} | |
| onMouseLeave={handleLongPressEnd} | |
| onTouchStart={handleLongPressStart} | |
| onTouchEnd={handleLongPressEnd} | |
| onTouchMove={handleTouchMove} | |
| style={{ | |
| transform: isPressed ? 'scale(0.98)' : 'scale(1)', | |
| transition: 'transform 0.1s ease', | |
| touchAction: 'manipulation', | |
| }} | |
| > | |
| return ( | |
| <Container | |
| shouldBlur={shouldBlur} | |
| onMouseDown={handleLongPressStart} | |
| onMouseUp={handleLongPressEnd} | |
| onMouseLeave={handleLongPressEnd} | |
| onTouchStart={handleLongPressStart} | |
| onTouchEnd={handleLongPressEnd} | |
| onTouchMove={handleTouchMove} | |
| role="button" | |
| tabIndex={0} | |
| aria-label={isMyRecord ? '더보기 메뉴 열기' : '신고 열기'} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| handleLongPress(); | |
| } | |
| }} | |
| onContextMenu={(e) => { | |
| e.preventDefault(); | |
| handleLongPress(); | |
| }} | |
| style={{ | |
| transform: isPressed ? 'scale(0.98)' : 'scale(1)', | |
| transition: 'transform 0.1s ease', | |
| touchAction: 'manipulation', | |
| }} | |
| > |
🤖 Prompt for AI Agents
In src/components/memory/RecordItem/RecordItem.tsx around lines 214 to 228, the
Container currently only supports pointer/touch long-press which is inaccessible
to keyboard and context-menu users; make the Container keyboard- and
context-menu-accessible by adding tabIndex={0} (or role="button" if semantic),
an onKeyDown handler that triggers the same long-press/activate logic for Enter
and Space, and an onContextMenu handler to open the same menu on right-click
(calling preventDefault if needed). Also provide visible focus styles (outline
or box-shadow) and, preferably, add a separate focusable "More" icon button with
an aria-label (e.g., "Open more actions") to expose the menu affordance to
screen reader users.
#️⃣ 연관된 이슈
#106
📝 작업 내용
기록장(Memory) 화면의 개별 기록 아이템에 길게 누르기를 통한 더보기 메뉴 기능과 기록/투표 삭제 API 연동을 구현했습니다.
🕸️ 주요 구현 사항
1. 기록 및 투표 삭제 API 함수 구현
deleteRecord.ts: 기록 삭제 API (DELETE /rooms/{roomId}/record/{recordId})deleteVote.ts: 투표 삭제 API (DELETE /rooms/{roomId}/vote/{voteId})2. RecordItem 컴포넌트에 길게 누르기 기능 추가
3. 권한 기반 더보기 메뉴 분기 처리
4. 안전한 삭제 프로세스 구현
5. UX 최적화
useCallback을 활용한 불필요한 리렌더링 방지💬 리뷰 요구사항
Summary by CodeRabbit