-
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
Changes from 6 commits
f320492
2c24b07
be838c8
e5ff15f
8cf9929
37d1cf1
ea0799e
2dbfd2b
53057b9
e35da1d
c405b46
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import axiosClient from '@/app/api/axiosClient' | ||
|
|
||
| // 카드 이동 - 해당 카드의 컬럼ID를 변경하는 방식(PUT) | ||
| 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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { useMutation, useQueryClient } from '@tanstack/react-query' | ||
|
|
||
| import type { CardResponse } from '@/app/api/useCards' | ||
|
|
||
| import { updateCardColumn } from './updateCardColumn' | ||
|
|
||
| export const useCardMutation = () => { | ||
| const queryClient = useQueryClient() | ||
|
|
||
| return useMutation({ | ||
| // 1. 서버 API 호출 | ||
| mutationFn: ({ cardId, columnId }: { cardId: number; columnId: number }) => | ||
| updateCardColumn(cardId, columnId), | ||
|
|
||
| // 2. 낙관적 UI 처리 (서버 요청 전에 실행됨) | ||
| onMutate: async ({ cardId, columnId }) => { | ||
| await queryClient.cancelQueries({ queryKey: ['columnId'] }) | ||
|
|
||
| // 업데이트 이전 데이터 챙겨뒀다가 롤백할때 사용 | ||
| const previousData = queryClient.getQueryData<CardResponse>([ | ||
| 'columnId', | ||
| columnId, | ||
| ]) | ||
|
|
||
|
Comment on lines
+30
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 롤백 데이터가 대상 컬럼만 저장되어 불완전합니다
Also applies to: 78-83 🤖 Prompt for AI Agents |
||
| // 낙관적 업데이트(캐시 업데이트) | ||
| 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 commentThe reason will be displayed to describe this comment to others. Learn more. 낙관적 UI 로직이 실질적으로 카드 이동을 반영하지 못합니다
최소한 다음과 같이 수정해야 합니다. - 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 }타입·변수(
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 카드의 컬럼 id를 바꾸는 방식으로 작동하기 때문에, 원래 컬럼에서 카드를 제거할 필요는 없을것 같지 않아? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dkslel1225 네, 이해했습니다. 말씀하신 대로 서버 응답 이후에 다시 fetch 해 정확한 상태를 맞춘다면 원본 컬럼 캐시를 직접 건드리지 않아도 동작에는 문제가 없습니다. 다만 몇 가지만 더 확인해 보시면 좋을 것 같아요.
// 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 }] };
});중복 표시나 롤백 시나리오가 실제로 문제되지 않는다면 지금 계획대로 진행하셔도 됩니다. 추가 논의가 필요하면 언제든 말씀 주세요! |
||
|
|
||
| // 3. 에러 발생 시 롤백 | ||
| onError: (error, variables, context) => { | ||
| if (context?.previousData) { | ||
| queryClient.setQueryData<CardResponse>( | ||
| ['columnId', variables.columnId], | ||
| context.previousData, | ||
| ) | ||
| } | ||
| console.error('카드 이동 실패:', error) | ||
| }, | ||
|
|
||
| // 4. 성공 시 서버 기준으로 다시 데이터 불러오도록 유도함. | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ | ||
| queryKey: ['columnId'], | ||
| }) | ||
| }, | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { create } from 'zustand' | ||
|
|
||
| interface DragStore { | ||
| draggingCard: { cardId: number; columnId: number } | null | ||
| setDraggingCard: (data: { cardId: number; columnId: number }) => void | ||
| clearDraggingCard: () => void | ||
| } | ||
| export const useDragStore = create<DragStore>((set) => ({ | ||
| draggingCard: null, | ||
| setDraggingCard: (data) => set({ draggingCard: data }), | ||
| clearDraggingCard: () => set({ draggingCard: null }), | ||
| })) |
Uh oh!
There was an error while loading. Please reload this page.