Skip to content

Commit db326bd

Browse files
committed
현재레벨 불러오기 오류 수정
1 parent 739adf6 commit db326bd

File tree

5 files changed

+243
-116
lines changed

5 files changed

+243
-116
lines changed

src/api/user/level.api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import type { UserGradeResponse } from '@/types/level';
33

44
export const getUserGrade = async (): Promise<UserGradeResponse> => {
55
try {
6-
const { data } = await api.get<{ data: UserGradeResponse }>('/profile/grades');
7-
return data.data;
6+
const response = await api.get<{ data: UserGradeResponse }>('/profile');
7+
return response.data.data; // ✅ 두 번 접근
88
} catch (err) {
99
console.error('레벨 정보 로딩 에러:', err);
1010
throw err;
Lines changed: 93 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,105 @@
1-
import type { ChangeEvent, KeyboardEvent } from 'react';
2-
import SearchIcon from '@/assets/icons/search.svg?react';
1+
import { useEffect, useState } from 'react';
2+
import { useNavigate } from 'react-router-dom';
3+
import { Header, SearchInput, Badge, BottomNavigation } from '@/components';
4+
import api from '@/api/api';
35

4-
// 부모 컴포넌트로부터 받을 props의 타입을 정의합니다.
5-
interface SearchInputProps {
6-
value: string; // input에 표시될 값
7-
placeholder?: string; // 플레이스홀더 텍스트
8-
onChange: (value: string) => void; // input 값이 변경될 때 호출될 함수
9-
onSearch?: () => void; // 검색을 실행할 때 호출될 함수 (돋보기 클릭 또는 엔터)
10-
className?: string; // 추가적인 스타일링을 위한 클래스
11-
}
6+
export default function ReviewSearchPage() {
7+
const [search, setSearch] = useState('');
8+
const [recentKeywords, setRecentKeywords] = useState<string[]>([]);
9+
const navigate = useNavigate();
10+
11+
const handleBackClick = () => {
12+
navigate(-1);
13+
};
1214

13-
export default function SearchInput({
14-
value,
15-
placeholder = '검색어를 입력해주세요',
16-
onChange,
17-
onSearch, // 부모로부터 onSearch 함수를 받아옵니다.
18-
className = '',
19-
}: SearchInputProps) {
20-
21-
/**
22-
* input에서 키보드를 눌렀을 때 실행되는 함수입니다.
23-
* 'Enter' 키가 눌렸을 경우 onSearch 함수를 호출합니다.
24-
*/
25-
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
26-
if (onSearch && e.key === 'Enter') {
27-
e.preventDefault(); // form 안에서 사용될 경우 페이지 새로고침 방지
28-
onSearch();
15+
// ✅ 최근 검색어 불러오기 (최대 6개)
16+
const fetchRecentKeywords = async () => {
17+
try {
18+
const keywords = await api.get<string[]>('/api/v1/search');
19+
setRecentKeywords(keywords.slice(0, 6));
20+
} catch (err) {
21+
console.error('최근 검색어 조회 실패:', err);
2922
}
3023
};
3124

32-
/**
33-
* 돋보기 아이콘 버튼을 클릭했을 때 실행되는 함수입니다.
34-
*/
35-
const handleSearchClick = () => {
36-
if (onSearch) {
37-
onSearch();
25+
// ✅ 검색어 삭제
26+
const handleRemove = async (index: number) => {
27+
const keyword = recentKeywords[index];
28+
try {
29+
await api.delete('/api/v1/search', { data: { keyword } });
30+
setRecentKeywords(prev => prev.filter((_, i) => i !== index));
31+
} catch (err) {
32+
console.error('검색어 삭제 실패:', err);
33+
}
34+
};
35+
36+
// ✅ 검색 및 저장
37+
const handleSearch = async () => {
38+
const trimmed = search.trim();
39+
if (!trimmed) {
40+
alert('검색어를 입력해주세요.');
41+
return;
42+
}
43+
44+
try {
45+
// 검색어 저장
46+
await api.post('/api/v1/search', { keyword: trimmed });
47+
48+
// 중복 제거 및 최신순 정렬
49+
setRecentKeywords(prev => {
50+
const filtered = prev.filter(word => word !== trimmed);
51+
return [trimmed, ...filtered].slice(0, 6);
52+
});
53+
54+
// 결과 페이지로 이동
55+
navigate(`/search/result?query=${encodeURIComponent(trimmed)}`);
56+
} catch (err) {
57+
console.error('검색어 저장 실패:', err);
3858
}
3959
};
4060

61+
// ✅ 컴포넌트 마운트 시 최근 검색어 불러오기
62+
useEffect(() => {
63+
fetchRecentKeywords();
64+
}, []);
65+
4166
return (
42-
<div className={`relative w-full ${className}`}>
43-
<input
44-
type="text"
45-
value={value}
46-
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
47-
onKeyDown={handleKeyDown}
48-
placeholder={placeholder}
49-
className="w-full rounded-md border border-gray-800 bg-transparent py-2 pl-4 pr-10 text-white outline-none placeholder:text-gray-500"
50-
/>
67+
<div className="relative flex justify-center min-h-screen text-white">
68+
<div className="w-full max-w-[400px] px-4 pt-16 pb-20">
69+
<Header leftSection="BACK" onBackClick={handleBackClick}>
70+
검색
71+
</Header>
5172

52-
{/*
53-
아이콘을 클릭 가능한 <button>으로 감싸고,
54-
onClick 이벤트에 검색 실행 함수를 연결합니다.
55-
*/}
56-
<button
57-
type="button"
58-
onClick={handleSearchClick}
59-
className="absolute right-3 top-1/2 -translate-y-1/2"
60-
aria-label="검색 실행" // 스크린 리더 사용자를 위한 접근성 라벨
61-
>
62-
<SearchIcon className="h-5 w-5 text-gray-700" />
63-
</button>
73+
<SearchInput
74+
value={search}
75+
onChange={setSearch}
76+
placeholder="검색어를 입력해주세요"
77+
onSearch={handleSearch}
78+
/>
79+
80+
<div className="mt-6">
81+
<h2 className="mb-2 text-title-3">최근 검색어</h2>
82+
{recentKeywords.length === 0 ? (
83+
<p className="text-sm text-gray-500">최근 검색어가 없습니다.</p>
84+
) : (
85+
<div className="flex flex-wrap gap-2">
86+
{recentKeywords.map((word, index) => (
87+
<Badge
88+
key={word}
89+
type="removable"
90+
onRemove={() => handleRemove(index)}
91+
>
92+
{word}
93+
</Badge>
94+
))}
95+
</div>
96+
)}
97+
</div>
98+
</div>
99+
100+
<div className="fixed bottom-0 left-1/2 w-full max-w-[430px] -translate-x-1/2">
101+
<BottomNavigation />
102+
</div>
64103
</div>
65104
);
66-
}
105+
}

src/pages/my/Level.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,38 @@ import { LEVELS_DATA } from '@/constants/levelcondition';
55
import type { UserGradeResponse } from '@/types/level';
66
import { getUserGrade } from '@/api/user/level.api';
77

8+
const gradeToLevelMap: { [key: string]: number } = {
9+
BRONZE: 1,
10+
SILVER: 2,
11+
GOLD: 3,
12+
PLATINUM: 4,
13+
};
14+
815
export default function LevelPage() {
916
const { data, isLoading, error } = useQuery<UserGradeResponse>({
1017
queryKey: ['userGrade'],
1118
queryFn: getUserGrade,
1219
});
1320

1421
if (isLoading) return <div className="text-center mt-10">로딩 중...</div>;
15-
if (error || !data) return <div className="text-center mt-10 text-red-400">데이터를 불러오지 못했습니다.</div>;
22+
if (error || !data || !data.grade) return <div className="text-center mt-10 text-red-400">데이터를 불러오지 못했습니다.</div>;
23+
24+
const userLevelNumber = gradeToLevelMap[data.grade];
25+
26+
if (userLevelNumber === undefined) {
27+
return <div className="text-center mt-10 text-red-400">알 수 없는 레벨입니다.</div>;
28+
}
1629

1730
return (
1831
<div className="px-4 py-3 text-white min-h-screen font-suit">
1932
<Header leftSection="BACK" rightSection="NONE"> </Header>
2033

21-
<div className="pt-[52px]">
22-
<MyLevelCard
23-
userLevel={data.grade}
24-
userProgress={data.levelExp}
25-
currentReviewCount={data.reviewCount}
26-
currentLikeCount={data.likeCount}
34+
<div className="pt-[44px]">
35+
<MyLevelCard
36+
userLevel={userLevelNumber}
37+
userProgress={data.levelExp || 0}
38+
currentReviewCount={data.reviewCount || 0}
39+
currentLikeCount={data.likeCount || 0}
2740
/>
2841
</div>
2942

@@ -42,4 +55,4 @@ export default function LevelPage() {
4255
</div>
4356
</div>
4457
);
45-
}
58+
}

src/pages/search/ReviewSearch.tsx

Lines changed: 81 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,105 @@
1-
import { useState } from 'react';
2-
import { useNavigate } from 'react-router-dom';
3-
import {SearchInput, Badge, BottomNavigation} from '@/components'
4-
const initialKeywords = [
5-
'뭔가검색했겠지...', '뭐가있지', '아무거나',
6-
'두줄은', '채워야되니까', '일단써보기'
7-
];
8-
9-
export default function ReviewSearchPage() {
1+
// 파일 경로: src/pages/search/ReviewSearch.tsx
2+
3+
import { useEffect, useState } from 'react';
4+
import { useNavigate } from 'react-router-dom';
5+
import { Header, SearchInput, Badge, BottomNavigation } from '@/components';
6+
import api from '@/api/api';
7+
8+
export default function ReviewSearch() { // 컴포넌트 이름을 파일 이름과 맞춥니다.
109
const [search, setSearch] = useState('');
11-
const [recentKeywords, setRecentKeywords] = useState(initialKeywords);
12-
const navigate = useNavigate();
10+
const [recentKeywords, setRecentKeywords] = useState<string[]>([]);
11+
const navigate = useNavigate();
1312

14-
const handleRemove = (index: number) => {
15-
setRecentKeywords(prev => prev.filter((_, i) => i !== index));
13+
const handleBackClick = () => {
14+
navigate(-1);
15+
};
16+
17+
// ✅ 최근 검색어 불러오기
18+
const fetchRecentKeywords = async () => {
19+
try {
20+
// [수정] baseURL을 사용하므로 상대 경로로 변경하고, .data로 데이터에 접근합니다.
21+
const response = await api.get<string[]>('/search');
22+
setRecentKeywords(response.data.slice(0, 6));
23+
} catch (err) {
24+
console.error('최근 검색어 조회 실패:', err);
25+
setRecentKeywords([]); // 오류 발생 시 빈 배열로 초기화
26+
}
1627
};
1728

18-
const handleSearch = () => {
19-
if (!search.trim()) {
29+
// ✅ 검색어 삭제
30+
const handleRemove = async (index: number) => {
31+
const keyword = recentKeywords[index];
32+
try {
33+
await api.delete('/search', { data: { keyword } });
34+
setRecentKeywords(prev => prev.filter((_, i) => i !== index));
35+
} catch (err) {
36+
console.error('검색어 삭제 실패:', err);
37+
}
38+
};
39+
40+
// ✅ 검색 및 저장
41+
const handleSearch = async () => {
42+
const trimmed = search.trim();
43+
if (!trimmed) {
2044
alert('검색어를 입력해주세요.');
2145
return;
2246
}
23-
navigate(`/search/result?query=${search}`);
47+
48+
try {
49+
await api.post('/search', { keyword: trimmed });
50+
51+
setRecentKeywords(prev => {
52+
const filtered = prev.filter(word => word !== trimmed);
53+
return [trimmed, ...filtered].slice(0, 6);
54+
});
55+
56+
navigate(`/search/result?query=${encodeURIComponent(trimmed)}`);
57+
} catch (err) {
58+
console.error('검색어 저장 실패:', err);
59+
}
2460
};
2561

26-
return (
27-
<div className="relative flex justify-center min-h-screen text-white ">
28-
<div className="w-full max-w-[400px] px-4 pt-4 pb-20">
62+
useEffect(() => {
63+
fetchRecentKeywords();
64+
}, []);
65+
66+
return (
67+
<div className="relative flex justify-center min-h-screen text-white">
68+
<div className="w-full max-w-[400px] px-4 pt-16 pb-20">
69+
<Header leftSection="BACK" onBackClick={handleBackClick}>
70+
검색
71+
</Header>
72+
2973
<SearchInput
3074
value={search}
3175
onChange={setSearch}
3276
placeholder="검색어를 입력해주세요"
33-
onSearch={handleSearch}
77+
onSearch={handleSearch}
3478
/>
3579

3680
<div className="mt-6">
3781
<h2 className="mb-2 text-title-3">최근 검색어</h2>
38-
<div className="flex flex-wrap gap-2">
39-
{recentKeywords.map((word, index) => (
40-
<Badge
41-
key={index}
42-
type="removable"
43-
onRemove={() => handleRemove(index)}
44-
>
45-
{word}
46-
</Badge>
47-
))}
48-
</div>
82+
{recentKeywords.length > 0 ? (
83+
<div className="flex flex-wrap gap-2">
84+
{recentKeywords.map((word, index) => (
85+
<Badge
86+
key={`${word}-${index}`}
87+
type="removable"
88+
onRemove={() => handleRemove(index)}
89+
>
90+
{word}
91+
</Badge>
92+
))}
93+
</div>
94+
) : (
95+
<p className="text-sm text-gray-500">최근 검색어가 없습니다.</p>
96+
)}
4997
</div>
5098
</div>
51-
99+
52100
<div className="fixed bottom-0 left-1/2 w-full max-w-[430px] -translate-x-1/2">
53101
<BottomNavigation />
54102
</div>
55103
</div>
56104
);
57-
}
58-
59-
60-
105+
}

0 commit comments

Comments
 (0)