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;
};