diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..69051a7 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## 개요 + + +- + +## 작업 내용 + + +- + +## 검증 결과 + +- **Test Files**: +- **Tests**: +- [ ] `tsc --noEmit` 통과 +- [ ] `biome check` (또는 `lint/format`) 통과 + +## 비고 + + +- diff --git a/apps/extension/domain/bookmarks/quick-bookmark.ts b/apps/extension/domain/bookmarks/quick-bookmark.ts new file mode 100644 index 0000000..d63fcb6 --- /dev/null +++ b/apps/extension/domain/bookmarks/quick-bookmark.ts @@ -0,0 +1,18 @@ +import { browser } from 'wxt/browser'; + +const POPUP_SIZE = { width: 440, height: 620 }; + +/** 대상 페이지의 제목/URL을 쿼리로 실어 북마크 추가 팝업 URL을 생성. */ +export function buildQuickBookmarkUrl(targetUrl: string, title: string): string { + const params = new URLSearchParams({ url: targetUrl, title }); + return browser.runtime.getURL(`/quick-bookmark.html?${params.toString()}`); +} + +/** 대상 페이지 정보를 실어 북마크 추가 팝업 창을 연다. */ +export function openQuickBookmarkPopup(targetUrl: string, title: string): void { + browser.windows.create({ + url: buildQuickBookmarkUrl(targetUrl, title), + type: 'popup', + ...POPUP_SIZE, + }); +} diff --git a/apps/extension/entrypoints/background/context-menu.ts b/apps/extension/entrypoints/background/context-menu.ts index b594330..072d22d 100644 --- a/apps/extension/entrypoints/background/context-menu.ts +++ b/apps/extension/entrypoints/background/context-menu.ts @@ -1,13 +1,8 @@ import { browser } from 'wxt/browser'; +import { openQuickBookmarkPopup } from '@/domain/bookmarks/quick-bookmark'; const CONTEXT_MENU_ID = 'dookmark-add-bookmark'; -// 대상 페이지의 제목/URL을 쿼리로 실어 북마크 추가 팝업 URL을 생성. -function setQuickBookmarkPopupUrl(targetUrl: string, title: string): string { - const params = new URLSearchParams({ url: targetUrl, title }); - return browser.runtime.getURL(`/quick-bookmark.html?${params.toString()}`); -} - export function setupContextMenu() { if (!browser.contextMenus) return; @@ -30,11 +25,6 @@ export function setupContextMenu() { const title = info.linkUrl ? '' : tab?.title || ''; if (!targetUrl) return; - browser.windows.create({ - url: setQuickBookmarkPopupUrl(targetUrl, title), - type: 'popup', - width: 440, - height: 620, - }); + openQuickBookmarkPopup(targetUrl, title); }); } diff --git a/apps/extension/entrypoints/quick-bookmark/App.tsx b/apps/extension/entrypoints/quick-bookmark/App.tsx index c6630a8..a513cbc 100644 --- a/apps/extension/entrypoints/quick-bookmark/App.tsx +++ b/apps/extension/entrypoints/quick-bookmark/App.tsx @@ -48,6 +48,26 @@ async function saveMetadataWithRetry( } export function CreateApp() { + useEffect(() => { + const syncTheme = () => { + const savedTheme = localStorage.getItem('theme') || 'system'; + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const isDark = savedTheme === 'dark' || (savedTheme === 'system' && systemPrefersDark); + document.documentElement.classList.toggle('dark', isDark); + }; + + syncTheme(); + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', syncTheme); + window.addEventListener('storage', syncTheme); + + return () => { + mediaQuery.removeEventListener('change', syncTheme); + window.removeEventListener('storage', syncTheme); + }; + }, []); + const params = useMemo(() => new URLSearchParams(window.location.search), []); const [title, setTitle] = useState(params.get('title') ?? ''); diff --git a/apps/extension/entrypoints/sidepanel/pages/main.tsx b/apps/extension/entrypoints/sidepanel/pages/main.tsx index 648fcc7..bc838d3 100644 --- a/apps/extension/entrypoints/sidepanel/pages/main.tsx +++ b/apps/extension/entrypoints/sidepanel/pages/main.tsx @@ -1,7 +1,7 @@ import { browser } from 'wxt/browser'; +import { openQuickBookmarkPopup } from '@/domain/bookmarks/quick-bookmark'; import { useLocalDnd } from '@/domain/dnd/use-local-dnd'; import { useSidepanel } from '@/entrypoints/sidepanel/providers/sidepanel-provider'; -import { useTheme } from '@/entrypoints/sidepanel/providers/theme-provider'; import { useNavigation } from '@/entrypoints/sidepanel/shared/hooks/use-navigation'; import { SidepanelContent } from '@/entrypoints/sidepanel/ui/shared/sidepanel-content'; import { SidepanelFooter } from '@/entrypoints/sidepanel/ui/shared/sidepanel-footer'; @@ -14,7 +14,6 @@ const WEB_BASE_URL = 'http://localhost:3000'; export function MainPage() { const nav = useNavigation(); - const { theme, toggleTheme } = useTheme(); const { loading, searchQuery, @@ -32,7 +31,7 @@ export function MainPage() { return ( } + header={} main={ { browser.tabs.create({ url: `${WEB_BASE_URL}/dashboard` }); }} + onAddClick={async () => { + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); + if (tab?.url) openQuickBookmarkPopup(tab.url, tab.title ?? ''); + }} > {!searchQuery && ( diff --git a/apps/extension/entrypoints/sidepanel/providers/theme-provider.tsx b/apps/extension/entrypoints/sidepanel/providers/theme-provider.tsx index 7f271cd..64d55bf 100644 --- a/apps/extension/entrypoints/sidepanel/providers/theme-provider.tsx +++ b/apps/extension/entrypoints/sidepanel/providers/theme-provider.tsx @@ -1,7 +1,9 @@ +'use client'; + import type React from 'react'; import { createContext, useContext, useEffect, useState } from 'react'; -type Theme = 'light' | 'dark'; +export type Theme = 'light' | 'dark' | 'system'; interface ThemeContextType { theme: Theme; @@ -12,28 +14,63 @@ interface ThemeContextType { const ThemeContext = createContext(undefined); export function ThemeProvider({ children }: { children: React.ReactNode }) { - const [theme, setTheme] = useState('light'); + const [theme, setTheme] = useState('system'); + const [systemPrefersDark, setSystemPrefersDark] = useState(false); - // Load saved theme on mount + // Load saved theme, sync system theme and setup storage listeners on mount useEffect(() => { - const savedTheme = localStorage.getItem('theme') as Theme | null; - const initialTheme = savedTheme || 'light'; - setTheme(initialTheme); - document.documentElement.classList.toggle('dark', initialTheme === 'dark'); + const loadTheme = () => { + const savedTheme = localStorage.getItem('theme') as Theme | null; + if (savedTheme) { + setTheme(savedTheme); + } else { + localStorage.setItem('theme', 'system'); + } + }; + + loadTheme(); + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + setSystemPrefersDark(mediaQuery.matches); + + const handleSystemThemeChange = (e: MediaQueryListEvent) => { + setSystemPrefersDark(e.matches); + }; + + // 다른 익스텐션 탭/창에서 테마 변경 시 동기화 + const handleStorageChange = (e: StorageEvent) => { + if (e.key === 'theme') { + loadTheme(); + } + }; + + mediaQuery.addEventListener('change', handleSystemThemeChange); + window.addEventListener('storage', handleStorageChange); + + return () => { + mediaQuery.removeEventListener('change', handleSystemThemeChange); + window.removeEventListener('storage', handleStorageChange); + }; }, []); - // Sync class list and localStorage whenever theme state changes + const isDark = theme === 'dark' || (theme === 'system' && systemPrefersDark); + + // Declarative side-effect to sync document class and localStorage + useEffect(() => { + document.documentElement.classList.toggle('dark', isDark); + localStorage.setItem('theme', theme); + }, [theme, isDark]); + const toggleTheme = () => { - const nextTheme = theme === 'light' ? 'dark' : 'light'; - setTheme(nextTheme); - localStorage.setItem('theme', nextTheme); - document.documentElement.classList.toggle('dark', nextTheme === 'dark'); + setTheme((prev) => { + if (prev === 'system') return 'light'; + if (prev === 'light') return 'dark'; + return 'system'; + }); }; return ( - - {children} - + {children} ); } diff --git a/apps/extension/entrypoints/sidepanel/ui/auth/login-required.tsx b/apps/extension/entrypoints/sidepanel/ui/auth/login-required.tsx index e78c076..e189932 100644 --- a/apps/extension/entrypoints/sidepanel/ui/auth/login-required.tsx +++ b/apps/extension/entrypoints/sidepanel/ui/auth/login-required.tsx @@ -11,9 +11,13 @@ export function LoginRequired({ onOpenLoginTab }: LoginRequiredProps) { - 스마트한 북마크 관리 + + 실시간 크로스 브라우저 북마크 동기화 서비스 + - 북마크를 인공지능으로 지능화하여 한눈에 관리하고 탐색해 보세요. + 여러 브라우저의 북마크를 한곳에서 관리하고 + + 어디서든 빠르게 찾아보세요. void; -} +export function SidepanelHeader() { + const { toggleTheme, isDark } = useTheme(); -export function SidepanelHeader({ theme, onToggleTheme }: SidepanelHeaderProps) { return ( - {theme === 'light' ? ( - - ) : ( + {isDark ? ( + ) : ( + )} diff --git a/apps/extension/entrypoints/sidepanel/ui/shared/sidepanel-main.tsx b/apps/extension/entrypoints/sidepanel/ui/shared/sidepanel-main.tsx index d3da39c..4f13aac 100644 --- a/apps/extension/entrypoints/sidepanel/ui/shared/sidepanel-main.tsx +++ b/apps/extension/entrypoints/sidepanel/ui/shared/sidepanel-main.tsx @@ -18,6 +18,7 @@ interface SidepanelMainProps { onDragOver: (event: DragOverEvent) => void; onDragEnd: (event: DragEndEvent) => void; onNavigateToDashboard: () => void; + onAddClick?: () => void; children: React.ReactNode; } @@ -31,6 +32,7 @@ export function SidepanelMain({ onDragOver, onDragEnd, onNavigateToDashboard, + onAddClick, children, }: SidepanelMainProps) { return ( @@ -57,6 +59,7 @@ export function SidepanelMain({ 북마크 diff --git a/apps/extension/entrypoints/welcome/App.tsx b/apps/extension/entrypoints/welcome/App.tsx index d5fec55..493ec54 100644 --- a/apps/extension/entrypoints/welcome/App.tsx +++ b/apps/extension/entrypoints/welcome/App.tsx @@ -1,4 +1,5 @@ import { Bookmark, MousePointerClick, PanelRight, Search } from 'lucide-react'; +import { useEffect } from 'react'; import { browser } from 'wxt/browser'; const WEB_BASE_URL = 'http://localhost:3000'; @@ -11,13 +12,13 @@ const STEPS = [ }, { icon: Bookmark, - title: '브라우저 북마크 자동 동기화', - desc: '기존 북마크가 자동으로 동기화되고, 여러 브라우저의 북마크를 한곳에서 관리해요.', + title: '크로스 브라우저 실시간 동기화', + desc: '크롬, 웨일, 엣지 등 어떤 브라우저를 쓰든 기존 북마크가 안전하고 실시간으로 연동됩니다.', }, { icon: MousePointerClick, - title: '우클릭으로 빠른 저장', - desc: '웹페이지에서 우클릭 → "Dookmark로 북마크하기"로 메모·태그·폴더까지 바로 지정해요.', + title: '자유로운 북마크 저장', + desc: '우클릭, 브라우저 기본 북마크 모두 자동으로 동기화됩니다.', }, { icon: Search, @@ -27,6 +28,26 @@ const STEPS = [ ]; export function WelcomeApp() { + useEffect(() => { + const syncTheme = () => { + const savedTheme = localStorage.getItem('theme') || 'system'; + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const isDark = savedTheme === 'dark' || (savedTheme === 'system' && systemPrefersDark); + document.documentElement.classList.toggle('dark', isDark); + }; + + syncTheme(); + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', syncTheme); + window.addEventListener('storage', syncTheme); + + return () => { + mediaQuery.removeEventListener('change', syncTheme); + window.removeEventListener('storage', syncTheme); + }; + }, []); + const handleLogin = () => { browser.tabs.create({ url: `${WEB_BASE_URL}/auth?deviceType=EXTENSION` }); }; @@ -41,7 +62,9 @@ export function WelcomeApp() { Dookmark에 오신 것을 환영합니다 - 북마크를 더 똑똑하게 — 여러 브라우저의 북마크를 한곳에서 관리하고, 메모·태그로 정리하고, + 여러 브라우저의 북마크를 한곳에서 관리하고 + + 어디서든 빠르게 찾아보세요. @@ -58,7 +81,9 @@ export function WelcomeApp() { {title} - {desc} + + {desc} + ))} diff --git a/apps/extension/public/icon/128.png b/apps/extension/public/icon/128.png index 9e35d13..994e36e 100644 Binary files a/apps/extension/public/icon/128.png and b/apps/extension/public/icon/128.png differ diff --git a/apps/extension/public/icon/16.png b/apps/extension/public/icon/16.png index cd09f8c..b86b695 100644 Binary files a/apps/extension/public/icon/16.png and b/apps/extension/public/icon/16.png differ diff --git a/apps/extension/public/icon/32.png b/apps/extension/public/icon/32.png index f51ce1b..e987cbd 100644 Binary files a/apps/extension/public/icon/32.png and b/apps/extension/public/icon/32.png differ diff --git a/apps/extension/public/icon/48.png b/apps/extension/public/icon/48.png index cb7a449..d08f3a1 100644 Binary files a/apps/extension/public/icon/48.png and b/apps/extension/public/icon/48.png differ diff --git a/apps/extension/public/icon/96.png b/apps/extension/public/icon/96.png index c28ad52..b4fe38d 100644 Binary files a/apps/extension/public/icon/96.png and b/apps/extension/public/icon/96.png differ diff --git a/apps/web/app/favicon.ico b/apps/web/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/apps/web/app/favicon.ico and /dev/null differ diff --git a/apps/web/app/icon.svg b/apps/web/app/icon.svg new file mode 100644 index 0000000..b3c464a --- /dev/null +++ b/apps/web/app/icon.svg @@ -0,0 +1,29 @@ + + + + D + diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index c98e3c7..dcd50f9 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -18,7 +18,9 @@ export default function RootLayout({
- 북마크를 인공지능으로 지능화하여 한눈에 관리하고 탐색해 보세요. + 여러 브라우저의 북마크를 한곳에서 관리하고 + + 어디서든 빠르게 찾아보세요.
- 북마크를 더 똑똑하게 — 여러 브라우저의 북마크를 한곳에서 관리하고, 메모·태그로 정리하고, + 여러 브라우저의 북마크를 한곳에서 관리하고 +
어디서든 빠르게 찾아보세요.