Skip to content

Commit 8b84ff0

Browse files
authored
[FEATURE] 토픽 보드 Context Menu 구현 (#63)
* chore: dev -> main으로 가는 PR에서는 빌드 워크플로우 작동 안하도록 설정 * fix: presigned image 로직 수정 * fix: 일부 기본 설정 변경 * chore: shadcn context menu 설치 * feat: context menu 사용할 수 있는 provider 컴포넌트 구현 * feat: 선택된 스티커가 없을 때에는 context menu 활성화 되지 않도록 구현
1 parent 1fb3521 commit 8b84ff0

10 files changed

Lines changed: 470 additions & 48 deletions

File tree

.github/workflows/check-build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55

66
jobs:
77
build:
8+
if: github.base_ref != 'main' || github.head_ref != 'dev'
89
runs-on: ubuntu-latest
910
steps:
1011
- uses: actions/checkout@v4

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@linkyboard/hooks": "workspace:*",
1818
"@linkyboard/utils": "workspace:*",
1919
"@next/third-parties": "^15.5.4",
20+
"@radix-ui/react-context-menu": "^2.2.16",
2021
"@tanstack/react-query": "^5.84.1",
2122
"@xyflow/react": "^12.8.2",
2223
"ky": "^1.8.2",

apps/web/src/components/(with-side-bar)/dashboard/dashboard-header.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,14 @@ export default function DashboardHeader() {
2727
infoToast("아직 준비 중 이에요.");
2828
};
2929

30-
const onLogout = async () => {
31-
try {
32-
// 쿠키 삭제
33-
await removeCookie("accessToken");
34-
await removeCookie("refreshToken");
35-
await removeCookie("loggedIn");
30+
const onLogout = () => {
31+
// 쿠키 삭제
32+
removeCookie("accessToken");
33+
removeCookie("refreshToken");
34+
removeCookie("loggedIn");
3635

37-
// 로그인 페이지로 리다이렉트
38-
router.push("/login");
39-
} catch (error) {
40-
console.error("로그아웃 중 오류 발생:", error);
41-
}
36+
// 로그인 페이지로 리다이렉트
37+
router.push("/login");
4238
};
4339

4440
const onUserButtonClick = () => {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import React, { useCallback, useEffect, useState } from "react";
2+
3+
import {
4+
ContextMenu,
5+
ContextMenuContent,
6+
ContextMenuItem,
7+
ContextMenuSeparator,
8+
ContextMenuTrigger,
9+
} from "@/components/common/context-menu";
10+
11+
interface ContextMenuProviderProps {
12+
children: React.ReactNode;
13+
isTriggerDisabled: boolean;
14+
}
15+
16+
const isMac = typeof window !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
17+
18+
const onEscapeContextMenu = () => {
19+
const escEvent = new KeyboardEvent("keydown", { key: "Escape" });
20+
document.dispatchEvent(escEvent);
21+
};
22+
23+
export default function ContextMenuProvider({
24+
children,
25+
isTriggerDisabled,
26+
}: ContextMenuProviderProps) {
27+
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
28+
29+
const onDuplicate = () => {
30+
console.log("Duplicate 실행");
31+
// 여기에 Duplicate 로직 추가
32+
};
33+
34+
const onLink = () => {
35+
console.log("Link 실행");
36+
// 여기에 Link 로직 추가
37+
};
38+
39+
const onUnlink = () => {
40+
console.log("Unlink 실행");
41+
// 여기에 Unlink 로직 추가
42+
};
43+
44+
const onSummary = () => {
45+
console.log("Summary 실행");
46+
// 여기에 Summary 로직 추가
47+
};
48+
49+
const onDelete = () => {
50+
console.log("Delete 실행");
51+
// 여기에 Delete 로직 추가
52+
};
53+
54+
const onCloseContextMenu = useCallback(() => {
55+
setIsContextMenuOpen(false);
56+
onEscapeContextMenu();
57+
}, []);
58+
59+
const onKeyDown = useCallback(
60+
(e: KeyboardEvent) => {
61+
const key = e.key.toLowerCase();
62+
const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;
63+
64+
switch (key) {
65+
case "d":
66+
if (!isModifierPressed) return;
67+
e.preventDefault();
68+
onDuplicate();
69+
onCloseContextMenu();
70+
break;
71+
case "l":
72+
e.preventDefault();
73+
onLink();
74+
onCloseContextMenu();
75+
break;
76+
case "u":
77+
e.preventDefault();
78+
onUnlink();
79+
onCloseContextMenu();
80+
break;
81+
case "s":
82+
e.preventDefault();
83+
onSummary();
84+
onCloseContextMenu();
85+
break;
86+
case "x":
87+
e.preventDefault();
88+
onDelete();
89+
onCloseContextMenu();
90+
break;
91+
}
92+
},
93+
[onCloseContextMenu]
94+
);
95+
96+
useEffect(() => {
97+
if (!open) return;
98+
99+
window.addEventListener("keydown", onKeyDown);
100+
101+
return () => {
102+
window.removeEventListener("keydown", onKeyDown);
103+
};
104+
}, [isContextMenuOpen, onKeyDown]);
105+
106+
return (
107+
<ContextMenu onOpenChange={setIsContextMenuOpen}>
108+
<ContextMenuTrigger asChild disabled={isTriggerDisabled}>
109+
{children}
110+
</ContextMenuTrigger>
111+
<ContextMenuContent className="min-w-60 rounded-2xl bg-[#333] p-5 text-white">
112+
<ContextMenuItem
113+
className="flex items-center justify-between rounded-xl px-4 py-1 focus:bg-[#7E4ED7] focus:text-white"
114+
onSelect={onDuplicate}
115+
>
116+
<p className="text-xl font-bold">Duplicate</p>
117+
<p className="font-light">{isMac ? "⌘D" : "Ctrl+D"}</p>
118+
</ContextMenuItem>
119+
<ContextMenuSeparator className="my-5" />
120+
<ContextMenuItem
121+
className="focus:bg-primary flex items-center justify-between rounded-xl px-4 py-1 focus:text-white"
122+
onSelect={onLink}
123+
>
124+
<p className="text-xl font-bold">Link</p>
125+
<p className="font-light">L</p>
126+
</ContextMenuItem>
127+
<ContextMenuItem
128+
className="focus:bg-primary flex items-center justify-between rounded-xl px-4 py-1 focus:text-white"
129+
onSelect={onUnlink}
130+
>
131+
<p className="text-xl font-bold">Unlink</p>
132+
<p className="font-light">U</p>
133+
</ContextMenuItem>
134+
<ContextMenuItem
135+
className="focus:bg-primary flex items-center justify-between rounded-xl px-4 py-1 focus:text-white"
136+
onSelect={onSummary}
137+
>
138+
<p className="text-xl font-bold">Summary</p>
139+
<p className="font-light">S</p>
140+
</ContextMenuItem>
141+
<ContextMenuItem
142+
className="focus:bg-primary flex items-center justify-between rounded-xl px-4 py-1 focus:text-white"
143+
onSelect={onDelete}
144+
>
145+
<p className="text-xl font-bold">Delete</p>
146+
<p className="font-light">X</p>
147+
</ContextMenuItem>
148+
</ContextMenuContent>
149+
</ContextMenu>
150+
);
151+
}

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

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717

1818
import { AlertTriangle } from "lucide-react";
1919

20+
import ContextMenuProvider from "./context-menu-provider";
2021
import Sticker from "./sticker";
2122

2223
interface ReactFlowCanvasProps {
@@ -70,6 +71,8 @@ function FlowCanvas({
7071
const { screenToFlowPosition } = useReactFlow();
7172
const { mutateAsync: createContent } = useCreateContent(id);
7273

74+
const isTriggerDisabled = selectedNodeIds.length === 0;
75+
7376
const nodeTypes: NodeTypes = useMemo(
7477
() => ({
7578
custom: (props: NodeProps) => (
@@ -111,37 +114,39 @@ function FlowCanvas({
111114
};
112115

113116
return (
114-
<div
115-
className="relative flex-1 overflow-hidden rounded-r-lg border border-l-0"
116-
onDragOver={onDragOver}
117-
onDrop={onDrop}
118-
>
119-
{isLoading ? (
120-
<div className="flex h-full w-full items-center justify-center">
121-
<Spinner className="text-muted-foreground size-16" />
122-
</div>
123-
) : isTopicError ? (
124-
<div className="flex h-full flex-col items-center justify-center gap-2">
125-
<AlertTriangle className="text-destructive size-16" />
126-
<p className="text-muted-foreground text-xl font-semibold">
127-
{isNotFoundError ? "토픽을 찾을 수 없어요." : "토픽을 불러오는데 실패했어요."}
128-
</p>
129-
</div>
130-
) : (
131-
<ReactFlow
132-
nodes={nodes}
133-
edges={edges}
134-
onNodesChange={onNodesChange}
135-
onEdgesChange={onEdgesChange}
136-
onConnect={onConnect}
137-
onEdgeClick={onEdgeClick}
138-
nodeTypes={nodeTypes}
139-
connectionLineStyle={connectionLineStyle}
140-
defaultEdgeOptions={defaultEdgeOptions}
141-
>
142-
<Background />
143-
</ReactFlow>
144-
)}
145-
</div>
117+
<ContextMenuProvider isTriggerDisabled={isTriggerDisabled}>
118+
<div
119+
className="relative flex-1 overflow-hidden rounded-r-lg border border-l-0"
120+
onDragOver={onDragOver}
121+
onDrop={onDrop}
122+
>
123+
{isLoading ? (
124+
<div className="flex h-full w-full items-center justify-center">
125+
<Spinner className="text-muted-foreground size-16" />
126+
</div>
127+
) : isTopicError ? (
128+
<div className="flex h-full flex-col items-center justify-center gap-2">
129+
<AlertTriangle className="text-destructive size-16" />
130+
<p className="text-muted-foreground text-xl font-semibold">
131+
{isNotFoundError ? "토픽을 찾을 수 없어요." : "토픽을 불러오는데 실패했어요."}
132+
</p>
133+
</div>
134+
) : (
135+
<ReactFlow
136+
nodes={nodes}
137+
edges={edges}
138+
onNodesChange={onNodesChange}
139+
onEdgesChange={onEdgesChange}
140+
onConnect={onConnect}
141+
onEdgeClick={onEdgeClick}
142+
nodeTypes={nodeTypes}
143+
connectionLineStyle={connectionLineStyle}
144+
defaultEdgeOptions={defaultEdgeOptions}
145+
>
146+
<Background />
147+
</ReactFlow>
148+
)}
149+
</div>
150+
</ContextMenuProvider>
146151
);
147152
}

0 commit comments

Comments
 (0)