diff --git a/packages/frontend/src/apis/queries/stocks/schema.ts b/packages/frontend/src/apis/queries/stocks/schema.ts index 4e14cb54..8b486e95 100644 --- a/packages/frontend/src/apis/queries/stocks/schema.ts +++ b/packages/frontend/src/apis/queries/stocks/schema.ts @@ -15,6 +15,7 @@ export const GetStockSchema = z.object({ volume: z.number(), marketCap: z.string(), rank: z.number(), + isRising: z.boolean(), }); export const GetStockListResponseSchema = z.object({ diff --git a/packages/frontend/src/apis/queries/user/index.ts b/packages/frontend/src/apis/queries/user/index.ts index 9e26a7fd..185cd442 100644 --- a/packages/frontend/src/apis/queries/user/index.ts +++ b/packages/frontend/src/apis/queries/user/index.ts @@ -2,3 +2,5 @@ export * from './schema'; export * from './useGetUserInfo'; export * from './useGetUserStock'; export * from './usePostUserNickname'; +export * from './useGetUserTheme'; +export * from './usePatchUserTheme'; diff --git a/packages/frontend/src/apis/queries/user/schema.ts b/packages/frontend/src/apis/queries/user/schema.ts index bc716011..6bd06f87 100644 --- a/packages/frontend/src/apis/queries/user/schema.ts +++ b/packages/frontend/src/apis/queries/user/schema.ts @@ -34,8 +34,17 @@ export const PostUserNicknameSchema = z.object({ export type PostUserNickname = z.infer; -export const GetUserThemeSchema = z.object({ +export const UserThemeSchema = z.object({ theme: z.enum(['light', 'dark']), }); -export type GetUserTheme = z.infer; +export type GetUserTheme = z.infer; + +export type PatchUserThemeRequest = z.infer; + +export const PatchUserThemeSchema = z.object({ + theme: z.enum(['light', 'dark']), + updatedAt: z.string().datetime(), +}); + +export type PatchUserTheme = z.infer; diff --git a/packages/frontend/src/apis/queries/user/useGetUserTheme.ts b/packages/frontend/src/apis/queries/user/useGetUserTheme.ts new file mode 100644 index 00000000..31291a65 --- /dev/null +++ b/packages/frontend/src/apis/queries/user/useGetUserTheme.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { UserThemeSchema, type GetUserTheme } from './schema'; +import { get } from '@/apis/utils/get'; + +const getUserTheme = () => + get({ + schema: UserThemeSchema, + url: '/api/user/theme', + }); + +export const useGetUserTheme = () => { + return useQuery({ + queryKey: ['userTheme'], + queryFn: getUserTheme, + staleTime: 1000 * 60 * 5, + }); +}; diff --git a/packages/frontend/src/apis/queries/user/usePatchUserTheme.ts b/packages/frontend/src/apis/queries/user/usePatchUserTheme.ts new file mode 100644 index 00000000..fe7daf12 --- /dev/null +++ b/packages/frontend/src/apis/queries/user/usePatchUserTheme.ts @@ -0,0 +1,26 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { + type PatchUserTheme, + type PatchUserThemeRequest, + UserThemeSchema, +} from './schema'; +import { patch } from '@/apis/utils/patch'; + +const patchUserTheme = ({ theme }: PatchUserThemeRequest) => + patch({ + params: { theme }, + schema: UserThemeSchema, + url: '/api/user/theme', + }); + +export const usePatchUserTheme = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['patchTheme'], + mutationFn: ({ theme }: PatchUserThemeRequest) => patchUserTheme({ theme }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['userTheme'] }); + }, + }); +}; diff --git a/packages/frontend/src/apis/utils/patch.ts b/packages/frontend/src/apis/utils/patch.ts new file mode 100644 index 00000000..9b535e6c --- /dev/null +++ b/packages/frontend/src/apis/utils/patch.ts @@ -0,0 +1,25 @@ +import { AxiosRequestConfig } from 'axios'; +import { z } from 'zod'; +import { instance } from '../config'; +import { formatZodError } from './formatZodError'; + +interface PatchParams { + params?: AxiosRequestConfig['params']; + schema: z.ZodType; + url: string; +} + +export const patch = async ({ + params, + schema, + url, +}: PatchParams): Promise => { + const { data } = await instance.patch(url, params); + const result = schema.safeParse(data); + + if (!result.success) { + throw new Error(formatZodError(result.error)); + } + + return data; +}; diff --git a/packages/frontend/src/components/layouts/Sidebar.tsx b/packages/frontend/src/components/layouts/Sidebar.tsx index 2a166981..9bc06d9d 100644 --- a/packages/frontend/src/components/layouts/Sidebar.tsx +++ b/packages/frontend/src/components/layouts/Sidebar.tsx @@ -1,10 +1,12 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import logoCharacter from '/logoCharacter.png'; import logoTitle from '/logoTitle.png'; import { Alarm } from './alarm'; import { MenuList } from './MenuList'; import { Search } from './search'; +import { useGetUserTheme } from '@/apis/queries/user/useGetUserTheme'; +import { usePatchUserTheme } from '@/apis/queries/user/usePatchUserTheme'; import { BOTTOM_MENU_ITEMS, TOP_MENU_ITEMS } from '@/constants/menuItems'; import { useOutsideClick } from '@/hooks/useOutsideClick'; import { type MenuSection } from '@/types/menu'; @@ -20,6 +22,17 @@ export const Sidebar = () => { alarm: false, }); + const { data } = useGetUserTheme(); + const { mutate } = usePatchUserTheme(); + + useEffect(() => { + if (data?.theme === 'light') { + document.body.classList.remove('dark'); + return; + } + document.body.classList.add('dark'); + }, [data]); + const ref = useOutsideClick(() => { setShowTabs({ search: false, alarm: false }); }); @@ -44,7 +57,13 @@ export const Sidebar = () => { } if (item.text === '다크모드') { - document.body.classList.toggle('dark'); + if (data?.theme === 'dark') { + mutate({ theme: 'light' }); + } + + if (data?.theme === 'light') { + mutate({ theme: 'dark' }); + } } }; diff --git a/packages/frontend/src/components/layouts/search/Search.tsx b/packages/frontend/src/components/layouts/search/Search.tsx index a82b3b5b..d5485406 100644 --- a/packages/frontend/src/components/layouts/search/Search.tsx +++ b/packages/frontend/src/components/layouts/search/Search.tsx @@ -26,6 +26,7 @@ export const Search = ({ className }: SearchProps) => {

setStockName(e.target.value)} autoFocus diff --git a/packages/frontend/src/components/ui/button/Button.tsx b/packages/frontend/src/components/ui/button/Button.tsx index 056a8c4b..e5f68527 100644 --- a/packages/frontend/src/components/ui/button/Button.tsx +++ b/packages/frontend/src/components/ui/button/Button.tsx @@ -3,7 +3,7 @@ import { cva, VariantProps } from 'class-variance-authority'; import { cn } from '@/utils/cn'; export const ButtonVariants = cva( - `display-bold12 border rounded shadow-black py-1 border-orange`, + `display-bold14 border rounded shadow-black py-1 border-orange`, { variants: { backgroundColor: { diff --git a/packages/frontend/src/pages/login/Login.tsx b/packages/frontend/src/pages/login/Login.tsx index 6b6f1793..a2e515ea 100644 --- a/packages/frontend/src/pages/login/Login.tsx +++ b/packages/frontend/src/pages/login/Login.tsx @@ -9,8 +9,8 @@ export const Login = () => { return (
-
-
+
+

스마트한 투자의 첫걸음,

주춤주춤과 함께해요!

diff --git a/packages/frontend/src/pages/stock-detail/TradingChart.tsx b/packages/frontend/src/pages/stock-detail/TradingChart.tsx index f0aa7ac2..537c1456 100644 --- a/packages/frontend/src/pages/stock-detail/TradingChart.tsx +++ b/packages/frontend/src/pages/stock-detail/TradingChart.tsx @@ -8,13 +8,8 @@ import { useGetStocksPriceSeries, } from '@/apis/queries/stocks'; import { TIME_UNIT } from '@/constants/timeUnit'; -import { ChartTheme, lightTheme } from '@/styles/theme'; -interface TradingChartProps { - theme?: ChartTheme; -} - -export const TradingChart = ({ theme = lightTheme }: TradingChartProps) => { +export const TradingChart = () => { const { stockId } = useParams(); const [timeunit, setTimeunit] = useState('day'); @@ -29,7 +24,6 @@ export const TradingChart = ({ theme = lightTheme }: TradingChartProps) => { priceData: data?.priceDtoList ?? [], volumeData: data?.volumeDtoList ?? [], containerRef, - theme, }); useChartResize({ containerRef, chart }); diff --git a/packages/frontend/src/pages/stock-detail/hooks/useChart.ts b/packages/frontend/src/pages/stock-detail/hooks/useChart.ts index 16ec9c90..c461c090 100644 --- a/packages/frontend/src/pages/stock-detail/hooks/useChart.ts +++ b/packages/frontend/src/pages/stock-detail/hooks/useChart.ts @@ -1,4 +1,3 @@ -import type { ChartTheme } from '@/styles/theme'; import { createChart, type IChartApi } from 'lightweight-charts'; import { useEffect, useRef, RefObject } from 'react'; import { @@ -6,6 +5,8 @@ import { StockTimeSeriesResponse, VolumeSchema, } from '@/apis/queries/stocks'; +import { useGetUserTheme } from '@/apis/queries/user'; +import { darkTheme, lightTheme } from '@/styles/theme'; import { createCandlestickOptions, createChartOptions, @@ -15,7 +16,6 @@ import { getHistogramColorData } from '@/utils/getHistogramColorData'; interface UseChartProps { containerRef: RefObject; - theme: ChartTheme; priceData: StockTimeSeriesResponse['priceDtoList']; volumeData: StockTimeSeriesResponse['volumeDtoList']; } @@ -35,19 +35,21 @@ const TransformVolumeData = VolumeSchema.transform((item) => ({ export const useChart = ({ containerRef, - theme, priceData, volumeData, }: UseChartProps) => { const chart = useRef(); + const { data } = useGetUserTheme(); + const graphTheme = data?.theme === 'light' ? lightTheme : darkTheme; + useEffect(() => { if (!containerRef.current) return; chart.current = createChart(containerRef.current, { width: containerRef.current.clientWidth, height: containerRef.current.clientHeight, - ...createChartOptions(theme), + ...createChartOptions(graphTheme), handleScroll: { mouseWheel: false, pressedMouseMove: false, @@ -74,7 +76,7 @@ export const useChart = ({ volumeSeries.setData(histogramData); const candleSeries = chart.current.addCandlestickSeries( - createCandlestickOptions(theme), + createCandlestickOptions(graphTheme), ); const transformedPriceData = priceData.map((item) => TransformPriceData.parse(item), @@ -84,7 +86,7 @@ export const useChart = ({ return () => { chart.current?.remove(); }; - }, [containerRef, theme, priceData, volumeData]); + }, [containerRef, graphTheme, priceData, volumeData]); return chart; }; diff --git a/packages/frontend/tailwind.config.ts b/packages/frontend/tailwind.config.ts index fdc77a65..38c7637e 100644 --- a/packages/frontend/tailwind.config.ts +++ b/packages/frontend/tailwind.config.ts @@ -7,6 +7,7 @@ const config: Config = { extend: { backgroundColor: { 'black/4': 'rgba(0, 0, 0, 0.4)', + 'white/4': 'rgba(255,255,255,0.4)', }, }, colors: {