[React] DragAndDrop(DND) 와 Resizing 사용하기 #2 (Typescript)

React DND


시작하기 전에

이전 챕터에서는 하나의 모달만을 사용했지만, 이번에는 복수개의 모달을 DND 와 Resizing 하는 방법을 알아보자.

프로젝트 구조

동일한 컴포넌트는 제외하고 작성을 하겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
src
 ┣ constants
 ┃ ┣ dnd.ts
 ┣ components
 ┃ ┣ DragAndResizeMultiple
 ┃   ┣ Boundary.tsx ## 동일
 ┃   ┣ BoundaryLayout.tsx
 ┃   ┣ DragLayout.tsx
 ┃   ┣ DragLayoutSecond.tsx
 ┃   ┣ DragMenu.tsx
 ┃   ┣ DragMenuSecond.tsx
 ┃   ┣ ResizeModal.tsx ## 동일
 ┃   ┣ ResizeModalSecond.tsx 
 ┣ utils
 ┃ ┣ Resize.ts
 ┃ ┣ registDragEvent.ts


바운더리 레아이웃 수정

BoundaryLayout.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import { useEffect, useRef, useState } from 'react';
import { inrange } from 'utils/resize';
import Boundary from './Boundary';
import registDragEvent from 'utils/registDragEvent';
import { BOUNDARY_MARGIN, DEFAULT_W, DEFAULT_H } from 'constants/dnd';
import DragMenu from './DragMenu';
import DragMenuSecond from './DragMenuSecond';

interface BoundaryLayoutProps {
    isShowFirstMenu: boolean;
    isShowSecondMenu: boolean;
}


const BoundaryLayout: React.FC<BoundaryLayoutProps> = ({
    isShowFirstMenu,
    isShowSecondMenu,
}: BoundaryLayoutProps) => {

    const boundaryRef = useRef<HTMLDivElement>(null);

    const [{ x, y, w, h }, setConfig] = useState({
        x: 0,
        y: 0,
        w: 0,
        h: 0,
    });

    useEffect(() => {
        const boundary = boundaryRef.current?.getBoundingClientRect();

        if (boundary) {
            setConfig({
                x: Math.floor(boundary.width / 2 - DEFAULT_W / 2),
                y: Math.floor(boundary.height / 2 - DEFAULT_H / 2),
                w: DEFAULT_W,
                h: DEFAULT_H,
            });

            setFirstMenuConfig({
                firstMenuX: Math.floor(boundary.width / 2 - DEFAULT_W / 2),
                firstMenuY: Math.floor(boundary.height / 2 - DEFAULT_H / 2),
                firstMenuW: DEFAULT_W,
                firstMenuH: DEFAULT_H,
            });

            setSecondMenuConfig({
                secondMenuX: Math.floor(boundary.width / 2 - DEFAULT_W / 2),
                secondMenuY: Math.floor(boundary.height / 2 - DEFAULT_H / 2),
                secondMenuW: DEFAULT_W,
                secondMenuH: DEFAULT_H,
            });
        }
    }, []);

    const [{ firstMenuX, firstMenuY, firstMenuW, firstMenuH }, setFirstMenuConfig] = useState({
        firstMenuX: 0,
        firstMenuY: 0,
        firstMenuW: 0,
        firstMenuH: 0,
    });
    const [firstMenuShow, setFirstMenuShow] = useState(true);
    const [isFirstMenuOpen, setFirstMenuIsOpen] = useState<boolean>(false);

    const [{ secondMenuX, secondMenuY, secondMenuW, secondMenuH }, setSecondMenuConfig] = useState({
        secondMenuX: 0,
        secondMenuY: 0,
        secondMenuW: 0,
        secondMenuH: 0,
    });
    const [secondMenuShow, setSecondMenuShow] = useState(true);
    const [isSecondMenuOpen, setSecondMenuIsOpen] = useState<boolean>(false);

    return (
        <>
            <div className="p-4">
                <Boundary ref={boundaryRef}>
                    <div
                        style=
                        className="absolute"
                        {...registDragEvent((deltaX: number, deltaY: number) => {
                            if (!boundaryRef.current) return;

                            const boundary = boundaryRef.current.getBoundingClientRect();
                            setFirstMenuConfig({
                                firstMenuX: inrange(firstMenuX + deltaX, BOUNDARY_MARGIN, boundary.width - firstMenuW - BOUNDARY_MARGIN),
                                firstMenuY: inrange(firstMenuY + deltaY, BOUNDARY_MARGIN, boundary.height - firstMenuH - BOUNDARY_MARGIN),
                                firstMenuW,
                                firstMenuH,
                            });
                            console.log('first ' + firstMenuX + " / " + firstMenuY + " / " + firstMenuW + " / " + firstMenuH)
                        })}
                    >
                        {isShowFirstMenu && <DragMenu
                            isOpen={isFirstMenuOpen}
                            setConfig={setFirstMenuConfig}
                            x={firstMenuX}
                            y={firstMenuY}
                            w={firstMenuW}
                            h={firstMenuH}
                            boundaryRef={boundaryRef}
                            show={firstMenuShow}
                        />}
                    </div>

                    <div
                        style=
                        className="absolute"
                        {...registDragEvent((deltaX: number, deltaY: number) => {
                            if (!boundaryRef.current) return;

                            const boundary = boundaryRef.current.getBoundingClientRect();
                            setSecondMenuConfig({
                                secondMenuX: inrange(secondMenuX + deltaX, BOUNDARY_MARGIN, boundary.width - secondMenuW - BOUNDARY_MARGIN),
                                secondMenuY: inrange(secondMenuY + deltaY, BOUNDARY_MARGIN, boundary.height - secondMenuH - BOUNDARY_MARGIN),
                                secondMenuW,
                                secondMenuH,
                            });
                            console.log('second ' + secondMenuX + " / " + secondMenuY + " / " + secondMenuW + " / " + secondMenuH)
                        })}
                    >
                        {isShowSecondMenu && <DragMenuSecond
                            isOpen={isSecondMenuOpen}
                            setConfig={setSecondMenuConfig}
                            x={secondMenuX}
                            y={secondMenuY}
                            w={secondMenuW}
                            h={secondMenuH}
                            boundaryRef={boundaryRef}
                            show={secondMenuShow}
                        />}
                    </div>
                </Boundary>
            </div>
        </>
    );
}

export default BoundaryLayout;

Drag Layout 추가 및 추가

DragLayout.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import { inrange } from 'utils/resize';
import registDragEvent from 'utils/registDragEvent';
import { BOUNDARY_MARGIN, MIN_W, MIN_H } from 'constants/dnd';


interface DragLayoutProps {
    setConfig: React.Dispatch<React.SetStateAction<{
        firstMenuX: number;
        firstMenuY: number;
        firstMenuW: number;
        firstMenuH: number;
    }>>;
    x: number;
    y: number;
    w: number;
    h: number;
    boundaryRef: React.RefObject<HTMLDivElement>;
    show: boolean;
}

const DragLayout: React.FC<DragLayoutProps> = ({
    setConfig,
    x, y, w, h,
    boundaryRef,
    show,
}: DragLayoutProps) => {

    return (
        <>

            {/* 좌상단  */}
            <div
                className="absolute -top-1 -left-1 h-4 w-4 cursor-nw-resize"
                style=
                {...registDragEvent((deltaX, deltaY) => {
                    setConfig({
                        firstMenuX: inrange(x + deltaX, BOUNDARY_MARGIN, x + w - MIN_W),
                        firstMenuY: inrange(y + deltaY, BOUNDARY_MARGIN, y + h - MIN_H),
                        firstMenuW: inrange(w - deltaX, MIN_W, x + w - BOUNDARY_MARGIN),
                        firstMenuH: inrange(h - deltaY, MIN_H, y + h - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 우상단 */}
            <div
                className="absolute -top-1 -right-1 h-4 w-4 cursor-ne-resize"
                style=
                {...registDragEvent((deltaX, deltaY) => {
                    if (!boundaryRef.current) return;

                    const boundary = boundaryRef.current.getBoundingClientRect();

                    setConfig({
                        firstMenuX: x,
                        firstMenuY: inrange(y + deltaY, BOUNDARY_MARGIN, y + h - MIN_H),
                        firstMenuW: inrange(w + deltaX, MIN_W, boundary.width - x - BOUNDARY_MARGIN),
                        firstMenuH: inrange(h - deltaY, MIN_H, y + h - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 좌하단 */}
            <div
                className="absolute -bottom-1 -left-1 h-4 w-4 cursor-ne-resize"
                style=
                {...registDragEvent((deltaX, deltaY) => {
                    if (!boundaryRef.current) return;

                    const boundary = boundaryRef.current.getBoundingClientRect();

                    setConfig({
                        firstMenuX: inrange(x + deltaX, BOUNDARY_MARGIN, x + w - MIN_W),
                        firstMenuY: y,
                        firstMenuW: inrange(w - deltaX, MIN_W, x + w - BOUNDARY_MARGIN),
                        firstMenuH: inrange(h + deltaY, MIN_H, boundary.height - y - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 우하단 */}
            <div
                className="absolute -bottom-1 -right-1 h-4 w-4 cursor-se-resize"
                style=
                {...registDragEvent((deltaX, deltaY) => {
                    if (!boundaryRef.current) return;

                    const boundary = boundaryRef.current.getBoundingClientRect();

                    setConfig({
                        firstMenuX: x,
                        firstMenuY: y,
                        firstMenuW: inrange(w + deltaX, MIN_W, boundary.width - x - BOUNDARY_MARGIN),
                        firstMenuH: inrange(h + deltaY, MIN_H, boundary.height - y - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 상단 */}
            <div
                className="absolute -top-0.5 left-3 right-3 h-2 cursor-n-resize"
                style=
                {...registDragEvent((_, deltaY) => {
                    setConfig({
                        firstMenuX: x,
                        firstMenuY: inrange(y + deltaY, BOUNDARY_MARGIN, y + h - MIN_H),
                        firstMenuW: w,
                        firstMenuH: inrange(h - deltaY, MIN_H, y + h - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 하단 */}
            <div
                className="absolute -bottom-0.5 left-3 right-3 h-2 cursor-s-resize"
                style=
                {...registDragEvent((_, deltaY) => {
                    if (!boundaryRef.current) return;

                    const boundary = boundaryRef.current.getBoundingClientRect();

                    setConfig({
                        firstMenuX: x,
                        firstMenuY: y,
                        firstMenuW: w,
                        firstMenuH: inrange(h + deltaY, MIN_H, boundary.height - y - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 우측 */}
            <div
                className="absolute bottom-3 top-3 -right-0.5 w-2 cursor-e-resize"
                style=
                {...registDragEvent((deltaX) => {
                    if (!boundaryRef.current) return;

                    const boundary = boundaryRef.current.getBoundingClientRect();

                    setConfig({
                        firstMenuX: x,
                        firstMenuY: y,
                        firstMenuW: inrange(w + deltaX, MIN_W, boundary.width - x - BOUNDARY_MARGIN),
                        firstMenuH: h,
                    });
                }, true)}
            />
            {/* 좌측 */}
            <div
                className="absolute bottom-3 top-3 -left-0.5 w-2 cursor-w-resize"
                style=
                {...registDragEvent((deltaX) => {
                    setConfig({
                        firstMenuX: inrange(x + deltaX, BOUNDARY_MARGIN, x + w - MIN_W),
                        firstMenuY: y,
                        firstMenuW: inrange(w - deltaX, MIN_W, x + w - BOUNDARY_MARGIN),
                        firstMenuH: h,
                    });
                }, true)}
            />
        </>

    );
}

export default DragLayout;
DragLayoutSecond.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import { inrange } from 'utils/resize';
import registDragEvent from 'utils/registDragEvent';
import { BOUNDARY_MARGIN, MIN_W, MIN_H } from 'constants/dnd';


interface DragLayoutProps {
    setConfig: React.Dispatch<React.SetStateAction<{
        secondMenuX: number;
        secondMenuY: number;
        secondMenuW: number;
        secondMenuH: number;
    }>>;
    x: number;
    y: number;
    w: number;
    h: number;
    boundaryRef: React.RefObject<HTMLDivElement>;
    show: boolean;
}

const DragLayoutSecond: React.FC<DragLayoutProps> = ({
    setConfig,
    x, y, w, h,
    boundaryRef,
    show,
}: DragLayoutProps) => {

    return (
        <>

            {/* 좌상단  */}
            <div
                className="absolute -top-1 -left-1 h-4 w-4 cursor-nw-resize"
                style=
                {...registDragEvent((deltaX, deltaY) => {
                    setConfig({
                        secondMenuX: inrange(x + deltaX, BOUNDARY_MARGIN, x + w - MIN_W),
                        secondMenuY: inrange(y + deltaY, BOUNDARY_MARGIN, y + h - MIN_H),
                        secondMenuW: inrange(w - deltaX, MIN_W, x + w - BOUNDARY_MARGIN),
                        secondMenuH: inrange(h - deltaY, MIN_H, y + h - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 우상단 */}
            <div
                className="absolute -top-1 -right-1 h-4 w-4 cursor-ne-resize"
                style=
                {...registDragEvent((deltaX, deltaY) => {
                    if (!boundaryRef.current) return;

                    const boundary = boundaryRef.current.getBoundingClientRect();

                    setConfig({
                        secondMenuX: x,
                        secondMenuY: inrange(y + deltaY, BOUNDARY_MARGIN, y + h - MIN_H),
                        secondMenuW: inrange(w + deltaX, MIN_W, boundary.width - x - BOUNDARY_MARGIN),
                        secondMenuH: inrange(h - deltaY, MIN_H, y + h - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 좌하단 */}
            <div
                className="absolute -bottom-1 -left-1 h-4 w-4 cursor-ne-resize"
                style=
                {...registDragEvent((deltaX, deltaY) => {
                    if (!boundaryRef.current) return;

                    const boundary = boundaryRef.current.getBoundingClientRect();

                    setConfig({
                        secondMenuX: inrange(x + deltaX, BOUNDARY_MARGIN, x + w - MIN_W),
                        secondMenuY: y,
                        secondMenuW: inrange(w - deltaX, MIN_W, x + w - BOUNDARY_MARGIN),
                        secondMenuH: inrange(h + deltaY, MIN_H, boundary.height - y - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 우하단 */}
            <div
                className="absolute -bottom-1 -right-1 h-4 w-4 cursor-se-resize"
                style=
                {...registDragEvent((deltaX, deltaY) => {
                    if (!boundaryRef.current) return;

                    const boundary = boundaryRef.current.getBoundingClientRect();

                    setConfig({
                        secondMenuX: x,
                        secondMenuY: y,
                        secondMenuW: inrange(w + deltaX, MIN_W, boundary.width - x - BOUNDARY_MARGIN),
                        secondMenuH: inrange(h + deltaY, MIN_H, boundary.height - y - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 상단 */}
            <div
                className="absolute -top-0.5 left-3 right-3 h-2 cursor-n-resize"
                style=
                {...registDragEvent((_, deltaY) => {
                    setConfig({
                        secondMenuX: x,
                        secondMenuY: inrange(y + deltaY, BOUNDARY_MARGIN, y + h - MIN_H),
                        secondMenuW: w,
                        secondMenuH: inrange(h - deltaY, MIN_H, y + h - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 하단 */}
            <div
                className="absolute -bottom-0.5 left-3 right-3 h-2 cursor-s-resize"
                style=
                {...registDragEvent((_, deltaY) => {
                    if (!boundaryRef.current) return;

                    const boundary = boundaryRef.current.getBoundingClientRect();

                    setConfig({
                        secondMenuX: x,
                        secondMenuY: y,
                        secondMenuW: w,
                        secondMenuH: inrange(h + deltaY, MIN_H, boundary.height - y - BOUNDARY_MARGIN),
                    });
                }, true)}
            />
            {/* 우측 */}
            <div
                className="absolute bottom-3 top-3 -right-0.5 w-2 cursor-e-resize"
                style=
                {...registDragEvent((deltaX) => {
                    if (!boundaryRef.current) return;

                    const boundary = boundaryRef.current.getBoundingClientRect();

                    setConfig({
                        secondMenuX: x,
                        secondMenuY: y,
                        secondMenuW: inrange(w + deltaX, MIN_W, boundary.width - x - BOUNDARY_MARGIN),
                        secondMenuH: h,
                    });
                }, true)}
            />
            {/* 좌측 */}
            <div
                className="absolute bottom-3 top-3 -left-0.5 w-2 cursor-w-resize"
                style=
                {...registDragEvent((deltaX) => {
                    setConfig({
                        secondMenuX: inrange(x + deltaX, BOUNDARY_MARGIN, x + w - MIN_W),
                        secondMenuY: y,
                        secondMenuW: inrange(w - deltaX, MIN_W, x + w - BOUNDARY_MARGIN),
                        secondMenuH: h,
                    });
                }, true)}
            />
        </>

    );
}

export default DragLayoutSecond;


모달 추가

ResizeModal.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const ResizeModal = () => {
    return (<>
        <>
            <div className="absolute h-full w-full cursor-move rounded-xl bg-white shadow-xl ring-1 ring-gray-100 transition-[shadow,transform] active:scale-[0.97] active:shadow-lg" >
                <div className="modal-header dark">
                    <h5 className="modal-title dark f14">테스트 모달 1</h5>
                </div>
            </div>
            <br />
        </>
    </>)
}

export default ResizeModal;
ResizeModalSecond.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const ResizeModalSecond = () => {

    return (<>
        <>
            <div className="absolute h-full w-full cursor-move rounded-xl bg-white shadow-xl ring-1 ring-gray-100 transition-[shadow,transform] active:scale-[0.97] active:shadow-lg" >
                <div className="modal-header dark">
                    <h5 className="modal-title dark f14">테스트 모달 2</h5>
                </div>
            </div>
            <br />
        </>
    </>)
}

export default ResizeModalSecond;


모달 추가

DragMenu.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import ResizeModal from "components/DragAndResizeMultiple/ResizeModal";
import DragLayout from "components/DragAndResizeMultiple/DragLayout";

interface DragMenuProps {
    isOpen: boolean;
    setConfig: React.Dispatch<React.SetStateAction<{
        firstMenuX: number;
        firstMenuY: number;
        firstMenuW: number;
        firstMenuH: number;
    }>>;
    x: number;
    y: number;
    w: number;
    h: number;
    boundaryRef: React.RefObject<HTMLDivElement>
    show: boolean;
}

const DragMenu: React.FC<DragMenuProps> = ({
    isOpen,
    setConfig,
    x, y, w, h,
    boundaryRef,
    show,
}: DragMenuProps) => {

    return (
        <>
            {isOpen &&
                <>
                    <div className="absolute h-full w-full cursor-move rounded-xl bg-white shadow-xl ring-1 ring-gray-100 transition-[shadow,transform] active:scale-[0.97] active:shadow-lg">
                        <ResizeModal />
                    </div>

                    <DragLayout
                        setConfig={setConfig}
                        x={x}
                        y={y}
                        w={w}
                        h={h}
                        boundaryRef={boundaryRef}
                        show={show}
                    />
                </>
            }

        </>

    );
}

export default DragMenu;
DragMenuSecond.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import ResizeModalSecond from "components/DragAndResizeMultiple/ResizeModalSecond";
import DragLayoutSecond from "components/DragAndResizeMultiple/DragLayoutSecond";

interface DragMenuSecondProps {
    isOpen: boolean;
    setConfig: React.Dispatch<React.SetStateAction<{
        secondMenuX: number;
        secondMenuY: number;
        secondMenuW: number;
        secondMenuH: number;
    }>>;
    x: number;
    y: number;
    w: number;
    h: number;
    boundaryRef: React.RefObject<HTMLDivElement>
    show: boolean;
}

const DragMenuSecond: React.FC<DragMenuSecondProps> = ({
    isOpen,
    setConfig,
    x, y, w, h,
    boundaryRef,
    show,
}: DragMenuSecondProps) => {

    return (
        <>
            {isOpen &&
                <>
                    <div className="absolute h-full w-full cursor-move rounded-xl bg-white shadow-xl ring-1 ring-gray-100 transition-[shadow,transform] active:scale-[0.97] active:shadow-lg">
                        <ResizeModalSecond />
                    </div>

                    <DragLayoutSecond
                        setConfig={setConfig}
                        x={x}
                        y={y}
                        w={w}
                        h={h}
                        boundaryRef={boundaryRef}
                        show={show}
                    />
                </>
            }

        </>

    );
}

export default DragMenuSecond;

결과화면

React

Tags:

Updated:

Leave a comment