From 2bf15c64a316b702a3481954a660295321c044cf Mon Sep 17 00:00:00 2001 From: Gwang Soo <114225974+gs0428@users.noreply.github.com> Date: Mon, 14 Jul 2025 22:10:54 +0900 Subject: [PATCH] =?UTF-8?q?SCRUM-238-QA=20=EB=B0=98=EC=98=81=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 북마크 탐색 페이지에서 그래프 제거 및 extension 아이콘 추가 * feat: 키워드 추가 시 입력 내용 기반 필터링 기능 추가 * feat: 로그아웃 버튼 우상단 메뉴로 이동 * refactor: ai 요약하기 버튼 위치 수정 * feat: 연관된 북마크 없을 때 현재 북마크만 보이게 구현 * fix: 기존 북마크 아님에도 수정 안되는 오류 해결 * feat: 북마크 생성 혹은 수정 시 현재 페이지에서 계속 수정할 수 있도록 로직 구현 * refactor: 테마 우상단 토글로 이동 및 색상, 그래프 종류 버튼 제거 * feat: 사이드바에서 카테고리 생성할 수 있도록 구현 * fix: 카테고리 추가 스타일 및 모달 로직 수정 * fix: 요약, 메모의 크기를 사용자가 조절하고, 2d 그래프의 기본 height 수정 * feat: 카테고리 중심으로 구체 군집화하도록 구현 * feat: 변경된 채팅 시작 로직 반영 * feat: 채팅 크기 조절 로직 구현 --- apps/extension/public/icon128.png | Bin 0 -> 2744 bytes apps/extension/src/assets/menu.svg | 3 + .../src/components/loading/index.tsx | 8 +- apps/extension/src/hooks/use-detect-path.ts | 12 +- apps/extension/src/index.css | 7 + apps/extension/src/pages/bookmark.tsx | 90 ++++--- apps/extension/src/pages/create-bookmark.tsx | 100 ++++---- apps/extension/src/state/mutation/star.ts | 13 +- apps/www/package.json | 2 + apps/www/src/app/api/chat/route.ts | 8 +- apps/www/src/app/bookmarks/page.tsx | 77 +----- apps/www/src/app/page/bookmarks/index.tsx | 87 +++++++ .../bookmarks/graph/chat-bot/chat.tsx | 174 +++++++------- .../bookmarks/graph/chat-bot/index.tsx | 219 ++++++++++++++++-- .../bookmarks/graph/graph-detail.tsx | 55 +++-- .../src/components/bookmarks/graph/index.tsx | 66 ++---- .../src/components/bookmarks/graph/planet.tsx | 87 ++----- .../components/bookmarks/sidebar/dropdown.tsx | 131 ++++++++--- .../components/bookmarks/sidebar/index.tsx | 190 +++++---------- .../src/components/bookmarks/tree/index.tsx | 1 - apps/www/src/components/common/icon.tsx | 14 ++ .../src/components/common/nav-dropdown.tsx | 52 ++++- .../src/components/layout/root-provider.tsx | 15 +- apps/www/src/components/ui/sonner.tsx | 26 +++ apps/www/src/constants/bookmark.ts | 10 - apps/www/src/lib/tanstack/mutation/chat.ts | 17 -- apps/www/src/lib/tanstack/mutation/star.ts | 3 +- apps/www/src/lib/tanstack/query/chat.ts | 10 +- apps/www/src/lib/zustand/bookmark.ts | 32 --- apps/www/src/models/chat.ts | 4 - apps/www/src/service/chat.ts | 14 +- apps/www/src/utils/params.ts | 31 +++ apps/www/src/utils/toast.ts | 13 ++ packages/ui/src/graph-2d/index.tsx | 4 +- packages/ui/src/index.ts | 1 + packages/ui/src/keyword/index.tsx | 12 +- packages/ui/src/modal/index.tsx | 7 +- packages/ui/src/spinner/index.tsx | 10 +- packages/ui/src/textarea/index.tsx | 10 +- packages/ui/utils/search.ts | 7 + pnpm-lock.yaml | 26 +++ 41 files changed, 982 insertions(+), 666 deletions(-) create mode 100644 apps/extension/public/icon128.png create mode 100644 apps/extension/src/assets/menu.svg create mode 100644 apps/www/src/app/page/bookmarks/index.tsx create mode 100644 apps/www/src/components/ui/sonner.tsx delete mode 100644 apps/www/src/lib/tanstack/mutation/chat.ts delete mode 100644 apps/www/src/lib/zustand/bookmark.ts create mode 100644 apps/www/src/utils/params.ts create mode 100644 apps/www/src/utils/toast.ts create mode 100644 packages/ui/utils/search.ts diff --git a/apps/extension/public/icon128.png b/apps/extension/public/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..be7a5e5e1bb953fe4d67e75c8b763f93e05e1808 GIT binary patch literal 2744 zcmcgu_gB-&7yX1KNCzPZf)I*+6<0tITp*B04blU+DAJX(bPP=h5NRU103peL0|ew3$Z!uqXa}S*P(ONZl?!8CN> z;=`87lD%Wah{g5krm)q|7n%yz{O4OME0^tj`KNOPDieoYDR_qzo8YAr!W>9Z#1h~Z zKjvPfpE_1NqLtICM?$wA6W_c9Z&Et$Rep(w|GEZ1Uepp_OM~9Ka zejIlabRX{CAAcBP=wP}SvSbU3-W0x56Ai*JcEM3wbHPmZi}VVIl0d4%Z~bQkDig>j zj*-i}1Jfy^6%)R|y2{b|wByAeXq1cGs)U1+;IFA;o?x9 zhi>k&p6(p-@x@-S$~H~S-`xwI^?~6uT6XOtK?R6Jh0#J7si?$_WTS`dB0nHc`y9RL z_HPZH4{<-HlSBlP2rV>MHrb{7WQ+`UiZtkL37mWeTzQdfJQ^(!Z1onW6LNat-kLlt z3dm{kjx6JUq8GZaa!V3$KYg!9CIgdUS(rOhZ6*Y;IA3d>k*TZS9yL7$Cl`il^Ys4t zWLC1u)V#V>H*RoLV@#+LgLxa|!G4{gVJ1T{m}B8&bp&@EI)QC?BKH$)(!pg+nDS#I8*SolA3oud=svp50wJ{?l6~4|kCWlWw zILDq?0Eg}TZHC^V_6sNm@}kOiHmbFTC>>}eK2d{@rT+BF&y_68bMg`imYn#mR3T~O zEQmS~VIVqX9gY>6468Y-+zd^esj`FSUJsUS4xFsFHH2g6o&^aVqj->a*o`DslpmcAR3Nq=Ush$p`<2cs#h7!yDU2I+iE_3VK+q?3l^1prw zq419t8LE&rStR}^nKRk?(e=lNen{<(LoOkY%T%2JT^>HWttCcpd*7i4|ifWxJ$}DOU{d-pEWt*&cK}ozCg? zzs{J)EkX0>Br$>U`tsKB$?%k!)IexN!r2KN+c})h))>6YermCm5>M{{i8%jU_AKS5W7L;h5ly}$!_XM-84aGnPx#o6 zLEf#dT?=Ac4Ci`R0Uw8SV4DaPFmte+Sqhqc&_$`Vk-Oa;quCrxKC|1aS6GX#c2{jZ z#_Qd+W{_KhlL@NdeZ-Hmd{*wa^ELX^BsK26z1yb#fq`FoJ(V`9)+{1*uo*Gi8Ch|_ zQt!A19CGL@ZO{3Hblk5fYwk*`xj0qtsgfLNCdvnG0vkDm$e~~BZlk7Xb21&*db#S5 zWZO!M_x|0?jQ`koI2V<>SJOG=(_`RU9Gi*fa0lBw#;(NW`>`X%Mvr|wl=A(sBgxz2NApPe@RF^ zpF_fzqO0%EA74LM#GIeh!Jef44neXGYsj8!8p-zGYJ9YetBIxPg~1#RkC4qHTCyuo z7$QCiv1UhzZ%^9^rZALD(jY}l4s(hDd1e3MG4Dq55_7WNO-RN38J4M>12hVSEy`vD zrr3Y!;7WdC=IIP4#WkzxaO1M|Z6&dUDb=w_!wkhe9c(0=8fI1Jb+3#|9hL2qB{7Rf ze?R=RA?ESxqQxpR3jSR0rw`_Rm9K9Mj?P0XPqY@bZdKbBxLM!G-SrzQ65&qH)HGvX zP*~`(xOy4){a)sLJz|Ar)@qy3$5deS4V%k0vw4cx?Q5C6^y)8S;1g- zoz;vF7E^5X=E3sBfUlSLG)ZX|+)hU)kkSD$pL8u&!dhVC#@Qe0eh@lkWSx5z)ETuQ_|=>1ES78C zL)>QCLd6VY_pze#`njt5L{(i%)lGJPx^izwuc#QZ%E}0=c#czr>O+C}k=(5mHp~h5 c&j3Tr>AZpEoj6wouF(W;n%SDx8+#`G0|A9SzW@LL literal 0 HcmV?d00001 diff --git a/apps/extension/src/assets/menu.svg b/apps/extension/src/assets/menu.svg new file mode 100644 index 0000000..7ff1068 --- /dev/null +++ b/apps/extension/src/assets/menu.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/extension/src/components/loading/index.tsx b/apps/extension/src/components/loading/index.tsx index 5f90798..f8e3684 100644 --- a/apps/extension/src/components/loading/index.tsx +++ b/apps/extension/src/components/loading/index.tsx @@ -3,15 +3,19 @@ import { Spinner } from "@repo/ui"; interface LoadingProps { title: string; + description?: string; children: React.ReactNode; } -const Loading = ({ title, children }: LoadingProps) => { +const Loading = ({ title, description, children }: LoadingProps) => { const isLoading = useLoadingStore((state) => state.isLoading); return isLoading ? (
- {title} +
+

{title}

+

{description}

+
) : ( diff --git a/apps/extension/src/hooks/use-detect-path.ts b/apps/extension/src/hooks/use-detect-path.ts index 8559f83..403e97d 100644 --- a/apps/extension/src/hooks/use-detect-path.ts +++ b/apps/extension/src/hooks/use-detect-path.ts @@ -2,6 +2,7 @@ import { useEffect } from "react"; import { useStarStore } from "@/state/zustand/star"; import { useTabStore } from "@/state/zustand/tab"; +import { useUserStore } from "@/state/zustand/user"; import { updateCurrentTab } from "@/utils/chrome"; import { useReplaceNavigate } from "./use-replace-navigate"; @@ -24,6 +25,7 @@ export function useDetectPath() { const { currentTab, setCurrentTab, setIsFindingExistPath } = useTabStore(); const stars = useStarStore((state) => state.stars); + const isLoggedIn = useUserStore((state) => state.isLoggedIn); const navigate = useReplaceNavigate(); @@ -36,8 +38,8 @@ export function useDetectPath() { }; const onTabUpdated = (_: number, changeInfo: TabChangeInfoProps) => { - setIsFindingExistPath(true); - if (changeInfo.status === "complete") { + if (changeInfo.url && changeInfo.status === "loading") { + setIsFindingExistPath(true); updateCurrentTab(setCurrentTab); } }; @@ -52,6 +54,10 @@ export function useDetectPath() { }, []); useEffect(() => { + if (!isLoggedIn) { + return navigate("/"); + } + const findStar = stars?.starListDto.find((star) => encodeURI(star.siteUrl) === currentTab.url); if (findStar) { @@ -60,7 +66,7 @@ export function useDetectPath() { navigate("/bookmark"); } setIsFindingExistPath(false); - }, [stars, currentTab]); + }, [stars, currentTab, isLoggedIn]); return { currentTab }; } diff --git a/apps/extension/src/index.css b/apps/extension/src/index.css index 5516b0d..f9b2a9c 100644 --- a/apps/extension/src/index.css +++ b/apps/extension/src/index.css @@ -53,3 +53,10 @@ textarea { } outline: none; } + +button { + cursor: pointer; + &:disabled { + cursor: not-allowed; + } +} diff --git a/apps/extension/src/pages/bookmark.tsx b/apps/extension/src/pages/bookmark.tsx index ca7f8a1..fb92624 100644 --- a/apps/extension/src/pages/bookmark.tsx +++ b/apps/extension/src/pages/bookmark.tsx @@ -1,12 +1,12 @@ -import { useMemo } from "react"; +import { useState } from "react"; +import Logo from "@/assets/logo.svg?react"; +import Menu from "@/assets/menu.svg?react"; import Loading from "@/components/loading"; import { useCreateStar } from "@/state/mutation/star"; -import { useStarStore } from "@/state/zustand/star"; import { useTabStore } from "@/state/zustand/tab"; import { getHtmlText } from "@/utils/chrome"; -import { Graph2D } from "@repo/ui"; -import { RectangleButton } from "@repo/ui"; +import { Modal, RectangleButton, useOutsideClick } from "@repo/ui"; import { useNavigate } from "react-router-dom"; @@ -14,27 +14,13 @@ const url = import.meta.env.VITE_BASE_URL; const Bookmark = () => { const { currentTab, isFindingExistPath } = useTabStore(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isLogoutModalOpen, setIsLogoutModalOpen] = useState(false); + + const [menuContainerRef] = useOutsideClick(() => setIsMenuOpen(false)); const navigate = useNavigate(); const { mutateAsync } = useCreateStar(); - const stars = useStarStore((state) => state.stars); - - const graphData = useMemo(() => { - if (!stars?.starListDto || !stars?.linkListDto) return { nodes: [], links: [] }; - - return { - nodes: stars.starListDto.map((star) => ({ - id: star.starId, - name: star.title, - val: Math.min(star.views, 10), - url: star.siteUrl, - })), - links: stars.linkListDto.map((link) => ({ - source: link.linkedNodeIdList[0], - target: link.linkedNodeIdList[1], - })), - }; - }, [stars]); const onClickLogout = async () => { await chrome.cookies.remove({ @@ -65,13 +51,38 @@ const Bookmark = () => { }); }; + const onToggleMenu = (e: React.MouseEvent) => { + e.stopPropagation(); + setIsMenuOpen((prev) => !prev); + }; + + const onOpenLogoutModal = (e: React.MouseEvent) => { + e.stopPropagation(); + setIsMenuOpen(false); + setIsLogoutModalOpen(true); + }; + return ( - + +
+ +
+ + {isMenuOpen && ( +
+ +
+ )} +
+
-
- -

현재 페이지 정보

-

URL

@@ -82,15 +93,24 @@ const Bookmark = () => {

{currentTab.title}

-
- - {isFindingExistPath ? "기존 북마크(추가 불가)" : "북마크에 추가하기"} - - - 로그아웃 - -
+ + {isFindingExistPath ? "기존 북마크(추가 불가)" : "북마크에 추가하기"} +
+ {isLogoutModalOpen && ( + setIsLogoutModalOpen(false)} + > +
+ setIsLogoutModalOpen(false)} variation="outline"> + 취소 + + 로그아웃 +
+
+ )}
); }; diff --git a/apps/extension/src/pages/create-bookmark.tsx b/apps/extension/src/pages/create-bookmark.tsx index edd75a4..3c93a8b 100644 --- a/apps/extension/src/pages/create-bookmark.tsx +++ b/apps/extension/src/pages/create-bookmark.tsx @@ -4,15 +4,15 @@ import AISummary from "@/assets/ai-summary.svg?react"; import Logo from "@/assets/logo.svg?react"; import CardWrapper from "@/components/create-bookmark/card-wrapper"; import Loading from "@/components/loading"; +import { useReplaceNavigate } from "@/hooks/use-replace-navigate"; import { useCreateCategory } from "@/state/mutation/category"; import { useCompleteCreateStar, useUpdateStar } from "@/state/mutation/star"; import { useGetKeywords } from "@/state/query/keyword"; import { useGetStarById } from "@/state/query/star"; import { useStarStore } from "@/state/zustand/star"; -import { useTabStore } from "@/state/zustand/tab"; import { CategoryListProps } from "@/types/category"; import { CompleteSummarizeStarProps } from "@/types/star"; -import { Graph2D } from "@repo/ui"; +import { cn, Graph2D } from "@repo/ui"; import { Keyword, RectangleButton, Spinner, Textarea } from "@repo/ui"; import { Navigate, useLocation, useSearchParams } from "react-router-dom"; @@ -41,9 +41,10 @@ const CreateBookmark = () => { const [searchParams] = useSearchParams(); const id = searchParams.get("id"); + // 추후 AI 요약 기능 추가 시 tanstack query의 isPending 사용 + const [isAISummaryPending, setIsAISummaryPending] = useState(false); const [bookmark, setBookmark] = useState(Object.assign(DEFAULT_BOOKMARK, state)); const stars = useStarStore((state) => state.stars); - const setIsFindingExistPath = useTabStore((state) => state.setIsFindingExistPath); const { mutateAsync: mutateAsyncCompleteCreateStar } = useCompleteCreateStar(); const { mutateAsync: mutateAsyncUpdateStar } = useUpdateStar(); @@ -52,6 +53,8 @@ const CreateBookmark = () => { const { data: keywords } = useGetKeywords(); const { data: star, isLoading: isLoadingGetStar } = useGetStarById(id); + const navigate = useReplaceNavigate(); + useEffect(() => { if (!isLoadingGetStar && star?.result) { setBookmark((prev) => ({ @@ -77,13 +80,31 @@ const CreateBookmark = () => { // 관련된 노드만 필터링 const relatedNodes = stars.starListDto.filter((star) => relatedNodeIds.has(star.starId)); + // 현재 북마크 정보 가져오기 + const currentStar = stars.starListDto.find((star) => star.starId === id); + + // 연관된 노드가 없으면 현재 북마크만 포함 + const nodes = + relatedNodes.length > 0 + ? relatedNodes.map((star) => ({ + id: star.starId, + name: star.title, + val: Math.min(star.views, 10), + url: star.siteUrl, + })) + : currentStar + ? [ + { + id: currentStar.starId, + name: currentStar.title, + val: 10, + url: currentStar.siteUrl, + }, + ] + : []; + return { - nodes: relatedNodes.map((star) => ({ - id: star.starId, - name: star.title, - val: Math.min(star.views, 10), - url: star.siteUrl, - })), + nodes, links: relatedLinks.map((link) => ({ source: link.linkedNodeIdList[0], target: link.linkedNodeIdList[1], @@ -100,8 +121,6 @@ const CreateBookmark = () => { return ; } - const saveDisabled = bookmark.categoryName.trim() === ""; - const onSelectCategory = (category: string) => { setBookmark((prev) => ({ ...prev, categoryName: category })); }; @@ -144,38 +163,22 @@ const CreateBookmark = () => { }; const onClickSave = async () => { - // id가 있으면 수정하기 API 요청 - const categoryName = bookmark.categories.find( - (category) => category.name === bookmark.categoryName - )?.name; - - if (!categoryName) { - return; - } - const body = { ...bookmark, keywordList: bookmark.keywords, }; if (id) { - await mutateAsyncUpdateStar( - { id, body }, - { - onSuccess: () => { - setIsFindingExistPath(true); - }, - } - ); + await mutateAsyncUpdateStar({ id, body }); } else { - await mutateAsyncCompleteCreateStar(body, { - onSuccess: () => { - setIsFindingExistPath(true); - }, - }); + await mutateAsyncCompleteCreateStar(body); } }; + const onClickCancel = () => { + navigate("/bookmark"); + }; + if (isLoadingGetStar) { return (
@@ -188,15 +191,9 @@ const CreateBookmark = () => { return (
-
-
- -

Nebula

-
- +
+ +

Nebula

{id && (
@@ -220,6 +217,17 @@ const CreateBookmark = () => { value={bookmark.summaryAI} onChange={onChangeText} placeholder="북마크에 대한 요약을 작성할 수 있어요." + rightElement={ + + } />