Skip to content

Commit 43fdc36

Browse files
authored
Merge pull request #40 from Leets-Official/feat/#39/레벨-페이지-구현
[feat] 레벨 페이지 구현
2 parents 2f86b57 + 7584462 commit 43fdc36

File tree

8 files changed

+246
-20
lines changed

8 files changed

+246
-20
lines changed

src/components/common/LevelCard/LeveCard.tsx renamed to src/components/common/LevelCard/LevelCard.tsx

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,51 @@
11
import React from 'react';
22
import { LEVEL_DATA } from '@/constants/level';
33
import type { Level } from '@/constants/level';
4+
import { LEVELS_DATA } from '@/constants/levelcondition';
45

56
interface LevelCardProps {
67
userLevel: number;
78
userProgress: number;
9+
currentReviewCount: number; // 사용자의 현재 후기 개수
10+
currentLikeCount: number; // 사용자의 현재 좋아요 개수
811
}
9-
const LevelCard: React.FC<LevelCardProps> = ({ userLevel, userProgress }) => {
10-
// userLevel에 해당하는 레벨 데이터를 찾습니다.
11-
const foundLevelData = LEVEL_DATA.find((level: Level) => level.id === userLevel);
1212

13-
// 만약 해당하는 데이터가 없으면, LEVEL_DATA의 첫 번째 항목(Lv. 1)을 기본값으로 사용합니다.
13+
const LevelCard: React.FC<LevelCardProps> = ({
14+
userLevel,
15+
userProgress,
16+
currentReviewCount,
17+
currentLikeCount
18+
}) => {
19+
const foundLevelData = LEVEL_DATA.find((level: Level) => level.id === userLevel);
1420
const levelDataToDisplay = foundLevelData || LEVEL_DATA[0];
1521

16-
// level.ts 파일이 비어있을 경우에 대한 방어 코드
1722
if (!levelDataToDisplay) {
1823
return <div>레벨 정보를 불러올 수 없습니다.</div>;
1924
}
2025

21-
// 화면에 표시할 데이터를 구조분해 할당합니다.
2226
const { id: displayLevel, title: levelTitle, CharacterComponent } = levelDataToDisplay;
2327

28+
const nextLevelGoalData = LEVELS_DATA.find(level => level.level === userLevel + 1);
29+
30+
let nextLevelTask: React.ReactNode = "최고 레벨입니다!";
31+
32+
if (nextLevelGoalData) {
33+
const reviewGoalMatch = nextLevelGoalData.condition.match(/ (\d+)/);
34+
const likeGoalMatch = nextLevelGoalData.condition.match(/ (\d+)/);
35+
36+
const reviewGoal = reviewGoalMatch ? parseInt(reviewGoalMatch[1], 10) : 0;
37+
const likeGoal = likeGoalMatch ? parseInt(likeGoalMatch[1], 10) : 0;
38+
39+
const remainingReviews = Math.max(0, reviewGoal - currentReviewCount);
40+
const remainingLikes = Math.max(0, likeGoal - currentLikeCount);
41+
42+
nextLevelTask = (
43+
<>
44+
후기 <span className="text-red-400">{remainingReviews}</span>개, 좋아요 <span className="text-red-400">{remainingLikes}</span>개 누르기
45+
</>
46+
);
47+
}
48+
2449
return (
2550
<section className="h-[140px] rounded-lg bg-gray-800/30 p-4">
2651
<div className="flex items-center gap-4">
@@ -34,8 +59,7 @@ const LevelCard: React.FC<LevelCardProps> = ({ userLevel, userProgress }) => {
3459
</div>
3560
<p className="mt-3 text-caption-3 text-gray-300">
3661
다음 레벨까지 할 일은... <br />
37-
후기 <span className="text-red-400">OO</span>개, 좋아요 <span className="text-red-400">OO</span>
38-
누르기
62+
{nextLevelTask}
3963
</p>
4064
</div>
4165
<div className="flex h-24 w-24 shrink-0 items-center justify-center">
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react';
2+
3+
interface LevelInfoCardProps {
4+
level: number;
5+
title: string;
6+
condition: string;
7+
Icon: React.ElementType;
8+
}
9+
10+
export default function LevelInfoCard({ level, title, condition, Icon }: LevelInfoCardProps) {
11+
return (
12+
<div className="w-[335px] h-[120px] mx-auto flex justify-between items-start">
13+
<div className="w-[120px] h-[120px] bg-[#4242424D] rounded-lg flex items-center justify-center shrink-0">
14+
<Icon className="w-[100px] h-[100px]" />
15+
</div>
16+
<div className="w-[195px] flex flex-col pt-2">
17+
<p className="text-title-3 text-white">
18+
Lv.{level} {title}
19+
</p>
20+
<div className="flex pt-1 text-body-2">
21+
<span className="text-gray-500 shrink-0 mr-2">달성 조건</span>
22+
<div className="flex flex-col">
23+
{condition.split('\n').map((line, index) => (
24+
<span key={index} className="text-red-300">
25+
{line}
26+
</span>
27+
))}
28+
</div>
29+
</div>
30+
</div>
31+
</div>
32+
);
33+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from 'react';
2+
import { LEVEL_DATA } from '@/constants/level';
3+
import { LEVELS_DATA } from '@/constants/levelcondition';
4+
5+
interface MyLevelCardProps {
6+
userLevel: number;
7+
userProgress: number;
8+
currentReviewCount: number;
9+
currentLikeCount: number;
10+
}
11+
const MyLevelCard: React.FC<MyLevelCardProps> = ({
12+
userLevel,
13+
userProgress,
14+
currentReviewCount,
15+
currentLikeCount
16+
}) => {
17+
const currentLevelData = LEVEL_DATA.find(level => level.id === userLevel);
18+
const nextLevelInfo = LEVELS_DATA.find(level => level.level === userLevel + 1);
19+
20+
if (!currentLevelData) {
21+
return <div>레벨 정보를 불러올 수 없습니다.</div>;
22+
}
23+
24+
const { title: levelTitle, CharacterComponent } = currentLevelData;
25+
26+
const renderNextLevelTask = () => {
27+
if (!nextLevelInfo) {
28+
return '최고 레벨입니다!';
29+
}
30+
31+
const reviewGoalMatch = nextLevelInfo.condition.match(/ (\d+)/);
32+
const likeGoalMatch = nextLevelInfo.condition.match(/ (\d+)/);
33+
34+
const reviewGoal = reviewGoalMatch ? parseInt(reviewGoalMatch[1], 10) : 0;
35+
const likeGoal = likeGoalMatch ? parseInt(likeGoalMatch[1], 10) : 0;
36+
37+
const remainingReviews = Math.max(0, reviewGoal - currentReviewCount);
38+
const remainingLikes = Math.max(0, likeGoal - currentLikeCount);
39+
40+
return (
41+
<>
42+
후기 <span className="text-red-400">{remainingReviews}</span>개, 좋아요 <span className="text-red-400">{remainingLikes}</span>개 누르기
43+
</>
44+
);
45+
};
46+
47+
return (
48+
<div className="w-[335px] h-[320px] bg-[#4242424D] rounded-lg p-4 flex flex-col items-center justify-around mt-4 mx-auto">
49+
<CharacterComponent className="w-[150px] h-[150px]" />
50+
51+
<p className="text-center">
52+
<span className="text-title-1 text-red-400">Lv.{userLevel}</span>
53+
<span className="text-title-4 text-white"> {levelTitle}</span>
54+
</p>
55+
56+
<div className="w-[295px] h-[12px] bg-gray-950 rounded-full">
57+
<div className="h-full bg-red-300 rounded-full" style={{ width: `${userProgress}%` }} />
58+
</div>
59+
60+
<p className="text-caption-3 text-gray-300 text-center">
61+
다음 레벨까지 할 일은?
62+
<br />
63+
{renderNextLevelTask()}
64+
</p>
65+
</div>
66+
);
67+
};
68+
69+
export default MyLevelCard;

src/components/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import ReviewCard from '@/components/common/ReviewCard/ReviewCard';
55
import Badge from '@/components/common/Badge/Badge';
66
import Image from '@/components/common/Image/Image';
77
import ToggleTab from '@/components/common/ToggleTab';
8-
import LevelCard from './common/LevelCard/LeveCard';
8+
import LevelCard from './common/LevelCard/LevelCard';
99
import ImagePreviewItem from './common/ImagePreview/ImagePreviewItem';
1010
import Header from '@/components/common/Header/Header';
1111
import InputField from '@/components/common/Input/Input';
@@ -15,7 +15,7 @@ import ReviewStepLayout from '@/components/review/ReviewLayout';
1515
import TagSection from '@/components/review/TagSection';
1616
import RatingCard from '@/components/review/RatingCard';
1717
import AccordionSection from '@/components/common/ReviewFilter/AccordionSection';
18-
18+
import LevelInfoCard from './common/LevelCard/LevelInfoCard';
1919
import BaseModal from '@/components/common/Modal/BaseModal';
2020
import SeatItem from '@/components/seat/SeatItem';
2121
import SeatMap from '@/components/seat/SeatMap';
@@ -27,8 +27,10 @@ import SeatFocusModal from '@/components/common/Modal/SeatModal/SeatFocusModal';
2727
import SeatWriteModal from '@/components/common/Modal/SeatModal/SeatWriteModal';
2828

2929
export { default as FilterCheckbox } from '@/components/common/ReviewFilter/FilterCheckbox';
30+
export { default as MyLevelCard } from './common/LevelCard/MyLevelCard';
3031

3132
export {
33+
LevelInfoCard,
3234
Button,
3335
BottomNavigation,
3436
BestCinemaCard,
@@ -56,3 +58,4 @@ export {
5658
SeatFocusModal,
5759
SeatWriteModal,
5860
};
61+

src/constants/levelcondition.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { LevelCharacter1, LevelCharacter2, LevelCharacter3, LevelCharacter4 } from '@/assets';
2+
import React from 'react';
3+
4+
export interface Level {
5+
level: number;
6+
title: string;
7+
condition: string;
8+
Icon: React.ElementType;
9+
}
10+
11+
export const LEVELS_DATA: Level[] = [
12+
{
13+
level: 1,
14+
title: '레벨 1 이름',
15+
condition: 'seeat 가입하기',
16+
Icon: LevelCharacter1,
17+
},
18+
{
19+
level: 2,
20+
title: '레벨 2 이름',
21+
condition: '후기 2개 작성하기\n좋아요 5개 누르기',
22+
Icon: LevelCharacter2,
23+
},
24+
{
25+
level: 3,
26+
title: '레벨 3 이름',
27+
condition: '후기 10개 작성하기\n좋아요 25개 누르기',
28+
Icon: LevelCharacter3,
29+
},
30+
{
31+
level: 4,
32+
title: '레벨 4 이름',
33+
condition: '후기 40개 작성하기\n좋아요 100개 누르기',
34+
Icon: LevelCharacter4,
35+
},
36+
];

src/pages/my/Level.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// import { HeaderBasic } from '@/components';
2+
import { LevelInfoCard, MyLevelCard } from '@/components';
3+
import { LEVELS_DATA } from '@/constants/levelcondition';
4+
5+
export default function LevelPage() {
6+
// 나중에 API
7+
const currentUser = {
8+
level: 3,
9+
progress: 45,
10+
};
11+
const currentUserStatus = {
12+
reviewCount: 5,
13+
likeCount: 12,
14+
};
15+
16+
return (
17+
<div className="px-4 py-3 text-white min-h-screen font-suit">
18+
{/* <HeaderBasic>{null}</HeaderBasic> */}
19+
20+
{/* 현재 레벨 박스 */}
21+
<MyLevelCard
22+
userLevel={currentUser.level}
23+
userProgress={currentUser.progress}
24+
currentReviewCount={currentUserStatus.reviewCount}
25+
currentLikeCount={currentUserStatus.likeCount}
26+
/>
27+
28+
{/* 구분선 */}
29+
<div className="w-[335px] h-[1px] bg-[#424242] mt-6 mb-6 mx-auto" />
30+
31+
{/* 레벨 리스트 */}
32+
<div className="space-y-4">
33+
{LEVELS_DATA.map((levelData) => (
34+
<LevelInfoCard
35+
key={levelData.level}
36+
level={levelData.level}
37+
title={levelData.title}
38+
condition={levelData.condition}
39+
Icon={levelData.Icon}
40+
/>
41+
))}
42+
</div>
43+
</div>
44+
);
45+
}

src/pages/my/MyPage.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ interface MenuItem {
1515
path: string;
1616
}
1717

18+
const currentUserStatus = {
19+
reviewCount: 5,
20+
likeCount: 12,
21+
};
22+
1823
const MyPage: React.FC = () => {
1924
const navigate = useNavigate();
2025

@@ -36,17 +41,17 @@ const MyPage: React.FC = () => {
3641
navigate('/my/profile-edit');
3742
};
3843

39-
// 참고: '설정' 페이지로 이동하는 기능은 이제 Header가 아닌
40-
// 다른 UI 요소(예: 프로필 수정 버튼 옆)에 연결하거나,
41-
// Header 컴포넌트 자체를 더 유연하게 만들어야 합니다.
42-
// const handleSettingsClick = () => {
43-
// navigate('/my/settings');
44-
// };
45-
4644
return (
4745
<div className="relative mx-auto w-full max-w-md">
4846
<div className="min-h-screen">
49-
<Header leftSection="LOGO" rightSection="SETTING" />
47+
{/* Header - 병합 결과 */}
48+
<Header
49+
title="마이페이지"
50+
showBack={false}
51+
showLike={false}
52+
showBookmark={false}
53+
rightSection="SETTING"
54+
/>
5055

5156
<main className="mt-2 flex flex-col gap-4 px-4 pt-[68px] pb-[83px]">
5257
{/* 프로필 카드 */}
@@ -92,8 +97,13 @@ const MyPage: React.FC = () => {
9297
</div>
9398
</section>
9499

95-
{/* 레벨 카드 (컴포넌트로 분리) */}
96-
<LevelCard userLevel={user.level} userProgress={user.progress} />
100+
{/* 레벨 카드 */}
101+
<LevelCard
102+
userLevel={user.level}
103+
userProgress={user.progress}
104+
currentReviewCount={currentUserStatus.reviewCount}
105+
currentLikeCount={currentUserStatus.likeCount}
106+
/>
97107

98108
{/* 메뉴 리스트 */}
99109
<section>
@@ -114,6 +124,7 @@ const MyPage: React.FC = () => {
114124
</main>
115125
</div>
116126

127+
{/* 하단 내비게이션 */}
117128
<div className="fixed bottom-0 left-1/2 w-full max-w-md -translate-x-1/2">
118129
<BottomNavigation />
119130
</div>

src/routes/route.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import OnboardingGenrePage from '@/pages/onboarding/OnboardingGenrePage';
2828
import OnboardingTheaterPage from '@/pages/onboarding/OnboardingTheaterPage';
2929
import SeatTest from '@/pages/seat/SeatTest';
3030
import SeatReviewPage from '@/pages/seat';
31+
import LevelPage from '@/pages/my/Level';
3132

3233
const router = createBrowserRouter([
3334
{
@@ -132,6 +133,10 @@ const router = createBrowserRouter([
132133
path: '/my/cinema-choice',
133134
element: <CinemaChoice />,
134135
},
136+
{
137+
path: '/my/level',
138+
element: <LevelPage />,
139+
},
135140
{
136141
path: '/seat',
137142
element: <SeatTest />,

0 commit comments

Comments
 (0)