Skip to content

Commit ef096f1

Browse files
authored
[FEATURE] 우클릭 시 복제 기능 구현 (#65)
* feat: 콘텐츠 스티커 색상 수정 API 연동 * feat: 복제 기능 구현 * refactor: context api로 props 전달하도록 리팩토링 * fix: 사이드 바에서 콘텐츠 수정 시 연관된 캐시 invalidate 되지 않던 오류 수정 * fix: invalidateMany 제거 * fix: 변경된 API 요청 양식 반영
1 parent 8b84ff0 commit ef096f1

14 files changed

Lines changed: 244 additions & 117 deletions

File tree

apps/web/src/app/globals.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,10 @@ div.customHandle {
9595
filter: blur(120px);
9696
opacity: 0.9;
9797
}
98+
99+
@utility triangle {
100+
background-color: rgba(0, 0, 0, 0.55);
101+
width: 20px;
102+
height: 12px;
103+
clip-path: polygon(45% 0%, 55% 0%, 100% 100%, 0% 100%);
104+
}

apps/web/src/components/(with-side-bar)/library/content-sidebar.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { useSearchParams } from "next/navigation";
66
import ResizeBar from "@/components/common/resize-bar";
77
import { CATEGORY } from "@/constants/category";
88
import { CONTENT } from "@/constants/content";
9-
import { invalidateMany } from "@/lib/tanstack";
9+
import { TOPIC } from "@/constants/topic";
10+
import { invalidateQueries } from "@/lib/tanstack";
1011
import { useRemoveContentById, useUpdateContent } from "@/lib/tanstack/mutation/content";
1112
import { useGetCategories } from "@/lib/tanstack/query/category";
1213
import { useGetContentById } from "@/lib/tanstack/query/content";
@@ -77,11 +78,11 @@ export default function ContentSidebar() {
7778
await updateContent(body, {
7879
onSuccess: () => {
7980
setIsEditing(false);
80-
invalidateMany([
81-
[CONTENT.GET_CONTENT_BY_ID, selectedContentId],
82-
[CONTENT.GET_CATEGORY_CONTENT_BY_ID, categoryId],
83-
[CATEGORY.GET_CATEGORIES],
84-
]);
81+
invalidateQueries([CONTENT.GET_CONTENT_BY_ID, selectedContentId]);
82+
invalidateQueries([CONTENT.GET_CATEGORY_CONTENT_BY_ID, categoryId]);
83+
invalidateQueries([CATEGORY.GET_CATEGORIES]);
84+
invalidateQueries([TOPIC.GET_ALL_CONTENTS]);
85+
invalidateQueries([TOPIC.GET_TOPIC_BOARD_BY_ID]);
8586
},
8687
});
8788
}
@@ -104,11 +105,11 @@ export default function ContentSidebar() {
104105
await removeContent(selectedContentId, {
105106
onSuccess: () => {
106107
onClose();
107-
invalidateMany([
108-
[CONTENT.GET_CONTENT_BY_ID, selectedContentId],
109-
[CONTENT.GET_CATEGORY_CONTENT_BY_ID, categoryId],
110-
[CATEGORY.GET_CATEGORIES],
111-
]);
108+
invalidateQueries([CONTENT.GET_CONTENT_BY_ID, selectedContentId]);
109+
invalidateQueries([CONTENT.GET_CATEGORY_CONTENT_BY_ID, categoryId]);
110+
invalidateQueries([CATEGORY.GET_CATEGORIES]);
111+
invalidateQueries([TOPIC.GET_ALL_CONTENTS]);
112+
invalidateQueries([TOPIC.GET_TOPIC_BOARD_BY_ID]);
112113
},
113114
});
114115
};

apps/web/src/components/(with-side-bar)/topic/context-menu-provider.tsx

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useEffect, useState } from "react";
1+
import React, { useCallback, useContext, useEffect, useState } from "react";
22

33
import {
44
ContextMenu,
@@ -7,6 +7,10 @@ import {
77
ContextMenuSeparator,
88
ContextMenuTrigger,
99
} from "@/components/common/context-menu";
10+
import { TopicContext } from "@/context/topic-context";
11+
import { useCreateContent } from "@/lib/tanstack/mutation/topic-content";
12+
import type { CategoryContentDTO } from "@linkyboard/types";
13+
import type { Node } from "@xyflow/react";
1014

1115
interface ContextMenuProviderProps {
1216
children: React.ReactNode;
@@ -24,32 +28,51 @@ export default function ContextMenuProvider({
2428
children,
2529
isTriggerDisabled,
2630
}: ContextMenuProviderProps) {
31+
const topicContext = useContext(TopicContext);
32+
if (!topicContext) {
33+
throw new Error("TopicContext not found");
34+
}
35+
36+
const { id, nodes, selectedNodeIds } = topicContext;
37+
2738
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
2839

29-
const onDuplicate = () => {
30-
console.log("Duplicate 실행");
31-
// 여기에 Duplicate 로직 추가
32-
};
40+
const { mutateAsync: createContent } = useCreateContent(id);
41+
42+
const onDuplicate = useCallback(() => {
43+
selectedNodeIds.forEach(async (nodeId) => {
44+
const node: Node | undefined = nodes.find((node) => node.id === nodeId);
45+
if (!node) return;
46+
const { item } = node.data as { item: CategoryContentDTO };
47+
48+
await createContent({
49+
topicId: id,
50+
contentId: item.id,
51+
posX: node.position.x + 100,
52+
posY: node.position.y + 100,
53+
});
54+
});
55+
}, [id, createContent, selectedNodeIds, nodes]);
3356

34-
const onLink = () => {
57+
const onLink = useCallback(() => {
3558
console.log("Link 실행");
3659
// 여기에 Link 로직 추가
37-
};
60+
}, []);
3861

39-
const onUnlink = () => {
62+
const onUnlink = useCallback(() => {
4063
console.log("Unlink 실행");
4164
// 여기에 Unlink 로직 추가
42-
};
65+
}, []);
4366

44-
const onSummary = () => {
67+
const onSummary = useCallback(() => {
4568
console.log("Summary 실행");
4669
// 여기에 Summary 로직 추가
47-
};
70+
}, []);
4871

49-
const onDelete = () => {
72+
const onDelete = useCallback(() => {
5073
console.log("Delete 실행");
5174
// 여기에 Delete 로직 추가
52-
};
75+
}, []);
5376

5477
const onCloseContextMenu = useCallback(() => {
5578
setIsContextMenuOpen(false);
@@ -90,11 +113,11 @@ export default function ContextMenuProvider({
90113
break;
91114
}
92115
},
93-
[onCloseContextMenu]
116+
[onCloseContextMenu, onDuplicate, onLink, onUnlink, onSummary, onDelete]
94117
);
95118

96119
useEffect(() => {
97-
if (!open) return;
120+
if (!isContextMenuOpen) return;
98121

99122
window.addEventListener("keydown", onKeyDown);
100123

apps/web/src/components/(with-side-bar)/topic/react-flow-canvas.tsx

Lines changed: 30 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,19 @@
11
"use client";
22

3-
import { useMemo } from "react";
3+
import { useContext, useMemo } from "react";
44

5+
import { TopicContext } from "@/context/topic-context";
56
import { useCreateContent } from "@/lib/tanstack/mutation/topic-content";
67
import { Spinner } from "@linkyboard/components";
78
import type { CategoryContentDTO } from "@linkyboard/types";
8-
import type { Connection, Edge, Node, NodeProps, NodeTypes } from "@xyflow/react";
9-
import {
10-
Background,
11-
type OnEdgesChange,
12-
type OnNodesChange,
13-
ReactFlow,
14-
ReactFlowProvider,
15-
useReactFlow,
16-
} from "@xyflow/react";
9+
import type { NodeProps, NodeTypes } from "@xyflow/react";
10+
import { Background, ReactFlow, ReactFlowProvider, useReactFlow } from "@xyflow/react";
1711

1812
import { AlertTriangle } from "lucide-react";
1913

2014
import ContextMenuProvider from "./context-menu-provider";
2115
import Sticker from "./sticker";
2216

23-
interface ReactFlowCanvasProps {
24-
isLoading: boolean;
25-
isTopicError: boolean;
26-
isNotFoundError: boolean;
27-
id: string;
28-
nodes: Node[];
29-
edges: Edge[];
30-
selectedNodeIds: string[];
31-
onNodesChange: OnNodesChange<Node>;
32-
onEdgesChange: OnEdgesChange<Edge>;
33-
onConnect: (params: Connection) => void;
34-
onEdgeClick: (e: React.MouseEvent, edge: Edge) => void;
35-
onNodeSelect: (nodeId: string) => void;
36-
}
37-
3817
const connectionLineStyle = {
3918
stroke: "#b1b1b7",
4019
strokeWidth: 3,
@@ -46,28 +25,35 @@ const defaultEdgeOptions = {
4625
},
4726
};
4827

49-
export default function ReactFlowCanvas(props: ReactFlowCanvasProps) {
28+
export default function ReactFlowCanvas() {
5029
return (
5130
<ReactFlowProvider>
52-
<FlowCanvas {...props} />
31+
<FlowCanvas />
5332
</ReactFlowProvider>
5433
);
5534
}
5635

57-
function FlowCanvas({
58-
isLoading,
59-
isTopicError,
60-
isNotFoundError,
61-
id,
62-
nodes,
63-
edges,
64-
onNodesChange,
65-
onEdgesChange,
66-
onConnect,
67-
onEdgeClick,
68-
selectedNodeIds,
69-
onNodeSelect,
70-
}: ReactFlowCanvasProps) {
36+
function FlowCanvas() {
37+
const topicContext = useContext(TopicContext);
38+
if (!topicContext) {
39+
throw new Error("TopicContext not found");
40+
}
41+
42+
const {
43+
isLoading,
44+
isTopicError,
45+
isNotFoundError,
46+
id,
47+
nodes,
48+
edges,
49+
selectedNodeIds,
50+
onNodesChange,
51+
onEdgesChange,
52+
onConnect,
53+
onEdgeClick,
54+
onNodeSelect,
55+
} = topicContext;
56+
7157
const { screenToFlowPosition } = useReactFlow();
7258
const { mutateAsync: createContent } = useCreateContent(id);
7359

@@ -135,13 +121,13 @@ function FlowCanvas({
135121
<ReactFlow
136122
nodes={nodes}
137123
edges={edges}
124+
nodeTypes={nodeTypes}
125+
connectionLineStyle={connectionLineStyle}
126+
defaultEdgeOptions={defaultEdgeOptions}
138127
onNodesChange={onNodesChange}
139128
onEdgesChange={onEdgesChange}
140129
onConnect={onConnect}
141130
onEdgeClick={onEdgeClick}
142-
nodeTypes={nodeTypes}
143-
connectionLineStyle={connectionLineStyle}
144-
defaultEdgeOptions={defaultEdgeOptions}
145131
>
146132
<Background />
147133
</ReactFlow>

apps/web/src/components/(with-side-bar)/topic/sticker/block-note.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useRouter } from "next/navigation";
99

1010
import { CUSTOM_STICKER } from "@/constants/custom-sticker";
1111
import { TOPIC } from "@/constants/topic";
12-
import { invalidateMany, invalidateQueries } from "@/lib/tanstack";
12+
import { invalidateQueries } from "@/lib/tanstack";
1313
import {
1414
useRemoveCustomSticker,
1515
useUpdateCustomSticker,
@@ -161,10 +161,8 @@ export default function BlockNote({ topicId, stickerId }: BlockNoteProps) {
161161
await removeCustomSticker(stickerId, {
162162
onSuccess: () => {
163163
successToast("스티커가 성공적으로 삭제되었어요.");
164-
invalidateMany([
165-
[TOPIC.GET_TOPIC_BOARD_BY_ID, topicId],
166-
[CUSTOM_STICKER.GET_CUSTOM_STICKER_BY_ID, stickerId],
167-
]);
164+
invalidateQueries([TOPIC.GET_TOPIC_BOARD_BY_ID, topicId]);
165+
invalidateQueries([CUSTOM_STICKER.GET_CUSTOM_STICKER_BY_ID, stickerId]);
168166
if (!topicStore.isOpen) {
169167
router.back();
170168
}

0 commit comments

Comments
 (0)