diff --git a/next.config.mjs b/next.config.mjs index b3498fe6..3c0515d4 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, + reactStrictMode: false, images: { remotePatterns: [ { diff --git a/package.json b/package.json index 18e5746c..4b7cd350 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,12 @@ "@reduxjs/toolkit": "^2.2.5", "@tanstack/react-query": "^5.48.0", "@tanstack/react-query-devtools": "^5.48.0", + "@types/react-beautiful-dnd": "^13.1.8", "axios": "^1.7.2", "lodash": "^4.17.21", "next": "14.2.4", "react": "^18", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^18", "react-hook-form": "^7.52.0", "react-query": "^3.39.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ef0d377..52958cf3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@tanstack/react-query-devtools': specifier: ^5.48.0 version: 5.48.0(@tanstack/react-query@5.48.0(react@18.3.1))(react@18.3.1) + '@types/react-beautiful-dnd': + specifier: ^13.1.8 + version: 13.1.8 axios: specifier: ^1.7.2 version: 1.7.2 @@ -32,6 +35,9 @@ importers: react: specifier: ^18 version: 18.3.1 + react-beautiful-dnd: + specifier: ^13.1.1 + version: 13.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: specifier: ^18 version: 18.3.1(react@18.3.1) @@ -386,6 +392,9 @@ packages: '@types/conventional-commits-parser@5.0.0': resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} + '@types/hoist-non-react-statics@3.3.5': + resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -398,9 +407,15 @@ packages: '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + '@types/react-beautiful-dnd@13.1.8': + resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==} + '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + '@types/react-redux@7.1.33': + resolution: {integrity: sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==} + '@types/react@18.3.3': resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} @@ -752,6 +767,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1264,6 +1282,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + htmlparser2@9.1.0: resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} @@ -1585,6 +1606,9 @@ packages: match-sorter@6.3.4: resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==} + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -1948,6 +1972,15 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + + react-beautiful-dnd@13.1.1: + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -1962,6 +1995,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-query@3.39.3: resolution: {integrity: sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==} peerDependencies: @@ -1974,6 +2010,18 @@ packages: react-native: optional: true + react-redux@7.2.9: + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + react-redux@9.1.2: resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==} peerDependencies: @@ -2011,6 +2059,9 @@ packages: peerDependencies: redux: ^5.0.0 + redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + redux@5.0.1: resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} @@ -2265,6 +2316,9 @@ packages: tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2336,6 +2390,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-memo-one@1.1.3: + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-sync-external-store@1.2.2: resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} peerDependencies: @@ -2692,6 +2751,11 @@ snapshots: dependencies: '@types/node': 20.14.5 + '@types/hoist-non-react-statics@3.3.5': + dependencies: + '@types/react': 18.3.3 + hoist-non-react-statics: 3.3.2 + '@types/json5@0.0.29': {} '@types/lodash@4.17.5': {} @@ -2702,10 +2766,21 @@ snapshots: '@types/prop-types@15.7.12': {} + '@types/react-beautiful-dnd@13.1.8': + dependencies: + '@types/react': 18.3.3 + '@types/react-dom@18.3.0': dependencies: '@types/react': 18.3.3 + '@types/react-redux@7.1.33': + dependencies: + '@types/hoist-non-react-statics': 3.3.5 + '@types/react': 18.3.3 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + '@types/react@18.3.3': dependencies: '@types/prop-types': 15.7.12 @@ -3131,6 +3206,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-box-model@1.2.1: + dependencies: + tiny-invariant: 1.3.3 + cssesc@3.0.0: {} csstype@3.1.3: {} @@ -3784,6 +3863,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + htmlparser2@9.1.0: dependencies: domelementtype: 2.3.0 @@ -4086,6 +4169,8 @@ snapshots: '@babel/runtime': 7.24.7 remove-accents: 0.5.0 + memoize-one@5.2.1: {} + meow@12.1.1: {} merge-stream@2.0.0: {} @@ -4372,6 +4457,22 @@ snapshots: queue-microtask@1.2.3: {} + raf-schd@4.0.3: {} + + react-beautiful-dnd@13.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.24.7 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + redux: 4.2.1 + use-memo-one: 1.1.3(react@18.3.1) + transitivePeerDependencies: + - react-native + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -4384,6 +4485,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-query@3.39.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.24.7 @@ -4393,6 +4496,18 @@ snapshots: optionalDependencies: react-dom: 18.3.1(react@18.3.1) + react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.24.7 + '@types/react-redux': 7.1.33 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 17.0.2 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.3 @@ -4424,6 +4539,10 @@ snapshots: dependencies: redux: 5.0.1 + redux@4.2.1: + dependencies: + '@babel/runtime': 7.24.7 + redux@5.0.1: {} reflect.getprototypeof@1.0.6: @@ -4705,6 +4824,8 @@ snapshots: tiny-case@1.0.3: {} + tiny-invariant@1.3.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -4788,6 +4909,10 @@ snapshots: dependencies: punycode: 2.3.1 + use-memo-one@1.1.3(react@18.3.1): + dependencies: + react: 18.3.1 + use-sync-external-store@1.2.2(react@18.3.1): dependencies: react: 18.3.1 diff --git a/src/containers/dashboard/Card.tsx b/src/containers/dashboard/Card.tsx index 0b4dba2a..2c418e1b 100644 --- a/src/containers/dashboard/Card.tsx +++ b/src/containers/dashboard/Card.tsx @@ -11,7 +11,7 @@ interface CardProps { export default function Card({ card }: CardProps) { return ( -
+
{/* Card Image */} {card.imageUrl && (
diff --git a/src/containers/dashboard/Column.tsx b/src/containers/dashboard/Column.tsx index 90368290..c837f1d6 100644 --- a/src/containers/dashboard/Column.tsx +++ b/src/containers/dashboard/Column.tsx @@ -1,33 +1,21 @@ import Image from 'next/image'; +import { Draggable, Droppable } from 'react-beautiful-dnd'; import Card from './Card'; -import useFetchData from '@/hooks/useFetchData'; import useModal from '@/hooks/useModal'; -import { getCardsList } from '@/services/getService'; -import { CardsListResponse } from '@/types/Card.interface'; +import { Card as CardType } from '@/types/Card.interface'; import { Column as ColumnType } from '@/types/Column.interface'; interface ColumnProps { column: ColumnType; columns: ColumnType[]; + index: number; + cards: CardType[]; } -function Column({ column, columns }: ColumnProps) { +function Column({ column, index, cards, columns }: ColumnProps) { const { openModal } = useModal(); - const { - data: cardList, - isLoading, - error, - } = useFetchData(['cardList', column.id], () => getCardsList(column.id)); - - if (isLoading) { - return
Loading...
; - } - - if (error) { - return
Error: {error.message}
; - } return (
@@ -38,7 +26,7 @@ function Column({ column, columns }: ColumnProps) { ๐’Šน

{column.title}

- {cardList?.totalCount || 0} {/* API์—์„œ ๊ฐ€์ ธ์˜จ ์นด๋“œ ๊ฐœ์ˆ˜ */} + {cards.length}
{/* Column Edit Button */} @@ -64,8 +52,23 @@ function Column({ column, columns }: ColumnProps) { {/* Card List Section */} -
- {cardList && cardList.cards.map((card) => )} +
+ + {(provided) => ( +
+ {cards.map((card, index) => ( + + {(provided) => ( +
+ +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
diff --git a/src/containers/dashboard/ColumnsSection.tsx b/src/containers/dashboard/ColumnsSection.tsx index 1526e9e3..6b85b42c 100644 --- a/src/containers/dashboard/ColumnsSection.tsx +++ b/src/containers/dashboard/ColumnsSection.tsx @@ -1,26 +1,77 @@ import Image from 'next/image'; +import React, { useState, useEffect } from 'react'; +import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; import Column from './Column'; import useFetchData from '@/hooks/useFetchData'; import useModal from '@/hooks/useModal'; -import { getColumnsList } from '@/services/getService'; +import { getColumnsList, getCardsList } from '@/services/getService'; +import { moveToOtherColumn } from '@/services/putService'; +import { Card as CardType } from '@/types/Card.interface'; import { ColumnsResponse } from '@/types/Column.interface'; interface ColumnsSectionProps { - id: string; // id : ๋Œ€์‹œ๋ณด๋“œ id (๋™์  ๋ผ์šฐํŒ… ๋งค๊ฐœ๋ณ€์ˆ˜) + id: string; } export default function ColumnsSection({ id }: ColumnsSectionProps) { const { openModal } = useModal(); const { - data: columns, // ์ปฌ๋Ÿผ ๋ชฉ๋ก ๋ฐฐ์—ด + data: columns, isLoading, error, } = useFetchData(['columns', id], () => getColumnsList(Number(id))); + const columnList = columns?.data; + + const [cardLists, setCardLists] = useState>({}); + + useEffect(() => { + if (columnList) { + columnList.forEach((column) => { + const fetchCards = async () => { + const { data } = await getCardsList(column.id); + setCardLists((prev) => ({ ...prev, [column.id]: data.cards })); + }; + fetchCards(); + }); + } + }, [columnList]); + + const onDragEnd = async (result: DropResult) => { + const { source, destination } = result; + + if (!destination || !columnList) { + return; + } + + const sourceColumnId = parseInt(source.droppableId.replace('column-', ''), 10); + const destinationColumnId = parseInt(destination.droppableId.replace('column-', ''), 10); + + const sourceCards = Array.from(cardLists[sourceColumnId]); + const [movedCard] = sourceCards.splice(source.index, 1); + + if (sourceColumnId !== destinationColumnId) { + const destinationCards = Array.from(cardLists[destinationColumnId]); + destinationCards.splice(destination.index, 0, movedCard); + + setCardLists((prev) => ({ + ...prev, + [sourceColumnId]: sourceCards, + [destinationColumnId]: destinationCards, + })); + + try { + // ๋‹ค๋ฅธ ์ปฌ๋Ÿผ์œผ๋กœ์˜ ์นด๋“œ ์ด๋™ API ์š”์ฒญ + await moveToOtherColumn(movedCard.id, destinationColumnId); + } catch (error) { + console.error(error); + } + } + }; if (isLoading) { - return
Loading...
; // ์Šคํ”ผ๋„ˆ๋กœ ๊ต์ฒด ์˜ˆ์ • + return
Loading...
; } if (error) { @@ -28,30 +79,42 @@ export default function ColumnsSection({ id }: ColumnsSectionProps) { } return ( -
-
-
    - {columns?.data && - columns.data.map((column) => )} - {columns?.data.length === 0 &&

    ์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค.

    } -
-
- + +
+
+
    + {columnList && + columnList.map((column, index) => ( + + {(provided) => ( +
  • + + {provided.placeholder} +
  • + )} +
    + ))} + {columnList?.length === 0 &&

    ์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค.

    } +
+
+ +
-
-
+
+ ); } diff --git a/src/services/putService.ts b/src/services/putService.ts index 88600747..622bd652 100644 --- a/src/services/putService.ts +++ b/src/services/putService.ts @@ -30,3 +30,11 @@ export const putPassword = async (formData: UpdatePasswordForm) => { export const putColumn = async (columnId: number, formData: { title: string }) => { return await instance.put(`/columns/${columnId}`, formData); }; + +// ์ปฌ๋Ÿผ ์•„์ด๋”” ๊ฐ’ ์ˆ˜์ • : ๋‹ค๋ฅธ ์ปฌ๋Ÿผ์œผ๋กœ ์นด๋“œ ์ด๋™ (DnD) +export const moveToOtherColumn = async (cardId: number, destinationColumnId: number) => { + const formData = { + columnId: destinationColumnId, + }; + return await instance.put(`/cards/${cardId}`, formData); +};