diff --git a/src/components/commons/Image/index.tsx b/src/components/commons/Image/index.tsx index 09108ad..014bab4 100644 --- a/src/components/commons/Image/index.tsx +++ b/src/components/commons/Image/index.tsx @@ -1,12 +1,9 @@ -import styles from "./Image.module.scss"; +import { ImageInfo } from "@/type/type"; import classNames from "classnames/bind"; +import styles from "./Image.module.scss"; const cx = classNames.bind(styles); -type ImageInfo = { - src: string; - alt: string; -}; type ObjectFit = "fill" | "contain" | "cover" | "scale-down" | "none"; interface ImageProps { diff --git a/src/components/domains/myPage/contents/Level/Level.module.scss b/src/components/domains/myPage/contents/Level/Level.module.scss new file mode 100644 index 0000000..303f976 --- /dev/null +++ b/src/components/domains/myPage/contents/Level/Level.module.scss @@ -0,0 +1,15 @@ +.level { + padding-bottom: 10rem; + + @include responsive("T") { + padding-bottom: 8rem; + } + + &__level { + padding-bottom: 4.8rem; + + &-info { + padding-bottom: 2.4rem; + } + } +} diff --git a/src/components/domains/myPage/contents/Level/LevelProgressBar/LevelProgressBar.module.scss b/src/components/domains/myPage/contents/Level/LevelProgressBar/LevelProgressBar.module.scss new file mode 100644 index 0000000..b1612d5 --- /dev/null +++ b/src/components/domains/myPage/contents/Level/LevelProgressBar/LevelProgressBar.module.scss @@ -0,0 +1,57 @@ +.progress-bar { + &__track { + width: 100%; + height: 1rem; + background-color: $gray20; + border-radius: 0.5rem; + + @include responsive("M") { + height: 0.8rem; + } + + &-fill { + background: linear-gradient(90deg, $primary30, $primary50); + border-radius: 0.5rem; + height: 100%; + animation: fill-up 0.8s linear forwards; + } + + .beginner { + --target-width: 20%; + } + + .intermediate { + --target-width: 60%; + } + + .expert { + --target-width: 100%; + } + } + + &__labels { + @include flexbox(between, center); + padding-top: 1rem; + + @include responsive("M") { + padding-top: 0.5rem; + } + + &-el { + @include text-style(1.6, 500, $black30); + + @include responsive("M") { + font-size: 1.4rem; + } + } + } +} + +@keyframes fill-up { + from { + width: 0%; + } + to { + width: var(--target-width, 100%); + } +} diff --git a/src/components/domains/myPage/contents/Level/LevelProgressBar/index.tsx b/src/components/domains/myPage/contents/Level/LevelProgressBar/index.tsx new file mode 100644 index 0000000..a611a7a --- /dev/null +++ b/src/components/domains/myPage/contents/Level/LevelProgressBar/index.tsx @@ -0,0 +1,41 @@ +import Image from "@/components/commons/Image"; +import { IMAGES } from "@/constants/images"; +import { authState } from "@/recoil/authAtom"; +import classNames from "classnames/bind"; +import { useRecoilState } from "recoil"; +import styles from "./LevelProgressBar.module.scss"; + +const cx = classNames.bind(styles); + +const LevelProgressBar = () => { + const [auth] = useRecoilState(authState); + + if (!auth) { + return ( +
+

사용자 정보를 불러올 수 없습니다.

+ +
+ ); + } + + return ( +
+
+
+
+
+ 초보여행가 + 중급여행가 + 프로여행가 +
+
+ ); +}; +export default LevelProgressBar; diff --git a/src/components/domains/myPage/contents/Level/MyBadge/Badge/Badge.module.scss b/src/components/domains/myPage/contents/Level/MyBadge/Badge/Badge.module.scss new file mode 100644 index 0000000..87e9176 --- /dev/null +++ b/src/components/domains/myPage/contents/Level/MyBadge/Badge/Badge.module.scss @@ -0,0 +1,27 @@ +.badge { + @include column-flexbox(); + gap: 1rem; + + &__img { + border-radius: 2rem; + height: 8rem; + background-color: $gray20; // fix: temp + + @include responsive("M") { + } + } + + &__description { + @include column-flexbox(); + + &-title { + @include text-style(1.8, 700, $black30); + } + + &-criteria { + @include text-style(1.4, 500, $black30); + word-break: keep-all; + text-align: center; + } + } +} diff --git a/src/components/domains/myPage/contents/Level/MyBadge/Badge/index.tsx b/src/components/domains/myPage/contents/Level/MyBadge/Badge/index.tsx new file mode 100644 index 0000000..82842d5 --- /dev/null +++ b/src/components/domains/myPage/contents/Level/MyBadge/Badge/index.tsx @@ -0,0 +1,27 @@ +import Image from "@/components/commons/Image"; + +import classNames from "classnames/bind"; +import styles from "./Badge.module.scss"; + +const cx = classNames.bind(styles); + +interface BadgeProps { + img: string; + title: string; + criteria: string; +} + +const Badge = ({ img, title, criteria }: BadgeProps) => { + return ( +
+
+ +
+
+

{title}

+

{criteria}

+
+
+ ); +}; +export default Badge; diff --git a/src/components/domains/myPage/contents/Level/MyBadge/MyBadge.module.scss b/src/components/domains/myPage/contents/Level/MyBadge/MyBadge.module.scss new file mode 100644 index 0000000..772076e --- /dev/null +++ b/src/components/domains/myPage/contents/Level/MyBadge/MyBadge.module.scss @@ -0,0 +1,22 @@ +.my-badge { + &__title { + @include text-style(2.6, 700, $black30); + padding-bottom: 3.2rem; + + @include responsive("M") { + font-size: 2.4rem; + padding-bottom: 2.4rem; + } + } + + &__list { + display: grid; + grid-template-columns: repeat(4, 8rem); + justify-content: space-between; + row-gap: 2rem; + + @include responsive("M") { + grid-template-columns: repeat(3, 8rem); + } + } +} diff --git a/src/components/domains/myPage/contents/Level/MyBadge/index.tsx b/src/components/domains/myPage/contents/Level/MyBadge/index.tsx new file mode 100644 index 0000000..a4208ac --- /dev/null +++ b/src/components/domains/myPage/contents/Level/MyBadge/index.tsx @@ -0,0 +1,34 @@ +import tempImg from "@/assets/images/child.svg"; +import Badge from "./Badge"; + +import classNames from "classnames/bind"; +import styles from "./MyBadge.module.scss"; + +const cx = classNames.bind(styles); + +const mockData = [ + { id: 1, img: tempImg, title: "여행입문자", criteria: "첫 여행지를 작성한 여행가" }, + { id: 2, img: tempImg, title: "여행입문자", criteria: "여행지를 n개 이상 작성한 여행가" }, + { id: 3, img: tempImg, title: "여행입문자", criteria: "첫 여행지를 작성한 여행가" }, + { id: 5, img: tempImg, title: "여행입문자", criteria: "첫 여행지를 작성한 여행가" }, + { id: 6, img: tempImg, title: "여행입문자", criteria: "첫 여행지를 작성한 여행가" }, + { id: 4, img: tempImg, title: "여행입문자", criteria: "첫 여행지를 작성한 여행가" }, + { id: 7, img: tempImg, title: "여행입문자", criteria: "첫 여행지를 작성한 여행가" }, + { id: 8, img: tempImg, title: "여행입문자", criteria: "첫 여행지를 작성한 여행가" }, +]; + +const MyBadge = () => { + return ( +
+

내 뱃지

+ +
+ ); +}; +export default MyBadge; diff --git a/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/AllLevelInfoModal.module.scss b/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/AllLevelInfoModal.module.scss new file mode 100644 index 0000000..ee6570f --- /dev/null +++ b/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/AllLevelInfoModal.module.scss @@ -0,0 +1,52 @@ +.all-level-info-modal { + @include column-flexbox(); + gap: 0.32rem; + background-color: white; + border-radius: 1rem; + width: 64rem; + padding: 2.4rem; + + @include responsive("M") { + width: calc(100% - 4rem); + padding: 2rem; + } + + &__close-btn { + align-self: flex-end; + width: 2.4rem; + height: 2.4rem; + cursor: pointer; + + @include responsive("M") { + width: 2rem; + height: 2rem; + } + } + + &__header { + @include flexbox(); + + &-logo { + width: 8.4rem; + + @include responsive("M") { + width: 6rem; + } + } + + &-title { + @include text-style(1.6, 700, $black30); + text-align: center; + + @include responsive("M") { + font-size: 1.2rem; + } + } + } + + &__level-info-list { + &-element:not(:last-child) { + border-bottom: 0.1rem solid $gray20; + } + } +} diff --git a/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/LevelInfoBox/LevelInfoBox.module.scss b/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/LevelInfoBox/LevelInfoBox.module.scss new file mode 100644 index 0000000..227a0e8 --- /dev/null +++ b/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/LevelInfoBox/LevelInfoBox.module.scss @@ -0,0 +1,83 @@ +.level-info-box { + display: grid; + grid-template-areas: + "level requirement" + ". description"; + justify-content: start; + align-items: center; + padding: 3rem 0; + gap: 1rem 1.4rem; + + @include responsive("M") { + padding: 2.4rem 0; + gap: 0.8rem 1rem; + } + + &__level { + @include flexbox(); + gap: 0.7rem; + grid-area: level; + + @include responsive("M") { + gap: 0.4rem; + } + + &-img { + grid-area: img; + width: 4rem; + height: 4rem; + border-radius: 50%; + box-shadow: 0px 3px 16px 0px rgba(0, 0, 0, 0.08); + overflow: hidden; + + @include responsive("M") { + width: 3.6rem; + height: 3.6rem; + } + } + + &-name { + @include text-style(1.4, 700, $black30); + + @include responsive("M") { + font-size: 1.2rem; + } + } + } + + &__requirement { + @include text-style(1.4, 600, $black30); + grid-area: requirement; + + @include responsive("M") { + font-size: 1.2rem; + } + + &-additionalInfo { + @include text-style(1.1, 500, $black30); + display: block; + + @include responsive("M") { + font-size: 1rem; + } + } + } + + &__description { + @include text-style(1.3, 500, $black30); + grid-area: description; + + @include responsive("M") { + font-size: 1.2rem; + } + + &-additionalInfo { + @include text-style(1.1, 500, $black30); + display: block; + + @include responsive("M") { + font-size: 1rem; + } + } + } +} diff --git a/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/LevelInfoBox/index.tsx b/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/LevelInfoBox/index.tsx new file mode 100644 index 0000000..8102abc --- /dev/null +++ b/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/LevelInfoBox/index.tsx @@ -0,0 +1,41 @@ +import Image from "@/components/commons/Image"; +import { UserLevelInfo } from "@/type/type"; + +import classNames from "classnames/bind"; +import styles from "./LevelInfoBox.module.scss"; + +const cx = classNames.bind(styles); + +interface LevelInfoBoxProps { + info: UserLevelInfo; +} + +const LevelInfoBox = ({ info }: LevelInfoBoxProps) => { + const { level, imgInfo, requirement, benefits } = info; + + return ( +
+
+
+ +
+

{level}

+
+ +

+ {requirement.description} + {requirement.additionalInfo && ( + {info.requirement.additionalInfo} + )} +

+ +

+ {benefits.description} + {benefits.additionalInfo && ( + {benefits.additionalInfo} + )} +

+
+ ); +}; +export default LevelInfoBox; diff --git a/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/index.tsx b/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/index.tsx new file mode 100644 index 0000000..56813dc --- /dev/null +++ b/src/components/domains/myPage/contents/Level/MyLevel/AllLevelInfoModal/index.tsx @@ -0,0 +1,41 @@ +import Image from "@/components/commons/Image"; +import Modal from "@/components/commons/Modal"; +import { IMAGES } from "@/constants/images"; +import { USER_LEVEL_INFO_LIST } from "@/constants/userlevelInfoList"; +import LevelInfoBox from "./LevelInfoBox"; + +import classNames from "classnames/bind"; +import styles from "./AllLevelInfoModal.module.scss"; + +const cx = classNames.bind(styles); + +interface AllLevelInfoModalProps { + isModalOpen: boolean; + onModalClose: () => void; +} + +const AllLevelInfoModal = ({ isModalOpen, onModalClose }: AllLevelInfoModalProps) => { + return ( + +
+ +
+
+ +

등급 안내

+
+
+ +
+
+ ); +}; +export default AllLevelInfoModal; diff --git a/src/components/domains/myPage/contents/Level/MyLevel/MyLevel.module.scss b/src/components/domains/myPage/contents/Level/MyLevel/MyLevel.module.scss new file mode 100644 index 0000000..73bfcbc --- /dev/null +++ b/src/components/domains/myPage/contents/Level/MyLevel/MyLevel.module.scss @@ -0,0 +1,43 @@ +.my-level { + &__all-info { + @include flexbox(stretch, center); + @include text-style(2.4, 700, $black30); + gap: 0.5rem; + + @include responsive("M") { + font-size: 2rem; + gap: 0.3rem; + } + + &-question-mark { + @include flexbox(); + @include text-style(1.6, 600, $gray30); + border: 0.1rem solid $gray30; + border-radius: 50%; + width: 2.4rem; + height: 2.4rem; + cursor: pointer; + + @include responsive("M") { + font-size: 1.2rem; + width: 2rem; + height: 2rem; + } + } + } + + &__my-info { + @include text-style(3.2, 500, $black30); + padding-top: 2rem; + + @include responsive("M") { + font-size: 2.4rem; + padding-top: 1.6rem; + } + + &--highlight { + font-weight: 700; + color: $primary40; + } + } +} diff --git a/src/components/domains/myPage/contents/Level/MyLevel/index.tsx b/src/components/domains/myPage/contents/Level/MyLevel/index.tsx new file mode 100644 index 0000000..972d5b1 --- /dev/null +++ b/src/components/domains/myPage/contents/Level/MyLevel/index.tsx @@ -0,0 +1,49 @@ +import { useState } from "react"; + +import Image from "@/components/commons/Image"; +import { IMAGES } from "@/constants/images"; +import { authState } from "@/recoil/authAtom"; +import { useRecoilState } from "recoil"; + +import classNames from "classnames/bind"; +import AllLevelInfoModal from "./AllLevelInfoModal"; +import styles from "./MyLevel.module.scss"; + +const cx = classNames.bind(styles); + +const MyLevel = () => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [auth] = useRecoilState(authState); + + if (!auth) { + return ( +
+

사용자 정보를 불러올 수 없습니다.

+ +
+ ); + } + + const { nickname, role } = auth; + + return ( + <> +
+
+ 내 등급 + +
+

+ {nickname} + {" 님은"} {role} + {" 입니다."} +

+
+ + setIsModalOpen(false)} /> + + ); +}; +export default MyLevel; diff --git a/src/components/domains/myPage/contents/Level/index.tsx b/src/components/domains/myPage/contents/Level/index.tsx index 9aefa77..b38802d 100644 --- a/src/components/domains/myPage/contents/Level/index.tsx +++ b/src/components/domains/myPage/contents/Level/index.tsx @@ -1,8 +1,27 @@ +import LevelProgressBar from "./LevelProgressBar"; +import MyBadge from "./MyBadge"; +import MyLevel from "./MyLevel"; + +import classNames from "classnames/bind"; +import styles from "./Level.module.scss"; + +const cx = classNames.bind(styles); + const Level = () => { return ( - <> -

Level Component

- +
+
+
+ +
+
+ +
+
+
+ +
+
); }; export default Level; diff --git a/src/constants/userlevelInfoList.ts b/src/constants/userlevelInfoList.ts new file mode 100644 index 0000000..0f84f37 --- /dev/null +++ b/src/constants/userlevelInfoList.ts @@ -0,0 +1,40 @@ +import { UserLevelInfo } from "@/type/type"; +import { IMAGES } from "./images"; + +export const USER_LEVEL_INFO_LIST: UserLevelInfo[] = [ + { + id: 1, + level: "초보여행자", + imgInfo: IMAGES.PartyIcon, + requirement: { + description: "코스메이커 회원가입 시", + }, + benefits: { + description: "여행지 등록/수정 요청 가능", + }, + }, + { + id: 2, + level: "중급여행자", + imgInfo: IMAGES.PartyIcon, + requirement: { + description: "리뷰 댓글 50개 작성 시", + }, + benefits: { + description: "코스 작성 가능", + }, + }, + { + id: 3, + level: "프로여행자", + imgInfo: IMAGES.PartyIcon, + requirement: { + description: "당월 작성된 게시글 중 조회수 1등, 찜수 1등, 좋아요 1등 코스/여행지 작성자", + additionalInfo: "(매월 말일 기준으로 산정)", + }, + benefits: { + description: "여행지 방문 시 특별 혜택", + additionalInfo: "(방문 시 로그인한 나의 등급 화면 제시 필요, 캡처한 이미지는 인정되지 않습니다.)", + }, + }, +]; diff --git a/src/type/type.d.ts b/src/type/type.d.ts index 571818f..5835111 100644 --- a/src/type/type.d.ts +++ b/src/type/type.d.ts @@ -2,6 +2,11 @@ import { CourseReview } from "@/api/course/type"; import { DestinationReview } from "@/api/destination/type"; import { tagResponseDto } from "@/api/tag/type"; +export type ImageInfo = { + src: string; + alt: string; +}; + export interface TabBarIconProps { title: string; color: string; @@ -81,3 +86,14 @@ export interface RefinedReview { export interface RequestOptions { requireAuth: boolean; } + +export interface UserLevelInfo { + id: number; + level: string; + imgInfo: ImageInfo; + requirement: { description: string; additionalInfo?: string }; + benefits: { + description: string; + additionalInfo?: string; + }; +}