Skip to content

Commit f3b4ab5

Browse files
authored
[FRONTEND] 프로필 화면 설정 (#59)
## 📝작업 내용 - 프로필 화면 사용자 정보 연결, 사용자 정보 자동 저장 기능 추가
1 parent ecf55f5 commit f3b4ab5

File tree

19 files changed

+343
-124
lines changed

19 files changed

+343
-124
lines changed

frontend/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Setting from "./pages/setting";
1111
import SecondSetting from "./pages/secondSetting";
1212
import RedirectPage from "./pages/oauth/kakao/redirectPage";
1313
import ExpertVerifyLayout from "./components/layout/expertVerifyLayout";
14-
import { ProtectedRoute } from "./components/common/ProtectedRoute";
14+
import { ProtectedRoute } from "./routes/ProtectedRoute";
1515

1616
const Router = () => {
1717
return (
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// src/components/common/UserInfoDisplay.tsx
2+
import { useAuthStore } from "../../stores/useAuthStore";
3+
import { useUserWithAutoSave } from "../userSetting/userService";
4+
5+
/**
6+
* 사용자 정보를 표시하고 새로고침할 수 있는 컴포넌트
7+
*/
8+
export default function UserInfoDisplay() {
9+
const { user, isAuthenticated } = useAuthStore();
10+
const { refetch, isLoading } = useUserWithAutoSave({ enabled: false });
11+
12+
const handleRefresh = () => {
13+
console.log("🔄 사용자 정보 수동 새로고침");
14+
refetch();
15+
};
16+
17+
if (!isAuthenticated) {
18+
return (
19+
<div
20+
style={{
21+
padding: "10px",
22+
border: "1px solid #ddd",
23+
borderRadius: "5px",
24+
}}
25+
>
26+
<p>로그인되지 않음</p>
27+
</div>
28+
);
29+
}
30+
31+
return (
32+
<div
33+
style={{
34+
padding: "10px",
35+
border: "1px solid #ddd",
36+
borderRadius: "5px",
37+
margin: "10px 0",
38+
}}
39+
>
40+
<h3>사용자 정보</h3>
41+
{user ? (
42+
<div>
43+
<p>
44+
<strong>이름:</strong> {user.name}
45+
</p>
46+
<p>
47+
<strong>이메일:</strong> {user.email}
48+
</p>
49+
<p>
50+
<strong>역할:</strong> {user.role}
51+
</p>
52+
<p>
53+
<strong>포인트:</strong> {user.point}
54+
</p>
55+
<p>
56+
<strong>초기화 완료:</strong> {user.initialized ? "✅" : "❌"}
57+
</p>
58+
<p>
59+
<strong>관심사:</strong> {user.interests?.length || 0}
60+
</p>
61+
{user.interests && user.interests.length > 0 && (
62+
<ul>
63+
{user.interests.map((interest) => (
64+
<li key={interest.interestId}>{interest.name}</li>
65+
))}
66+
</ul>
67+
)}
68+
</div>
69+
) : (
70+
<p>사용자 정보 없음</p>
71+
)}
72+
73+
<button
74+
onClick={handleRefresh}
75+
disabled={isLoading}
76+
style={{
77+
marginTop: "10px",
78+
padding: "5px 10px",
79+
backgroundColor: "#007bff",
80+
color: "white",
81+
border: "none",
82+
borderRadius: "3px",
83+
cursor: isLoading ? "not-allowed" : "pointer",
84+
opacity: isLoading ? 0.6 : 1,
85+
}}
86+
>
87+
{isLoading ? "새로고침 중..." : "사용자 정보 새로고침"}
88+
</button>
89+
</div>
90+
);
91+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// src/components/common/UserManager.tsx
2+
import { useEffect } from "react";
3+
import { useUserManager } from "../../hooks/useUserManager";
4+
5+
/**
6+
* 전역 사용자 정보 관리 컴포넌트
7+
* 앱 최상위에서 사용자 정보를 자동으로 관리
8+
*/
9+
export default function UserManager() {
10+
const { isError, error, shouldFetchUser } = useUserManager();
11+
12+
useEffect(() => {
13+
if (shouldFetchUser) {
14+
console.log("🔄 UserManager: 사용자 정보 자동 조회 시작");
15+
}
16+
17+
if (isError && error) {
18+
console.error("❌ UserManager: 사용자 정보 조회 실패", error);
19+
}
20+
}, [shouldFetchUser, isError, error]);
21+
return null;
22+
}

frontend/src/api/userSetting/useKakaoLogin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export const useKakaoLogin = () => {
3131
setTokens(access_token, refresh_token);
3232
setAuthenticated(true);
3333

34-
console.log('🔄 토큰 저장 완료, RedirectPage에서 라우팅 처리 예정');
35-
// 사용자 정보는 별도의 /users/me API 호출로 가져와야 함
34+
console.log('🔄 토큰 저장 완료');
35+
console.log('📋 사용자 정보는 HomeProtectedRoute에서 자동으로 조회됩니다');
3636
}
3737
setLoading(false);
3838
},

frontend/src/api/userSetting/userService.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// src/api/userService.ts
22
import { useTokenStore } from "../../stores/useTokenStore";
3+
import { useAuthStore } from "../../stores/useAuthStore";
34
import type { UserData } from "../../types/user";
45
import apiClient from "./apiClient";
56
import { useQuery } from "@tanstack/react-query";
6-
7+
import { useEffect } from "react";
78

89
const API_URL = import.meta.env.VITE_API_URL;
910

@@ -19,6 +20,32 @@ export const useUser = (options?: { enabled?: boolean }) => {
1920
})
2021
}
2122

23+
// 사용자 정보를 자동으로 스토어에 저장하는 훅
24+
export const useUserWithAutoSave = (options?: { enabled?: boolean }) => {
25+
const { updateUserFromApi } = useAuthStore();
26+
27+
const query = useQuery({
28+
queryKey: ['user', 'me'],
29+
queryFn: async () => {
30+
console.log('🔍 사용자 정보 API 호출: /users/me');
31+
const response = await apiClient.get('/users/me');
32+
console.log('📦 사용자 정보 API 응답:', response.data);
33+
return response.data;
34+
},
35+
staleTime: 5 * 60 * 1000,
36+
enabled: options?.enabled ?? true,
37+
});
38+
39+
// API 호출 성공 시 자동으로 스토어에 저장
40+
useEffect(() => {
41+
if (query.data && query.isSuccess) {
42+
updateUserFromApi(query.data);
43+
}
44+
}, [query.data, query.isSuccess, updateUserFromApi]);
45+
46+
return query;
47+
}
48+
2249
export const initializeUserProfile = async (userData: {
2350
role: UserData['role'];
2451
interest_ids: number[];
Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import style from './contentArea.module.css'
2-
import React from 'react'
3-
import categoryImg from '@/assets/image/guideImg.png'
4-
import { useGuidePart } from '../../../../stores/useGuidePart';
1+
import style from "./contentArea.module.css";
2+
import categoryImg from "@/assets/image/guideImg.png";
3+
import { useGuidePart } from "../../../../stores/useGuidePart";
54
import data from "../../../../dummy/dummy_guide.json";
6-
import category from '@/assets/image/guideNav/category.svg';
7-
import mainboard from '@/assets/image/guideNav/mainboard.svg';
8-
import GuidePartButton from '../guidePartButton/guidePartButton';
9-
import info from '@/assets/image/info.svg'
10-
import check from '@/assets/image/check.svg'
11-
import warning from '@/assets/image/warning.svg'
12-
import dropdown from '@/assets/image/dropdown.svg'
5+
import category from "@/assets/image/guideNav/category.svg";
6+
import mainboard from "@/assets/image/guideNav/mainboard.svg";
7+
import GuidePartButton from "../guidePartButton/guidePartButton";
8+
import info from "@/assets/image/info.svg";
9+
import check from "@/assets/image/check.svg";
10+
import warning from "@/assets/image/warning.svg";
11+
import dropdown from "@/assets/image/dropdown.svg";
1312

1413
const iconMap: Record<string, string> = {
1514
CPU: category,
@@ -18,68 +17,76 @@ const iconMap: Record<string, string> = {
1817
"그래픽 카드": category,
1918
"저장 장치": category,
2019
"파워 서플라이": category,
21-
"케이스": category,
20+
케이스: category,
2221
"쿨러/팬": category,
2322
"기타 입출력 장치": category,
2423
};
2524

2625
const imageMap: Record<string, string> = {
27-
CPU: categoryImg,
26+
CPU: categoryImg,
2827
메인보드: categoryImg,
2928
RAM: categoryImg,
3029
"그래픽 카드": categoryImg,
3130
"저장 장치": categoryImg,
3231
"파워 서플라이": categoryImg,
33-
"케이스": categoryImg,
32+
케이스: categoryImg,
3433
"쿨러/팬": categoryImg,
3534
"기타 입출력 장치": categoryImg,
3635
};
3736

38-
const partIconMap : Record<string, string> = {
39-
개요: info,
40-
"주요 특징": check,
41-
"유의사항": warning,
42-
"초보자용 설명": warning
43-
}
37+
const partIconMap: Record<string, string> = {
38+
개요: info,
39+
"주요 특징": check,
40+
유의사항: warning,
41+
"초보자용 설명": warning,
42+
};
4443

45-
export default function ContentArea(){
46-
const { selectCategory, contentPart, setContentPart } = useGuidePart();
47-
const currentData = data.find(item => item.category === selectCategory);
44+
export default function ContentArea() {
45+
const { selectCategory, contentPart, setContentPart } = useGuidePart();
46+
const currentData = data.find((item) => item.category === selectCategory);
4847

49-
if (!currentData) {
50-
return <div>오류 발생</div>;
51-
}
48+
if (!currentData) {
49+
return <div>오류 발생</div>;
50+
}
5251

53-
const currentPart = currentData.content.find(item => item.title === contentPart)
52+
const currentPart = currentData.content.find(
53+
(item) => item.title === contentPart
54+
);
5455

55-
return(
56-
<div className={style.container}>
57-
<img src={imageMap[selectCategory]} alt="category icon" className={style.categoryImg} />
58-
<div className={style.content}>
59-
<div className={style.title}>
60-
<img src={iconMap[selectCategory]} alt="category icon" />
61-
<div className={style.category}>{selectCategory}</div>
62-
<div className={style.order}>조립순서 #{currentData.id}</div>
63-
</div>
64-
<div className={style.btnContainer}>
65-
{currentData.content.map((item)=>(
66-
<GuidePartButton
67-
key={item.title}
68-
img={partIconMap[item.title]}
69-
content={item.title}
70-
isActive={item.title === contentPart}
71-
onClick={()=>{setContentPart(item.title)}}
72-
/>
73-
))}
74-
</div>
75-
<div className={style.textContent}>
76-
<div className={style.text}>{currentPart?.text}</div>
77-
<div className={style.btn}>
78-
<span>자세히보기</span>
79-
<img src={dropdown} alt="dropdown"/>
80-
</div>
81-
</div>
82-
</div>
56+
return (
57+
<div className={style.container}>
58+
<img
59+
src={imageMap[selectCategory]}
60+
alt="category icon"
61+
className={style.categoryImg}
62+
/>
63+
<div className={style.content}>
64+
<div className={style.title}>
65+
<img src={iconMap[selectCategory]} alt="category icon" />
66+
<div className={style.category}>{selectCategory}</div>
67+
<div className={style.order}>조립순서 #{currentData.id}</div>
68+
</div>
69+
<div className={style.btnContainer}>
70+
{currentData.content.map((item) => (
71+
<GuidePartButton
72+
key={item.title}
73+
img={partIconMap[item.title]}
74+
content={item.title}
75+
isActive={item.title === contentPart}
76+
onClick={() => {
77+
setContentPart(item.title);
78+
}}
79+
/>
80+
))}
8381
</div>
84-
)
85-
}
82+
<div className={style.textContent}>
83+
<div className={style.text}>{currentPart?.text}</div>
84+
<div className={style.btn}>
85+
<span>자세히보기</span>
86+
<img src={dropdown} alt="dropdown" />
87+
</div>
88+
</div>
89+
</div>
90+
</div>
91+
);
92+
}
Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
11
import StepIndicatorItem from "./stepIndicatorItem";
2-
import style from './stepIndicator.module.css'
2+
import style from "./stepIndicator.module.css";
33
import { useGuidePart } from "../../../../stores/useGuidePart";
44
import data from "../../../../dummy/dummy_guide.json";
5-
import React from "react";
65

7-
export default function StepIndicator(){
8-
const { currentStep,setCurrentStep, setSelectCategory, setContentPart } = useGuidePart();
9-
const isActive = (stepId: number) => currentStep === stepId;
6+
export default function StepIndicator() {
7+
const { currentStep, setCurrentStep, setSelectCategory, setContentPart } =
8+
useGuidePart();
9+
const isActive = (stepId: number) => currentStep === stepId;
1010

11-
return(
12-
<div className={style.container}>
13-
{data.map((step) => (
14-
<div className={style.content} key={step.id}>
15-
<div className={style.indicator}>
16-
<StepIndicatorItem
17-
isActive={isActive(step.id)}
18-
onClick={()=>{
19-
setCurrentStep(step.id);
20-
setSelectCategory(step.category);
21-
setContentPart("개요");
22-
}}
23-
/>
24-
<div className={`${style.label} ${isActive(step.id)? style.active:''}`}>{step.category}</div>
25-
</div>
26-
{step.id < data.length && <div className={style.separator}/> }
27-
</div>
28-
))}
11+
return (
12+
<div className={style.container}>
13+
{data.map((step) => (
14+
<div className={style.content} key={step.id}>
15+
<div className={style.indicator}>
16+
<StepIndicatorItem
17+
isActive={isActive(step.id)}
18+
onClick={() => {
19+
setCurrentStep(step.id);
20+
setSelectCategory(step.category);
21+
setContentPart("개요");
22+
}}
23+
/>
24+
<div
25+
className={`${style.label} ${
26+
isActive(step.id) ? style.active : ""
27+
}`}
28+
>
29+
{step.category}
30+
</div>
31+
</div>
32+
{step.id < data.length && <div className={style.separator} />}
2933
</div>
30-
)
31-
}
34+
))}
35+
</div>
36+
);
37+
}

0 commit comments

Comments
 (0)