diff --git a/index.html b/index.html index 4c61ca9..3fb0a43 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,12 @@ + + + Check Locatiion diff --git a/package.json b/package.json index ac5351b..6707df3 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,10 @@ "@radix-ui/react-slot": "^1.1.1", "@tanstack/react-query": "^5.65.0", "axios": "^1.7.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "es-toolkit": "^1.31.0", "lucide-react": "^0.473.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02ad15a..282ff18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,18 @@ importers: axios: specifier: ^1.7.9 version: 1.7.9 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + es-toolkit: + specifier: ^1.31.0 + version: 1.31.0 lucide-react: specifier: ^0.473.0 version: 0.473.0(react@18.3.1) @@ -116,18 +128,9 @@ importers: autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.5.1) - class-variance-authority: - specifier: ^0.7.1 - version: 0.7.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 cssnano: specifier: ^7.0.6 version: 7.0.6(postcss@8.5.1) - es-toolkit: - specifier: ^1.32.0 - version: 1.32.0 eslint: specifier: ^8.57.0 version: 8.57.1 @@ -4530,6 +4533,12 @@ packages: } engines: { node: ">=12" } + date-fns@4.1.0: + resolution: + { + integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== + } + dayjs@1.11.13: resolution: { @@ -4907,10 +4916,10 @@ packages: } engines: { node: ">= 0.4" } - es-toolkit@1.32.0: + es-toolkit@1.31.0: resolution: { - integrity: sha512-ZfSfHP1l6ubgW/B/FRtqb9bYdMvI6jizbOSfbwwJNcOQ1QE6TFsC3jpQkZ900uUPSR3t3SU5Ds7UWKnYz+uP8Q== + integrity: sha512-vwS0lv/tzjM2/t4aZZRAgN9I9TP0MSkWuvt6By+hEXfG/uLs8yg2S1/ayRXH/x3pinbLgVJYT+eppueg3cM6tg== } esbuild-register@3.6.0: @@ -12323,6 +12332,8 @@ snapshots: whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 + date-fns@4.1.0: {} + dayjs@1.11.13: {} debug@3.2.7: @@ -12546,7 +12557,7 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 - es-toolkit@1.32.0: {} + es-toolkit@1.31.0: {} esbuild-register@3.6.0(esbuild@0.21.5): dependencies: diff --git a/src/app/router.tsx b/src/app/router.tsx index b4bb4bb..c6e740b 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -4,12 +4,14 @@ import { useMemo } from "react"; import KakaoCallback from "@/app/routes/auth/kakao-callback"; import LoginView from "@/app/routes/auth/login"; +import CreateGoalView from "@/app/routes/goal/create-goal"; import { default as AppRoot, ErrorBoundary as RootErrorBoundary } from "@/app/routes/index.tsx"; import PrivateRoute from "@/app/routes/PrivateRoute"; import GoogleCallback from "@/features/auth/routes/GoogleCallback"; +import DatePickView from "@/features/goal/components/create-goal/date-pick"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; import { createBrowserRouter, RouterProvider } from "react-router"; @@ -63,12 +65,20 @@ const createAppRouter = (queryClient: QueryClient) => import("./routes/profile/reward").then(convert(queryClient)) }, { - path: paths.goal.path, + path: paths.goal.root.path, lazy: () => import("./routes/goal").then(convert(queryClient)) }, { path: "*", lazy: () => import("./routes/not-found").then(convert(queryClient)) + }, + { + path: paths.goal.create.path, + element: + }, + { + path: paths.goal.date.path, + element: } ] }, @@ -83,6 +93,10 @@ const createAppRouter = (queryClient: QueryClient) => { path: paths.auth.login.path, element: + }, + { + path: "*", + lazy: () => import("./routes/not-found").then(convert(queryClient)) } ]); diff --git a/src/app/routes/goal/create-goal.tsx b/src/app/routes/goal/create-goal.tsx new file mode 100644 index 0000000..907ec0a --- /dev/null +++ b/src/app/routes/goal/create-goal.tsx @@ -0,0 +1,11 @@ +import CreateGoal from "@/features/goal/routes/CreateGoal"; +import { useParams } from "react-router"; + +const CreateGoalView = () => { + const { goalId } = useParams<{ goalId?: string }>(); + const parsedGoalId: number | undefined = goalId ? Number(goalId) : undefined; + + return ; +}; + +export default CreateGoalView; diff --git a/src/asset/goal/check-gray.svg b/src/asset/goal/check-gray.svg new file mode 100644 index 0000000..69c6856 --- /dev/null +++ b/src/asset/goal/check-gray.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/asset/goal/check-green.svg b/src/asset/goal/check-green.svg new file mode 100644 index 0000000..7c16a93 --- /dev/null +++ b/src/asset/goal/check-green.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/ui/header/index.const.ts b/src/components/ui/header/index.const.ts index b54de0f..d9d62f9 100644 --- a/src/components/ui/header/index.const.ts +++ b/src/components/ui/header/index.const.ts @@ -13,7 +13,8 @@ export const NOT_VISIBLE_HEADER_PAGES = [ export const HEADER_TITLE_MAP = new Map([ [paths.map.certification.getHref(), "목표인증"], - [paths.goal.getHref(), "목표"], + [paths.goal.root.getHref(), "목표"], + [paths.goal.create.getHref(), "목표추가"], [paths.profile.reward.getHref(), "리워드 신청"] ]); diff --git a/src/config/paths.ts b/src/config/paths.ts index 6694d8d..88e9307 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -28,12 +28,17 @@ export const paths = { }, goal: { - path: "/goal", - getHref: () => "/goal", - + root: { + path: "/goal", + getHref: () => "/goal" + }, create: { path: "/goal/create", getHref: () => "/goal/create" + }, + date: { + path: "/goal/date", + getHref: () => "/goal/date" } }, diff --git a/src/features/auth/routes/GoogleCallback.tsx b/src/features/auth/routes/GoogleCallback.tsx index c0e932f..70f278c 100644 --- a/src/features/auth/routes/GoogleCallback.tsx +++ b/src/features/auth/routes/GoogleCallback.tsx @@ -21,13 +21,13 @@ const GoogleCallback = (): JSX.Element | null => { async (code: string): Promise => { try { const response = await postGoogleLogin({ data: { code } }); - if (!response.ok) { + if (response.status < 200 || response.status >= 300) { throw new Error("구글 인증에 실패했습니다."); } - const { accessToken, refreshToken } = response.data; + const { accessToken, refreshToken, userId } = response.data; - setTokens(accessToken, refreshToken); + setTokens(accessToken, refreshToken, userId); navigate(paths.home.path); } catch (error) { diff --git a/src/features/auth/routes/KakaoCallback.tsx b/src/features/auth/routes/KakaoCallback.tsx index 8462054..b71bae7 100644 --- a/src/features/auth/routes/KakaoCallback.tsx +++ b/src/features/auth/routes/KakaoCallback.tsx @@ -27,9 +27,9 @@ const KakaoCallback = (): JSX.Element | null => { throw new Error("카카오 인증에 실패했습니다."); } - const { accessToken, refreshToken } = response.data; + const { accessToken, refreshToken, userId } = response.data; - setTokens(accessToken, refreshToken); + setTokens(accessToken, refreshToken, userId); navigate(paths.home.path); } catch (error) { diff --git a/src/features/goal/api/goal.ts b/src/features/goal/api/goal.ts index a5d8690..cb39fb2 100644 --- a/src/features/goal/api/goal.ts +++ b/src/features/goal/api/goal.ts @@ -1,5 +1,8 @@ +import { BASE_PATH } from "@/features/goal/api/path"; +import { GoalData } from "@/features/goal/types/goal-create"; + import type { R } from "@/types/common.ts"; -import { GET } from "@/lib/axios"; +import { GET, POST } from "@/lib/axios"; import type { CompleteGoal, ProgressGoal } from "../types"; import { GOALS_CHECK, GOALS_COMPLETE } from "./path"; @@ -10,3 +13,23 @@ export function getGoalsCheck(): R { export function getGoalsComplete(): R { return GET({ url: GOALS_COMPLETE }); } + +export function postCreateGoal({ data }: { data: GoalData }) { + return POST({ + url: `${BASE_PATH}`, + data + }); +} + +export function postCreateTempSaveGoal({ data }: { data: GoalData }) { + return POST({ + url: `${BASE_PATH}`, + data + }); +} + +export function getTempGoal(pathParam: { goalId: number }) { + return GET({ + url: `${BASE_PATH}/check/${pathParam.goalId}` + }); +} diff --git a/src/features/goal/components/create-goal/BalanceInfo.tsx b/src/features/goal/components/create-goal/BalanceInfo.tsx new file mode 100644 index 0000000..8c56aca --- /dev/null +++ b/src/features/goal/components/create-goal/BalanceInfo.tsx @@ -0,0 +1,34 @@ +interface BalanceInfoProps { + balancePoint: number; +} + +const BalanceInfo: React.FC = ({ balancePoint }) => ( +
+
+ 차감 포인트: + + + 200 + + + p + + +
+
+ + 보유 포인트 + +
+ + {balancePoint} + + + p + +
+
+
+); + +export default BalanceInfo; diff --git a/src/features/goal/components/create-goal/DatePicker.tsx b/src/features/goal/components/create-goal/DatePicker.tsx new file mode 100644 index 0000000..c9fa90f --- /dev/null +++ b/src/features/goal/components/create-goal/DatePicker.tsx @@ -0,0 +1,71 @@ +import { format } from "date-fns"; + +interface DatePickerProps { + startDate: Date | null; + endDate: Date | null; + onDateClick: (mode: "start" | "end") => void; +} + +const DatePicker: React.FC = ({ + startDate, + endDate, + onDateClick +}) => ( +
+ +
+ + ~ + +
+

+ 최소 7일, 최대 3개월까지 설정할 수 있어요. +

+
+); + +export default DatePicker; diff --git a/src/features/goal/components/create-goal/DayPicker.tsx b/src/features/goal/components/create-goal/DayPicker.tsx new file mode 100644 index 0000000..3571fde --- /dev/null +++ b/src/features/goal/components/create-goal/DayPicker.tsx @@ -0,0 +1,61 @@ +import checkGrayUrl from "@/asset/goal/check-gray.svg?url"; +import checkGreenUrl from "@/asset/goal/check-green.svg?url"; +import { DAYS } from "@/features/goal/components/create-goal/goal.constants"; + +interface DayPickerProps { + selectedDays: string[]; + onToggleDay: (day: string) => void; + onSelectAllDays: () => void; +} + +const DayPicker: React.FC = ({ + selectedDays, + onToggleDay, + onSelectAllDays +}) => { + const isDaily = selectedDays.length === DAYS.length; + + return ( +
+
+
+ 반복요일 +
+
+ (주  + {selectedDays.length}일) +
+
+ +
+ {DAYS.map((day, index) => ( + + ))} +
+ +
+ ); +}; + +export default DayPicker; diff --git a/src/features/goal/components/create-goal/LocationSelector.tsx b/src/features/goal/components/create-goal/LocationSelector.tsx new file mode 100644 index 0000000..7ac67fa --- /dev/null +++ b/src/features/goal/components/create-goal/LocationSelector.tsx @@ -0,0 +1,14 @@ +const LocationSelector = ({ location, setLocation }) => { + return ( +
+ +
+ 📍 지도 (추후 추가) +
+
+ ); +}; + +export default LocationSelector; diff --git a/src/features/goal/components/create-goal/SaveButtons.tsx b/src/features/goal/components/create-goal/SaveButtons.tsx new file mode 100644 index 0000000..b2569a7 --- /dev/null +++ b/src/features/goal/components/create-goal/SaveButtons.tsx @@ -0,0 +1,31 @@ +interface SaveButtonsProps { + onTempSave: () => void; + onSave: () => void; + isFormValid: boolean; +} + +const SaveButtons: React.FC = ({ + onTempSave, + onSave, + isFormValid +}) => ( +
+ + + +
+); + +export default SaveButtons; diff --git a/src/features/goal/components/create-goal/date-pick.tsx b/src/features/goal/components/create-goal/date-pick.tsx new file mode 100644 index 0000000..c36f713 --- /dev/null +++ b/src/features/goal/components/create-goal/date-pick.tsx @@ -0,0 +1,175 @@ +import { useState } from "react"; + +import { DAYS } from "@/features/goal/components/create-goal/goal.constants"; +import { + addMonths, + differenceInDays, + eachDayOfInterval, + endOfMonth, + format, + getDay, + startOfMonth, + subDays, + subMonths +} from "date-fns"; +import { useLocation, useNavigate } from "react-router"; + +import { paths } from "@/config/paths"; + +const DatePick = () => { + const location = useLocation(); + const navigate = useNavigate(); + + const goalName = location.state?.goalName || ""; + const mode: "start" | "end" = location.state?.mode || "start"; + + const [dateState, setDateState] = useState({ + currentMonth: new Date(), + selectedDate: null as Date | null, + startDate: location.state?.startDate + ? new Date(location.state.startDate) + : null + }); + + const firstDayOfMonth = startOfMonth(dateState.currentMonth); + const lastDayOfMonth = endOfMonth(dateState.currentMonth); + const firstWeekday = getDay(firstDayOfMonth); + + const prevMonthLastDay = subDays(firstDayOfMonth, firstWeekday); + const days = eachDayOfInterval({ + start: prevMonthLastDay, + end: lastDayOfMonth + }); + + const handleDateClick = (date: Date) => { + setDateState((prev) => ({ ...prev, selectedDate: date })); + }; + + const handleChangeMonth = (type: "prev" | "next") => { + setDateState((prev) => ({ + ...prev, + currentMonth: + type === "prev" + ? subMonths(prev.currentMonth, 1) + : addMonths(prev.currentMonth, 1) + })); + }; + + const handleConfirm = () => { + if (!dateState.selectedDate) return; + if (mode === "end") { + navigate(paths.goal.create.path, { + state: { + startDate: dateState.startDate, + endDate: dateState.selectedDate, + goalName + } + }); + } else { + navigate(paths.goal.create.path, { + state: { startDate: dateState.selectedDate, goalName } + }); + } + }; + + return ( +
+

+ {mode === "end" ? "목표 날짜 설정" : "시작 날짜 설정"} +

+ +
+ + {format(dateState.currentMonth, "yyyy.MM")} + +
+
+
+
+ {DAYS.map((day) => ( +
+ {day} +
+ ))} +
+ +
+ {days.map((date, index) => { + const isBeforeToday = date < new Date(); + const isInRange = + dateState.startDate && + dateState.selectedDate && + date > dateState.startDate && + date < dateState.selectedDate; + const isStartOrEnd = + (dateState.startDate && + format(dateState.startDate, "yyyy-MM-dd") === + format(date, "yyyy-MM-dd")) || + (dateState.selectedDate && + format(dateState.selectedDate, "yyyy-MM-dd") === + format(date, "yyyy-MM-dd")); + + return ( + + ); + })} +
+
+ +

+ 최소 7일, 최대 3개월까지 설정할 수 있어요. +

+
+
+ {mode === "end" && dateState.startDate && dateState.selectedDate && ( +
+ {format(dateState.startDate, "yyyy-MM-dd")} ~{" "} + {format(dateState.selectedDate, "yyyy-MM-dd")}{" "} + + {differenceInDays(dateState.selectedDate, dateState.startDate) + + 1} + 일 + +
+ )} + + +
+
+ ); +}; + +export default DatePick; diff --git a/src/features/goal/components/create-goal/goal.constants.ts b/src/features/goal/components/create-goal/goal.constants.ts new file mode 100644 index 0000000..c259f79 --- /dev/null +++ b/src/features/goal/components/create-goal/goal.constants.ts @@ -0,0 +1,11 @@ +export const DAYS = ["일", "월", "화", "수", "목", "금", "토"]; + +export const DAY_MAPPING: Record = { + 일: "SUN", + 월: "MON", + 화: "TUE", + 수: "WED", + 목: "THU", + 금: "FRI", + 토: "SAT" +}; diff --git a/src/features/goal/routes/CreateGoal.tsx b/src/features/goal/routes/CreateGoal.tsx new file mode 100644 index 0000000..eb0e7db --- /dev/null +++ b/src/features/goal/routes/CreateGoal.tsx @@ -0,0 +1,187 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; + +import { + getTempGoal, + postCreateGoal, + postCreateTempSaveGoal +} from "@/features/goal/api/goal"; +import BalanceInfo from "@/features/goal/components/create-goal/BalanceInfo"; +import DatePicker from "@/features/goal/components/create-goal/DatePicker"; +import DayPicker from "@/features/goal/components/create-goal/DayPicker"; +import { DAY_MAPPING } from "@/features/goal/components/create-goal/goal.constants"; +import SaveButtons from "@/features/goal/components/create-goal/SaveButtons"; +import { GoalData, GoalStatus } from "@/features/goal/types/goal-create"; +import { getPoint } from "@/features/point/api/point"; +import { useLocation, useNavigate } from "react-router"; +import { toast } from "react-toastify"; + +import { Nullable } from "@/types/common"; +import { useAuthStore } from "@/stores/auth-store"; +import { paths } from "@/config/paths"; + +interface CreateGoalProps { + goalId?: number; +} + +const CreateGoal: React.FC = ({ goalId }) => { + const location = useLocation(); + const navigate = useNavigate(); + const userId = useAuthStore((state) => state.userId); + + const [goalName, setGoalName] = useState( + location.state?.goalName || "" + ); + + const [startDate, setStartDate] = useState>( + location.state?.startDate ? new Date(location.state.startDate) : null + ); + const [endDate, setEndDate] = useState>( + location.state?.endDate ? new Date(location.state.endDate) : null + ); + const [targetLocation, setTargetLocation] = useState(""); + const [balancePoint, setBalancePoint] = useState(0); + const [selectedDays, setSelectedDays] = useState([]); + const isFormValid = useMemo(() => { + return ( + goalName.trim().length >= 2 && + !!startDate && + !!endDate && + selectedDays.length > 0 + ); + }, [goalName, startDate, endDate, selectedDays]); + + const fetchBalancePoint = useCallback(async (): Promise => { + if (!userId) return; + try { + const { totalPoints } = await getPoint({ userId }); + setBalancePoint(totalPoints); + } catch (error) { + console.error("포인트 불러오기 실패:", error); + } + }, [userId]); + + useEffect(() => { + if (!userId) return; + fetchBalancePoint(); + }, [fetchBalancePoint, userId]); + + useEffect(() => { + if (!goalId) return; + + const fetchGoalData = async (): Promise => { + try { + const { goal } = await getTempGoal({ goalId }); + const { name = "", startDate, endDate, locationName = "" } = goal; // ✅ goal 내부 값도 구조 분해! + + setGoalName(name); + setStartDate(startDate ? new Date(startDate) : null); + setEndDate(endDate ? new Date(endDate) : null); + setTargetLocation(locationName); + } catch (error) { + console.error("임시 목표 데이터 불러오기 실패:", error); + } + }; + + fetchGoalData(); + }, [goalId]); + + const handleDateClick = (mode: "start" | "end"): void => { + navigate(paths.goal.date.path, { + state: { + mode, + goalName, + ...(mode === "end" && { startDate }) + } + }); + }; + + const handleSaveWithStatus = async (status: GoalStatus): Promise => { + if (!userId) return; + + if (status === GoalStatus.ACTIVE) { + if (!goalName.trim() || !startDate || !endDate || !targetLocation) { + toast.error("모든 필수 항목을 입력해주세요."); + return; + } + } + + const goalData: GoalData = { + goal: { + userId, + name: goalName, + startDate: startDate ? startDate.toISOString() : null, + endDate: endDate ? endDate.toISOString() : null, + locationName: targetLocation + }, + status, + days: selectedDays.map((day) => DAY_MAPPING[day]) + }; + + try { + status === GoalStatus.DRAFT + ? await postCreateTempSaveGoal({ data: goalData }) + : await postCreateGoal({ data: goalData }); + navigate(paths.goal.root.path); + } catch (error) { + console.error(error); + } + }; + + return ( +
+
+ + setGoalName(e.target.value)} + placeholder="최소 2자이상~20자까지 입력해 주세요." + /> +
+ + + + setSelectedDays((prev) => + prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day] + ) + } + onSelectAllDays={() => + setSelectedDays( + selectedDays.length === 7 ? [] : Object.keys(DAY_MAPPING) + ) + } + /> + +
+ + +
+
+ 📍 지도 (추후 추가) +
+ + handleSaveWithStatus(GoalStatus.DRAFT)} + onSave={() => handleSaveWithStatus(GoalStatus.ACTIVE)} + isFormValid={isFormValid} + /> +
+ ); +}; + +export default CreateGoal; diff --git a/src/features/goal/types/goal-create.tsx b/src/features/goal/types/goal-create.tsx new file mode 100644 index 0000000..86bdb62 --- /dev/null +++ b/src/features/goal/types/goal-create.tsx @@ -0,0 +1,16 @@ +export enum GoalStatus { + DRAFT = "DRAFT", + ACTIVE = "ACTIVE" +} + +export type GoalData = { + goal: { + userId: number; + name: string; + startDate: string | null; + endDate: string | null; + locationName: string; + }; + status: GoalStatus; + days: string[]; +}; diff --git a/src/features/point/api/path.ts b/src/features/point/api/path.ts new file mode 100644 index 0000000..f262bcd --- /dev/null +++ b/src/features/point/api/path.ts @@ -0,0 +1,3 @@ +import { genreateBasePath } from "@/lib/axios/utils"; + +export const BASE_PATH = genreateBasePath("point"); diff --git a/src/features/point/api/point.ts b/src/features/point/api/point.ts new file mode 100644 index 0000000..2cf2179 --- /dev/null +++ b/src/features/point/api/point.ts @@ -0,0 +1,8 @@ +import { BASE_PATH } from "@/features/point/api/path"; +import { Point } from "@/features/point/types/get-point"; + +import { GET } from "@/lib/axios"; + +export function getPoint(pathParam: { userId: number }): Promise { + return GET({ url: `${BASE_PATH}/${pathParam.userId}` }); +} diff --git a/src/features/point/types/get-point.tsx b/src/features/point/types/get-point.tsx new file mode 100644 index 0000000..c7a6496 --- /dev/null +++ b/src/features/point/types/get-point.tsx @@ -0,0 +1,3 @@ +export type Point = { + totalPoints: number; +}; diff --git a/src/index.css b/src/index.css index 82c2571..056fbbd 100644 --- a/src/index.css +++ b/src/index.css @@ -64,6 +64,7 @@ @apply bg-background text-foreground; max-width: 375px; margin: 0 auto; + font-family: "Pretendard", sans-serif; } .content-height { @apply h-[calc(100vh-60px)] overflow-auto; diff --git a/src/lib/axios/index.ts b/src/lib/axios/index.ts index 4b26dd6..88894d4 100644 --- a/src/lib/axios/index.ts +++ b/src/lib/axios/index.ts @@ -146,7 +146,6 @@ axios.interceptors.response.use( responseInterceptor.onRejected ); -// const curringMethod = (method: Method) => async ({ diff --git a/src/stores/auth-store.ts b/src/stores/auth-store.ts index 56c96a4..66dc627 100644 --- a/src/stores/auth-store.ts +++ b/src/stores/auth-store.ts @@ -3,14 +3,20 @@ import { create } from "zustand"; interface AuthState { accessToken: string | null; refreshToken: string | null; + userId: number | null; isAuthenticated: boolean; - setTokens: (accessToken: string, refreshToken: string) => void; + setTokens: ( + accessToken: string, + refreshToken: string, + userId: number + ) => void; } export const useAuthStore = create((set) => ({ accessToken: null, refreshToken: null, isAuthenticated: false, - setTokens: (accessToken, refreshToken) => - set({ accessToken, refreshToken, isAuthenticated: true }) + userId: null, + setTokens: (accessToken, refreshToken, userId) => + set({ accessToken, refreshToken, isAuthenticated: true, userId }) })); diff --git a/tailwind.config.ts b/tailwind.config.ts index 2771c37..6fcfe1c 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -53,6 +53,9 @@ module.exports = { "4": "hsl(var(--chart-4))", "5": "hsl(var(--chart-5))" } + }, + fontFamily: { + sans: ["Pretendard", "sans-serif"] } } },