From 22588269beeb68c44fc5e3486d55514a2af01067 Mon Sep 17 00:00:00 2001 From: kimminna Date: Mon, 13 Jul 2026 19:37:21 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(web):=20=EC=84=A4=EC=A0=95=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=C2=B7=EC=96=B8=EC=96=B4?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20API=20=EC=97=B0=EB=8F=99=20(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 내 프로필 조회(GET /api/v1/users) API를 useSuspenseQuery로 연동했습니다 - 서비스 언어 수정(PATCH /api/v1/users) API를 저장 흐름에 연동했습니다 - 응답 검증용 로컬 zod 스키마를 추가했습니다 - 더 이상 사용하지 않는 프로필 mock 파일을 제거했습니다 --- .../_containers/SettingsTabsContainer.tsx | 7 ++- .../settings/_hooks/useSettingsProfile.ts | 44 +++++++++++++------ .../(main)/settings/_mocks/profile-mock.ts | 8 ---- .../settings/_queries/use-settings-profile.ts | 16 +++++++ .../(main)/settings/_types/profile-type.ts | 17 ++++--- 5 files changed, 64 insertions(+), 28 deletions(-) delete mode 100644 apps/timo-web/app/[locale]/(main)/settings/_mocks/profile-mock.ts create mode 100644 apps/timo-web/app/[locale]/(main)/settings/_queries/use-settings-profile.ts 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[]; From 7f566cf555f8fc8678ec529413928cbce870cb3f Mon Sep 17 00:00:00 2001 From: kimminna Date: Mon, 13 Jul 2026 19:37:51 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(web):=20OAuth=20=EC=BD=9C=EB=B0=B1=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=BA=90=EC=8B=9C=20=EC=8B=9C?= =?UTF-8?q?=EB=94=A9=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인 성공 시 프로필 쿼리 캐시에 BaseResponse 래퍼 없는 UserInfo 모양 데이터를 직접 심어두던 코드를 제거했습니다 - 해당 캐시가 설정 페이지의 실제 프로필 쿼리와 모양이 달라 파싱 에러를 유발하고 있었습니다 --- .../oauth/callback/_containers/OauthCallbackContainer.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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; };