diff --git a/.vscode/settings.json b/.vscode/settings.json index b3261f1..e19064d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "cSpell.words": ["Buil"] + "cSpell.words": ["Buil", "buildone"] } diff --git a/chrome-extension/manifest.ts b/chrome-extension/manifest.ts index e76f6cb..4e4d1a9 100644 --- a/chrome-extension/manifest.ts +++ b/chrome-extension/manifest.ts @@ -29,8 +29,8 @@ const manifest = { }, version: packageJson.version, description: '__MSG_extensionDescription__', - host_permissions: [''], - permissions: ['storage', 'scripting', 'tabs', 'notifications'], + host_permissions: ['*://*.buildone.me/'], + permissions: ['storage', 'scripting', 'tabs', 'notifications', 'cookies'], background: { service_worker: 'background.js', type: 'module', diff --git a/packages/hmr/lib/consts.ts b/packages/hmr/lib/consts.ts index 8166866..3e3b314 100644 --- a/packages/hmr/lib/consts.ts +++ b/packages/hmr/lib/consts.ts @@ -1,4 +1,4 @@ -export const LOCAL_RELOAD_SOCKET_PORT = 8081; +export const LOCAL_RELOAD_SOCKET_PORT = 8083; export const LOCAL_RELOAD_SOCKET_URL = `ws://localhost:${LOCAL_RELOAD_SOCKET_PORT}`; export const DO_UPDATE = 'do_update'; diff --git a/packages/storage/lib/impl/authStorage.ts b/packages/storage/lib/impl/authStorage.ts new file mode 100644 index 0000000..cfdf284 --- /dev/null +++ b/packages/storage/lib/impl/authStorage.ts @@ -0,0 +1,25 @@ +import type { BaseStorage } from '../base/index.js'; +import { createStorage, StorageEnum } from '../base/index.js'; + +type AccessToken = string | null; + +type AccessTokenStorage = BaseStorage & { + setAccessToken: (newAccessToken: string) => Promise; + removeAccessToken: () => Promise; +}; + +const storage = createStorage('access-token-key', null, { + storageEnum: StorageEnum.Local, + liveUpdate: true, +}); + +// You can extend it with your own methods +export const authStorage: AccessTokenStorage = { + ...storage, + setAccessToken: async (newAccessToken: string) => { + await storage.set(newAccessToken); + }, + removeAccessToken: async () => { + await storage.set(null); + }, +}; diff --git a/packages/storage/lib/impl/index.ts b/packages/storage/lib/impl/index.ts index 49f6f17..3aad9b4 100644 --- a/packages/storage/lib/impl/index.ts +++ b/packages/storage/lib/impl/index.ts @@ -1 +1,2 @@ export * from './exampleThemeStorage.js'; +export * from './authStorage.js'; diff --git a/packages/ui/lib/global.css b/packages/ui/lib/global.css index b5c61c9..bd6213e 100644 --- a/packages/ui/lib/global.css +++ b/packages/ui/lib/global.css @@ -1,3 +1,3 @@ @tailwind base; @tailwind components; -@tailwind utilities; +@tailwind utilities; \ No newline at end of file diff --git a/pages/popup/public/arrow_dropdown.svg b/pages/popup/public/arrow_dropdown.svg new file mode 100644 index 0000000..f9fb91e --- /dev/null +++ b/pages/popup/public/arrow_dropdown.svg @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/pages/popup/public/arrow_dropdown_reverse.svg b/pages/popup/public/arrow_dropdown_reverse.svg new file mode 100644 index 0000000..c16a3dc --- /dev/null +++ b/pages/popup/public/arrow_dropdown_reverse.svg @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/pages/popup/src/Popup.tsx b/pages/popup/src/Popup.tsx index 4bd21b4..6c2aed5 100644 --- a/pages/popup/src/Popup.tsx +++ b/pages/popup/src/Popup.tsx @@ -1,38 +1,79 @@ import { withErrorBoundary, withSuspense } from '@extension/shared'; -import Input from '@src/components/Input'; -import { useEffect, useState } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import type { Dispatch, SetStateAction } from 'react'; +import { createContext, useContext, useMemo, useState } from 'react'; +import type { + FieldErrors, + UseFormHandleSubmit, + UseFormRegister, + UseFormSetValue, + UseFormTrigger, + UseFormWatch, +} from 'react-hook-form'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import TodoCreate from './components/todo-create'; -function Popup() { - const [link, setLink] = useState(''); +export const todoFormSchema = z.object({ + title: z.string().min(1, '제목을 입력해주세요').max(30, '30자 이내로 입력해주세요'), + link: z.union([z.literal(''), z.string().trim().url('올바른 URL을 입력해주세요')]), +}); + +export type TodoFormSchema = z.infer; + +interface TodoModalFormContextProps { + register: UseFormRegister; + watch: UseFormWatch; + formState: { + errors: FieldErrors; + isValid: boolean; + }; + trigger: UseFormTrigger; + handleSubmit: UseFormHandleSubmit; + setValue: UseFormSetValue; + selectedGoalId?: number; + setSelectedGoalId: Dispatch>; +} - const logo = 'popup/logo.svg'; - const goGithubSite = () => chrome.tabs.create({ url: 'https://buildone.me' }); +const TodoFormContext = createContext(null); - useEffect(() => { - const getPageUrl = () => { - chrome.tabs.query({ active: true, currentWindow: true }, tabs => { - const { url } = tabs[0]; - setLink(url!); - }); - }; - getPageUrl(); - }, []); +export const useTodoFormContext = () => { + const context = useContext(TodoFormContext); + if (!context) { + throw new Error('할일 모달 폼 내부에서 context를 사용해주세요요'); + } + return context; +}; + +function Popup() { + const { register, handleSubmit, watch, setValue, formState, trigger } = useForm({ + resolver: zodResolver(todoFormSchema), + mode: 'onBlur', + defaultValues: { + title: '', + link: '', + }, + }); + const [selectedGoalId, setSelectedGoalId] = useState(); + + const formContextValue = useMemo( + () => ({ + handleSubmit, + setValue, + register, + watch, + formState, + trigger, + selectedGoalId, + setSelectedGoalId, + }), + [register, watch, formState, trigger, handleSubmit, setValue, selectedGoalId], + ); return ( -
-
- -
-
-
- - - {/*목표 선택은 추후에 구현*/} -
-
-
+ + + ); } diff --git a/pages/popup/src/Router.tsx b/pages/popup/src/Router.tsx index 90ac518..c11b739 100644 --- a/pages/popup/src/Router.tsx +++ b/pages/popup/src/Router.tsx @@ -4,7 +4,10 @@ import type { Dispatch, PropsWithChildren, SetStateAction } from 'react'; import { createContext, useContext, useEffect, useState } from 'react'; import Login from './components/Login'; import PrivateRoute from './components/PrivateRoute'; - +import { useStorage } from '@extension/shared'; +import { authStorage } from '@extension/storage'; +import TanstackQueryProvider from '@src/lib/tanstack-query-provider'; +import { reissueAccessToken } from './services/auth/token'; interface AuthContextProps { authenticated: boolean; setAuthenticated: Dispatch>; @@ -25,10 +28,33 @@ export function useAuthContext() { export const AuthProvider = ({ children }: PropsWithChildren) => { const [authenticated, setAuthenticated] = useState(true); + const auth = useStorage(authStorage); + + useEffect(() => { + const getNewToken = async () => { + const refreshToken = await chrome.cookies.get({ name: 'REFRESH_TOKEN', url: 'https://dev.api.buildone.me' }); + if (!refreshToken) { + return await authStorage.removeAccessToken(); + } + const newAccessToken = await reissueAccessToken(); + + if (newAccessToken) { + return await authStorage.setAccessToken(newAccessToken); + } + return await authStorage.removeAccessToken(); + }; + if (!auth) { + getNewToken(); + } + }, [auth]); useEffect(() => { - // 토큰이 있는지 검사 - }, []); + if (auth) { + setAuthenticated(true); + } else { + setAuthenticated(false); + } + }, [auth]); return {children}; }; @@ -36,14 +62,16 @@ export const AuthProvider = ({ children }: PropsWithChildren) => { export default function Router() { return ( - - - }> - } /> - - } /> - - + + + + }> + } /> + + } /> + + + ); } diff --git a/pages/popup/src/components/Input.tsx b/pages/popup/src/components/Input.tsx index 16b013b..27d254d 100644 --- a/pages/popup/src/components/Input.tsx +++ b/pages/popup/src/components/Input.tsx @@ -1,11 +1,13 @@ 'use client'; -import { forwardRef, InputHTMLAttributes, Ref, useState } from 'react'; +import type { InputHTMLAttributes, Ref } from 'react'; +import { forwardRef, useState } from 'react'; import Label from './Label'; +import { cn } from '@extension/ui'; export const BASE_CLASS = - 'flex items-center justify-center space-x-8 rounded-xl border border-slate-50 bg-slate-50 px-24 py-12 text-base font-normal focus-within:border-dark-blue-500 hover:border-dark-blue-300'; + 'flex items-center justify-center space-x-8 rounded-xl border border-slate-50 bg-slate-50 px-24 py-12 font-normal focus-within:border-dark-blue-500 hover:border-dark-blue-300'; export const RESPONSIVE_CLASS = 'h-44 w-343 md:h-48 md:w-612'; @@ -25,7 +27,7 @@ export default forwardRef(function Input( return ( <> {label &&