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) => {