-
Notifications
You must be signed in to change notification settings - Fork 2
✨ Feat: 대시보드 상세 페이지 - 드래그 앤 드롭 #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
""" Walkthrough이 변경사항은 대시보드 카드와 컬럼 컴포넌트에 드래그 앤 드롭 기능을 추가합니다. 카드 컴포넌트가 드래그 시작 이벤트를 처리하고, 컬럼 컴포넌트는 드래그 오버 및 드롭 이벤트를 처리하며, 전역 드래그 상태 스토어와 카드 컬럼 업데이트 API 및 뮤테이션 훅이 도입되었습니다. 일부 스타일도 업데이트되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Card
participant useDragStore
participant Column
participant useCardMutation
participant updateCardColumn(API)
User->>Card: 카드 드래그 시작
Card->>useDragStore: setDraggingCard({cardId, columnId})
User->>Column: 카드 드롭
Column->>useDragStore: draggingCard 조회
Column->>useCardMutation: mutate({cardId, targetColumnId})
useCardMutation->>updateCardColumn: PUT /cards/{cardId} (columnId)
updateCardColumn-->>useCardMutation: {success: boolean}
useCardMutation->>Column: 쿼리 무효화 및 상태 업데이트
Column->>useDragStore: clearDraggingCard()
Possibly related PRs
Poem
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
npm error Exit handler never called! 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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
🧹 Nitpick comments (4)
src/app/dashboard/[id]/api/updateCardColumn.ts (1)
4-12: API 호출 예외 처리를 명시적으로 해주세요현재 오류 발생 시 Axios 예외가 상위로 전파되어 호출부에서만 처리해야 합니다.
try / catch로 감싸서 로그를 남기거나Error객체를 래핑해주면 문제 디버깅이 수월합니다.export async function updateCardColumn( cardId: number, columnId: number, ): Promise<{ success: boolean }> { - const res = await axiosClient.put<{ success: boolean }>(`/cards/${cardId}`, { - columnId: columnId, - }) - return res.data + try { + const res = await axiosClient.put<{ success: boolean }>( + `/cards/${cardId}`, + { columnId }, + ) + return res.data + } catch (error) { + // TODO: sentry 등 중앙화 로깅 처리 + console.error('updateCardColumn 실패', error) + throw error + } }src/app/dashboard/[id]/Card/Card.tsx (1)
18-22: 드래그 종료 시 상태 초기화 누락
onDragEnd또는onDragLeave이벤트에서clearDraggingCard()를 호출하지 않으면
드래그가 취소됐을 때 전역 상태가 남아 의도치 않은 드롭 동작을 유발할 수 있습니다.<div draggable="true" onDragStart={() => setDraggingCard({ cardId: id, columnId })} + onDragEnd={() => clearDraggingCard()} className="BG-white Border-section relative w-314 rounded-6 border-solid px-20 py-16" >src/app/dashboard/[id]/Column/Column.tsx (2)
23-30: 드래그 오버 상태 변경 빈도 최적화 제안
onDragOver이벤트는 매우 자주 발생합니다.!isDraggingover체크로setState호출을 줄이고 있지만,isDraggingover가true인 동안 계속onDragOver가 불필요하게 실행됩니다.throttle혹은pointerenter/pointerleave로 대체하면 리렌더 횟수를 크게 줄일 수 있습니다.
40-44: 접근성: 드래그 오버 시 시각적 피드백 외 ARIA 속성 추가 고려파란 테두리만으로는 스크린리더 사용자가 상태 변화를 인지하기 어렵습니다.
aria-dropeffect="move"또는aria-describedby등을 활용해 접근성을 개선해 주세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/app/dashboard/[id]/Card/Card.tsx(2 hunks)src/app/dashboard/[id]/Column/Column.tsx(2 hunks)src/app/dashboard/[id]/api/updateCardColumn.ts(1 hunks)src/app/dashboard/[id]/api/useCardMutation.ts(1 hunks)src/app/dashboard/[id]/page.tsx(1 hunks)src/app/dashboard/[id]/store/useDragStore.ts(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/app/dashboard/[id]/api/useCardMutation.ts (2)
src/app/dashboard/[id]/api/updateCardColumn.ts (1)
updateCardColumn(4-12)src/app/api/useCards.ts (1)
CardResponse(25-29)
src/app/dashboard/[id]/Card/Card.tsx (2)
src/app/api/useCards.ts (1)
Card(11-24)src/app/dashboard/[id]/store/useDragStore.ts (1)
useDragStore(8-12)
🪛 Biome (1.9.4)
src/app/dashboard/[id]/Column/Column.tsx
[error] 11-11: Shouldn't redeclare 'Column'. Consider to delete it or rename it.
'Column' is defined here:
(lint/suspicious/noRedeclare)
src/app/dashboard/[id]/Card/Card.tsx
[error] 8-8: Shouldn't redeclare 'Card'. Consider to delete it or rename it.
'Card' is defined here:
(lint/suspicious/noRedeclare)
🔇 Additional comments (2)
src/app/dashboard/[id]/store/useDragStore.ts (1)
8-12: Zustand 스토어 설계 적절간단·명확하게 drag 상태를 관리하고 있으며 불필요한 복잡도가 없습니다. 현 단계에서 추가 코멘트 없습니다.
src/app/dashboard/[id]/page.tsx (1)
17-17:select-none추가 👍드래그 중 텍스트 선택 방지 목적이 분명하며 사이드-이펙트도 없으므로 그대로 유지해도 좋습니다.
| onMutate: async ({ cardId, columnId }) => { | ||
| await queryClient.cancelQueries({ queryKey: ['columnId'] }) | ||
|
|
||
| // 업데이트 이전 데이터 챙겨뒀다가 롤백할때 사용 | ||
| const previousData = queryClient.getQueryData<CardResponse>([ | ||
| 'columnId', | ||
| columnId, | ||
| ]) | ||
|
|
||
| // 낙관적 업데이트(캐시 업데이트) | ||
| queryClient.setQueryData<CardResponse>( | ||
| ['columnId', columnId], | ||
| (oldCards) => { | ||
| if (!oldCards) return undefined | ||
| return { | ||
| ...oldCards, | ||
| cards: oldCards.cards.map((card) => | ||
| card.id === cardId ? { ...card, columnId: columnId } : card, | ||
| ), | ||
| } | ||
| }, | ||
| ) | ||
|
|
||
| return { previousData } | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
낙관적 UI 로직이 실질적으로 카드 이동을 반영하지 못합니다
- 대상 컬럼 캐시에 카드가 없을 경우
map연산으로는 아무 변화가 없어 UI가 그대로입니다. - 원본 컬럼에서 카드를 제거하지 않아 드래그 이후 두 컬럼에 동시에 표시될 가능성이 있습니다.
cancelQueries/invalidateQueries키가['columnId']로 뭉뚱그려져 있어 모든 컬럼을 불필요하게 무효화합니다.
최소한 다음과 같이 수정해야 합니다.
- await queryClient.cancelQueries({ queryKey: ['columnId'] })
+ await queryClient.cancelQueries({ queryKey: ['columnId', columnId] })
...
- const previousData = queryClient.getQueryData<CardResponse>([
- 'columnId',
- columnId,
- ])
+ const previousTarget = queryClient.getQueryData<CardResponse>([
+ 'columnId',
+ columnId,
+ ])
+ // 드래그 전 컬럼도 기억해야 롤백 가능
+ const previousSource = queryClient
+ .getQueryData<CardResponse>(['columnId', sourceColumnId])
...
- queryClient.setQueryData<CardResponse>(
- ['columnId', columnId],
- (oldCards) => {
- if (!oldCards) return undefined
- return {
- ...oldCards,
- cards: oldCards.cards.map((card) =>
- card.id === cardId ? { ...card, columnId: columnId } : card,
- ),
- }
- },
- )
+ // 1) 대상 컬럼에 카드 추가
+ queryClient.setQueryData<CardResponse>(['columnId', columnId], (old) => {
+ if (!old) return old
+ const movedCard =
+ old.cards.find((c) => c.id === cardId) ??
+ ({ ...variables.card, columnId } as Card) // fallback
+ return { ...old, cards: [...old.cards, movedCard] }
+ })
+
+ // 2) 원본 컬럼에서 카드 제거
+ queryClient.setQueryData<CardResponse>(['columnId', sourceColumnId], (old) => {
+ if (!old) return old
+ return { ...old, cards: old.cards.filter((c) => c.id !== cardId) }
+ })
return { previousTarget, previousSource }타입·변수(sourceColumnId, variables.card)는 실제 구현에 맞게 조정하세요.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/app/dashboard/[id]/api/useCardMutation.ts lines 16 to 40, the optimistic
UI update does not properly reflect card movement because it only updates the
target column cache without removing the card from the source column, and it
uses a generic query key that invalidates all columns unnecessarily. Fix this by
updating both the source and target column caches: remove the card from the
source column's cache and add it to the target column's cache. Also, replace the
generic query key ['columnId'] in cancelQueries and invalidateQueries with
specific keys for the affected columns to avoid unnecessary cache invalidation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
카드의 컬럼 id를 바꾸는 방식으로 작동하기 때문에, 원래 컬럼에서 카드를 제거할 필요는 없을것 같지 않아?
1,3은 반영해야겠다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dkslel1225 네, 이해했습니다. 말씀하신 대로 서버 응답 이후에 다시 fetch 해 정확한 상태를 맞춘다면 원본 컬럼 캐시를 직접 건드리지 않아도 동작에는 문제가 없습니다. 다만 몇 가지만 더 확인해 보시면 좋을 것 같아요.
-
UX 이슈
• 네트워크 지연이 있는 환경에서는 드래그 직후 순간적으로 두 컬럼에 카드가 동시 표시될 수 있습니다. 사용자가 눈치채기 힘들 정도라면 괜찮지만, 실사용-latency(모바일 LTE 등)에서 한 번 체크해 보시면 좋겠습니다. -
롤백 안정성
•onError에서 target 컬럼만 롤백하면, 낙관적으로 추가됐던 카드가 그대로 남을 수 있습니다. 최소한 target 컬럼의previousData만 아니라 source 컬럼의previousData도 같이 저장해 두면 더 안전합니다. -
구현 팁 (1, 3 반영용 예시)
// 1️⃣ 대상 컬럼만 선택적으로 cancel / invalidate
await queryClient.cancelQueries({ queryKey: ['columnId', columnId] });
// ...
queryClient.invalidateQueries({ queryKey: ['columnId', columnId] });
// 2️⃣ 대상 컬럼 캐시에 카드가 없을 때 push
queryClient.setQueryData<CardResponse>(['columnId', columnId], old => {
if (!old) return undefined;
const exists = old.cards.some(c => c.id === cardId);
return exists ? old : { ...old, cards: [...old.cards, { ...variables.card, columnId }] };
});중복 표시나 롤백 시나리오가 실제로 문제되지 않는다면 지금 계획대로 진행하셔도 됩니다. 추가 논의가 필요하면 언제든 말씀 주세요!
Insung-Jo
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
드래그 앤 드롭 구현 수고 많으셨습니다! 낙관적 UI에 대한 내용이 코드로 담겨 있어서 공부가 되었습니다 👍 👍
yuj2n
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지윤님 드래그 앤 드롭 구현 수고 많으셨습니다!!
로직이 많아서 어렵네요 ㅎㅎ😅
그래도 설명으로 적어주셔서 어떤 식으로 동작하는 지 이해했습니다!!
LeeCh0129
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
낙관적 UI까지 적용하셨네요!👍, 상세내용과 주석에 다 설명을 적어주셔서 이해하는데 도움이 되었습니다
고생 많으셨습니다
clearDraggingCard가 뮤테이션보다 먼저 실행되어 setQueryData 시 currentCard = draggingCard가 null로 출력됨. clearDraggingCard 호출을 캐시 업데이트 이후로 이동하여 정상 반영되도록 수정. 또한, 카드 이동은 컬럼에서 제거/추가하는 방식으로 수정함.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/app/dashboard/[id]/api/useCardMutation.ts (2)
38-52:filter내부 부수효과는 가독성과 타입 안정성을 떨어뜨립니다카드 추출(
extractedCard) 과 제거 로직을 분리하면 의도가 명확해집니다.-const filtered = oldData.cards.filter((card) => { - if (card.id === cardId) extractedCard = card - return card.id !== cardId -}) +const extracted = oldData.cards.find((c) => c.id === cardId) +const filtered = oldData.cards.filter((c) => c.id !== cardId) +extractedCard = extracted
61-65: 컬럼 카드 수(totalCount) 동기화 누락
cards배열만 수정하면 UI 상 카드 개수 뱃지가 즉시 맞지 않을 수 있습니다.
totalCount를 함께 증감시켜 낙관적 UI의 완성도를 높이세요.-return { - ...oldData, - cards: [...oldData.cards, movedCard], -} +return { + ...oldData, + totalCount: oldData.totalCount + 1, + cards: [...oldData.cards, movedCard], +}src/app/dashboard/[id]/Column/Column.tsx (1)
1-1: 사용하지 않는useQueryClientimport 제거미사용 import 는 번들 크기와 린트 경고를 불필요하게 증가시킵니다.
-import { useQueryClient } from '@tanstack/react-query'
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/app/dashboard/[id]/Card/Card.tsx(2 hunks)src/app/dashboard/[id]/Column/Column.tsx(2 hunks)src/app/dashboard/[id]/api/useCardMutation.ts(1 hunks)src/app/dashboard/[id]/store/useDragStore.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/app/dashboard/[id]/store/useDragStore.ts
- src/app/dashboard/[id]/Card/Card.tsx
| const previousData = queryClient.getQueryData<CardResponse>([ | ||
| 'columnId', | ||
| columnId, | ||
| ]) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
롤백 데이터가 대상 컬럼만 저장되어 불완전합니다
previousData 에서 원본·대상 두 컬럼을 모두 보관하지 않으면
서버 오류 시 원본 컬럼에 카드가 영영 사라질 수 있습니다.
onError 에서 두 컬럼을 모두 복구하도록 구조를 확장하세요.
Also applies to: 78-83
🤖 Prompt for AI Agents
In src/app/dashboard/[id]/api/useCardMutation.ts around lines 23 to 27 and also
lines 78 to 83, the rollback data only saves the target column, which is
incomplete. Modify the code to retrieve and store both the original and target
columns' data in previousData. Then update the onError handler to restore both
columns to ensure no data loss occurs if the server operation fails.
…시에도 clearDraggingCard 호출하며 종료.
📌 변경 사항 개요
추가 수정사항
See commit 2a4f1c3
### ✨ What’s changed
🐛 Background
✅ Checklist
✨ 요약
📝 상세 내용
🎄 useCardMutation - 수정되었음, 아래 내용은 무효함
🎄 컴포넌트에 추가한 드래그 이벤트
카드 컴포넌트: dragStart 이벤트: draggingCard에 카드 정보 저장함(아이디,컬럼아이디)
컬럼 컴포넌트:
b,d 에서 사용된 함수는 draggingCard 전역상태를 관리하는, useDragStore에서 가져옴(zustand 사용)
setDraggingover에서 설정된 값을 체크해서, 카드가 컬럼에 over 되었을때 해당 컬럼의 border컬러가 바뀜
🔗 관련 이슈
🖼️ 스크린샷
2025-06-13.3.51.10.mov
✅ 체크리스트
💡 참고 사항
Summary by CodeRabbit
Summary by CodeRabbit
신규 기능
스타일
버그 수정