diff --git a/apps/timo-web/app/[locale]/(main)/settings/_containers/SettingsTabsContainer.tsx b/apps/timo-web/app/[locale]/(main)/settings/_containers/SettingsTabsContainer.tsx index 7f070be4..fe1fba6e 100644 --- a/apps/timo-web/app/[locale]/(main)/settings/_containers/SettingsTabsContainer.tsx +++ b/apps/timo-web/app/[locale]/(main)/settings/_containers/SettingsTabsContainer.tsx @@ -4,11 +4,16 @@ import { SettingsProfileContainer } from "@/app/[locale]/(main)/settings/_contai import { useSettingsTab } from "@/app/[locale]/(main)/settings/_hooks/useSettingsTab"; import { SettingsPolicyContainer } from "@/app/[locale]/(main)/settings/policy/_containers/SettingsPolicyContainer"; import { SettingsWithdrawalContainer } from "@/app/[locale]/(main)/settings/withdrawal/_containers/SettingsWithdrawalContainer"; +import { AsyncBoundary } from "@/components/boundary/AsyncBoundary"; export const SettingsTabsContainer = () => { const tab = useSettingsTab(); if (tab === "policy") return ; if (tab === "withdrawal") return ; - return ; + return ( + + + + ); }; diff --git a/apps/timo-web/app/[locale]/(main)/settings/_hooks/useSettingsProfile.ts b/apps/timo-web/app/[locale]/(main)/settings/_hooks/useSettingsProfile.ts index 1fcf8707..6fdcd48b 100644 --- a/apps/timo-web/app/[locale]/(main)/settings/_hooks/useSettingsProfile.ts +++ b/apps/timo-web/app/[locale]/(main)/settings/_hooks/useSettingsProfile.ts @@ -6,12 +6,22 @@ import { useForm } from "react-hook-form"; import type { SettingsDefaultTagKey, - SettingsProfile, + SettingsLanguage, SettingsProfileFormValues, } from "@/app/[locale]/(main)/settings/_types/profile-type"; +import { useUpdateLanguage } from "@/api/generated/endpoints/user/user"; +import { UpdateLanguageRequestLanguage } from "@/api/generated/models"; import { useSettingsLanguageParam } from "@/app/[locale]/(main)/settings/_hooks/useSettingsLanguageParam"; -import { settingsProfileMock } from "@/app/[locale]/(main)/settings/_mocks/profile-mock"; +import { useSettingsProfileQuery } from "@/app/[locale]/(main)/settings/_queries/use-settings-profile"; + +const LANGUAGE_REQUEST_MAP: Record< + SettingsLanguage, + (typeof UpdateLanguageRequestLanguage)[keyof typeof UpdateLanguageRequestLanguage] +> = { + ko: UpdateLanguageRequestLanguage.KO, + en: UpdateLanguageRequestLanguage.EN, +}; const DEFAULT_TAG_KEYS: SettingsDefaultTagKey[] = [ "assignment", @@ -28,11 +38,16 @@ export const useSettingsProfile = () => { const { language, locale, setLanguage, commitLanguage } = useSettingsLanguageParam(); - const [profile, setProfile] = useState(settingsProfileMock); + const { data: profile } = useSettingsProfileQuery(); + const [isCalendarConnected, setIsCalendarConnected] = useState( + profile.calendarConnected, + ); + const { mutateAsync: updateLanguage } = useUpdateLanguage(); + // TODO: 태그 목록 조회 API 연동 후 기본 태그 대신 실제 응답으로 교체 const { watch, setValue, handleSubmit, reset, formState } = useForm({ - defaultValues: { tags: profile.tags }, + defaultValues: { tags: DEFAULT_TAG_KEYS }, }); const tags = watch("tags"); @@ -47,20 +62,20 @@ export const useSettingsProfile = () => { }); const handleConnectCalendar = () => { - if (profile.isCalendarConnected) { + if (isCalendarConnected) { // TODO: 실제 확인 모달로 교체 const confirmed = window.confirm("구글 캘린더 연동을 해제하시겠습니까?"); if (!confirmed) return; // TODO: API - 연동 토큰 파기 console.log("구글 캘린더 연동 토큰을 파기합니다."); - setProfile((prev) => ({ ...prev, isCalendarConnected: false })); + setIsCalendarConnected(false); return; } // TODO: Google 계정 인증 및 캘린더 접근 권한 동의 팝업 호출 console.log("Google Calendar 연동 인증 팝업을 호출합니다."); - setProfile((prev) => ({ ...prev, isCalendarConnected: true })); + setIsCalendarConnected(true); }; const handleAddTag = () => { @@ -86,10 +101,15 @@ export const useSettingsProfile = () => { console.log("[Login] 페이지로 이동합니다."); }; + const isLanguageDirty = language !== locale; + const handleSave = handleSubmit(async (values) => { try { - // TODO: API 연동 - await new Promise((resolve) => setTimeout(resolve, 1000)); + if (isLanguageDirty) { + await updateLanguage({ + data: { language: LANGUAGE_REQUEST_MAP[language] }, + }); + } reset(values); commitLanguage(); } catch { @@ -98,13 +118,11 @@ export const useSettingsProfile = () => { } }); - const isLanguageDirty = language !== locale; - return { profileState: { name: profile.name, - googleEmail: profile.googleEmail, - isCalendarConnected: profile.isCalendarConnected, + googleEmail: profile.email, + isCalendarConnected, language, tags: tagItems, isSaveDisabled: diff --git a/apps/timo-web/app/[locale]/(main)/settings/_mocks/profile-mock.ts b/apps/timo-web/app/[locale]/(main)/settings/_mocks/profile-mock.ts deleted file mode 100644 index 9226811a..00000000 --- a/apps/timo-web/app/[locale]/(main)/settings/_mocks/profile-mock.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { SettingsProfile } from "@/app/[locale]/(main)/settings/_types/profile-type"; - -export const settingsProfileMock: SettingsProfile = { - name: "김도연", - googleEmail: "timogoogle@gmail.com", - isCalendarConnected: false, - tags: ["assignment", "work", "exercise", "dailyLife", "안녕"], -}; diff --git a/apps/timo-web/app/[locale]/(main)/settings/_queries/use-settings-profile.ts b/apps/timo-web/app/[locale]/(main)/settings/_queries/use-settings-profile.ts new file mode 100644 index 00000000..a556f1d8 --- /dev/null +++ b/apps/timo-web/app/[locale]/(main)/settings/_queries/use-settings-profile.ts @@ -0,0 +1,16 @@ +"use client"; + +import { useSuspenseQuery } from "@tanstack/react-query"; + +import { + getGetMyProfileQueryKey, + getMyProfile, +} from "@/api/generated/endpoints/user/user"; +import { settingsProfileResponseSchema } from "@/app/[locale]/(main)/settings/_types/profile-type"; + +export const useSettingsProfileQuery = () => + useSuspenseQuery({ + queryKey: getGetMyProfileQueryKey(), + queryFn: ({ signal }) => getMyProfile(undefined, signal), + select: ({ data }) => settingsProfileResponseSchema.parse(data), + }); diff --git a/apps/timo-web/app/[locale]/(main)/settings/_types/profile-type.ts b/apps/timo-web/app/[locale]/(main)/settings/_types/profile-type.ts index 6d746036..2279fcf9 100644 --- a/apps/timo-web/app/[locale]/(main)/settings/_types/profile-type.ts +++ b/apps/timo-web/app/[locale]/(main)/settings/_types/profile-type.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export type SettingsLanguage = "ko" | "en"; export type SettingsDefaultTagKey = @@ -6,12 +8,15 @@ export type SettingsDefaultTagKey = | "exercise" | "dailyLife"; -export interface SettingsProfile { - name: string; - googleEmail: string; - isCalendarConnected: boolean; - tags: string[]; -} +export const settingsProfileResponseSchema = z.object({ + name: z.string(), + email: z.string(), + calendarConnected: z.boolean(), +}); + +export type SettingsProfileResponse = z.infer< + typeof settingsProfileResponseSchema +>; export interface SettingsProfileFormValues { tags: string[]; diff --git a/apps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx b/apps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx index e642ca34..fe40cbe8 100644 --- a/apps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx +++ b/apps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx @@ -1,11 +1,9 @@ "use client"; -import { useQueryClient } from "@tanstack/react-query"; import { useSearchParams } from "next/navigation"; import { useEffect, useRef } from "react"; import { useToken } from "@/api/generated/endpoints/auth/auth"; -import { getGetMyProfileQueryKey } from "@/api/generated/endpoints/user/user"; import { ROUTES } from "@/constants/routes"; import { useRouter } from "@/i18n/navigation"; import { useAuthStore } from "@/stores/auth/useAuthStore"; @@ -13,7 +11,6 @@ import { useAuthStore } from "@/stores/auth/useAuthStore"; export const OauthCallbackContainer = () => { const code = useSearchParams().get("code"); const router = useRouter(); - const queryClient = useQueryClient(); const setAccessToken = useAuthStore((state) => state.setAccessToken); const { mutate } = useToken(); const hasRequested = useRef(false); @@ -35,7 +32,6 @@ export const OauthCallbackContainer = () => { return; } setAccessToken(data.accessToken); - queryClient.setQueryData(getGetMyProfileQueryKey(), data.user); router.replace(data.isNewUser ? ROUTES.ONBOARDING : ROUTES.HOME); }, onError: () => { @@ -43,7 +39,7 @@ export const OauthCallbackContainer = () => { }, }, ); - }, [code, mutate, queryClient, router, setAccessToken]); + }, [code, mutate, router, setAccessToken]); return null; };