diff --git a/apps/client/index.html b/apps/client/index.html index fcb3ac29..0dfa48a5 100644 --- a/apps/client/index.html +++ b/apps/client/index.html @@ -2,7 +2,6 @@ - Vite + React + TS diff --git a/apps/extension/package.json b/apps/extension/package.json index 52784eec..093a90e3 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -15,10 +15,12 @@ "@pivanov/vite-plugin-svg-sprite": "^3.0.0", "@radix-ui/react-switch": "^1.2.5", "@tailwindcss/vite": "^4.1.11", + "@tanstack/react-query": "^5.81.5", "axios": "^1.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "firebase": "^11.10.0", + "i": "^0.3.7", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/apps/extension/src/App.tsx b/apps/extension/src/App.tsx index f43834c5..d6aa82e9 100644 --- a/apps/extension/src/App.tsx +++ b/apps/extension/src/App.tsx @@ -38,7 +38,9 @@ const App = () => { setTitle(imageUrl?.title ?? ''); setDescription(imageUrl?.description ?? ''); setImgUrl(imageUrl?.image ?? ''); - localStorage.setItem('titleSave', title); + chrome.storage.local.set({ titleSave: title }, () => { + console.log('Title saved to chrome storage'); + }); } }); }, []); diff --git a/apps/extension/src/api/axios.ts b/apps/extension/src/api/axios.ts new file mode 100644 index 00000000..e357869f --- /dev/null +++ b/apps/extension/src/api/axios.ts @@ -0,0 +1,16 @@ +import apiRequest from './axiosInstance'; + +export const getCategoriesDash = async () => { + const { data } = await apiRequest.get('/api/v1/categories/dashboard', {}); + return data; +}; + +export const postArticles = async (payload: { + url: string; + categoryId: number | null; + memo: string; + remindTime: string | null; +}) => { + const response = await apiRequest.post('/api/v1/articles', payload); + return response.data; +}; diff --git a/apps/extension/src/api/axiosInstance.ts b/apps/extension/src/api/axiosInstance.ts index 6826f891..77adb0d5 100644 --- a/apps/extension/src/api/axiosInstance.ts +++ b/apps/extension/src/api/axiosInstance.ts @@ -15,23 +15,20 @@ const fetchToken = async (email?: string) => { } ); const newToken = response.data.token; - localStorage.setItem('jwtToken', newToken); - return newToken; -}; -const getChromeToken = async (): Promise => { - return new Promise((resolve) => { - chrome.storage.local.get(['jwtToken'], (result) => { - resolve(result.jwtToken ?? null); - }); + chrome.storage.local.set({ jwtToken: newToken }, () => { + console.log('Token saved to chrome storage'); }); + return newToken; }; apiRequest.interceptors.request.use(async (config) => { - const noAuthNeeded = ['/api/v1/auth/token', '/api/v1/auth/signin']; + const noAuthNeeded = ['/api/v1/auth/token', '/api/v1/auth/signup']; const isNoAuth = noAuthNeeded.some((url) => config.url?.includes(url)); if (!isNoAuth) { - let token = await getChromeToken(); + let token = + 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwaW5iYWNrIiwiaWQiOiJhOTA1NGFjOS03MTg0LTQ3NjktYWY4Mi1jNGViYTg0YzYxYTIiLCJzdWIiOiJBY2Nlc3NUb2tlbiIsImV4cCI6MTc1MjgwMDY1Nn0.hXti-Jlnhg8mRoPl5nB8Vi8UV6HPdZYAtgtpTuqtH39lQWle8T5GlX0ug0nNVUqu5B_Pyzafck7lhfXN6ArHOA'; + if (!token || token === 'undefined') { token = await fetchToken('test@gmail.com'); } @@ -44,7 +41,7 @@ apiRequest.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; - const noAuthNeeded = ['/api/v1/auth/token', '/api/v1/auth/signin']; + const noAuthNeeded = ['/api/v1/auth/token', '/api/v1/auth/signup']; const isNoAuth = noAuthNeeded.some((url) => originalRequest.url?.includes(url) ); diff --git a/apps/extension/src/api/index.ts b/apps/extension/src/api/index.ts new file mode 100644 index 00000000..2a7cba69 --- /dev/null +++ b/apps/extension/src/api/index.ts @@ -0,0 +1 @@ +export { default as apiRequest } from './axiosInstance'; diff --git a/apps/extension/src/api/modalAxios.ts b/apps/extension/src/api/modalAxios.ts new file mode 100644 index 00000000..548d6896 --- /dev/null +++ b/apps/extension/src/api/modalAxios.ts @@ -0,0 +1,9 @@ +import apiRequest from './axiosInstance'; + +export const getRemindTime = async (time: String) => { + const { data } = await apiRequest.get( + `/api/v1/users/remind-time?now=${time}`, + {} + ); + return data; +}; diff --git a/apps/extension/src/api/modalQueries.ts b/apps/extension/src/api/modalQueries.ts new file mode 100644 index 00000000..d2334e67 --- /dev/null +++ b/apps/extension/src/api/modalQueries.ts @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; +import { getRemindTime } from './modalAxios'; + +export const useGetRemindTime = (time: string) => { + return useQuery({ + queryKey: ['remindTime'], + queryFn: () => getRemindTime(time), + }); +}; diff --git a/apps/extension/src/api/queries.ts b/apps/extension/src/api/queries.ts new file mode 100644 index 00000000..aa05e418 --- /dev/null +++ b/apps/extension/src/api/queries.ts @@ -0,0 +1,14 @@ +import { getCategoriesDash, postArticles } from './axios'; +import { useQuery, useMutation } from '@tanstack/react-query'; +export const useGetCategoriesDash = () => { + return useQuery({ + queryKey: ['categoriesDash'], + queryFn: getCategoriesDash, + }); +}; + +export const usePostArticles = () => { + return useMutation({ + mutationFn: postArticles, + }); +}; diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts index 60920287..9117722d 100644 --- a/apps/extension/src/background.ts +++ b/apps/extension/src/background.ts @@ -9,7 +9,6 @@ chrome.runtime.onInstalled.addListener((details) => { if (details.reason === 'install') { chrome.identity.getProfileUserInfo(function (info) { console.log('google email:', info.email); - setTimeout(() => { chrome.tabs.create({ url: `http://localhost:5180/onboarding?email=${info.email}`, @@ -18,6 +17,7 @@ chrome.runtime.onInstalled.addListener((details) => { }); } }); + chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message.type === 'SAVE_BOOKMARK') { const { url, title } = message.payload; @@ -28,7 +28,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { chrome.bookmarks.create( { - title: localStorage.getItem('titleSave') ?? '핀백 저장소', + title: '핀백 저장소', url: url, parentId: '1', }, diff --git a/apps/extension/src/components/modalPop/ModalPop.tsx b/apps/extension/src/components/modalPop/ModalPop.tsx index cd497997..5d450328 100644 --- a/apps/extension/src/components/modalPop/ModalPop.tsx +++ b/apps/extension/src/components/modalPop/ModalPop.tsx @@ -1,4 +1,4 @@ -import { POP_TEXTAREA_MAX_LENGTH } from '@constants/index'; +import { useState, useEffect } from 'react'; import { CategoryDropDown, CommonBtn, @@ -8,8 +8,11 @@ import { TimePicker, ToggleButton, } from '@pinback/design-system/ui'; -import { useState } from 'react'; import ModalHeader from './ModalHeader'; +import { POP_TEXTAREA_MAX_LENGTH } from '@constants/index'; +import { useGetCategoriesDash, usePostArticles } from '@api/queries'; +import { useGetRemindTime } from '../../api/modalQueries'; +import { fomatToday } from '@pinback/design-system/utils'; interface ModalPopProps { urlInfo: string; @@ -17,7 +20,11 @@ interface ModalPopProps { titleInfo?: string; desInfo?: string; } - +interface Category { + categoryId: number; + categoryName: string; + unreadCount: number; +} const ModalPop = ({ urlInfo, imgInfo, titleInfo, desInfo }: ModalPopProps) => { const [formState, setFormState] = useState({ date: '', @@ -25,14 +32,48 @@ const ModalPop = ({ urlInfo, imgInfo, titleInfo, desInfo }: ModalPopProps) => { time: '', timeError: '', }); + const [categories, setCategories] = useState(['']); + const [memo, setMemo] = useState(''); + const { mutate: postArticle } = usePostArticles(); + + const { + data: remindTime, + isLoading, + error, + } = useGetRemindTime(fomatToday(new Date())); + if (error) { + console.error('Error fetching remind time:', error); + return
Error loading remind time
; + } + const { data: categoriesData } = useGetCategoriesDash(); + useEffect(() => { + if (categoriesData) { + const categoryNames: string[] = categoriesData.data.categories.map( + (item: Category) => item.categoryName + ); + setCategories(categoryNames); + } + }, [categoriesData]); const [categoryPopupMode, setCategoryPopupMode] = useState< 'edit' | 'add' | '' >(''); const [selectedCategory, setSelectedCategory] = useState(''); - const [categories, setCategories] = useState(['기획', '취미', '요리']); + const [matchId, setMatchedId] = useState(null); + + useEffect(() => { + if (categoriesData?.data?.categories && selectedCategory) { + const matched = categoriesData.data.categories.find( + (item: Category) => item.categoryName === selectedCategory + ); + setMatchedId(matched?.categoryId ?? null); + } + }, [selectedCategory, categoriesData]); const handleSave = () => { + chrome.storage.local.set({ savedTitle: titleInfo }, () => { + console.log('📦 storage 저장 완료:', titleInfo); + }); chrome.runtime.sendMessage( { type: 'SAVE_BOOKMARK', @@ -45,7 +86,24 @@ const ModalPop = ({ urlInfo, imgInfo, titleInfo, desInfo }: ModalPopProps) => { console.log('✅ 응답 받음:', response); } ); - window.close(); + postArticle( + { + url: urlInfo, + categoryId: matchId, + memo: memo, + remindTime: fomatToday(new Date()), + }, + { + onSuccess: (data) => { + console.log('✅ 저장 성공:', data); + // TODO : 저장 시. 창 원래 닫아야하나 우선 개발 중이라 열어두었습니당 + // window.close(); + }, + onError: (error) => { + console.error('❌ 저장 실패:', error); + }, + } + ); }; const handleFieldChange = ( @@ -67,6 +125,7 @@ const ModalPop = ({ urlInfo, imgInfo, titleInfo, desInfo }: ModalPopProps) => { setCategoryPopupMode('add'); } else { setSelectedCategory(value); + console.log(value); setCategoryPopupMode(''); } }; @@ -93,7 +152,7 @@ const ModalPop = ({ urlInfo, imgInfo, titleInfo, desInfo }: ModalPopProps) => { text.length > maxLength ? `${text.slice(0, maxLength)}...` : text; return ( -
+
window.close()} />
@@ -118,6 +177,8 @@ const ModalPop = ({ urlInfo, imgInfo, titleInfo, desInfo }: ModalPopProps) => { size="medium" maxLength={POP_TEXTAREA_MAX_LENGTH} placeholder="메모를 입력하고 도토리를 받아보세요!" + value={memo} + onChange={(e) => setMemo(e.target.value)} />
diff --git a/apps/extension/src/popup.tsx b/apps/extension/src/popup.tsx index cb8876f1..efebde7a 100644 --- a/apps/extension/src/popup.tsx +++ b/apps/extension/src/popup.tsx @@ -3,10 +3,21 @@ import * as React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; import './App.css'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +const queryClient = new QueryClient(); const rootEl = document.getElementById('root'); if (rootEl) { - createRoot(rootEl).render(); + createRoot(rootEl).render( + + + + ); + createRoot(rootEl).render( + + + + ); } else { console.error('❌ root element not found!'); } diff --git a/apps/extension/src/utils/index.ts b/apps/extension/src/utils/index.ts new file mode 100644 index 00000000..48bbb399 --- /dev/null +++ b/apps/extension/src/utils/index.ts @@ -0,0 +1 @@ +export { queryClient } from '@utils/queryClient'; diff --git a/apps/extension/src/utils/queryClient.ts b/apps/extension/src/utils/queryClient.ts new file mode 100644 index 00000000..a6ac1694 --- /dev/null +++ b/apps/extension/src/utils/queryClient.ts @@ -0,0 +1,11 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnWindowFocus: false, + throwOnError: true, + }, + }, +}); diff --git a/apps/extension/vite.config.ts b/apps/extension/vite.config.ts index 41dab87b..c459bc30 100644 --- a/apps/extension/vite.config.ts +++ b/apps/extension/vite.config.ts @@ -41,7 +41,7 @@ export default defineConfig({ resolve: { alias: { '@': resolve(__dirname, './src'), - '@api': resolve(__dirname, './api'), + '@api': resolve(__dirname, './src/api'), '@utils': resolve(__dirname, './src/utils'), '@constants': resolve(__dirname, './src/constants'), '@shared-types': resolve(__dirname, './src/types'), diff --git a/packages/design-system/src/components/category_dropdown/CategoryDropDown.tsx b/packages/design-system/src/components/category_dropdown/CategoryDropDown.tsx index 24890319..939e3aac 100644 --- a/packages/design-system/src/components/category_dropdown/CategoryDropDown.tsx +++ b/packages/design-system/src/components/category_dropdown/CategoryDropDown.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Icon } from '@pinback/design-system/icons'; import { cva } from 'class-variance-authority'; @@ -42,14 +42,18 @@ const CategoryDropDown = ({ onSelect, }: CategoryDropDownProps) => { const [isDropDownOpen, setIsDropDownOpen] = useState(false); - const [selectedCategory, setSelectedCategory] = useState( - categories.length > 0 ? categories[0] : '' - ); + const [selectedCategory, setSelectedCategory] = useState(''); + + useEffect(() => { + if (categories.length > 0) { + setSelectedCategory(categories[0]); + } + }, [categories]); const handleCategoryClick = (category: string) => { setSelectedCategory(category); setIsDropDownOpen(false); - // onSelect?.(category); + onSelect?.(category); }; return ( diff --git a/packages/design-system/src/lib/fomatToday.ts b/packages/design-system/src/lib/fomatToday.ts new file mode 100644 index 00000000..3aea9b8f --- /dev/null +++ b/packages/design-system/src/lib/fomatToday.ts @@ -0,0 +1,12 @@ +function formatToday(date: Date): string { + const year = date.getFullYear(); + const month = `${date.getMonth() + 1}`.padStart(2, '0'); + const day = `${date.getDate()}`.padStart(2, '0'); + const hours = `${date.getHours()}`.padStart(2, '0'); + const minutes = `${date.getMinutes()}`.padStart(2, '0'); + const seconds = `${date.getSeconds()}`.padStart(2, '0'); + + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +export default formatToday; diff --git a/packages/design-system/src/lib/index.ts b/packages/design-system/src/lib/index.ts index 04bca77e..8439f763 100644 --- a/packages/design-system/src/lib/index.ts +++ b/packages/design-system/src/lib/index.ts @@ -1 +1,2 @@ +export { default as fomatToday } from './fomatToday'; export * from './utils'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 852fe8e0..2bde2b28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,7 +137,14 @@ importers: version: 1.2.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tailwindcss/vite': specifier: ^4.1.11 +<<<<<<< HEAD + version: 4.1.11(vite@7.0.0(@types/node@20.19.2)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@1.10.2)) + '@tanstack/react-query': + specifier: ^5.81.5 + version: 5.81.5(react@19.1.0) +======= version: 4.1.11(vite@7.0.0(@types/node@20.19.2)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)) +>>>>>>> origin/develop axios: specifier: ^1.10.0 version: 1.10.0 @@ -150,6 +157,9 @@ importers: firebase: specifier: ^11.10.0 version: 11.10.0 + i: + specifier: ^0.3.7 + version: 0.3.7 react: specifier: ^19.1.0 version: 19.1.0 @@ -2719,6 +2729,13 @@ packages: human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} +<<<<<<< HEAD + + i@0.3.7: + resolution: {integrity: sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==} + engines: {node: '>=0.4'} +======= +>>>>>>> origin/develop iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} @@ -3090,6 +3107,8 @@ packages: loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} +<<<<<<< HEAD +======= lottie-react@2.4.1: resolution: {integrity: sha512-LQrH7jlkigIIv++wIyrOYFLHSKQpEY4zehPicL9bQsrt1rnoKRYCYgpCUe5maqylNtacy58/sQDZTkwMcTRxZw==} @@ -3099,6 +3118,7 @@ packages: lottie-web@5.13.0: resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==} +>>>>>>> origin/develop lower-case-first@1.0.2: resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==} @@ -4166,6 +4186,13 @@ packages: yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} +<<<<<<< HEAD + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} +======= +>>>>>>> origin/develop yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} @@ -6766,6 +6793,8 @@ snapshots: human-signals@2.1.0: {} + i@0.3.7: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..aa200006 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["apps/**/*", "packages/**/*"], + "references": [ + { "path": "./apps/extension" }, + { "path": "./apps/client" }, + { "path": "./apps/landing" }, + { "path": "./packages/design-system" } + ] +}