Skip to content

Commit 4ee9918

Browse files
authored
SCRUM-223-익스텐션 화면 로직 수정 (#35)
* feat: 북마크 관련 공통 타입 packages/types로 분리 * feat: layout에서 북마크 데이터 받고 저장하는 로직 구현 * feat: extension에서 url 변경 감지해서 라우팅 시키는 커스텀 훅 구현 및 적용 * feat: 이미 요약했던 url이면 수정하러 들어오도록 로직 구현 * feat: 수정 모드일 때 수정 API 요청하도록 로직 구현 * chore: stagewise 세팅 * fix: 버튼 크게 보이는 UI 수정 * feat: 북마크 추가하기 페이지에서 기존에 추가해둔 북마크들 2d graph 형식으로 보여주도록 구현 * feat: 2D 그래프 컴포넌트 공통으로 분리 및 노드 선택 시 해당 url로 이동하는 기능 구현 * fix: 코드 리뷰 반영 * fix: window 예외처리 * fix: 컴포넌트 분리 및 next/dynamic을 이용하여 window is not defined 오류 해결
1 parent 9dc6bd4 commit 4ee9918

39 files changed

Lines changed: 752 additions & 231 deletions

File tree

apps/extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"lettercoder": "^0.1.1",
1616
"react": "^18.3.1",
1717
"react-dom": "^18.3.1",
18+
"react-force-graph-2d": "^1.27.1",
1819
"react-router-dom": "^6.28.0",
1920
"zustand": "^5.0.1"
2021
},

apps/extension/src/App.tsx

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { HashRouter as Router, Route, Routes } from "react-router-dom";
22

33
import Layout from "./components/layout";
4+
import StarLayout from "./components/layout/star-layout";
45
import Agreement from "./pages/agreement";
56
import Bookmark from "./pages/bookmark";
67
import CreateBookmark from "./pages/create-bookmark";
@@ -12,28 +13,30 @@ function App() {
1213
<Router>
1314
<Routes>
1415
<Route element={<Layout />}>
15-
<Route path="/" element={<Home />} />
16-
<Route path="/agreement" element={<Agreement />} />
17-
<Route path="/bookmark" element={<Bookmark />} />
18-
<Route path="/create-bookmark" element={<CreateBookmark />} />
19-
<Route
20-
path="/no-summary"
21-
element={
22-
<ErrorPage
23-
title="생성된 북마크 요약이 없습니다."
24-
subtitle="북마크를 요약하러 가볼까요?"
25-
/>
26-
}
27-
/>
28-
<Route
29-
path="*"
30-
element={
31-
<ErrorPage
32-
title="요청하신 경로가 없습니다."
33-
subtitle="경로를 다시 한 번 확인해주세요."
34-
/>
35-
}
36-
/>
16+
<Route element={<StarLayout />}>
17+
<Route path="/" element={<Home />} />
18+
<Route path="/agreement" element={<Agreement />} />
19+
<Route path="/bookmark" element={<Bookmark />} />
20+
<Route path="/create-bookmark" element={<CreateBookmark />} />
21+
<Route
22+
path="/no-summary"
23+
element={
24+
<ErrorPage
25+
title="생성된 북마크 요약이 없습니다."
26+
subtitle="북마크를 요약하러 가볼까요?"
27+
/>
28+
}
29+
/>
30+
<Route
31+
path="*"
32+
element={
33+
<ErrorPage
34+
title="요청하신 경로가 없습니다."
35+
subtitle="경로를 다시 한 번 확인해주세요."
36+
/>
37+
}
38+
/>
39+
</Route>
3740
</Route>
3841
</Routes>
3942
</Router>

apps/extension/src/apis/api-v1.ts

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,3 @@ export const apiV1 = axios.create({
66
baseURL,
77
withCredentials: true,
88
});
9-
10-
apiV1.interceptors.response.use(
11-
(response) => response,
12-
async (error) => {
13-
const originalRequest = error.config;
14-
15-
if (
16-
error.response &&
17-
error.response.data &&
18-
error.response.data.code === "TOKEN4001" &&
19-
!originalRequest._retry
20-
) {
21-
originalRequest._retry = true;
22-
23-
try {
24-
const { data } = await apiV1.get("/oauth/reissue", {
25-
withCredentials: true,
26-
});
27-
28-
await chrome.cookies.set({
29-
url: baseURL,
30-
name: "accessToken",
31-
value: data.result.accessToken,
32-
path: "/",
33-
httpOnly: true,
34-
});
35-
36-
return apiV1(originalRequest);
37-
} catch (refreshError) {
38-
return Promise.reject(refreshError);
39-
}
40-
}
41-
return Promise.reject(error);
42-
}
43-
);

apps/extension/src/apis/api-v2.ts

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,3 @@ export const apiV2 = axios.create({
66
baseURL,
77
withCredentials: true,
88
});
9-
10-
apiV2.interceptors.response.use(
11-
(response) => response,
12-
async (error) => {
13-
const originalRequest = error.config;
14-
15-
if (
16-
error.response &&
17-
error.response.data &&
18-
error.response.data.code === "TOKEN4001" &&
19-
!originalRequest._retry
20-
) {
21-
originalRequest._retry = true;
22-
23-
try {
24-
const { data } = await apiV2.get("/oauth/reissue", {
25-
withCredentials: true,
26-
});
27-
28-
await chrome.cookies.set({
29-
url: baseURL,
30-
name: "accessToken",
31-
value: data.result.accessToken,
32-
path: "/",
33-
httpOnly: true,
34-
});
35-
36-
return apiV2(originalRequest);
37-
} catch (refreshError) {
38-
return Promise.reject(refreshError);
39-
}
40-
}
41-
return Promise.reject(error);
42-
}
43-
);
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { lazy, Suspense, useEffect, useRef } from "react";
3+
4+
const ForceGraph2D = lazy(() => import("react-force-graph-2d"));
5+
6+
interface NodeType {
7+
id: string;
8+
name: string;
9+
val: number;
10+
x?: number;
11+
y?: number;
12+
url?: string;
13+
}
14+
15+
interface LinkType {
16+
source: string;
17+
target: string;
18+
}
19+
20+
interface ForceGraphProps {
21+
graphData: {
22+
nodes: NodeType[];
23+
links: LinkType[];
24+
};
25+
width?: number;
26+
height?: number;
27+
}
28+
29+
const ForceGraph = ({ graphData, width = 500, height = 300 }: ForceGraphProps) => {
30+
const fgRef = useRef<any>(null);
31+
32+
useEffect(() => {
33+
if (fgRef.current) {
34+
fgRef.current.d3Force("link").distance(120);
35+
fgRef.current.zoom(0.7);
36+
}
37+
}, [graphData]);
38+
39+
const onNodeClick = (node: any) => {
40+
if (node.url) {
41+
window.open(node.url, "_blank");
42+
}
43+
};
44+
45+
return (
46+
<div className="relative h-[300px] w-full overflow-hidden rounded-lg border border-gray3 bg-white">
47+
<Suspense
48+
fallback={
49+
<div className="flex h-full items-center justify-center">그래프를 불러오는 중...</div>
50+
}
51+
>
52+
<ForceGraph2D
53+
ref={fgRef}
54+
graphData={graphData}
55+
nodeLabel="name"
56+
nodeColor={() => "#1A1A1A"}
57+
linkColor={() => "#E5E5E5"}
58+
nodeRelSize={3}
59+
linkWidth={1}
60+
width={width}
61+
height={height}
62+
cooldownTicks={30}
63+
backgroundColor="white"
64+
nodeCanvasObject={(node, ctx, globalScale) => {
65+
if (typeof node.x === "undefined" || typeof node.y === "undefined") return;
66+
67+
const maxLabelLength = 15;
68+
const label =
69+
node.name.length > maxLabelLength
70+
? node.name.slice(0, maxLabelLength) + "..."
71+
: node.name;
72+
const fontSize = 12 / globalScale;
73+
const nodeSize = 5;
74+
75+
// 동그라미 그리기
76+
ctx.beginPath();
77+
ctx.arc(node.x, node.y, nodeSize, 0, 2 * Math.PI);
78+
ctx.fillStyle = "#1A1A1A";
79+
ctx.fill();
80+
81+
// 텍스트 배경
82+
ctx.font = `${fontSize}px Sans-Serif`;
83+
const textWidth = ctx.measureText(label).width;
84+
const bckgDimensions = [textWidth, fontSize].map((n) => n + fontSize * 0.2);
85+
86+
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
87+
ctx.fillRect(
88+
node.x - bckgDimensions[0] / 2,
89+
node.y + nodeSize + 2,
90+
bckgDimensions[0],
91+
bckgDimensions[1]
92+
);
93+
94+
// 텍스트 그리기
95+
ctx.textAlign = "center";
96+
ctx.textBaseline = "middle";
97+
ctx.fillStyle = "#1A1A1A";
98+
ctx.fillText(label, node.x, node.y + nodeSize + 2 + bckgDimensions[1] / 2);
99+
}}
100+
onNodeClick={onNodeClick}
101+
d3AlphaDecay={0.02}
102+
d3VelocityDecay={0.3}
103+
minZoom={0.5}
104+
maxZoom={2}
105+
enablePointerInteraction={true}
106+
enableZoomInteraction={true}
107+
onEngineStop={() => {
108+
const canvas = document.querySelector("canvas");
109+
if (canvas) {
110+
canvas.style.willChange = "auto";
111+
}
112+
}}
113+
/>
114+
</Suspense>
115+
</div>
116+
);
117+
};
118+
119+
export default ForceGraph;

apps/extension/src/components/layout/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { useEffect } from "react";
22

3+
import { useDetectPath } from "@/hooks/use-detect-path";
34
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
45

56
import { Outlet } from "react-router-dom";
67

78
export const queryClient = new QueryClient();
89

910
const Layout = () => {
11+
useDetectPath();
12+
1013
useEffect(() => {
1114
window.Kakao.init(import.meta.env.VITE_KAKAO_JS_KEY);
1215
}, []);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useEffect } from "react";
2+
3+
import { useGetStars } from "@/state/query/star";
4+
import { useStarStore } from "@/state/zustand/star";
5+
import { useUserStore } from "@/state/zustand/user";
6+
7+
import { Outlet } from "react-router-dom";
8+
9+
const StarLayout = () => {
10+
const { isLoggedIn, setIsLoggedIn } = useUserStore();
11+
const { setStars } = useStarStore();
12+
const { data, isPending, refetch } = useGetStars(isLoggedIn);
13+
14+
useEffect(() => {
15+
(async function checkLogin() {
16+
try {
17+
const token = await chrome.cookies.get({
18+
url: import.meta.env.VITE_BASE_URL,
19+
name: "accessToken",
20+
});
21+
if (token) {
22+
setIsLoggedIn(true);
23+
refetch();
24+
}
25+
} catch (error) {
26+
console.error("Error checking initial token:", error);
27+
}
28+
})();
29+
}, []);
30+
31+
useEffect(() => {
32+
if (isLoggedIn && !isPending) {
33+
setStars(data?.result ?? null);
34+
}
35+
}, [isLoggedIn, isPending, data]);
36+
37+
return <Outlet />;
38+
};
39+
40+
export default StarLayout;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export enum CATEGORY {
2-
GET = "get-categories",
2+
GET = "GET_CATEGORIES",
33
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum STAR {
2+
GET_ALL = "GET_STARS",
3+
GET_BY_ID = "GET_STAR_BY_ID",
4+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export enum TERM {
2-
CONTENT = "term-content",
2+
CONTENT = "TERM_CONTENT",
33
}

0 commit comments

Comments
 (0)