[FEAT] 설정 프로필 조회·언어 수정 API 연동 - #173
Conversation
- 내 프로필 조회(GET /api/v1/users) API를 useSuspenseQuery로 연동했습니다 - 서비스 언어 수정(PATCH /api/v1/users) API를 저장 흐름에 연동했습니다 - 응답 검증용 로컬 zod 스키마를 추가했습니다 - 더 이상 사용하지 않는 프로필 mock 파일을 제거했습니다
- 로그인 성공 시 프로필 쿼리 캐시에 BaseResponse 래퍼 없는 UserInfo 모양 데이터를 직접 심어두던 코드를 제거했습니다 - 해당 캐시가 설정 페이지의 실제 프로필 쿼리와 모양이 달라 파싱 에러를 유발하고 있었습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough설정 프로필 mock 데이터를 React Query 기반 API 조회와 Zod 검증으로 교체했습니다. 언어 변경은 실제 API로 저장하며, 프로필 렌더링에는 비동기 경계를 추가했습니다. OAuth 콜백의 프로필 캐시 시딩 로직은 제거했습니다. Changes설정 프로필 API 연동
OAuth 프로필 캐시 처리
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant SettingsProfileContainer
participant useSettingsProfile
participant useSettingsProfileQuery
participant getMyProfile
SettingsProfileContainer->>useSettingsProfile: 프로필 상태 요청
useSettingsProfile->>useSettingsProfileQuery: suspense 프로필 조회
useSettingsProfileQuery->>getMyProfile: GET 내 프로필
getMyProfile-->>useSettingsProfileQuery: 응답 데이터
useSettingsProfileQuery-->>useSettingsProfile: Zod 검증 프로필
useSettingsProfile-->>SettingsProfileContainer: 프로필 및 폼 상태 반환
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/timo-web/app/`[locale]/(main)/settings/_containers/SettingsTabsContainer.tsx:
- Around line 14-18: Update the AsyncBoundary usage in SettingsTabsContainer to
provide an errorFallback so zod errors thrown by useSettingsProfileQuery’s
select validation are handled by an ErrorBoundary instead of crashing the
settings page. Use the existing application error fallback component or pattern
defined for AsyncBoundary.
In `@apps/timo-web/app/`[locale]/(main)/settings/_hooks/useSettingsProfile.ts:
- Around line 104-114: Update handleSave in useSettingsProfile so that, after
updateLanguage succeeds, the React Query cache entry identified by
getGetMyProfileQueryKey is invalidated before completing the existing
reset(values) and commitLanguage() flow.
In `@apps/timo-web/app/`[locale]/(main)/settings/_types/profile-type.ts:
- Around line 11-15: Update the email field in settingsProfileResponseSchema to
use Zod 4’s z.email() format schema instead of z.string(), while leaving the
name and calendarConnected validations unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: de8eeef6-db33-4d00-be2e-0cbc8c3ced9f
📒 Files selected for processing (6)
apps/timo-web/app/[locale]/(main)/settings/_containers/SettingsTabsContainer.tsxapps/timo-web/app/[locale]/(main)/settings/_hooks/useSettingsProfile.tsapps/timo-web/app/[locale]/(main)/settings/_mocks/profile-mock.tsapps/timo-web/app/[locale]/(main)/settings/_queries/use-settings-profile.tsapps/timo-web/app/[locale]/(main)/settings/_types/profile-type.tsapps/timo-web/app/[locale]/oauth/callback/_containers/OauthCallbackContainer.tsx
💤 Files with no reviewable changes (1)
- apps/timo-web/app/[locale]/(main)/settings/_mocks/profile-mock.ts
| return ( | ||
| <AsyncBoundary> | ||
| <SettingsProfileContainer /> | ||
| </AsyncBoundary> | ||
| ); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
AsyncBoundary에 errorFallback가 누락되어 zod 검증 실패 시 페이지가 크래시됩니다.
useSettingsProfileQuery의 select에서 settingsProfileResponseSchema.parse(data)를 호출합니다. API 응답이 스키마와 불일치하면 zod가 에러를 throw하는데, 현재 AsyncBoundary에 errorFallback을 전달하지 않아 ErrorBoundary가 래핑되지 않습니다. 결과적으로 처리되지 않은 에러가 발생해 설정 페이지 전체가 크래시됩니다.
🔒️ 제안하는 개선
return (
- <AsyncBoundary>
+ <AsyncBoundary
+ pendingFallback={<SettingsProfileSkeleton />}
+ errorFallback={<SettingsProfileError />}
+ >
<SettingsProfileContainer />
</AsyncBoundary>
);AsyncBoundary 컴포넌트는 errorFallback을 전달받아야 ErrorBoundary로 래핑합니다. 컴포넌트 정의를 참고하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return ( | |
| <AsyncBoundary> | |
| <SettingsProfileContainer /> | |
| </AsyncBoundary> | |
| ); | |
| return ( | |
| <AsyncBoundary | |
| pendingFallback={<SettingsProfileSkeleton />} | |
| errorFallback={<SettingsProfileError />} | |
| > | |
| <SettingsProfileContainer /> | |
| </AsyncBoundary> | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/timo-web/app/`[locale]/(main)/settings/_containers/SettingsTabsContainer.tsx
around lines 14 - 18, Update the AsyncBoundary usage in SettingsTabsContainer to
provide an errorFallback so zod errors thrown by useSettingsProfileQuery’s
select validation are handled by an ErrorBoundary instead of crashing the
settings page. Use the existing application error fallback component or pattern
defined for AsyncBoundary.
| 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(); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
언어 업데이트 후 프로필 쿼리 캐시 무효화가 필요합니다.
updateLanguage 성공 후 reset(values)와 commitLanguage()는 실행되지만, 프로필 쿼리(getGetMyProfileQueryKey) 캐시가 무효화되지 않습니다. 동일한 엔드포인트(/api/v1/users)를 사용하므로, mutation 성공 후 캐시를 무효화하지 않으면 재조회 시 stale data가 반환될 수 있습니다.
♻️ 제안하는 개선
+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의 invalidateQueries 문서를 참고하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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(); | |
| const queryClient = useQueryClient(); | |
| const isLanguageDirty = language !== locale; | |
| const handleSave = handleSubmit(async (values) => { | |
| try { | |
| if (isLanguageDirty) { | |
| await updateLanguage({ | |
| data: { language: LANGUAGE_REQUEST_MAP[language] }, | |
| }); | |
| await queryClient.invalidateQueries({ | |
| queryKey: getGetMyProfileQueryKey(), | |
| }); | |
| } | |
| reset(values); | |
| commitLanguage(); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/app/`[locale]/(main)/settings/_hooks/useSettingsProfile.ts
around lines 104 - 114, Update handleSave in useSettingsProfile so that, after
updateLanguage succeeds, the React Query cache entry identified by
getGetMyProfileQueryKey is invalidated before completing the existing
reset(values) and commitLanguage() flow.
| export const settingsProfileResponseSchema = z.object({ | ||
| name: z.string(), | ||
| email: z.string(), | ||
| calendarConnected: z.boolean(), | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
이메일 필드 검증을 z.email()로 강화해 보세요.
Zod 4부터 z.email()이 독립적인 문자열 포맷 스키마로 제공됩니다. 현재 z.string()을 사용 중인데, 이메일 형식 검증을 추가하면 API 응답의 신뢰성을 더욱 높일 수 있습니다.
♻️ 제안하는 개선
export const settingsProfileResponseSchema = z.object({
name: z.string(),
- email: z.string(),
+ email: z.email(),
calendarConnected: z.boolean(),
});Zod 4 문서의 String Enhancements 섹션을 참고하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const settingsProfileResponseSchema = z.object({ | |
| name: z.string(), | |
| email: z.string(), | |
| calendarConnected: z.boolean(), | |
| }); | |
| export const settingsProfileResponseSchema = z.object({ | |
| name: z.string(), | |
| email: z.email(), | |
| calendarConnected: z.boolean(), | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/timo-web/app/`[locale]/(main)/settings/_types/profile-type.ts around
lines 11 - 15, Update the email field in settingsProfileResponseSchema to use
Zod 4’s z.email() format schema instead of z.string(), while leaving the name
and calendarConnected validations unchanged.
ISSUE 🔗
close #172
What is this PR? 🔍
설정 페이지의 프로필/언어 mock 데이터를 실제 백엔드 API로 교체했습니다. 내 프로필 조회와 서비스 언어 수정을 React Query로 연동하고, 연동 과정에서 발견된 OAuth 콜백의 캐시 시딩 버그를 함께 수정했습니다.
배경
settingsProfileMock으로 이름·이메일·캘린더 연동 여부를 하드코딩해 렌더링했고, 언어 변경도 저장 시setTimeout으로 흉내만 냈습니다.GET /api/v1/users,PATCH /api/v1/users엔드포인트가 존재했지만 클라이언트가 붙어있지 않아, 새로고침해도 항상 같은 mock 값이 보였고 언어 변경도 서버에 반영되지 않았습니다.useGetMyProfile/useUpdateLanguage훅을 로컬 zod 스키마로 한 번 더 검증한 뒤_queries/use-settings-profile.ts와_hooks/useSettingsProfile.ts에서 사용하도록 연결했습니다.프로필 조회
_queries/use-settings-profile.ts를 추가해GET /api/v1/users를useSuspenseQuery로 연동했습니다.SettingsTabsContainer에AsyncBoundary를 추가하고useSuspenseQuery를 쓰는 쪽으로 맞췄습니다.queryKey/queryFn은 생성된getGetMyProfileQueryKey+getMyProfilefetcher로 직접 조립했습니다 (생성된getGetMyProfileQueryOptions를 그대로 스프레드하면skipToken타입 충돌이 발생합니다).select에서BaseResponseUserProfileResponse.data를 언랩한 뒤, UI가 실제로 쓰는name/email/calendarConnected세 필드만 검증하는 로컬 zod 스키마(settingsProfileResponseSchema)로.parse()합니다.tag.ts) 영역이라 이번 PR 범위에서 제외했고, 기본 태그 4개로만 초기화해두었습니다. 캘린더 연동/해제도 백엔드에 아직 대응 엔드포인트가 없어(/v3/api-docs확인 결과/api/v1/users,/api/v1/users/timezone,/api/v1/users/onboarding만 존재) 기존 로컬 스텁을 그대로 유지했습니다.서비스 언어 수정
PATCH /api/v1/users로 실제 반영하도록useUpdateLanguage를 연결했습니다.SettingsLanguage("ko"/"en")를 생성된UpdateLanguageRequestLanguage("KO"/"EN") enum으로 매핑하는 테이블을 두고,isLanguageDirty일 때만 mutation을 호출한 뒤 기존과 동일하게commitLanguage()로 로케일 라우트를 커밋합니다.OAuth 콜백 버그 수정
queryClient.setQueryData(getGetMyProfileQueryKey(), data.user)로 프로필 쿼리 캐시를 미리 채워두던 코드를 제거했습니다.data.user는 토큰 발급 응답의UserInfo(BaseResponse래퍼 없음,calendarConnected필드 없음)였고, 이 값이 프로필 쿼리 캐시에 그대로 들어가 있으면 설정 페이지가 실제 fetch 없이 이 캐시를 읽어select의 zod 파싱이 항상 실패했습니다. 실제 로그인 후 이 문제가 재현되어 발견했습니다.useQueryClient/getGetMyProfileQueryKeyimport를 제거했습니다. 설정 페이지 진입 시useSettingsProfileQuery가 정상적으로 네트워크 요청을 보내 실제 프로필을 받아옵니다.To Reviewers
프로필 응답 로컬 zod 스키마를
name/email/calendarConnected세 필드로만 좁혀뒀는데, UI가 늘어나면 스키마도 같이 넓혀야 합니다.Screenshot 📷
Test Checklist ✔
pnpm check-types통과pnpm lint통과