Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/app/dashboard/[id]/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import Image from 'next/image'

import type { Card } from '@/app/api/useCards'
import type { Card as CardType } from '@/app/api/useCards'

import { useDragStore } from '../store/useDragStore'
import Tags from './Tags'

export default function Card({ card }: { card: Card }) {
const { imageUrl, title, tags, dueDate, assignee } = card
export default function Card({
card,
columnId,
}: {
card: CardType
columnId: number
}) {
const { id, imageUrl, title, tags, dueDate, assignee } = card
const { setDraggingCard } = useDragStore()
return (
<div className="BG-white Border-section relative w-314 rounded-6 border-solid px-20 py-16">
<div
draggable="true"
onDragStart={() => setDraggingCard({ cardId: id, columnId: columnId })}
className="BG-white Border-section relative w-314 rounded-6 border-solid px-20 py-16"
>
Todo Card
{imageUrl && (
<Image
Expand All @@ -17,6 +29,7 @@ export default function Card({ card }: { card: Card }) {
height={600}
className="h-auto w-full rounded-6 object-contain"
priority
draggable="false"
/>
)}
<p>{title}</p>
Expand Down
49 changes: 45 additions & 4 deletions src/app/dashboard/[id]/Column/Column.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,57 @@
import { useQueryClient } from '@tanstack/react-query'
import Image from 'next/image'
import { useState } from 'react'

import useCards from '@/app/api/useCards'
import type { Column } from '@/app/api/useColumns'
import type { Column as ColumnType } from '@/app/api/useColumns'
import { cn } from '@/app/shared/lib/cn'

import { useCardMutation } from '../api/useCardMutation'
import Card from '../Card/Card'
export default function Column({ column }: { column: Column }) {
import { useDragStore } from '../store/useDragStore'
export default function Column({ column }: { column: ColumnType }) {
const { id, title }: { id: number; title: string } = column
const { data, isLoading, error } = useCards(id)
const [isDraggingover, setDraggingover] = useState(false)
const { clearDraggingCard } = useDragStore()
const cardMutation = useCardMutation()

if (isLoading) return <p>loading...</p>
if (error) return <p>error...{error.message}</p>

return (
<div className="BG-gray Border-column flex w-354 shrink-0 flex-col gap-16 p-20">
<div
onDragOver={(e) => {
e.preventDefault() //브라우저 기본은 드롭 비허용. 이걸 막아줘야 drop 가능
if (!isDraggingover) setDraggingover(true) //dragOver 이벤트 발생하는 내내 setState 실행 방지(처음 false일때만 setDraggingOver실행)
}}
onDragLeave={(e) => {
e.preventDefault()
if (isDraggingover) setDraggingover(false)
}}
onDrop={(e) => {
e.preventDefault()
if (isDraggingover) setDraggingover(false)
const draggingCard = useDragStore.getState().draggingCard

if (!draggingCard) {
console.log('no dragging card') //TODO - toast 처리 🍞
return
}
// 동일 컬럼이면 무시
if (draggingCard.columnId === id) {
clearDraggingCard()
return
}
cardMutation.mutate({ cardId: draggingCard.cardId, columnId: id })
}}
className={cn(
'BG-gray Border-column flex w-354 shrink-0 flex-col gap-16 p-20',
{
'!border-blue-500': isDraggingover,
},
)}
>
<div className="mb-24 flex items-center justify-between">
<div className="flex items-center">
<div className="mb-7 mr-8 size-8 rounded-25 bg-blue-500"></div>
Expand All @@ -38,7 +77,9 @@ export default function Column({ column }: { column: Column }) {
/>
</div>
</button>
{data?.cards.map((card) => <Card key={card.id} card={card} />)}
{data?.cards.map((card) => (
<Card key={card.id} card={card} columnId={id} />
))}
</div>
)
}
12 changes: 12 additions & 0 deletions src/app/dashboard/[id]/api/updateCardColumn.ts
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
}
102 changes: 102 additions & 0 deletions src/app/dashboard/[id]/api/useCardMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'

import type { Card, CardResponse } from '@/app/api/useCards'

import { useDragStore } from '../store/useDragStore'
import { updateCardColumn } from './updateCardColumn'

export const useCardMutation = () => {
const queryClient = useQueryClient()
const { clearDraggingCard } = useDragStore()

return useMutation({
// 1. 서버 API 호출
// cardId: 드래그한 카드 아이디, columnId: dragOver된 타겟 컬럼 아이디
mutationFn: ({ cardId, columnId }: { cardId: number; columnId: number }) =>
updateCardColumn(cardId, columnId),

// 2. 낙관적 UI 처리 (서버 요청 전에 실행됨)
onMutate: async ({ cardId, columnId }) => {
const currentCard = useDragStore.getState().draggingCard

await Promise.all([
queryClient.cancelQueries({ queryKey: ['columnId', columnId] }),
queryClient.cancelQueries({
queryKey: ['columnId', currentCard?.columnId],
}),
])

// 업데이트 이전 데이터 챙겨뒀다가 롤백할때 사용
const previousData = queryClient.getQueryData<CardResponse>([
'columnId',
columnId,
])

Comment on lines +30 to +34
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

롤백 데이터가 대상 컬럼만 저장되어 불완전합니다

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.

// Guard return
if (
!currentCard ||
currentCard.cardId !== cardId ||
currentCard.columnId === columnId
) {
console.log('no dragging card || is not a dragging card || same column')
clearDraggingCard()
return
}

let extractedCard: Card | undefined // B. 에서 사용할 예정(추가할 카드 데이터는 Card여야 해서)
// A. 이전 컬럼에서 카드 제거 & 카드 추출
// setQueryData의 콜백함수의 리턴값이 쿼리키 캐시에 저장됨(캐시 업데이트)
queryClient.setQueryData<CardResponse>(
['columnId', currentCard.columnId],
(oldData) => {
if (!oldData) return

const filtered = oldData.cards.filter((card) => {
if (card.id === cardId) extractedCard = card
return card.id !== cardId
})

return { ...oldData, cards: filtered }
},
)
// B. 새 컬럼에 카드 추가
if (extractedCard) {
queryClient.setQueryData<CardResponse>(
['columnId', columnId],
(oldData) => {
if (!oldData) return

const movedCard = { ...extractedCard!, columnId }
return {
...oldData,
cards: [...oldData.cards, movedCard],
}
},
)
} else {
console.log('카드가 제거 중에 undefined가 됨')
}

clearDraggingCard()
return { previousData }
},

// 3. 에러 발생 시 롤백
onError: (error, variables, context) => {
if (context?.previousData) {
queryClient.setQueryData<CardResponse>(
['columnId', variables.columnId],
context.previousData,
)
}
console.error('카드 이동 실패:', error)
},

// 4. 성공 시 서버 기준으로 다시 데이터 불러오도록 유도함.
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({
queryKey: ['columnId', variables.columnId],
})
},
})
}
2 changes: 1 addition & 1 deletion src/app/dashboard/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function DashboardID() {
return (
<>
<div className="fixed left-0 h-1080 w-300 bg-gray-100">사이드바</div>
<div className="ml-300">
<div className="ml-300 select-none">
<div className="flex">
{columns?.map((column) => <Column key={column.id} column={column} />)}
<div className="BG-gray Border-column p-20">
Expand Down
15 changes: 15 additions & 0 deletions src/app/dashboard/[id]/store/useDragStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { create } from 'zustand'
interface draggingCard {
cardId: number
columnId: number
}
interface DragStore {
draggingCard: draggingCard | 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 }),
}))