-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 설정 프로필 조회·언어 수정 API 연동 #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<SettingsProfile>(settingsProfileMock); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: profile } = useSettingsProfileQuery(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [isCalendarConnected, setIsCalendarConnected] = useState( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| profile.calendarConnected, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { mutateAsync: updateLanguage } = useUpdateLanguage(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: 태그 목록 조회 API 연동 후 기본 태그 대신 실제 응답으로 교체 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { watch, setValue, handleSubmit, reset, formState } = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useForm<SettingsProfileFormValues>({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+104
to
114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win 언어 업데이트 후 프로필 쿼리 캐시 무효화가 필요합니다.
♻️ 제안하는 개선+import { useQueryClient } from "`@tanstack/react-query`";
+import { getGetMyProfileQueryKey } from "`@/api/generated/endpoints/user/user`";
+
// ... inside useSettingsProfile
+ const queryClient = useQueryClient();
const handleSave = handleSubmit(async (values) => {
try {
if (isLanguageDirty) {
await updateLanguage({
data: { language: LANGUAGE_REQUEST_MAP[language] },
});
+ await queryClient.invalidateQueries({
+ queryKey: getGetMyProfileQueryKey(),
+ });
}
reset(values);
commitLanguage();
} catch {React Query의 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } 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: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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), | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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(), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
Comment on lines
+11
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win 이메일 필드 검증을 Zod 4부터 ♻️ 제안하는 개선 export const settingsProfileResponseSchema = z.object({
name: z.string(),
- email: z.string(),
+ email: z.email(),
calendarConnected: z.boolean(),
});Zod 4 문서의 String Enhancements 섹션을 참고하세요. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export type SettingsProfileResponse = z.infer< | ||||||||||||||||||||||
| typeof settingsProfileResponseSchema | ||||||||||||||||||||||
| >; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export interface SettingsProfileFormValues { | ||||||||||||||||||||||
| tags: string[]; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
AsyncBoundary에errorFallback가 누락되어 zod 검증 실패 시 페이지가 크래시됩니다.useSettingsProfileQuery의select에서settingsProfileResponseSchema.parse(data)를 호출합니다. API 응답이 스키마와 불일치하면 zod가 에러를 throw하는데, 현재AsyncBoundary에errorFallback을 전달하지 않아ErrorBoundary가 래핑되지 않습니다. 결과적으로 처리되지 않은 에러가 발생해 설정 페이지 전체가 크래시됩니다.🔒️ 제안하는 개선
AsyncBoundary컴포넌트는errorFallback을 전달받아야ErrorBoundary로 래핑합니다. 컴포넌트 정의를 참고하세요.📝 Committable suggestion
🤖 Prompt for AI Agents