Conversation
- 프로필 설정 및 활동 내역(좋아요/내가 쓴 글) 탭 분리 - URL 파라미터 연동으로 새로고침 시에도 탭 상태 보존 - Relative Time(상대 시간) 로직 적용 및 카드 클릭 핸들러 추가 - 이벤트 전파 방지를 통한 복사 버튼 기능 독립성 확보
…eat/mypage-logic#103
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough마이페이지 관련 UI와 로직이 새로 추가되었습니다. 페이지 레이아웃과 탭 네비게이션을 도입하고(MypageTabButtons, layout, page), 좌/우 섹션 컴포넌트(MypageLeftSection, MypageRightSection, MypageActivitySection)를 추가했습니다. 사용자 프로필 조회·수정 및 활동(내 게시물/좋아요)용 API 클라이언트와 React Query 훅(hooks/use-mypage, queries/api/mypage, queries/api/types), Zustand 스토어(app/store/mypageStore), 복사·페이징 UI(articleCard, PaginationButton), 재사용 컴포넌트(UserProfile, UserExperience, UserKeyword), MSW 핸들러(mocks/handlers/mypage-handlers 등) 및 Storybook 스토리가 포함됩니다. Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant Browser as 브라우저
participant MyPage as /mypage 페이지
participant Left as MypageLeftSection
participant Right as MypageRightSection
participant Query as useUserProfile
participant API as getUserProfile
participant Server as 백엔드
rect rgba(200,150,255,0.5)
Note over User,Server: 프로필 조회 흐름
User->>Browser: /mypage?tab=profile 접근
Browser->>MyPage: 페이지 렌더링
MyPage->>Left: 좌측 섹션 마운트
MyPage->>Right: 우측 섹션 마운트
Right->>Query: useUserProfile() 호출
Query->>API: getUserProfile()
API->>Server: GET /users/me/profile
Server-->>API: UserProfileData 반환
API-->>Query: 데이터 전달
Query-->>Right: 프로필 데이터 제공 및 렌더
end
sequenceDiagram
participant User as 사용자
participant Right as MypageRightSection
participant Store as Zustand Store
participant QueryMut as useUpdateProfile
participant API as updateUserProfile
participant Server as 백엔드
rect rgba(200,200,150,0.5)
Note over User,Server: 프로필 수정 저장 흐름
User->>Right: 폼 수정 후 저장 클릭
Right->>Store: 로컬 상태(폼 값) 수집
Right->>QueryMut: mutate(payload)
QueryMut->>API: PUT /users/me/profile
API->>Server: 서버 요청
Server-->>API: 성공/에러 응답
API-->>QueryMut: 응답 전달
QueryMut-->>Right: 성공 콜백(토스트, 캐시 무효화)
end
sequenceDiagram
participant User as 사용자
participant Browser as 브라우저
participant MyPage as /mypage 페이지
participant Activity as MypageActivitySection
participant Query1 as useUserPrompts
participant Query2 as useLikedPrompts
participant API as getUserPrompts/getLikedPrompts
participant Server as 백엔드
rect rgba(150,200,150,0.5)
Note over User,Server: 활동 탭 로드 및 페이지네이션
User->>Browser: /mypage?tab=activity 접근
Browser->>MyPage: 페이지 렌더링
MyPage->>Activity: 활동 섹션 마운트
par 병렬 요청
Activity->>Query1: useUserPrompts(userId,page)
Activity->>Query2: useLikedPrompts(userId,page)
and
Query1->>API: getUserPrompts(...)
Query2->>API: getLikedPrompts(...)
API->>Server: GET /users/:userId/prompts 및 /liked
Server-->>API: 목록 응답
API-->>Query1: 데이터 제공
API-->>Query2: 데이터 제공
end
Query1-->>Activity: 게시물 데이터 제공
Query2-->>Activity: 좋아요 데이터 제공
Activity-->>Browser: 카드 리스트 및 페이지네이션 렌더
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 19
🧹 Nitpick comments (26)
app/(beforeLogin)/(auth)/signup/component/Detail.tsx (1)
89-103: JSX 문자열 prop에서 불필요한 중괄호 제거 권장정적 문자열 리터럴을 JSX prop으로 전달할 때, 중괄호(
{})는 불필요합니다. JSX 컨벤션상 문자열 prop은 따옴표만 사용하는 것이 더 관용적입니다.✨ 제안하는 개선 사항
- label={"현재 상태를 선택해주세요."} + label="현재 상태를 선택해주세요."- label={"목표 직무를 선택해주세요 (복수선택가능)."} + label="목표 직무를 선택해주세요 (복수선택가능)."참고: React JSX in Depth — String Literals
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(beforeLogin)/(auth)/signup/component/Detail.tsx around lines 89 - 103, Replace JSX prop usages that wrap static string literals in braces with plain string literals: change label={"현재 상태를 선택해주세요."} and label={"목표 직무를 선택해주세요 (복수선택가능)."} to label="현재 상태를 선택해주세요." and label="목표 직무를 선택해주세요 (복수선택가능)." respectively; update the label prop on the component that has value={currentState} and otherValue={otherInput} as well as on TargetJobsSelect to remove the unnecessary {} around static strings.queries/api/auth.ts (2)
31-33:ApiResponse<object>는 타입 정보가 부족합니다
object는 TypeScript에서 가장 넓은 비-원시(non-primitive) 타입 중 하나입니다. 로그아웃 API가 실제로 반환하는 응답 구조(예:{ message: string }또는null)를 명시하면 자동완성과 타입 안전성이 향상됩니다. 반환값이 없다면ApiResponse<null>도 명시적인 선택입니다.♻️ 개선 제안
export const postLogout = async () => { - return await fetcher.post("auth/logout").json<ApiResponse<object>>(); + return await fetcher.post("auth/logout").json<ApiResponse<null>>(); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@queries/api/auth.ts` around lines 31 - 33, postLogout currently types the response as ApiResponse<object>, which is too broad; update the generic to the actual shape returned by the logout endpoint (e.g., ApiResponse<{ message: string }> if it returns a message, or ApiResponse<null> if no body) by changing the type in the fetcher call inside the postLogout function so the return signature accurately reflects the API response and improves type safety and autocompletion.
39-40:uid파라미터 타입을string으로 좁혀보세요
basicInfo.uid는 확실히string | number로 정의되어 있지만,deleteUserAccountAPI 응답 타입에서는uid: string으로만 반환됩니다. 이는 서버가 실제로는string만 허용한다는 신호입니다.현재
string | number유니온은 불필요한 유연성을 제공하고 있습니다. 대신 파라미터를string으로 정확히 정의하면:
- 타입 안정성이 높아집니다
- 잘못된 타입의 uid를 실수로 넘기는 것을 방지할 수 있습니다
- 서버 스펙과 더 명확하게 일치합니다
basicInfo.uid를string으로 수정하거나,deleteUserAccount에 진입하기 전에 타입을 검증하는 것을 추천합니다. 📌🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@queries/api/auth.ts` around lines 39 - 40, Narrow the deleteUserAccount(uid: string | number) parameter to uid: string and update call sites or validate/convert basicInfo.uid before calling; locate the deleteUserAccount function and change its signature to accept only string, and either change basicInfo.uid to be string at its source or add a clear runtime check/coercion where deleteUserAccount is invoked so only a string is passed.app/(afterLogin)/mypage/MypageTabButtons.tsx (1)
8-8:activeTabprop 타입이 너무 넓습니다
activeTab: string은 잘못된 값을 전달해도 TypeScript가 오류를 잡아주지 못합니다.handleTabChange의 파라미터는 이미"profile" | "activity"유니온 타입으로 잘 정의되어 있으므로, prop도 동일하게 좁혀주세요.♻️ 개선 제안
-export default function MypageTabButtons({ activeTab }: { activeTab: string }) { +type Tab = "profile" | "activity"; + +export default function MypageTabButtons({ activeTab }: { activeTab: Tab }) { const router = useRouter(); - const handleTabChange = (tab: "profile" | "activity") => { + const handleTabChange = (tab: Tab) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/MypageTabButtons.tsx at line 8, The MypageTabButtons component currently types the prop too broadly as activeTab: string; change the prop signature in the MypageTabButtons function to use the tighter union type activeTab: "profile" | "activity" (matching the existing handleTabChange parameter) and update any related local variables or callers if necessary to satisfy the narrowed type; reference the MypageTabButtons function and the handleTabChange union to locate where to adjust types.app/(afterLogin)/mypage/MypageLeftSection.tsx (1)
19-19:confirm()은 브라우저를 블로킹합니다
window.confirm()은 JavaScript 실행을 블로킹하고 스타일을 커스터마이즈할 수 없으며, 일부 브라우저(특히 모바일)에서는 억제될 수도 있습니다. Radix UI의<Dialog>또는 프로젝트에서 이미 사용 중인@radix-ui/react-dialog로 확인 모달을 구현하면 더 나은 UX를 제공할 수 있습니다.Also applies to: 38-40
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/MypageLeftSection.tsx at line 19, Replace blocking window.confirm usage in MypageLeftSection's logout flow with a non-blocking Radix Dialog: remove the direct confirm("...") call inside the logout handler (the code around the confirm at line ~19 and the similar block at lines ~38-40) and implement a controlled <Dialog> (from `@radix-ui/react-dialog`) that opens when the user clicks "로그아웃", presents the message and explicit Cancel/Confirm actions, and only calls the existing logout function (or dispatch/handleLogout method) when the Dialog's Confirm button is clicked; ensure the dialog state is tracked in the component (e.g., isLogoutDialogOpen) and accessibility props (aria-labels) are set.app/(afterLogin)/mypage/layout.tsx (1)
3-13: 컴포넌트 이름이 역할을 반영하지 않습니다함수 이름
DashboardLayout은app/(afterLogin)/(dashboard)/layout.tsx와 동일하지만, 이 파일은 마이페이지 전용입니다.MypageLayout처럼 컨텍스트에 맞는 이름을 사용하면 혼동을 줄일 수 있습니다.또한 두 레이아웃의 JSX 구조가 완전히 동일합니다. 공통 레이아웃 컴포넌트(
SharedPageLayout등)를 추출하면 향후 변경 시 한 곳만 수정하면 됩니다.♻️ 개선 제안
-export default function DashboardLayout({ +export default function MypageLayout({ children, }: { children: React.ReactNode; }) {또는
components/shared/PageLayout.tsx로 공통 레이아웃을 추출:// components/shared/PageLayout.tsx import { Header } from "@/components/header/header"; export default function PageLayout({ children }: { children: React.ReactNode }) { return ( <div className="w-full min-h-screen bg-gray-50"> <Header /> <div>{children}</div> </div> ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/layout.tsx around lines 3 - 13, The component name DashboardLayout is misleading for the mypage layout and duplicates JSX used elsewhere; rename the exported function to MypageLayout (update the default export identifier DashboardLayout -> MypageLayout) and extract the shared JSX into a single reusable component (e.g., PageLayout or SharedPageLayout) that renders Header and {children}; then import and use that shared component from this file (replace the current JSX in DashboardLayout/MypageLayout with a call to the shared PageLayout) so both layouts share one implementation.lib/utils.ts (1)
20-23: 유효하지 않은 날짜 문자열에 대한 방어 처리가 없습니다
parseISO는 유효하지 않은 문자열을 받았을 때 예외를 던지지 않고Invalid Date객체를 반환합니다. 이를formatDistanceToNow에 전달하면RangeError가 발생합니다. API 응답이 항상 올바르다는 보장이 없으므로, 유틸리티 함수에서 방어적으로 처리하는 것이 좋습니다.🛡️ 개선 제안
+import { formatDistanceToNow, isValid, parseISO } from "date-fns"; -import { formatDistanceToNow, parseISO } from "date-fns"; export function formatRelativeDate(dateString: string) { const date = parseISO(dateString); + if (!isValid(date)) return dateString; return formatDistanceToNow(date, { addSuffix: true, locale: ko }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/utils.ts` around lines 20 - 23, The function formatRelativeDate currently calls parseISO(dateString) and passes the result directly to formatDistanceToNow, which throws if given an invalid Date; update formatRelativeDate to defensively validate the parsed date (using date-fns isValid or checking isNaN(date.getTime())) and return a safe fallback (e.g., empty string, original string, or a localized "unknown" message) when the date is invalid or input is missing; ensure you still import any helper used (isValid) and keep existing behavior for valid dates in formatRelativeDate that uses parseISO and formatDistanceToNow with locale ko.app/types/type.ts (1)
27-41:BasePrompt와PromptBase의 이름이 혼동을 일으킵니다
BasePrompt와 기존의PromptBase는 이름이 매우 유사하지만 완전히 다른 구조(id: stringvspromptId: number, 다른 필드 집합)를 가집니다. 마이페이지 전용 타입임을 명확히 드러내는 이름으로 구분하면 실수를 방지할 수 있습니다.♻️ 개선 제안
-export interface BasePrompt { +// 마이페이지 전용 프롬프트 기반 타입 +export interface MypagePromptBase { promptId: number; title: string; description: string; } -export interface LikedPrompt extends BasePrompt { +export interface LikedPrompt extends MypagePromptBase { isLiked: boolean; } -export interface MyPrompt extends BasePrompt { +export interface MyPrompt extends MypagePromptBase { createdAt: string; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/types/type.ts` around lines 27 - 41, Summary: The name BasePrompt is easily confused with the existing PromptBase; rename it to clearly indicate it's for the My Page shapes (and update any usages). Change the interface name BasePrompt to a descriptive MyPagePromptBase (or MyPromptBase) and update the derived interfaces LikedPrompt and MyPrompt to extend that new name; search for and update all references to BasePrompt and any code that expects promptId:number (vs PromptBase.id:string) to use the new type so callers compile—make sure to adjust import/export names where this file is consumed.components/mypage/user-profile.tsx (2)
36-52:getTheme을 컴포넌트 외부 상수로 추출하면 더 깔끔합니다.매 렌더마다 함수를 재생성할 필요 없이, provider → theme 매핑을 컴포넌트 밖에 정적 객체로 선언하면 의도가 더 명확해집니다.
♻️ 리팩토링 제안
+const PROVIDER_THEME = { + NAVER: { + bg: "bg-[`#01C73C`]", + label: "네이버", + image: STATIC_IMAGES.naver, + }, + KAKAO: { + bg: "bg-[`#F9DB00`]", + label: "카카오", + image: STATIC_IMAGES.kakao, + }, +} as const; + export default function UserProfile({ ... }: userProfileProps) { - const getTheme = () => { - switch (provider) { - case "NAVER": - return { ... }; - case "KAKAO": - default: - return { ... }; - } - }; - const theme = getTheme(); + const theme = PROVIDER_THEME[provider] ?? PROVIDER_THEME.KAKAO;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/mypage/user-profile.tsx` around lines 36 - 52, Move the provider→theme mapping out of the component by replacing the inline getTheme function with a top-level constant (e.g., PROVIDER_THEMES or THEME_MAP) that maps provider keys ("NAVER", "KAKAO") to their theme objects (bg, label, image using STATIC_IMAGES); then update usages that call getTheme() to reference the mapping by key (fall back to the KAKAO entry when provider is missing/unknown) so the component no longer recreates the function each render.
21-27: 인터페이스 이름을 PascalCase로 변경해 주세요.TypeScript 컨벤션상 인터페이스/타입 이름은 PascalCase를 사용합니다.
userProfileProps→UserProfileProps로 변경을 권장합니다.♻️ 네이밍 수정
-interface userProfileProps { +interface UserProfileProps { nickname: string; email: string; profileImage: string; provider: "KAKAO" | "NAVER"; introduction: string; }Line 35도 함께 수정:
-}: userProfileProps) { +}: UserProfileProps) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/mypage/user-profile.tsx` around lines 21 - 27, Rename the interface userProfileProps to PascalCase UserProfileProps and update all references to it (e.g., any component prop annotations or type usages such as the props parameter in the UserProfile component or other files importing this type); ensure the exported name (if exported) and any imports/exports are updated to the new identifier so TypeScript typing remains consistent (also update the occurrence mentioned near the component props usage that currently references userProfileProps).components/mypage/user-experience.tsx (2)
60-83:maxItems도달 시 입력창은 숨겨지지만 "추가하기" 버튼은 여전히 노출됩니다.Line 61에서
value.length < maxItems조건으로 입력창을 숨기고 있지만, Line 75의 버튼은 항상 렌더링됩니다.maxItems에 도달하면 입력도 불가능한 상태에서 disabled 버튼만 남게 되어 UX가 어색할 수 있습니다. 버튼도 같은 조건으로 감싸거나, 입력창과 버튼을 함께 관리하는 것을 고려해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/mypage/user-experience.tsx` around lines 60 - 83, The Add button remains visible even when maxItems is reached causing awkward UX; wrap the BaseButton render in the same condition as the input (value.length < maxItems) or otherwise coordinate their visibility so both BaseInput and BaseButton are shown/hidden together. Locate the JSX that uses value, maxItems, BaseInput, BaseButton, addItem and inputValue and change the render logic so the button is only rendered (or the entire input+button block is rendered) when value.length < maxItems, keeping the disabled check (disabled={!inputValue.trim() || value.length >= maxItems}) for safety.
9-15: 인터페이스 이름userExperienceProps→UserExperienceProps
user-profile.tsx와 동일하게 PascalCase 컨벤션을 적용해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/mypage/user-experience.tsx` around lines 9 - 15, Rename the interface userExperienceProps to PascalCase UserExperienceProps and update all references to it: change the declaration name and replace usages in the component props typing, any function signatures (e.g., the component definition that consumes value, label, placeholder, onChange, maxItems), prop type annotations, exports/imports, and any tests or call sites that reference userExperienceProps so they now reference UserExperienceProps.queries/api/mypage.ts (1)
18-29:sort기본값 빈 문자열이?sort=로 전송됩니다.
sort = ""는 searchParams에sort=(빈 값)로 포함됩니다. 백엔드가 이를 정상적으로 처리하는지 확인이 필요합니다. 정렬이 불필요할 때는 파라미터 자체를 제외하는 것이 더 안전합니다.♻️ 조건부 searchParams 구성 제안
export const getUserPrompts = async ({ userId, page = 0, size = 4, - sort = "", + sort, }: GetUserPromptsParams): Promise<UserPromptsResponse> => { + const searchParams: Record<string, string | number> = { page, size }; + if (sort) searchParams.sort = sort; + return await fetcher .get(`users/${userId}/prompts`, { - searchParams: { page, size, sort }, + searchParams, }) .json<UserPromptsResponse>(); };
getLikedPrompts에도 동일하게 적용해 주세요.Also applies to: 31-42
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@queries/api/mypage.ts` around lines 18 - 29, The current getUserPrompts sends sort="" which results in ?sort=; change the searchParams construction to omit the sort key when it's empty: inside getUserPrompts build a params object (e.g., start with { page, size }) and only add params.sort = sort if sort is truthy, then pass that params to fetcher.get; apply the same conditional params pattern to getLikedPrompts so neither function sends an empty sort query param.app/(afterLogin)/mypage/MypageActivity.tsx (1)
23-30: 활성 탭과 무관하게 양쪽 데이터를 모두 fetch하고 있습니다.
useLikedPrompts와useUserPrompts가 항상 호출되어, "좋아요" 탭만 보는 사용자도 "게시한 프롬프트" API를 호출합니다. React Query의enabled옵션으로 활성 탭의 데이터만 fetch하면 불필요한 네트워크 요청을 줄일 수 있습니다.// 예시: hooks에서 enabled 옵션 지원 시 const { data: likedData, isLoading: isLikedLoading } = useLikedPrompts( userId, currentPage, { enabled: activeSub === "liked" } );프리페칭 의도라면 현재 구현도 합리적이나, 그 경우 주석으로 의도를 명시해 두면 좋겠습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/MypageActivity.tsx around lines 23 - 30, The current implementation always calls useLikedPrompts and useUserPrompts regardless of which tab is active, causing unnecessary network requests; update the hook invocations (useLikedPrompts and useUserPrompts) to pass an options object that uses React Query's enabled flag tied to the active tab (e.g., enabled: activeSub === "liked" for liked data and enabled: activeSub === "posted" for posted data) so only the visible tab triggers fetching, or if the double-fetch is intentional add a clear inline comment near these calls stating that prefetching is desired.components/mypage/user-keyword.tsx (1)
30-36:useEffect로 width를 측정하는 패턴은 동작하지만, 레이아웃 시프트 가능성이 있습니다.
useEffect는 paint 이후 실행되므로, 입력값이 바뀔 때 한 프레임 동안 이전 너비가 보일 수 있습니다. React 18+에서는useLayoutEffect를 사용하면 paint 전에 DOM 측정 → 상태 업데이트가 이루어져 시각적 깜빡임을 방지할 수 있습니다.♻️ useLayoutEffect 적용 제안
-import { KeyboardEvent, useEffect, useRef, useState } from "react"; +import { KeyboardEvent, useLayoutEffect, useRef, useState } from "react"; ... - useEffect(() => { + useLayoutEffect(() => { if (spanRef.current) { const measuredWidth = spanRef.current.offsetWidth; setInputWidth(Math.max(60, measuredWidth + 36)); } }, [inputValue, isFocus]);단, SSR 환경이므로
"use client"선언이 되어 있어 클라이언트에서만 실행됩니다. Next.js에서useLayoutEffect사용 시 SSR 경고가 발생할 수 있으니, 필요하다면typeof window !== 'undefined'가드 또는 동적 import를 고려해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/mypage/user-keyword.tsx` around lines 30 - 36, Replace the post-paint measurement in the effect that sets input width with a layout-effect to avoid a visual layout shift: change the useEffect that reads spanRef.current.offsetWidth and calls setInputWidth to use useLayoutEffect (or conditionally use useLayoutEffect only on the client via typeof window !== 'undefined') so DOM measurement happens before paint; keep the same logic and references to spanRef and state variables (inputValue, isFocus) and preserve the Math.max(60, measuredWidth + 36) calculation.components/mypage/articleCard.tsx (1)
34-34: 클릭 가능한<article>에 키보드 접근성이 누락되어 있습니다.
<article onClick={onClick}>은 마우스 사용자에게만 동작합니다. 키보드 사용자는 이 카드에 포커스하거나 Enter로 활성화할 수 없습니다.role="button",tabIndex={0},onKeyDown핸들러를 추가하거나, 시맨틱하게<button>또는 Next.js<Link>로 감싸는 것을 권장합니다.♻️ 접근성 개선 제안 (LikedArticleCard 예시)
- <article className={STYLES.CARD_CONTAINER} onClick={onClick}> + <article + className={STYLES.CARD_CONTAINER} + onClick={onClick} + role={onClick ? "button" : undefined} + tabIndex={onClick ? 0 : undefined} + onKeyDown={onClick ? (e) => { if (e.key === "Enter" || e.key === " ") onClick(); } : undefined} + >
MyArticleCard(Line 63)에도 동일하게 적용해 주세요.Also applies to: 63-63
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/mypage/articleCard.tsx` at line 34, The clickable <article> element (className STYLES.CARD_CONTAINER) with onClick lacks keyboard accessibility; update the ArticleCard and MyArticleCard implementations so the interactive element is keyboard-focusable and activatable by Enter/Space — either replace the <article> with a semantic interactive element (a <button> or Next.js <Link>) or add role="button", tabIndex={0}, and an onKeyDown handler that triggers the same onClick callback when Enter or Space is pressed (ensure onKeyDown delegates to the onClick handler and prevents default for Space). Also keep existing onClick behavior and any styling/aria attributes intact.app/(afterLogin)/mypage/MypageRightSection.tsx (2)
173-187: 저장/취소 버튼 블록이 3회 반복됩니다.세 섹션 모두 동일한 저장/취소 버튼 UI를 가지고 있습니다. 별도 컴포넌트로 추출하면 유지보수가 훨씬 쉬워집니다.
♻️ 버튼 컴포넌트 추출 예시
function SectionActionButtons({ onSave, onCancel }: { onSave: () => void; onCancel: () => void }) { return ( <div className="flex gap-5 justify-end mt-2"> <BaseButton onClick={onCancel} variant="secondary" className="bg-gray-0 px-4 py-2.5 min-w-[197px]"> 취소 </BaseButton> <BaseButton onClick={onSave} className="px-4 py-2.5 min-w-[197px]"> 변경사항 저장 </BaseButton> </div> ); }Also applies to: 209-223, 249-263
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/MypageRightSection.tsx around lines 173 - 187, The three identical save/cancel button blocks in MypageRightSection.tsx (currently using handleCancel and handleGlobalSave) should be extracted into a reusable component (e.g., SectionActionButtons) that accepts onSave and onCancel props; replace the repeated JSX at the three locations (around the blocks using handleCancel/handleGlobalSave and the other two similar blocks) with this new component to remove duplication and centralize styling/behavior.
122-126:window.location.reload()는 SPA에서 비용이 큰 취소 방식입니다.전체 페이지를 새로고침하면 모든 클라이언트 상태가 날아가고, 네트워크 요청이 다시 발생합니다. 서버 데이터(
profile)로 로컬 상태를 리셋하는 방식이 더 자연스럽습니다.♻️ 상태 리셋 방식 제안
const handleCancel = () => { if (confirm("변경사항이 저장되지 않을 수 있습니다. 취소하시겠습니까?")) { - window.location.reload(); + // profile 데이터로 상태를 다시 동기화 + if (profile) { + const { careerInfo, selfIntro } = profile; + const jobsInKorean = careerInfo.targetJob.map( + (code) => JOB_MAP.get(code) || code + ); + setTargetJobs(jobsInKorean as JobType[]); + const serverStateKey = careerInfo.currentStatus[0]; + updateField("currentState", (STATE_MAP.get(serverStateKey) || "") as "" | StateType); + updateField("educationLevel", careerInfo.education); + const parsed = Number(careerInfo.careerYear); + updateField("career", careerInfo.careerYear === "신입" || Number.isNaN(parsed) ? 0 : parsed); + setExperiences(selfIntro.experiences || []); + setKeywords(selfIntro.keywords || []); + } } };참고: 이 로직은
useEffect의 초기화 로직과 중복되므로, 별도 함수로 추출하면 더 깔끔합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/MypageRightSection.tsx around lines 122 - 126, Replace the full-page reload in handleCancel with a local state reset: remove window.location.reload() from handleCancel and instead reset the component's editable state values (the same fields initialized from profile in the useEffect) back to profile's current values; extract that reset logic into a named function (e.g., resetFormFromProfile) so both useEffect's initialization and handleCancel call it, and ensure any derived UI flags (like isEditing or validation errors) are also reset.app/(beforeLogin)/(auth)/constant.ts (1)
25-39: Map 생성 로직이 반복됩니다 — 유틸리티 함수로 추출할 수 있습니다.
STATE_MAP/REVERSE_STATE_MAP와JOB_MAP/REVERSE_JOB_MAP의 생성 패턴이 동일합니다. 향후 매핑이 추가될 때 유지보수를 쉽게 하려면 헬퍼 함수를 고려해보세요.♻️ 유틸리티 함수 예시
function createBidiMaps<T extends Record<string, string>>(obj: T) { const forward = new Map<string, string>(Object.entries(obj)); const reverse = new Map<string, string>( Object.entries(obj).map(([k, v]) => [v, k]) ); return { forward, reverse } as const; } const { forward: STATE_MAP, reverse: REVERSE_STATE_MAP } = createBidiMaps(STATE_VALUES); const { forward: JOB_MAP, reverse: REVERSE_JOB_MAP } = createBidiMaps(JOB_DATA);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(beforeLogin)/(auth)/constant.ts around lines 25 - 39, The map creation is duplicated for STATE_VALUES and JOB_DATA; extract a small generic helper (e.g., createBidiMaps<T extends Record<string,string>>) that returns { forward, reverse } and use it to produce STATE_MAP, REVERSE_STATE_MAP and JOB_MAP, REVERSE_JOB_MAP; replace the existing new Map(...) calls with const { forward: STATE_MAP, reverse: REVERSE_STATE_MAP } = createBidiMaps(STATE_VALUES) and similarly for JOB_DATA, preserving string types and returning the pair as const so existing code continues to work.stories/Mypage/UserProfile.stories.tsx (1)
28-57: 스토리 구성이 잘 되어있습니다 👏카카오/네이버 프로바이더와 긴 소개글 케이스까지 다양한 시나리오를 커버합니다. 한 가지 참고사항:
UserProfile컴포넌트가profileImageprop도 받는데, 모든 스토리에서 빠져 있습니다. 프로필 이미지가 있을 때의 렌더링도 확인할 수 있도록 하나의 스토리에 추가하면 더 완성도 높은 커버리지가 됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stories/Mypage/UserProfile.stories.tsx` around lines 28 - 57, Add a new story case that supplies the missing profileImage prop so rendering with an avatar is covered: update the stories file to include profileImage in one existing or a new Story export (e.g., add profileImage to KakaoProfile or create a new WithImage story) and pass a representative image URL or mock asset to the UserProfile args; ensure the story uses the same arg shape as KakaoProfile/NaverProfile/LongIntroduction and references the UserProfile component props so the avatar rendering is exercised.mocks/handlers/mypage-handlers.ts (1)
41-101: prompts / liked 핸들러가 거의 동일합니다.두 GET 핸들러의 응답 구조가 URL 경로만 다르고 본문이 완전히 같습니다. Mock 코드라 중요도는 낮지만, 헬퍼 함수로 추출하면 mock 데이터 수정 시 한 곳만 변경하면 됩니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mocks/handlers/mypage-handlers.ts` around lines 41 - 101, The two http.get handlers for `${BASE_URL}/users/:userId/prompts` and `${BASE_URL}/users/:userId/liked` return identical UserPromptsResponse bodies; extract the duplicated response construction into a helper (e.g., makeUserPromptsResponse(userId, page, size) or buildUserPromptsData) that returns the { content, pageInfo } object (or full HttpResponse payload), then replace the inline response objects inside both handlers with a call to that helper using params.userId and the parsed page/size so future changes are made in one place.hooks/use-mypage.ts (1)
28-47: useUserPrompts / useLikedPrompts에 에러 처리가 빠져 있습니다.
useUserProfile에서는throwOnError를 설정했지만, 이 두 훅에는 에러 처리 전략이 없습니다. 서버 에러 발생 시 사용자에게 아무런 피드백이 전달되지 않을 수 있습니다. 일관성을 위해 동일한throwOnError패턴을 적용하거나, 명시적으로 에러 상태를 반환하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-mypage.ts` around lines 28 - 47, Add consistent error handling to the query hooks: update useUserPrompts and useLikedPrompts to mirror useUserProfile’s strategy by passing throwOnError: true (or another chosen error strategy) into useQuery options and/or return error state so callers can handle/display errors; locate the hooks useUserPrompts and useLikedPrompts and add the throwOnError option (or explicit onError/onSettled handlers) alongside queryKey, queryFn, placeholderData, and select to ensure server errors propagate consistently.mocks/handlers/auth-handlers.ts (1)
52-80: Mock 응답 형식이 기존 핸들러와 일관되지 않습니다.기존
social-login핸들러는status: 200(number)를 사용하지만, 새로 추가된 logout/delete 핸들러는code: "200"(string)을 사용합니다. 또한ApiResponse<T>인터페이스({ status: number; success: boolean; data: T })와도 다른 형태입니다.Mock 데이터가 실제 API 응답과 다르면, 개발 중 문제를 놓칠 수 있습니다. 실제 서버 응답 스펙에 맞춰 통일하는 것을 권장합니다.
♻️ ApiResponse 형식에 맞춘 수정 예시 (logout)
http.post(`${BASE_URL}/auth/logout`, () => { return HttpResponse.json( { success: true, - code: "200", + status: 200, data: {}, timestamp: new Date().toISOString(), }, { status: 200 } ); }),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mocks/handlers/auth-handlers.ts` around lines 52 - 80, The logout (http.post handler for `${BASE_URL}/auth/logout`) and delete (http.delete for `${BASE_URL}/users/:uid`) mocks use a nonstandard "code" string and a different shape; update both to match the ApiResponse<T> interface used elsewhere (e.g., social-login) by returning an object with numeric status (status: 200), success: true, and data: {...} containing the original payload (for logout an empty data object, for delete the uid/unlinkedProvider/terminatedAt fields), and remove the "code" string so the mock responses are consistent with ApiResponse and other handlers.queries/api/types.ts (1)
9-9:EducationValue가string으로 해소됩니다.
EDUCATION_OPTIONS가SelectOption[]로 타입 지정되어 있어서,(typeof EDUCATION_OPTIONS)[number]["value"]는string으로 확장됩니다. 리터럴 유니온("HIGH_SCHOOL" | "ASSOCIATE" | ...)으로 좁히려면EDUCATION_OPTIONS에as constassertion을 적용하고 타입 선언(SelectOption[])을 제거하거나,satisfies연산자를 사용해야 합니다.현재 동작에 문제는 없지만, 타입 안전성을 높이고 싶다면 고려해보세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@queries/api/types.ts` at line 9, EDUCATION_OPTIONS is currently typed as SelectOption[] so (typeof EDUCATION_OPTIONS)[number]["value"] widens to string; change the declaration of EDUCATION_OPTIONS to use a const assertion or the satisfies operator (e.g., remove the explicit SelectOption[] annotation and add "as const", or declare EDUCATION_OPTIONS using "satisfies readonly SelectOption[]") so that EducationValue (the type alias using (typeof EDUCATION_OPTIONS)[number]["value"]) resolves to the literal union (e.g., "HIGH_SCHOOL" | "ASSOCIATE" | ...).app/(afterLogin)/mypage/page.tsx (2)
20-20: JSX 문자열 prop에서 불필요한 중괄호 제거 (선택사항)
title={"마이페이지"}처럼 단순 문자열 리터럴에는 중괄호가 필요하지 않습니다.✨ 제안 수정
- <PageTitleGroup title={"마이페이지"} /> + <PageTitleGroup title="마이페이지" />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/page.tsx at line 20, The JSX passes a string literal to PageTitleGroup as title={"마이페이지"} with unnecessary braces; change the prop to use a plain string literal (title="마이페이지") in the PageTitleGroup usage to remove the redundant JSX expression and keep the markup idiomatic.
37-41:MypageActivitySection을Suspense로 감싸는 것을 권장합니다.
MypageActivitySection은 내부적으로useSearchParams()를 사용합니다. Next.js는useSearchParams()를 호출하는 클라이언트 컴포넌트를Suspense경계 안에 배치하도록 권장합니다. 이 페이지는 이미await searchParams때문에 동적 렌더링이므로 성능 저하의 위험은 낮지만, 탭 전환 시 스트리밍 UX와 로딩 상태 표시를 위해 추가하는 것이 좋습니다.💡 제안 수정: Suspense 경계 추가
+ import { Suspense } from "react"; {activeTab === "profile" ? ( <MypageRightSection /> ) : ( - <MypageActivitySection /> + <Suspense fallback={<div className="py-20 text-center text-gray-400">로딩 중...</div>}> + <MypageActivitySection /> + </Suspense> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/page.tsx around lines 37 - 41, Wrap the MypageActivitySection render in a React.Suspense boundary (with a small fallback UI like a spinner or placeholder) because MypageActivitySection uses useSearchParams() and Next.js recommends mounting client components that call useSearchParams inside Suspense; update the conditional rendering where activeTab === "activity" to render <Suspense fallback={...}><MypageActivitySection /></Suspense> so tab switches show a loading state and support streaming UX.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(afterLogin)/mypage/MypageActivity.tsx:
- Around line 19-21: activeSub is currently cast with searchParams.get("sub") as
"liked" | "posted" which only affects types and doesn't prevent invalid runtime
values; instead, read the raw value from searchParams.get("sub") and validate it
against the allowed set ("liked" and "posted") before assigning to activeSub
(e.g., if val === "liked" || val === "posted" then use val else default to
"liked"); update the code around activeSub and searchParams.get to use this
runtime check or a small type-guard helper so invalid query values fall back to
"liked".
In `@app/`(afterLogin)/mypage/MypageLeftSection.tsx:
- Around line 44-47: The delete call in MypageLeftSection uses a hard-coded
reason string; update this by wiring deleteUserAccount(userData.basicInfo.uid, {
reason }) to a real source instead of the literal "서비스 이용 불편": either (1) add a
UI hook (e.g., modal with radio/input) to collect the user's selected reason and
pass that variable into deleteUserAccount, or if immediate UI work is not
possible, extract the string into a named constant like
DEFAULT_ACCOUNT_DELETE_REASON (and add a TODO comment) and replace the literal
with that constant so the intent is clear and easy to replace later.
- Around line 49-58: Ensure both tokens are removed on successful withdrawal by
calling Cookies.remove("refreshToken") in addition to
Cookies.remove("accessToken") in the withdrawal success path (look for
Cookies.remove calls in MypageLeftSection's withdrawal handler). Also restore
user-facing error feedback by re-enabling or adding a toasts.error(...) inside
the catch block (refer to the commented toasts.error and the catch(error) block)
and include a brief, localized message plus optional error details for logging
(console.error already present) so users see a failure notification.
- Around line 30-33: The catch block in MypageLeftSection.tsx only removes
"accessToken" on logout failure, leaving "refreshToken" in cookies and providing
no user feedback; update the catch handler to also remove
Cookies.remove("refreshToken") and surface an error to the user (use the
component's existing notification mechanism such as a toast/alert or setError
state) so tokens are cleared and the user is informed when logout fails.
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 91-120: handleGlobalSave currently builds an UpdateProfileRequest
payload but omits the marketingAgree state managed earlier (marketingAgree set
around lines ~35 and bound to the checkbox at lines ~235-240), so changes to
that checkbox are never sent; update handleGlobalSave to include marketingAgree
in the payload in the correct section (e.g., add marketingAgree: marketingAgree
under basicInfo or the agreed schema used by UpdateProfileRequest) and ensure
the UpdateProfileRequest type (or backend contract) includes that field — if the
type lacks it, add the field to the interface or remove/disable the checkbox UI
to avoid misleading users.
- Around line 241-247: The Link in MypageRightSection (the Link element
rendering "보기") uses href="#" together with target="_blank"; update this by
either replacing href="#" with the actual target URL when available, or remove
target="_blank" if the URL is not set, or disable the link behavior until a real
URL exists (e.g., keep href="#" but add an onClick that calls
event.preventDefault() and set aria-disabled/class to indicate disabled); locate
the Link in the MypageRightSection component and apply one of these fixes so
clicking does not open the current page in a new tab.
- Around line 56-59: The code currently takes only the first element of
careerInfo.currentStatus (serverStateKey = careerInfo.currentStatus[0]) and maps
it via STATE_MAP, which drops any additional status items; instead, handle the
array properly by mapping each element in careerInfo.currentStatus through
STATE_MAP to produce an array (or a joined string) of Korean labels (e.g.,
map/filter undefined entries), and update the consumer (stateInKorean or the UI
component StatusSelect) to accept multiple values or a concatenated
representation so no server-side statuses are lost; verify whether StatusSelect
should be switched to multi-select or the server spec updated and implement the
matching change where currentStatus, serverStateKey, stateInKorean, and
StatusSelect are used.
- Around line 37-44: The Mypage component is reusing the signup global store
(useSignupStore) exposing targetJobs, setTargetJobs, currentState, career,
educationLevel, updateField which causes lifecycle and reset() coupling; fix by
introducing a dedicated local state or separate store (e.g., useMypageStore or
React useState hooks for targetJobs, currentState, career, educationLevel and
local handlers replacing updateField/setTargetJobs) and migrate all Mypage
reads/writes to that new state so reset() on the signup store no longer affects
the page; if immediate separation isn't possible, add a clear TODO comment in
MypageRightSection noting the dependency on useSignupStore and the need to
create a local store to avoid cross-feature coupling.
- Around line 65-67: serverCareer (from careerInfo.careerYear) is being
converted with Number(...) which can produce NaN for non-numeric strings; change
the logic in MypageRightSection so you map "신입" to 0 and for other values
parseInt(serverCareer, 10) (or Number) then check for NaN (Number.isFinite or
isNaN) and use a safe fallback (e.g., 0 or null) before calling
updateField("career", ...); update the code around serverCareer, careerNumber,
and updateField to validate/normalize the parsed value and avoid passing NaN to
updateField.
In `@app/`(afterLogin)/mypage/page.tsx:
- Around line 13-14: The code uses a type assertion for searchParams.tab and
then falls back with ||, which lets invalid truthy strings (e.g. "settings")
pass through; update the runtime validation so activeTab is explicitly derived
by checking the actual value of searchParams.tab (or the local variable tab)
against the allowed union values "profile" and "activity" and only assign one of
those, otherwise default to "profile"; specifically modify the logic around tab
and activeTab (the variables referenced in this file) to perform a runtime check
(e.g., conditional or switch) and ensure downstream checks like activeTab ===
"profile" behave correctly.
In `@app/`(beforeLogin)/(auth)/constant.ts:
- Line 51: The frontend uses the constant entry with value "BACHELOR" in
app/(beforeLogin)/(auth)/constant.ts but there is no reference to "BACHELOR_4";
confirm with the backend/DB team whether existing records store "BACHELOR_4" or
"BACHELOR" and whether a migration or mapping exists, and then either (A) ask
backend to implement a mapping/migration from "BACHELOR_4" → "BACHELOR", or (B)
add compatibility in the frontend by updating the education options handling
(the array entry containing { label: "대학교 졸업", value: "BACHELOR" } and any
serializer/deserializer that reads user.education) to accept and normalize
"BACHELOR_4" to "BACHELOR" before display or submit.
In `@components/mypage/user-experience.tsx`:
- Around line 26-31: The addItem function currently trims inputValue and appends
it without checking for duplicates, allowing the same experience to be added
multiple times; modify addItem to compute const trimmedValue =
inputValue.trim(), then return early if !trimmedValue, value.length >= maxItems,
or value.includes(trimmedValue) to prevent duplicates, and only call
onChange([...value, trimmedValue]) and setInputValue("") when all checks pass;
reference the addItem function and the variables inputValue, value, maxItems,
onChange, and setInputValue when making this change.
In `@components/mypage/user-keyword.tsx`:
- Around line 88-91: The className template injects the string "false" when
isFocus is falsy; change the className call to use the cn(...) utility's
conditional argument form so falsy values are filtered out (i.e., pass the base
class string and then isFocus && "bg-gray-0 border border-gray-100 rounded-full"
as separate arguments to cn). Update the div's className expression that
currently uses a template literal with ${isFocus && "..."} to instead call
cn(baseClasses, isFocus && conditionalClasses) so only the intended classes are
applied.
In `@hooks/use-mypage.ts`:
- Around line 58-75: The onError handler in use-mypage.ts leaves user feedback
commented out and uses console.error; update the ApiError branch where
error.status === 401 (inside onError) to show a user-facing toast (use the
existing toasts API) and optionally call router.push('/login') for
session-expiry redirect, or if you intend to implement later, replace commented
lines with a single TODO comment referencing toasts.error and router.push and
keep behavior explicit; for non-ApiError/network errors replace the
console.error calls with a user-facing toast (or a TODO if not yet supported)
and remove or guard the production console.error calls (e.g., log only in
development) so no stray console errors remain in prod.
In `@mocks/handlers/mypage-handlers.ts`:
- Line 67: The isLast flag is hardcoded (isLast: page >= 11) which breaks when
size changes; replace both hardcoded occurrences with a dynamic calculation
using total and size (e.g., compute totalPages = Math.ceil(total / size) and set
isLast = page >= totalPages - 1) so pagination respects variable page size and
total items.
In `@mocks/handlers/onboarding-handlers.ts`:
- Line 3: BASE_URL is set from process.env.NEXT_PUBLIC_BACKEND_API_URL and can
be undefined, causing MSW handlers to register with "undefined/..." and silently
fail; update the BASE_URL definition in onboarding-handlers.ts (the BASE_URL
constant) to provide a safe fallback (e.g., a default localhost/test URL) or to
throw/log a clear runtime warning when the env var is missing so handlers
register correctly; ensure the change affects all handler registrations that
reference BASE_URL so mocks will match even when NEXT_PUBLIC_BACKEND_API_URL is
not set.
In `@queries/api/types.ts`:
- Around line 19-25: CareerInfo currently mismatches the request model
(UpdateProfileRequest) for careerYear (string vs number), currentStatus
(StateKey[] vs single string via REVERSE_STATE_MAP) and education
(EducationValue vs string); either add a clear comment above the
CareerInfo/UpdateProfileRequest type declarations stating these are
intentionally different read/write models and describing the required
transformations, or implement a single conversion function (e.g.,
normalizeCareerInfo(career: CareerInfo): UpdateProfileRequest) that converts
careerYear ("신입" -> 0 or Number(...)), maps currentStatus array -> single status
string via REVERSE_STATE_MAP, and serializes education to the request string,
then use that function wherever UpdateProfileRequest is created to centralize
and type-safe the transformations.
In `@stories/Mypage/UserExperience.stories.tsx`:
- Around line 6-53: The meta for the UserExperience stories is missing default
props for required fields; update the exported meta object (meta) to include an
args entry that provides default label and placeholder (e.g., label and
placeholder strings) so all stories (including Interactive which uses args)
inherit them; leave Interactive.args (value, maxItems) as-is so it can override
or extend those defaults if needed.
In `@stories/Mypage/UserKeyword.stories.tsx`:
- Around line 6-30: meta for the UserKeyword story is missing a default label
arg; add a meta.args object containing a label default (e.g., label: "라벨") so
the UserKeyword component receives the required prop in stories. Locate the
exported meta constant (const meta: Meta<typeof UserKeyword>) and add or extend
meta.args to include label, optionally also setting sensible defaults for
maxTags and value if desired to mirror UserExperience.stories.tsx.
---
Nitpick comments:
In `@app/`(afterLogin)/mypage/layout.tsx:
- Around line 3-13: The component name DashboardLayout is misleading for the
mypage layout and duplicates JSX used elsewhere; rename the exported function to
MypageLayout (update the default export identifier DashboardLayout ->
MypageLayout) and extract the shared JSX into a single reusable component (e.g.,
PageLayout or SharedPageLayout) that renders Header and {children}; then import
and use that shared component from this file (replace the current JSX in
DashboardLayout/MypageLayout with a call to the shared PageLayout) so both
layouts share one implementation.
In `@app/`(afterLogin)/mypage/MypageActivity.tsx:
- Around line 23-30: The current implementation always calls useLikedPrompts and
useUserPrompts regardless of which tab is active, causing unnecessary network
requests; update the hook invocations (useLikedPrompts and useUserPrompts) to
pass an options object that uses React Query's enabled flag tied to the active
tab (e.g., enabled: activeSub === "liked" for liked data and enabled: activeSub
=== "posted" for posted data) so only the visible tab triggers fetching, or if
the double-fetch is intentional add a clear inline comment near these calls
stating that prefetching is desired.
In `@app/`(afterLogin)/mypage/MypageLeftSection.tsx:
- Line 19: Replace blocking window.confirm usage in MypageLeftSection's logout
flow with a non-blocking Radix Dialog: remove the direct confirm("...") call
inside the logout handler (the code around the confirm at line ~19 and the
similar block at lines ~38-40) and implement a controlled <Dialog> (from
`@radix-ui/react-dialog`) that opens when the user clicks "로그아웃", presents the
message and explicit Cancel/Confirm actions, and only calls the existing logout
function (or dispatch/handleLogout method) when the Dialog's Confirm button is
clicked; ensure the dialog state is tracked in the component (e.g.,
isLogoutDialogOpen) and accessibility props (aria-labels) are set.
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 173-187: The three identical save/cancel button blocks in
MypageRightSection.tsx (currently using handleCancel and handleGlobalSave)
should be extracted into a reusable component (e.g., SectionActionButtons) that
accepts onSave and onCancel props; replace the repeated JSX at the three
locations (around the blocks using handleCancel/handleGlobalSave and the other
two similar blocks) with this new component to remove duplication and centralize
styling/behavior.
- Around line 122-126: Replace the full-page reload in handleCancel with a local
state reset: remove window.location.reload() from handleCancel and instead reset
the component's editable state values (the same fields initialized from profile
in the useEffect) back to profile's current values; extract that reset logic
into a named function (e.g., resetFormFromProfile) so both useEffect's
initialization and handleCancel call it, and ensure any derived UI flags (like
isEditing or validation errors) are also reset.
In `@app/`(afterLogin)/mypage/MypageTabButtons.tsx:
- Line 8: The MypageTabButtons component currently types the prop too broadly as
activeTab: string; change the prop signature in the MypageTabButtons function to
use the tighter union type activeTab: "profile" | "activity" (matching the
existing handleTabChange parameter) and update any related local variables or
callers if necessary to satisfy the narrowed type; reference the
MypageTabButtons function and the handleTabChange union to locate where to
adjust types.
In `@app/`(afterLogin)/mypage/page.tsx:
- Line 20: The JSX passes a string literal to PageTitleGroup as title={"마이페이지"}
with unnecessary braces; change the prop to use a plain string literal
(title="마이페이지") in the PageTitleGroup usage to remove the redundant JSX
expression and keep the markup idiomatic.
- Around line 37-41: Wrap the MypageActivitySection render in a React.Suspense
boundary (with a small fallback UI like a spinner or placeholder) because
MypageActivitySection uses useSearchParams() and Next.js recommends mounting
client components that call useSearchParams inside Suspense; update the
conditional rendering where activeTab === "activity" to render <Suspense
fallback={...}><MypageActivitySection /></Suspense> so tab switches show a
loading state and support streaming UX.
In `@app/`(beforeLogin)/(auth)/constant.ts:
- Around line 25-39: The map creation is duplicated for STATE_VALUES and
JOB_DATA; extract a small generic helper (e.g., createBidiMaps<T extends
Record<string,string>>) that returns { forward, reverse } and use it to produce
STATE_MAP, REVERSE_STATE_MAP and JOB_MAP, REVERSE_JOB_MAP; replace the existing
new Map(...) calls with const { forward: STATE_MAP, reverse: REVERSE_STATE_MAP }
= createBidiMaps(STATE_VALUES) and similarly for JOB_DATA, preserving string
types and returning the pair as const so existing code continues to work.
In `@app/`(beforeLogin)/(auth)/signup/component/Detail.tsx:
- Around line 89-103: Replace JSX prop usages that wrap static string literals
in braces with plain string literals: change label={"현재 상태를 선택해주세요."} and
label={"목표 직무를 선택해주세요 (복수선택가능)."} to label="현재 상태를 선택해주세요." and label="목표 직무를
선택해주세요 (복수선택가능)." respectively; update the label prop on the component that has
value={currentState} and otherValue={otherInput} as well as on TargetJobsSelect
to remove the unnecessary {} around static strings.
In `@app/types/type.ts`:
- Around line 27-41: Summary: The name BasePrompt is easily confused with the
existing PromptBase; rename it to clearly indicate it's for the My Page shapes
(and update any usages). Change the interface name BasePrompt to a descriptive
MyPagePromptBase (or MyPromptBase) and update the derived interfaces LikedPrompt
and MyPrompt to extend that new name; search for and update all references to
BasePrompt and any code that expects promptId:number (vs PromptBase.id:string)
to use the new type so callers compile—make sure to adjust import/export names
where this file is consumed.
In `@components/mypage/articleCard.tsx`:
- Line 34: The clickable <article> element (className STYLES.CARD_CONTAINER)
with onClick lacks keyboard accessibility; update the ArticleCard and
MyArticleCard implementations so the interactive element is keyboard-focusable
and activatable by Enter/Space — either replace the <article> with a semantic
interactive element (a <button> or Next.js <Link>) or add role="button",
tabIndex={0}, and an onKeyDown handler that triggers the same onClick callback
when Enter or Space is pressed (ensure onKeyDown delegates to the onClick
handler and prevents default for Space). Also keep existing onClick behavior and
any styling/aria attributes intact.
In `@components/mypage/user-experience.tsx`:
- Around line 60-83: The Add button remains visible even when maxItems is
reached causing awkward UX; wrap the BaseButton render in the same condition as
the input (value.length < maxItems) or otherwise coordinate their visibility so
both BaseInput and BaseButton are shown/hidden together. Locate the JSX that
uses value, maxItems, BaseInput, BaseButton, addItem and inputValue and change
the render logic so the button is only rendered (or the entire input+button
block is rendered) when value.length < maxItems, keeping the disabled check
(disabled={!inputValue.trim() || value.length >= maxItems}) for safety.
- Around line 9-15: Rename the interface userExperienceProps to PascalCase
UserExperienceProps and update all references to it: change the declaration name
and replace usages in the component props typing, any function signatures (e.g.,
the component definition that consumes value, label, placeholder, onChange,
maxItems), prop type annotations, exports/imports, and any tests or call sites
that reference userExperienceProps so they now reference UserExperienceProps.
In `@components/mypage/user-keyword.tsx`:
- Around line 30-36: Replace the post-paint measurement in the effect that sets
input width with a layout-effect to avoid a visual layout shift: change the
useEffect that reads spanRef.current.offsetWidth and calls setInputWidth to use
useLayoutEffect (or conditionally use useLayoutEffect only on the client via
typeof window !== 'undefined') so DOM measurement happens before paint; keep the
same logic and references to spanRef and state variables (inputValue, isFocus)
and preserve the Math.max(60, measuredWidth + 36) calculation.
In `@components/mypage/user-profile.tsx`:
- Around line 36-52: Move the provider→theme mapping out of the component by
replacing the inline getTheme function with a top-level constant (e.g.,
PROVIDER_THEMES or THEME_MAP) that maps provider keys ("NAVER", "KAKAO") to
their theme objects (bg, label, image using STATIC_IMAGES); then update usages
that call getTheme() to reference the mapping by key (fall back to the KAKAO
entry when provider is missing/unknown) so the component no longer recreates the
function each render.
- Around line 21-27: Rename the interface userProfileProps to PascalCase
UserProfileProps and update all references to it (e.g., any component prop
annotations or type usages such as the props parameter in the UserProfile
component or other files importing this type); ensure the exported name (if
exported) and any imports/exports are updated to the new identifier so
TypeScript typing remains consistent (also update the occurrence mentioned near
the component props usage that currently references userProfileProps).
In `@hooks/use-mypage.ts`:
- Around line 28-47: Add consistent error handling to the query hooks: update
useUserPrompts and useLikedPrompts to mirror useUserProfile’s strategy by
passing throwOnError: true (or another chosen error strategy) into useQuery
options and/or return error state so callers can handle/display errors; locate
the hooks useUserPrompts and useLikedPrompts and add the throwOnError option (or
explicit onError/onSettled handlers) alongside queryKey, queryFn,
placeholderData, and select to ensure server errors propagate consistently.
In `@lib/utils.ts`:
- Around line 20-23: The function formatRelativeDate currently calls
parseISO(dateString) and passes the result directly to formatDistanceToNow,
which throws if given an invalid Date; update formatRelativeDate to defensively
validate the parsed date (using date-fns isValid or checking
isNaN(date.getTime())) and return a safe fallback (e.g., empty string, original
string, or a localized "unknown" message) when the date is invalid or input is
missing; ensure you still import any helper used (isValid) and keep existing
behavior for valid dates in formatRelativeDate that uses parseISO and
formatDistanceToNow with locale ko.
In `@mocks/handlers/auth-handlers.ts`:
- Around line 52-80: The logout (http.post handler for
`${BASE_URL}/auth/logout`) and delete (http.delete for `${BASE_URL}/users/:uid`)
mocks use a nonstandard "code" string and a different shape; update both to
match the ApiResponse<T> interface used elsewhere (e.g., social-login) by
returning an object with numeric status (status: 200), success: true, and data:
{...} containing the original payload (for logout an empty data object, for
delete the uid/unlinkedProvider/terminatedAt fields), and remove the "code"
string so the mock responses are consistent with ApiResponse and other handlers.
In `@mocks/handlers/mypage-handlers.ts`:
- Around line 41-101: The two http.get handlers for
`${BASE_URL}/users/:userId/prompts` and `${BASE_URL}/users/:userId/liked` return
identical UserPromptsResponse bodies; extract the duplicated response
construction into a helper (e.g., makeUserPromptsResponse(userId, page, size) or
buildUserPromptsData) that returns the { content, pageInfo } object (or full
HttpResponse payload), then replace the inline response objects inside both
handlers with a call to that helper using params.userId and the parsed page/size
so future changes are made in one place.
In `@queries/api/auth.ts`:
- Around line 31-33: postLogout currently types the response as
ApiResponse<object>, which is too broad; update the generic to the actual shape
returned by the logout endpoint (e.g., ApiResponse<{ message: string }> if it
returns a message, or ApiResponse<null> if no body) by changing the type in the
fetcher call inside the postLogout function so the return signature accurately
reflects the API response and improves type safety and autocompletion.
- Around line 39-40: Narrow the deleteUserAccount(uid: string | number)
parameter to uid: string and update call sites or validate/convert basicInfo.uid
before calling; locate the deleteUserAccount function and change its signature
to accept only string, and either change basicInfo.uid to be string at its
source or add a clear runtime check/coercion where deleteUserAccount is invoked
so only a string is passed.
In `@queries/api/mypage.ts`:
- Around line 18-29: The current getUserPrompts sends sort="" which results in
?sort=; change the searchParams construction to omit the sort key when it's
empty: inside getUserPrompts build a params object (e.g., start with { page,
size }) and only add params.sort = sort if sort is truthy, then pass that params
to fetcher.get; apply the same conditional params pattern to getLikedPrompts so
neither function sends an empty sort query param.
In `@queries/api/types.ts`:
- Line 9: EDUCATION_OPTIONS is currently typed as SelectOption[] so (typeof
EDUCATION_OPTIONS)[number]["value"] widens to string; change the declaration of
EDUCATION_OPTIONS to use a const assertion or the satisfies operator (e.g.,
remove the explicit SelectOption[] annotation and add "as const", or declare
EDUCATION_OPTIONS using "satisfies readonly SelectOption[]") so that
EducationValue (the type alias using (typeof
EDUCATION_OPTIONS)[number]["value"]) resolves to the literal union (e.g.,
"HIGH_SCHOOL" | "ASSOCIATE" | ...).
In `@stories/Mypage/UserProfile.stories.tsx`:
- Around line 28-57: Add a new story case that supplies the missing profileImage
prop so rendering with an avatar is covered: update the stories file to include
profileImage in one existing or a new Story export (e.g., add profileImage to
KakaoProfile or create a new WithImage story) and pass a representative image
URL or mock asset to the UserProfile args; ensure the story uses the same arg
shape as KakaoProfile/NaverProfile/LongIntroduction and references the
UserProfile component props so the avatar rendering is exercised.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
app/(afterLogin)/mypage/page.tsx (1)
14-15:VALID_TABS와ActiveTab을 모듈 레벨로 이동하는 것을 고려해보세요.현재는 서버 컴포넌트 함수 내부에 정의되어 있어 요청마다 배열이 재생성됩니다. 큰 문제는 아니지만 모듈 상단으로 이동하면 코드 의도가 더 명확하게 드러나고, 향후
MypageTabButtons의 허용 탭 목록과 공유하기도 쉬워집니다.♻️ 모듈 레벨 이동 제안
+const VALID_TABS = ["profile", "activity"] as const; +type ActiveTab = (typeof VALID_TABS)[number]; export default async function MyPage({ searchParams }: Props) { const { tab } = await searchParams; - const VALID_TABS = ["profile", "activity"] as const; - type ActiveTab = (typeof VALID_TABS)[number]; const activeTab: ActiveTab = VALID_TABS.includes(tab as ActiveTab) ? (tab as ActiveTab) : "profile";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/page.tsx around lines 14 - 15, Move the constants out of the server component and up to module scope: take VALID_TABS and the derived type ActiveTab currently defined inside the component in page.tsx and declare them at the top of the module so they are created once per module load; then import or reference the module-level VALID_TABS/ActiveTab from the component and from MypageTabButtons to share the canonical tab list. Ensure any usages (e.g., type annotations or runtime checks) continue to use the moved symbols and remove the inner definitions to avoid shadowing.app/(afterLogin)/mypage/MypageRightSection.tsx (1)
109-113:window.location.reload()대신 Next.js 라우터를 활용하세요.풀 페이지 리로드는 Flash of Unstyled Content와 불필요한 네트워크 비용을 발생시킵니다. Next.js App Router 환경에서는
router.refresh()로 서버 데이터를 재검증하고reset()으로 스토어를 초기화하는 조합이 더 적합합니다.♻️ 개선 제안
+ const router = useRouter(); const handleCancel = () => { if (confirm("변경사항이 저장되지 않을 수 있습니다. 취소하시겠습니까?")) { - window.location.reload(); + reset(); + router.refresh(); } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/MypageRightSection.tsx around lines 109 - 113, The handleCancel function currently calls window.location.reload(); replace this with Next.js router usage: get the router via useRouter() from 'next/navigation' inside MypageRightSection and call router.refresh() instead of reload to revalidate server data, and also call your local store/form reset function (e.g., reset() from the zustand or form store you use) to clear client state — update handleCancel to import/useRouter and the store's reset function and invoke router.refresh() followed by reset() to avoid a full page reload.app/store/mypageStore.ts (1)
9-22:updateField의 제네릭 제약을 데이터 필드만으로 제한하세요.현재
K extends keyof ProfileState는 액션 메서드(updateField,setTargetJobs,reset)도 포함합니다. 따라서updateField("reset", someFunction)같은 호출이 타입 오류 없이 허용되어 Zustand 액션을 덮어쓸 수 있습니다.signUpStore.ts는 별도의SignupFieldKey유니언 타입으로 이 문제를 방지하고 있으므로, 같은 패턴을 적용하는 것이 좋습니다.♻️ 데이터 필드 전용 키 타입 제한 제안
+type ProfileDataKey = "currentState" | "targetJobs" | "career" | "educationLevel"; interface ProfileState { currentState: "" | StateType; targetJobs: JobType[]; career: number; educationLevel: "" | EducationValue; // 상태 변경 액션 - updateField: <K extends keyof ProfileState>( + updateField: <K extends ProfileDataKey>( field: K, value: ProfileState[K] ) => void; setTargetJobs: (jobs: JobType[]) => void; reset: () => void; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/store/mypageStore.ts` around lines 9 - 22, The updateField generic currently allows keys of ProfileState including methods, so define a new union type (e.g., ProfileFieldKey) listing only data keys ("currentState" | "targetJobs" | "career" | "educationLevel") and change updateField's signature to use K extends ProfileFieldKey with value: ProfileState[K]; update the ProfileState interface to reference this new key type for updateField to prevent overwriting actions like setTargetJobs or reset (symbols: ProfileState, updateField, setTargetJobs, reset).app/(beforeLogin)/(auth)/constant.ts (1)
38-46:StateKey와EducationValue타입이queries/api/types.ts와 중복 정의됩니다.
queries/api/types.ts에 이미 동일한 이름의 타입이 존재합니다:
StateKey = keyof typeof STATE_VALUES(동일)EducationValue = (typeof EDUCATION_OPTIONS)[number]["value"](파생형으로 동일한 유니언 해석)특히
EducationValue의 경우,constant.ts는 수동 유니언으로 정의하고,types.ts는EDUCATION_OPTIONS에서 자동 파생합니다.EDUCATION_OPTIONS가 수정되면types.ts는 자동 반영되지만constant.ts의 수동 유니언은 그렇지 않아 타입 불일치 위험이 있습니다.중복 제거를 위해
queries/api/types.ts의StateKey/EducationValue를 삭제하고constant.ts에서만 단일 출처로 내보내거나, 반대로constant.ts에서 re-export하도록 정리하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(beforeLogin)/(auth)/constant.ts around lines 38 - 46, Remove the duplicate type definitions by keeping a single source of truth: export StateKey and EducationValue from the constants module (use the existing STATE_VALUES and EDUCATION_OPTIONS to derive their types) and update the other module to import/re-export them instead of redefining; specifically, remove the manual EducationValue and StateKey definitions from the other types file and replace any uses with imports of StateKey and EducationValue from the module that defines STATE_VALUES and EDUCATION_OPTIONS so the union types stay derived and synchronized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Line 27: Destructure the pending state from useUpdateProfile (e.g., const {
mutate: updateProfile, isPending } = useUpdateProfile()) and use that to prevent
duplicate submissions: add disabled={isPending} to each "변경사항 저장" button in
MypageRightSection and, defensively, early-return in the save handlers when
isPending is true before calling updateProfile. Ensure you update all three save
buttons/handlers where updateProfile is used so the UI is disabled and the
mutation is not invoked while isPending is true.
- Line 32: The marketingAgree state is always initialized to false and never set
from the server; update initialization by loading the user's terms agreements
from the GET /users/me/terms-agreements API and set marketingAgree (via
setMarketingAgree) to true if the returned list contains termId === 3 as agreed;
implement this in the component (e.g., inside the existing useEffect that loads
profile data or a new useEffect) so the checkbox reflects the server value on
mount and handle errors gracefully.
In `@app/`(beforeLogin)/(auth)/constant.ts:
- Around line 58-59: The current conversion of careerInfo.careerYear to
careerNumber using Number(...) can produce NaN for non-numeric strings; update
the logic around careerNumber (based on careerInfo.careerYear) to safely parse
and validate the numeric value (e.g., use parseInt/Number and then
Number.isFinite or isNaN checks) and default to 0 for any non-numeric/invalid
input so the Zustand store's career field never receives NaN; ensure the
normalized value is used wherever careerNumber is referenced (same flow that
MypageRightSection.tsx relied on).
In `@mocks/handlers/onboarding-handlers.ts`:
- Around line 3-4: The BASE_URL constant in the real API client (BASE_URL in
onboarding.ts) lacks the nullish-fallback present in the mock
(onboarding-handlers.ts), causing "undefined" requests if
NEXT_PUBLIC_BACKEND_API_URL is unset; add the same nullish fallback (??
"http://localhost:8080/api/v1") to the BASE_URL declaration in
queries/api/onboarding.ts, audit other handlers that currently use || or no
fallback (e.g., mypage-handlers, nickname-handlers, auth-handlers) and replace
|| with ?? or add the same nullish fallback for BASE_URL to ensure consistent
behavior across mocks and real client, and confirm the team-wide canonical
fallback value (including whether to include /api/v1).
In `@stories/Mypage/UserExperience.stories.tsx`:
- Around line 62-108: The three stories DefaultWithData, MaxReached and
ManyItemsAllowed are missing the onChange prop expected by the UserExperience
component and thus throw TypeError when interacting; fix by providing an
onChange handler for each story (either wrap the story with a stateful decorator
like the existing Interactive story to manage value via useState and pass
setValue as onChange, or add a no-op onChange = () => {} via each story's args
or meta.args) so that UserExperience's calls to onChange are always a function.
---
Duplicate comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 228-234: The Link element in MypageRightSection (the <Link
href="#" ... target="_blank"> 보기 </Link>) currently uses target="_blank" with a
placeholder href; remove the target attribute or add an onClick handler that
calls event.preventDefault() to block navigation until a real URL is
available—locate the Link in MypageRightSection.tsx and either delete
target="_blank" or augment the Link with onClick={(e) => e.preventDefault()}
(and optionally a comment noting to restore target when href is finalized).
- Around line 91-107: handleGlobalSave currently builds an UpdateProfileRequest
and calls updateProfile(payload) but omits the marketingAgree value; instead of
adding marketingAgree to UpdateProfileRequest, call the separate
terms-agreements API (e.g. a client function like saveTermsAgreements or
postUserTermsAgreements) with the current marketingAgree state immediately after
updateProfile(payload) (or in parallel) so the user's marketing consent is
persisted via POST /users/me/terms-agreements; reference handleGlobalSave,
UpdateProfileRequest, and updateProfile when making this change.
---
Nitpick comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 109-113: The handleCancel function currently calls
window.location.reload(); replace this with Next.js router usage: get the router
via useRouter() from 'next/navigation' inside MypageRightSection and call
router.refresh() instead of reload to revalidate server data, and also call your
local store/form reset function (e.g., reset() from the zustand or form store
you use) to clear client state — update handleCancel to import/useRouter and the
store's reset function and invoke router.refresh() followed by reset() to avoid
a full page reload.
In `@app/`(afterLogin)/mypage/page.tsx:
- Around line 14-15: Move the constants out of the server component and up to
module scope: take VALID_TABS and the derived type ActiveTab currently defined
inside the component in page.tsx and declare them at the top of the module so
they are created once per module load; then import or reference the module-level
VALID_TABS/ActiveTab from the component and from MypageTabButtons to share the
canonical tab list. Ensure any usages (e.g., type annotations or runtime checks)
continue to use the moved symbols and remove the inner definitions to avoid
shadowing.
In `@app/`(beforeLogin)/(auth)/constant.ts:
- Around line 38-46: Remove the duplicate type definitions by keeping a single
source of truth: export StateKey and EducationValue from the constants module
(use the existing STATE_VALUES and EDUCATION_OPTIONS to derive their types) and
update the other module to import/re-export them instead of redefining;
specifically, remove the manual EducationValue and StateKey definitions from the
other types file and replace any uses with imports of StateKey and
EducationValue from the module that defines STATE_VALUES and EDUCATION_OPTIONS
so the union types stay derived and synchronized.
In `@app/store/mypageStore.ts`:
- Around line 9-22: The updateField generic currently allows keys of
ProfileState including methods, so define a new union type (e.g.,
ProfileFieldKey) listing only data keys ("currentState" | "targetJobs" |
"career" | "educationLevel") and change updateField's signature to use K extends
ProfileFieldKey with value: ProfileState[K]; update the ProfileState interface
to reference this new key type for updateField to prevent overwriting actions
like setTargetJobs or reset (symbols: ProfileState, updateField, setTargetJobs,
reset).
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
mocks/handlers/prompt-list-handlers.ts (1)
264-276:⚠️ Potential issue | 🟡 Minor
console.log디버그 로그 제거 필요Line 267에
console.log디버그 출력이 남아 있습니다. PR 체크리스트에서도 불필요한console.log제거를 명시하고 있습니다. 이 핸들러가 MSW mock이라 프로덕션에 직접 영향을 주지는 않지만, 테스트·스토리북 실행 시 콘솔 노이즈가 발생합니다.🧹 수정 제안
http.delete(`${BASE_URL}/prompts/like/:promptId`, ({ params }) => { const { promptId } = params; - console.log(`Prompt ${promptId} 좋아요 취소됨`); return HttpResponse.json(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mocks/handlers/prompt-list-handlers.ts` around lines 264 - 276, Remove the stray debug console.log from the MSW delete handler for prompt likes: locate the http.delete(`${BASE_URL}/prompts/like/:promptId`, ({ params }) => { ... }) handler and delete the line console.log(`Prompt ${promptId} 좋아요 취소됨`); so the handler only returns the HttpResponse.json payload; ensure no other console.* calls remain in that handler to avoid test/storybook console noise.
🧹 Nitpick comments (4)
mocks/handlers/terms-handlers.ts (1)
3-4:BASE_URL상수 분리 권장현재
BASE_URL상수가terms-handlers.ts,nickname-handlers.ts,prompt-list-handlers.ts,auth-handlers.ts,onboarding-handlers.ts,mypage-handlers.ts등 여러 파일에 동일하게 정의되어 있습니다. 나중에 URL 구조가 바뀌면 모든 파일을 수동으로 수정해야 하는 번거로움이 생깁니다.공유 상수 파일 하나를 두면 변경 포인트가 단일화됩니다.
♻️ 리팩터링 제안: 공유 상수 파일 분리
새 파일
mocks/handlers/config.ts생성:+ // mocks/handlers/config.ts + export const BASE_URL = + process.env.NEXT_PUBLIC_BACKEND_API_URL ?? "http://localhost:8080/api/v1";
terms-handlers.ts에 적용:import { http, HttpResponse } from "msw"; +import { BASE_URL } from "./config"; -const BASE_URL = - process.env.NEXT_PUBLIC_BACKEND_API_URL ?? "http://localhost:8080/api/v1";나머지 핸들러 파일들(
nickname-handlers.ts,prompt-list-handlers.ts등)에도 동일하게 적용하면 됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mocks/handlers/terms-handlers.ts` around lines 3 - 4, Multiple handler files duplicate the BASE_URL constant; create a single shared constant and import it instead: add a new file mocks/handlers/config.ts that exports BASE_URL (derived from process.env.NEXT_PUBLIC_BACKEND_API_URL with the fallback "http://localhost:8080/api/v1"), then replace the local BASE_URL definitions in terms-handlers.ts, nickname-handlers.ts, prompt-list-handlers.ts, auth-handlers.ts, onboarding-handlers.ts, mypage-handlers.ts, etc., to import { BASE_URL } from './config' (or the correct relative path) so all handlers use the centralized BASE_URL constant.stories/Mypage/UserExperience.stories.tsx (1)
18-35:onChange를argTypes에도 선언하면 Storybook Actions 패널과 연동됩니다.현재
onChange는meta.args에 no-op으로만 존재하고argTypes에 선언되지 않아, Actions 패널에서 호출 로그를 볼 수 없습니다.fn()또는action()을 사용하면 Controls·Actions 통합 경험을 개선할 수 있습니다.🔧 `onChange` argType 추가 및 action 연동 예시
+import { fn } from "@storybook/test"; const meta: Meta<typeof UserExperience> = { ... args: { label: "경험", placeholder: "경험을 입력해주세요", - onChange: () => {}, + onChange: fn(), }, argTypes: { ... + onChange: { + action: "onChange", + description: "아이템 목록이 변경될 때 호출되는 콜백", + }, }, };참고:
@storybook/test의fn()또는@storybook/addon-actions의action()을 활용하면 Actions 탭에서 호출 인자를 실시간으로 확인할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stories/Mypage/UserExperience.stories.tsx` around lines 18 - 35, Add an argType for onChange and wire it to Storybook Actions so the Actions panel logs calls: update the story's argTypes to include an onChange entry (e.g., control: { type: "action" } or description) and set meta.args.onChange to a test/action helper (fn() or action()) instead of a no-op; ensure the component stories use the onChange arg so interactions surface in the Actions tab (refer to the existing meta.args and the onChange prop in the component story to locate where to add this).app/(beforeLogin)/(auth)/constant.ts (1)
41-46:EducationValue타입 정의가queries/api/types.ts와 중복되어 드리프트(drift) 위험이 있습니다.
queries/api/types.ts8번째 줄에 이미EducationValue = (typeof EDUCATION_OPTIONS)[number]["value"]로 파생 타입이 정의되어 있습니다. 여기서 리터럴 유니온으로 명시적으로 다시 정의하면,EDUCATION_OPTIONS에 새 항목이 추가될 때 두 정의가 불일치할 수 있습니다.타입을 한 곳에서만 관리하는 것이 더 안전합니다.
♻️ 단일 출처로 개선 제안
-export type EducationValue = - | "HIGH_SCHOOL" - | "ASSOCIATE" - | "BACHELOR" - | "MASTER" - | "DOCTOR"; +export type EducationValue = (typeof EDUCATION_OPTIONS)[number]["value"];단, 이 경우
EDUCATION_OPTIONS선언이EducationValue타입 선언보다 먼저 위치해야 합니다. 파일 내 선언 순서 조정이 필요합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(beforeLogin)/(auth)/constant.ts around lines 41 - 46, The EducationValue literal-union in EducationValue should be removed and the type should be derived from the single source of truth (EDUCATION_OPTIONS) used in queries/api/types.ts; replace the local declaration with an import or re-export of the existing type (e.g., import { EducationValue } from "queries/api/types" or export type EducationValue = (typeof EDUCATION_OPTIONS)[number]["value"] only after ensuring EDUCATION_OPTIONS is declared first in this module) so the code uses the same type symbol as queries/api/types.ts and avoids drift between EDUCATION_OPTIONS and EducationValue.app/(afterLogin)/mypage/MypageRightSection.tsx (1)
111-115:window.confirm()대신 커스텀 확인 모달 사용을 고려해 주세요.
window.confirm()은 브라우저의 네이티브 다이얼로그로, 스타일 커스터마이징이 불가능하고 일부 환경(테스트, iframe)에서 예상대로 동작하지 않을 수 있습니다. 또한window.location.reload()로 전체 페이지를 새로고침하면 다른 섹션에서 변경 중이던 내용도 함께 사라집니다.프로젝트에 이미 사용 중인 모달 컴포넌트가 있다면, 그것을 활용하거나 스토어의
reset()을 직접 호출하는 방식으로 개선할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/MypageRightSection.tsx around lines 111 - 115, Replace the native confirm + full reload behavior in handleCancel: remove the window.confirm(...) and window.location.reload() calls and instead open the project modal component (or dispatch a UI state flag) to show a styled confirmation dialog; when the user confirms, call the form/store reset method (e.g., invoke reset() on the relevant store or form handler used in this component) and close the modal, and when the user cancels simply close the modal. Locate handleCancel in MypageRightSection.tsx and wire it to the existing modal component/state so the cancel flow uses the app modal and store.reset() rather than a full page reload.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 50-64: The effect that initializes form state (useEffect that
reads profile and calls transformCareerInfoToState, setTargetJobs, updateField,
setExperiences, setKeywords) can run on background refetch and overwrite user
edits; add a useRef flag (e.g., initializedRef) to ensure this initialization
runs only once per mount: check initializedRef.current before applying the
profile-derived updates, set initializedRef.current = true after the first
successful initialization, and keep existing cleanup/reset behavior so remounts
still reinitialize; ensure the check wraps the whole initialization logic inside
the useEffect that depends on profile, setTargetJobs, updateField.
In `@app/`(beforeLogin)/(auth)/constant.ts:
- Around line 82-87: The currentStatus field is unnecessarily wrapped with
String(stateCode); remove the String constructor and assign the existing
stateCode string directly (currentStatus: stateCode) — locate where
REVERSE_STATE_MAP.get(...) result is stored in stateCode and update the return
object in transformStateToPayload (or the function returning { currentStatus,
targetJob, careerYear, education }) so currentStatus uses stateCode without
conversion; no other type changes are needed.
---
Outside diff comments:
In `@mocks/handlers/prompt-list-handlers.ts`:
- Around line 264-276: Remove the stray debug console.log from the MSW delete
handler for prompt likes: locate the
http.delete(`${BASE_URL}/prompts/like/:promptId`, ({ params }) => { ... })
handler and delete the line console.log(`Prompt ${promptId} 좋아요 취소됨`); so the
handler only returns the HttpResponse.json payload; ensure no other console.*
calls remain in that handler to avoid test/storybook console noise.
---
Duplicate comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 234-240: The Link in MypageRightSection (the <Link ... href="#"
... target="_blank"> instance) currently opens the same page in a new tab;
remove the target="_blank" or disable the link until the real URL is available
by either omitting the target prop or rendering a non-clickable element (e.g.,
plain text or a button with disabled state) and keep the visible label ("보기")
unchanged; update only the Link instance in MypageRightSection to prevent
opening an empty/new tab.
- Around line 83-109: handleGlobalSave currently builds an UpdateProfileRequest
and calls updateProfile but ignores the marketingAgree state; since
UpdateProfileRequest doesn't include terms flags, detect the marketingAgree
boolean (from the component state/prop named marketingAgree) and call the
separate terms-agreement API (POST /users/me/terms-agreements) — e.g., invoke
the existing service method that posts terms agreements (or add one named
postTermsAgreements/updateTermsAgreements) either after or alongside
updateProfile, passing the marketingAgree value; ensure this call is made
whenever handleGlobalSave runs and handle its success/error path consistently
with updateProfile.
- Line 33: The marketingAgree state is always initialized to false so users who
already agreed see the checkbox unchecked; in MypageRightSection, call the
terms-agreements API (GET /users/me/terms-agreements) on mount (or reuse the
existing data-fetch effect referenced by the TODO), inspect whether any returned
agreement has termId === 3, and call setMarketingAgree(true) when present
(otherwise false); update the initialization flow so marketingAgree reflects the
API result rather than always defaulting to false, ensuring you use the existing
state setters (marketingAgree, setMarketingAgree) and place the fetch/update in
the component mount effect or the same handler that loads user terms.
In `@mocks/handlers/nickname-handlers.ts`:
- Around line 3-4: BASE_URL is duplicated in nickname-handlers.ts; extract this
constant into a shared module (e.g., export const BASE_URL or API_BASE_URL from
a new/shared file) and update nickname-handlers.ts to import that shared
constant instead of defining it locally; also update terms-handlers.ts to import
the same shared constant so both handlers use a single source of truth (keep the
existing fallback value process.env.NEXT_PUBLIC_BACKEND_API_URL ??
"http://localhost:8080/api/v1" in the shared module).
In `@mocks/handlers/prompt-list-handlers.ts`:
- Around line 219-220: The BASE_URL constant is defined here after MOCK_PROMPTS
and interfaces and duplicates other handlers; extract BASE_URL into a shared
constants module (e.g., export from a new/shared file) and import it into
prompt-list-handlers.ts, then move the local reference so the imported BASE_URL
is declared before the MOCK_PROMPTS array and any interface declarations in this
file to match other handler files; update other handler files (like
terms-handlers.ts) to use the same shared constant to remove duplication.
---
Nitpick comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 111-115: Replace the native confirm + full reload behavior in
handleCancel: remove the window.confirm(...) and window.location.reload() calls
and instead open the project modal component (or dispatch a UI state flag) to
show a styled confirmation dialog; when the user confirms, call the form/store
reset method (e.g., invoke reset() on the relevant store or form handler used in
this component) and close the modal, and when the user cancels simply close the
modal. Locate handleCancel in MypageRightSection.tsx and wire it to the existing
modal component/state so the cancel flow uses the app modal and store.reset()
rather than a full page reload.
In `@app/`(beforeLogin)/(auth)/constant.ts:
- Around line 41-46: The EducationValue literal-union in EducationValue should
be removed and the type should be derived from the single source of truth
(EDUCATION_OPTIONS) used in queries/api/types.ts; replace the local declaration
with an import or re-export of the existing type (e.g., import { EducationValue
} from "queries/api/types" or export type EducationValue = (typeof
EDUCATION_OPTIONS)[number]["value"] only after ensuring EDUCATION_OPTIONS is
declared first in this module) so the code uses the same type symbol as
queries/api/types.ts and avoids drift between EDUCATION_OPTIONS and
EducationValue.
In `@mocks/handlers/terms-handlers.ts`:
- Around line 3-4: Multiple handler files duplicate the BASE_URL constant;
create a single shared constant and import it instead: add a new file
mocks/handlers/config.ts that exports BASE_URL (derived from
process.env.NEXT_PUBLIC_BACKEND_API_URL with the fallback
"http://localhost:8080/api/v1"), then replace the local BASE_URL definitions in
terms-handlers.ts, nickname-handlers.ts, prompt-list-handlers.ts,
auth-handlers.ts, onboarding-handlers.ts, mypage-handlers.ts, etc., to import {
BASE_URL } from './config' (or the correct relative path) so all handlers use
the centralized BASE_URL constant.
In `@stories/Mypage/UserExperience.stories.tsx`:
- Around line 18-35: Add an argType for onChange and wire it to Storybook
Actions so the Actions panel logs calls: update the story's argTypes to include
an onChange entry (e.g., control: { type: "action" } or description) and set
meta.args.onChange to a test/action helper (fn() or action()) instead of a
no-op; ensure the component stories use the onChange arg so interactions surface
in the Actions tab (refer to the existing meta.args and the onChange prop in the
component story to locate where to add this).
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
app/(beforeLogin)/(auth)/constant.ts (1)
41-46:EducationValue타입과EDUCATION_OPTIONS값이 두 곳에서 중복 관리됩니다.현재
EducationValue와EDUCATION_OPTIONS의 value 목록이 별도로 유지되어 있어, 학력 항목 추가/변경 시 두 곳을 모두 수정해야 합니다.EDUCATION_OPTIONS를 파일 상단으로 이동하고as const로 선언하면 타입을 안전하게 파생시킬 수 있습니다.♻️ EDUCATION_OPTIONS에서 타입 파생 제안
-export type EducationValue = - | "HIGH_SCHOOL" - | "ASSOCIATE" - | "BACHELOR" - | "MASTER" - | "DOCTOR"; +// EDUCATION_OPTIONS를 파일 상단으로 이동하고 as const 선언 +export const EDUCATION_OPTIONS = [ + { label: "고등학교 졸업", value: "HIGH_SCHOOL" }, + { label: "전문대 졸업", value: "ASSOCIATE" }, + { label: "대학교 졸업", value: "BACHELOR" }, + { label: "석사 졸업", value: "MASTER" }, + { label: "박사 졸업", value: "DOCTOR" }, +] as const satisfies ReadonlyArray<{ label: string; value: string }>; + +export type EducationValue = (typeof EDUCATION_OPTIONS)[number]["value"];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(beforeLogin)/(auth)/constant.ts around lines 41 - 46, Move the EDUCATION_OPTIONS constant to the top of the file, declare it with "as const", and derive the EducationValue type from it instead of keeping a separate union; specifically, replace the manual EducationValue union with a type like "type EducationValue = typeof EDUCATION_OPTIONS[number]['value']" (referencing EDUCATION_OPTIONS and EducationValue) so the value list is maintained in one place and stays type-safe.app/(afterLogin)/mypage/MypageRightSection.tsx (1)
114-118:window.confirm()대신 커스텀 확인 다이얼로그 사용을 권장합니다.
window.confirm()은 브라우저 네이티브 UI로 디자인 시스템과 일치하지 않고, 일부 브라우저(특히 임베딩 환경)에서 차단될 수 있습니다. 프로젝트의 모달 컴포넌트가 있다면 활용하는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/MypageRightSection.tsx around lines 114 - 118, The handleCancel function currently uses the native window.confirm and window.location.reload; replace the native confirm with the app's design-system modal/confirm component (e.g., ConfirmDialog or openConfirmModal) and wire its confirm callback to perform the reload/navigation (keep the existing reload behavior only when the modal confirm is accepted). Update handleCancel to open the custom modal, and ensure you import and use the project's modal hook/component instead of window.confirm so the UI matches the design system and avoids native blocking dialogs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 77-84: The UI allows entering a custom status but that text
(otherInput) is never sent; update the save flow so transformStateToPayload and
the payload type include the custom text when StatusSelect/currentStatus is
"OTHER": add a field (e.g., customStatus or otherText) to
UpdateProfileRequest.careerInfo, update the TypeScript interface accordingly,
and modify transformStateToPayload and handleGlobalSave to include otherInput
whenever currentState/currentStatus === "OTHER"; alternatively, if backend
should not accept custom text, remove the StatusSelect "other" input or add UI
copy clarifying it's not saved.
---
Duplicate comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 237-243: The Link element with href="#" in MypageRightSection.tsx
(the Link rendering "보기") currently includes target="_blank" which opens the
current page in a new tab; either remove target="_blank" or prevent navigation
until the real URL is available by adding an onClick handler that calls
e.preventDefault(); update the Link in MypageRightSection.tsx (the Link that has
className "text-frog-600 label-medium hover:underline px-2") accordingly so it
no longer opens a blank/new tab while href="#" is used.
- Around line 86-112: handleGlobalSave currently omits marketingAgree and the
local marketingAgree state is initialized to false instead of reflecting
profile; initialize the marketingAgree state from profile (use
profile.termsAgreements or equivalent) instead of defaulting to false, and then
in handleGlobalSave either (A) include marketingAgree in the
UpdateProfileRequest payload if the backend accepts it (add marketingAgree:
marketingAgree to the payload built after transformStateToPayload) or (B) if
marketing agreements are handled by a separate endpoint, detect changes to
marketingAgree and call the terms API (POST /users/me/terms-agreements with
termId: 3 and the consent value) before/after updateProfile; if you choose (B)
also disable the Section 3 "변경사항 저장" button unless marketingAgree changes are
handled. Ensure you reference the marketingAgree state, handleGlobalSave,
transformStateToPayload and updateProfile when adding this logic.
---
Nitpick comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 114-118: The handleCancel function currently uses the native
window.confirm and window.location.reload; replace the native confirm with the
app's design-system modal/confirm component (e.g., ConfirmDialog or
openConfirmModal) and wire its confirm callback to perform the reload/navigation
(keep the existing reload behavior only when the modal confirm is accepted).
Update handleCancel to open the custom modal, and ensure you import and use the
project's modal hook/component instead of window.confirm so the UI matches the
design system and avoids native blocking dialogs.
In `@app/`(beforeLogin)/(auth)/constant.ts:
- Around line 41-46: Move the EDUCATION_OPTIONS constant to the top of the file,
declare it with "as const", and derive the EducationValue type from it instead
of keeping a separate union; specifically, replace the manual EducationValue
union with a type like "type EducationValue = typeof
EDUCATION_OPTIONS[number]['value']" (referencing EDUCATION_OPTIONS and
EducationValue) so the value list is maintained in one place and stays
type-safe.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
app/(afterLogin)/mypage/MypageRightSection.tsx (1)
115-119:confirm()네이티브 다이얼로그 — 커스텀 모달로 교체를 권장합니다.
window.confirm()은 브라우저 기본 다이얼로그로 스타일 적용이 불가능하고, 일부 임베디드 환경(iframe 등)에서는 차단됩니다. 또한 포커스 관리 측면에서 접근성에 불리합니다. 기존에 사용하는 모달/다이얼로그 컴포넌트가 있다면 대체를 검토해보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/mypage/MypageRightSection.tsx around lines 115 - 119, The handleCancel function uses the native confirm() dialog which prevents styling and accessible focus management; replace it with your app's modal/dialog component (e.g., the global Dialog/ConfirmationModal or useModal hook) instead of window.confirm: open the modal from handleCancel, show the same warning text, wire its Confirm button to perform the reload (window.location.reload() or preferably a navigate/back action) and its Cancel button to simply close the modal, and ensure focus is moved into the dialog when opened and returned when closed; update any imports and state used by MypageRightSection accordingly.app/(beforeLogin)/(auth)/constant.ts (1)
38-46:StateKey·EducationValue타입이queries/api/types.ts와 중복 정의되어 있습니다.
queries/api/types.ts에 이미 동일한 타입이 존재합니다:// queries/api/types.ts (기존) export type StateKey = keyof typeof STATE_VALUES; export type EducationValue = (typeof EDUCATION_OPTIONS)[number]["value"];
constant.ts의EducationValue는 하드코딩된 유니언 타입이라EDUCATION_OPTIONS가 변경되면 수동으로 동기화해야 합니다. 반면queries/api/types.ts버전은EDUCATION_OPTIONS에서 자동으로 파생됩니다.단일 출처(single source of truth)를 유지하려면 두 가지 방향 중 하나를 선택하세요:
- Option A (권장):
constant.ts의 중복 정의를 제거하고, 해당 타입이 필요한 곳에서queries/api/types.ts를 import- Option B:
queries/api/types.ts의 정의를 제거하고,constant.ts에서 import하도록 수정 (단,EducationValue는 파생 타입으로 수정 권장)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(beforeLogin)/(auth)/constant.ts around lines 38 - 46, Remove the duplicated type definitions in constant.ts by deleting StateKey and the hardcoded EducationValue and instead import the canonical types from queries/api/types.ts; specifically, stop defining StateKey and EducationValue locally (and stop hardcoding the union) and import the exported StateKey and EducationValue (which derive from STATE_VALUES and EDUCATION_OPTIONS) so STATE_VALUES / EDUCATION_OPTIONS remain the single source of truth and types stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 86-113: handleGlobalSave currently sends "OTHER" when currentState
=== "기타" and otherInput is empty because transformStateToPayload falls back to
"OTHER"; add a guard inside handleGlobalSave to detect currentState === "기타" &&
otherInput.trim() === "" and prevent sending that payload — either return early
and surface a validation error to the user (e.g., set a local error state or
show a toast) or explicitly set the generated careerPayload.stateCode to an
empty string before building payload; update references: handleGlobalSave,
transformStateToPayload, currentState, otherInput, updateProfile so the server
never receives the unintended "OTHER" code.
---
Duplicate comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 86-113: The UpdateProfileRequest payload built in handleGlobalSave
is missing the marketingAgree field so changes aren't persisted; update the
payload construction in handleGlobalSave (the UpdateProfileRequest object) to
include marketingAgree from the component state (e.g., marketingAgree variable
or profile.basicInfo.marketingAgree as appropriate) in the correct place
expected by the backend (likely under basicInfo or the specific field name used
by UpdateProfileRequest), then call updateProfile(payload) with the augmented
payload.
- Around line 238-244: The Link element in MypageRightSection.tsx uses href="#"
together with target="_blank" which opens a blank-tab of the same page; fix by
either removing target="_blank" (preferred until a real URL is available) or add
an onClick handler to the Link element that calls e.preventDefault() to avoid
navigation when href is "#" (adjust the JSX for the Link component accordingly
so the element with href="#" no longer opens a new tab unintentionally).
- Line 33: The marketingAgree state in MypageRightSection is always initialized
to false and must reflect the server value; update initialization by fetching
the user's terms agreements on mount (call the existing API used for GET
/users/me/terms-agreements or the helper that retrieves user terms) and set
marketingAgree via setMarketingAgree(true) when the response includes an entry
with termId === 3; ensure this runs in a useEffect on component mount and also
handle loading/errors so the checkbox reflects the server state rather than the
hardcoded false.
In `@app/`(beforeLogin)/(auth)/constant.ts:
- Around line 86-91: Remove the unnecessary String(...) wrapper around stateCode
in the return object: set currentStatus to stateCode directly (e.g.,
currentStatus: stateCode) since stateCode is already a string; update the return
in constant.ts where currentStatus is assigned to avoid reintroducing the
redundant String() call and keep targetJob, careerYear, and education
assignments unchanged.
---
Nitpick comments:
In `@app/`(afterLogin)/mypage/MypageRightSection.tsx:
- Around line 115-119: The handleCancel function uses the native confirm()
dialog which prevents styling and accessible focus management; replace it with
your app's modal/dialog component (e.g., the global Dialog/ConfirmationModal or
useModal hook) instead of window.confirm: open the modal from handleCancel, show
the same warning text, wire its Confirm button to perform the reload
(window.location.reload() or preferably a navigate/back action) and its Cancel
button to simply close the modal, and ensure focus is moved into the dialog when
opened and returned when closed; update any imports and state used by
MypageRightSection accordingly.
In `@app/`(beforeLogin)/(auth)/constant.ts:
- Around line 38-46: Remove the duplicated type definitions in constant.ts by
deleting StateKey and the hardcoded EducationValue and instead import the
canonical types from queries/api/types.ts; specifically, stop defining StateKey
and EducationValue locally (and stop hardcoding the union) and import the
exported StateKey and EducationValue (which derive from STATE_VALUES and
EDUCATION_OPTIONS) so STATE_VALUES / EDUCATION_OPTIONS remain the single source
of truth and types stay in sync.
🛠️ 변경 사항
📸 스크린샷 (선택)
✅ 체크리스트
🔗 연결된 이슈
Summary by CodeRabbit
새로운 기능
활동
컴포넌트
Docs
Chores