Skip to content

Commit cb0227d

Browse files
authored
Fix mypage (#46)
* fix: improve onboarding redirect logic and implement logout - Add logout functionality in MyPage component using auth store - Optimize onboarding page redirect by using useGoals hook with loading state - Replace window.location.href with Next.js router.replace() for better performance - Add loading spinner during goal data fetch in onboarding - Remove SSR/CSR conflicts by moving redirect logic to useEffect * fix: handle Zustand hydration delay in onboarding page - Add hydration state check to prevent premature login state evaluation - Wait 50ms for Zustand persist data to hydrate before checking login status - Show loading spinner during hydration process - Add debug console logs for hydration and auth states - Fix redirect logic to only execute after store has been properly hydrated * fix: resolve 401 error and smooth onboarding page transitions - Fix premature API call issue where useGoals was called before login completion - Add error handling and retry logic for 401 authentication errors - Use SWR mutate function to retry goals API after successful login - Improve loading state logic to prevent flickering during page transitions - Set shouldRetryOnError to false to prevent automatic retries on auth errors - Show loading spinner until proper authentication and data fetching is complete * fix: improve loading spinner visibility and eliminate page flash - Change spinner color from border-primary-500 to border-gray-800 for better visibility on white background - Add additional loading state for when goals exist and redirect is about to happen - Prevent brief flash of onboarding content during page transitions - Show consistent loading spinner during all redirect scenarios * feat: add useCompletedGoals hook for completed goals API - Add completedGoals query to useQuery object - Export useCompletedGoals hook for backward compatibility - Connect to 목표Api.getCompletedGoals endpoint - Return CompletedGoalListRs with todo counts and result counts - Follow existing hook patterns and naming conventions * refactor: integrate useCompletedGoals hook in DoneItems component - Replace mock data with real API data using useCompletedGoals hook - Update interface to use CompletedGoalItemRs from generated API types - Map API response fields to component display (todoCount, dueDate) - Remove hardcoded defaultGoals array and use dynamic data - Maintain existing UI layout and styling patterns * refactor: clean up component formatting and remove redundant elements - Remove redundant status bar elements from onboarding components - Standardize code formatting with Prettier for mypage components - Clean up whitespace and indentation inconsistencies - Remove hardcoded status bar time displays - Maintain consistent React component structure patterns * feat: integrate real API data in DoneItemDetail component - Add useGoalWithSubGoalsAndTodos hook to API hooks file - Update DoneItemDetail to use GoalWithSubGoalTodoRs from API - Replace mock data with real API data in done item detail page - Map TodoRs properties to component display (date, todoResult.fileUrl) - Fix hook naming consistency (goalWithSubGoal vs goalWithSubGoals) - Remove hardcoded mock goal details and use dynamic data * fix: update DoneItems statistics display and layout - Add flex-1 to statistics spans for better layout - Replace hardcoded 0 with actual todoResultCount for record count * fix: improve UserProfile edit button styling and accessibility * feat: sync TextField styling with Figma design - Update TextField to use design tokens (Color-gray-20, Color-primary-50) - Add proper border styling with 1px solid border - Implement correct focus states with ring styling - Use consistent color scheme matching Figma specifications - Remove custom color overrides from EditProfile component * fix: handle empty string in profile image fallback Changed from ?? to || operator to properly handle both null/undefined and empty string values for profileImageUrl fallback to /profile-default.png * feat: add save success toast notification to EditProfile Added toast notification that displays "저장 되었습니다" when user clicks save button in profile edit page. Follows existing toast implementation pattern with 2-second auto-hide duration. * feat: implement interest selection bottom sheet with Korean localization - Created InterestSelectionBottomSheet component using vaul library following existing patterns - Added state management for interest selection in EditProfile component - Updated UserUpdateRqInterestsEnumToKr converter with correct API enum mappings - Integrated Korean display for all interest chips in both bottom sheet and main interface - Added visual feedback with selected interests display and placeholder text - Maintained type safety with proper TypeScript integration Interest mappings: - HEALTH → 건강, READING → 독서, STUDY → 학업, LANGUAGE → 어학 - SPORTS → 운동, PROGRAMMING → 프로그래밍, CAREER → 취업/이직, SELF_IMPROVEMENT → 자기개발 * fix: update EditProfile component styling and functionality * deleteAccount is not implemeneted alert * fix: remove non-existent props from EditProfile stories Remove initialName, initialBio, and profileImageUrl from argTypes and story args as these props don't exist in the EditProfileProps interface. The component gets data from useMyProfile hook instead. * feat: replace point icon placeholder with money image in PointsDisplay * fix: resolve TypeScript error in useGoalWithSubGoalTodo hook Switch from useGoalWithSubGoals to useGoalWithSubGoalsAndTodos to access todos property on SubGoalWithTodosRs type. * feat: implement feed page with Figma design Add new /feed route with coming soon message and image from Figma design. Excludes system status bar as requested. * refactor: restructure feed page architecture and fix height issues - Extract FeedPage component to components/feed/ - Follow group page pattern with dynamic BottomTabBar import - Fix height calculation to prevent scrolling (calc(100vh-56px)) - Add Storybook integration for FeedPage component - Improve component separation and maintainability * style: update feed page styling to exactly match Figma design - Change background from gray to white - Remove notification button from AppBar - Add w-full to content container for proper width - Clean up component styling and formatting * feat: connect MyPage interests button to EditProfile interests selection - Update MyPage onAddInterests to navigate to /mypage/edit?openInterests=true - Add openInterests prop to EditProfile component - Auto-open interests bottom sheet when openInterests query param is present * feat: implement profile image upload functionality in EditProfile - Add file input state and ref for image selection - Implement handleImageUpload function to process selected files - Connect edit button to file picker with handleEditImage - Update onSave callback to pass selected file to parent component - Add hidden file input with image/* accept filter * feat: add image preview for profile picture upload - Add previewUrl state to show selected image immediately - Update handleImageUpload to create object URL for preview - Update img src to display preview before save - Add URL cleanup in useEffect to prevent memory leaks * style: remove background from profile pictures to fill full circle - Remove gray background and padding from EditProfile profile image - Update UserProfile to show full-size images without background - Keep background only for placeholder icon in UserProfile - Increase image size to fill the full 100px circle * style: update MyPage layout to match Figma design - Add gray background to UserProfile component with proper padding - Update MyPage layout structure for correct background colors - Format NavigationList component and improve hover states - Align profile section styling with Figma specifications * refactor: replace custom AppBar with shared AppBar component in EditProfile - Replace custom header implementation with AppBar component - Use AppBar type="back" with title and onBackClick handler - Position save button as absolute overlay on right side - Maintain consistent header height with standardized spacing * style: update EditProfile layout to match UserProfile styling - Replace simple profile image section with structured layout - Add consistent background and padding to profile section - Use same gap and spacing pattern as UserProfile component - Maintain visual consistency between profile components * refactor: simplify UserProfile image handling with fallback - Remove conditional rendering for profile image - Use fallback to default profile image when no profileImage provided - Consistent with EditProfile approach using profile-default.png - Simplify component logic while maintaining functionality * feat: update API types with new message features and user deletion - Add file metadata (fileName, fileMimeType) to TodoResultRs - Add GoalTitleUpdatedContent and MessageReactionContent types - Add MESSAGE_REACTION and GOAL_TITLE_UPDATE message types - Add user deletion endpoint - Add new goal query endpoint with all todos - Update group exit response status code * feat: implement user account deletion functionality - Replace placeholder alert with actual deleteUser API call - Add toast notification for deletion confirmation - Add automatic logout and redirect to home after deletion - Import required useToast and useAuthStore hooks * implement todoResult for both completed and incompleted * refactor: replace manual toast with useToast hook in EditProfile * fix: wrap useSearchParams in Suspense boundary for EditProfile page * feat: update button text based on initial interests state * style: add cursor pointer and improve formatting in EditIcon
1 parent 0cb30a3 commit cb0227d

File tree

27 files changed

+1020
-855
lines changed

27 files changed

+1020
-855
lines changed

api/generated/motimo/Api.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,25 @@ export class Api<SecurityDataType extends unknown> {
11811181
...params,
11821182
}),
11831183

1184+
/**
1185+
* No description
1186+
*
1187+
* @tags 사용자 API
1188+
* @name DeleteUser
1189+
* @summary 유저 탈퇴
1190+
* @request DELETE:/v1/users
1191+
* @secure
1192+
* @response `200` `void` 탈퇴 성공
1193+
* @response `401` `void` 인증되지 않은 사용자
1194+
*/
1195+
deleteUser: (params: RequestParams = {}) =>
1196+
this.http.request<void, void>({
1197+
path: `/v1/users`,
1198+
method: "DELETE",
1199+
secure: true,
1200+
...params,
1201+
}),
1202+
11841203
/**
11851204
* @description 사용자가 보유한 관심사를 수정합니다.
11861205
*
@@ -1566,18 +1585,41 @@ export class Api<SecurityDataType extends unknown> {
15661585
}),
15671586

15681587
/**
1569-
* @description 목표 ID에 해당하는 목표 정보와 모든 세부 목표 및 오늘의 미완료 투두 목록을 조회합니다.
1588+
* @description 목표 ID에 해당하는 목표 정보와 모든 세부 목표 및 투두 목록을 조회합니다.
15701589
*
15711590
* @tags 목표 API
15721591
* @name GetGoalWithSubGoalAndTodos
1592+
* @summary 목표 + 세부목표 + 투두 조회 API
1593+
* @request GET:/v1/goals/{goalId}/sub-goals/todos/all
1594+
* @secure
1595+
* @response `200` `GoalWithSubGoalTodoRs` 목표, 세부목표, 투두 목록 반환
1596+
* @response `401` `void` 인증되지 않은 사용자
1597+
* @response `404` `void` 해당 목표를 찾을 수 없음
1598+
*/
1599+
getGoalWithSubGoalAndTodos: (goalId: string, params: RequestParams = {}) =>
1600+
this.http.request<GoalWithSubGoalTodoRs, void>({
1601+
path: `/v1/goals/${goalId}/sub-goals/todos/all`,
1602+
method: "GET",
1603+
secure: true,
1604+
...params,
1605+
}),
1606+
1607+
/**
1608+
* @description 목표 ID에 해당하는 목표 정보와 모든 세부 목표 및 오늘의 미완료 투두 목록을 조회합니다.
1609+
*
1610+
* @tags 목표 API
1611+
* @name GetGoalWithSubGoalAndIncompleteOrTodayTodos
15731612
* @summary 목표 + 세부목표 + 오늘의 미완료 투두 조회 API
15741613
* @request GET:/v1/goals/{goalId}/sub-goals/all
15751614
* @secure
15761615
* @response `200` `GoalWithSubGoalTodoRs` 목표, 세부목표, 오늘의 미완료 투두 목록 반환
15771616
* @response `401` `void` 인증되지 않은 사용자
15781617
* @response `404` `void` 해당 목표를 찾을 수 없음
15791618
*/
1580-
getGoalWithSubGoalAndTodos: (goalId: string, params: RequestParams = {}) =>
1619+
getGoalWithSubGoalAndIncompleteOrTodayTodos: (
1620+
goalId: string,
1621+
params: RequestParams = {},
1622+
) =>
15811623
this.http.request<GoalWithSubGoalTodoRs, void>({
15821624
path: `/v1/goals/${goalId}/sub-goals/all`,
15831625
method: "GET",

api/hooks.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ export const useQuery = {
2929
config,
3030
),
3131

32-
goalWithSubGoals: (goalId: string | null, config?: SWRConfiguration) =>
32+
goalWithSubGoal: (goalId: string | null, config?: SWRConfiguration) =>
3333
useApiQuery(
3434
"목표Api",
35-
"getGoalWithSubGoalAndTodos",
35+
"getGoalWithSubGoal",
3636
goalId ? [goalId] : null,
3737
undefined,
3838
config,
@@ -41,6 +41,18 @@ export const useQuery = {
4141
goalsNotInGroup: (config?: SWRConfiguration) =>
4242
useApiQuery("목표Api", "getGoalNotJoinGroup", [], undefined, config),
4343

44+
completedGoals: (config?: SWRConfiguration) =>
45+
useApiQuery("목표Api", "getCompletedGoals", [], undefined, config),
46+
47+
// // Sub Goal API
48+
// subGoalTodos: (subGoalId: string | null, config?: SWRConfiguration) =>
49+
// useApiQuery(
50+
// "세부목표Api",
51+
// "getIncompleteOrTodayTodos",
52+
// subGoalId ? [subGoalId] : null,
53+
// undefined,
54+
// config,
55+
// ),
4456
// Sub Goal API
4557
subGoalTodos: (
4658
subGoalId: string | null,
@@ -132,9 +144,14 @@ export const useQuery = {
132144
cheerPhrase: (config?: SWRConfiguration) =>
133145
useApiQuery("응원Api", "getCheerPhrase", [], undefined, config),
134146

135-
// Completed Goals API
136-
completedGoals: (config?: SWRConfiguration) =>
137-
useApiQuery("목표Api", "getCompletedGoals", [], undefined, config),
147+
goalWithSubGoalAndTodos: (goalId: string | null, config?: SWRConfiguration) =>
148+
useApiQuery(
149+
"목표Api",
150+
"getGoalWithSubGoalAndTodos",
151+
goalId ? [goalId] : null,
152+
undefined,
153+
config,
154+
),
138155

139156
// Health API
140157
health: (config?: SWRConfiguration) =>
@@ -156,8 +173,11 @@ export const useTodos = useQuery.myTodos;
156173
export const useTodoResult = useQuery.todoResult;
157174
export const useGoals = useQuery.goals;
158175
export const useGoalDetail = useQuery.goalDetail;
159-
export const useGoalWithSubGoals = useQuery.goalWithSubGoals;
176+
export const useGoalWithSubGoals = useQuery.goalWithSubGoal;
177+
export const useGoalWithSubGoalsAndTodos = useQuery.goalWithSubGoalAndTodos;
160178
export const useGoalsNotInGroup = useQuery.goalsNotInGroup;
179+
export const useCompletedGoals = useQuery.completedGoals;
180+
// export const useSubGoalTodos = useQuery.subGoalTodos;
161181
export const useSubGoalTodos = useQuery.subGoalTodos;
162182
export const useMyProfile = useQuery.myProfile;
163183
export const useGroupMembers = useQuery.groupMembers;
@@ -168,6 +188,5 @@ export const useJoinedGroups = useQuery.joinedGroups;
168188
export const usePoints = useQuery.points;
169189
export const useCheerPhrase = useQuery.cheerPhrase;
170190
export const useNotifications = useQuery.notifications;
171-
export const useCompletedGoals = useQuery.completedGoals;
172191
export const useHealth = useQuery.health;
173192
export const useAllSubGoalTodos = useQuery.allSubGoalTodos;

app/feed/page.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use client";
2+
3+
import dynamic from "next/dynamic";
4+
import { FeedPage } from "@/components/feed";
5+
6+
// 클라이언트에서만 렌더링되는 BottomTabBar (SSR 제외)
7+
const BottomTabBar = dynamic(
8+
() =>
9+
import("@/components/shared/BottomTabBar/BottomTabBar").then((mod) => ({
10+
default: mod.BottomTabBar,
11+
})),
12+
{ ssr: false },
13+
);
14+
15+
export default function FeedRoute() {
16+
const handleNotificationClick = () => {
17+
console.log("Notification clicked");
18+
// TODO: Implement notification logic
19+
};
20+
21+
return (
22+
<>
23+
<FeedPage onNotificationClick={handleNotificationClick} />
24+
25+
{/* Bottom Tab Bar */}
26+
<BottomTabBar type="3" />
27+
</>
28+
);
29+
}
30+

app/mypage/done/[id]/page.tsx

Lines changed: 13 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,23 @@
11
"use client";
22

3+
import { useGoalDetail, useGoalWithSubGoalsAndTodos } from "@/api/hooks";
34
import { DoneItemDetail } from "@/components/mypage";
45
import { useParams } from "next/navigation";
56

67
export default function DoneItemDetailPage() {
7-
const params = useParams();
8-
const goalId = params.id as string;
8+
const params = useParams();
9+
const goalId = params.id as string;
910

10-
// Mock data - in a real app, this would come from an API based on goalId
11-
const getGoalDetailById = (id: string) => {
12-
const goalDetails = {
13-
"1": {
14-
id: "1",
15-
title: "해외로 취업하자!",
16-
subGoals: [
17-
{
18-
id: "1",
19-
title: "포트폴리오 및 이력서 준비하기",
20-
completedTodos: [
21-
{
22-
id: "1",
23-
title: "투두 리스트 타이틀이 들어갑니다.",
24-
completedDate: "2025.05.12",
25-
attachment: {
26-
type: "image" as const,
27-
url: "https://picsum.photos/200",
28-
},
29-
},
30-
{
31-
id: "2",
32-
title: "투두 리스트 타이틀이 들어갑니다.",
33-
completedDate: "2025.05.12",
34-
attachment: {
35-
type: "file" as const,
36-
url: "https://picsum.photos/200",
37-
name: "파일명이 여기에.pdf",
38-
},
39-
},
40-
{
41-
id: "3",
42-
title: "투두 리스트 타이틀이 들어갑니다.",
43-
completedDate: "2025.05.12",
44-
},
45-
],
46-
},
47-
{
48-
id: "2",
49-
title: "세부목표명이 여기에 들어갑니다.",
50-
completedTodos: [
51-
{
52-
id: "4",
53-
title: "투두 리스트 타이틀이 들어갑니다.",
54-
completedDate: "2025.05.12",
55-
attachment: {
56-
type: "image" as const,
57-
url: "https://picsum.photos/200",
58-
},
59-
},
60-
{
61-
id: "5",
62-
title: "투두 리스트 타이틀이 들어갑니다.",
63-
completedDate: "2025.05.12",
64-
},
65-
],
66-
},
67-
],
68-
},
69-
"2": {
70-
id: "2",
71-
title: "건강한 생활 습관 만들기",
72-
subGoals: [
73-
{
74-
id: "3",
75-
title: "매일 운동하기",
76-
completedTodos: [
77-
{
78-
id: "6",
79-
title: "헬스장 등록하기",
80-
completedDate: "2025.03.01",
81-
},
82-
{
83-
id: "7",
84-
title: "운동 계획표 작성",
85-
completedDate: "2025.03.02",
86-
attachment: {
87-
type: "file" as const,
88-
url: "/workout-plan.pdf",
89-
name: "운동계획표.pdf",
90-
},
91-
},
92-
],
93-
},
94-
],
95-
},
96-
};
11+
const { data } = useGoalWithSubGoalsAndTodos(goalId);
9712

98-
return goalDetails[id as keyof typeof goalDetails] || goalDetails["1"];
99-
};
13+
if (!data) {
14+
return (
15+
<div className="flex items-center justify-center min-h-screen">
16+
<p>Loading...</p>
17+
</div>
18+
);
19+
}
10020

101-
const goalDetail = getGoalDetailById(goalId);
21+
return <DoneItemDetail goalDetail={data} />;
22+
}
10223

103-
return <DoneItemDetail goalDetail={goalDetail} />;
104-
}

app/mypage/edit/page.tsx

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,59 @@
11
"use client";
22

3+
import {
4+
UserUpdateRq,
5+
UserUpdateRqInterestsEnum,
6+
} from "@/api/generated/motimo/Api";
7+
import { userApi } from "@/api/service";
38
import { EditProfile } from "@/components/mypage";
9+
import useToast from "@/hooks/useToast";
10+
import useAuthStore from "@/stores/useAuthStore";
11+
import { useSearchParams } from "next/navigation";
12+
import { Suspense } from "react";
413

5-
export default function EditProfilePage() {
6-
const handleSave = (data: { name: string; bio: string }) => {
7-
console.log("Saving profile data:", data);
8-
// TODO: Implement actual save logic
9-
};
14+
function EditProfileContent() {
15+
const searchParams = useSearchParams();
16+
const openInterests = searchParams.get("openInterests") === "true";
17+
const handleSave = (request: UserUpdateRq, file?: File) => {
18+
userApi.updateMyProfile({
19+
request,
20+
file,
21+
});
22+
};
23+
24+
const { toastInfo, setToast } = useToast();
25+
const { logout } = useAuthStore();
1026

11-
const handleDeleteAccount = () => {
12-
console.log("Delete account requested");
13-
// TODO: Implement account deletion logic
14-
};
27+
const handleDeleteAccount = () => {
28+
userApi.deleteUser().then(() => {
29+
setToast("User 가 삭제 되었습니다");
30+
// wait 2 sec and redirect to home
31+
setTimeout(() => {
32+
logout();
33+
window.location.href = "/";
34+
}, 2000);
35+
});
36+
};
1537

16-
const handleAddInterests = () => {
17-
console.log("Add interests requested");
18-
// TODO: Navigate to interests selection page
19-
};
38+
const handleAddInterests = () => {
39+
console.log("Add interests requested");
40+
// TODO: Navigate to interests selection page
41+
};
2042

21-
return (
22-
<EditProfile
23-
initialName="홍길동"
24-
initialBio=""
25-
profileImageUrl="/profile-default.png"
26-
onSave={handleSave}
27-
onDeleteAccount={handleDeleteAccount}
28-
onAddInterests={handleAddInterests}
29-
/>
30-
);
31-
}
43+
return (
44+
<EditProfile
45+
onSave={handleSave}
46+
onDeleteAccount={handleDeleteAccount}
47+
onAddInterests={handleAddInterests}
48+
openInterests={openInterests}
49+
/>
50+
);
51+
}
52+
53+
export default function EditProfilePage() {
54+
return (
55+
<Suspense fallback={<div>Loading...</div>}>
56+
<EditProfileContent />
57+
</Suspense>
58+
);
59+
}

app/onboarding/_components/CompletionScreen.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,6 @@ export default function CompletionScreen({
2929

3030
return (
3131
<div className="min-h-screen bg-background-alternative flex flex-col">
32-
{/* Status Bar */}
33-
<div className="flex justify-between items-end gap-[286px] px-6 py-[10px] h-[52px]">
34-
{/* <div className="text-sm font-medium text-label-normal">9:30</div> */}
35-
<div className="flex items-center gap-4">
36-
<div className="w-[46px] h-[17px]"></div>
37-
</div>
38-
</div>
39-
4032
{/* Content */}
4133
<div className="flex-1 flex flex-col items-center justify-center px-6">
4234
{/* Check Icon Animation */}

0 commit comments

Comments
 (0)