diff --git a/public/svg/ic_quesitonmark.svg b/public/svg/ic_quesitonmark.svg new file mode 100644 index 0000000..6cf6416 --- /dev/null +++ b/public/svg/ic_quesitonmark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/App.tsx b/src/App.tsx index 533b2ef..da53e75 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,7 +28,6 @@ const App = () => { return ( -
DASH
); }; diff --git a/src/assets/svg/IcCalendarcheckMono3D24.tsx b/src/assets/svg/IcCalendarcheckMono3D24.tsx index 56d43c9..fda4cea 100644 --- a/src/assets/svg/IcCalendarcheckMono3D24.tsx +++ b/src/assets/svg/IcCalendarcheckMono3D24.tsx @@ -1,4 +1,3 @@ -import * as React from "react"; import type { SVGProps } from "react"; const SvgIcCalendarcheckMono3D24 = (props: SVGProps) => ; export default SvgIcCalendarcheckMono3D24; \ No newline at end of file diff --git a/src/assets/svg/IcQuesitonmark.tsx b/src/assets/svg/IcQuesitonmark.tsx new file mode 100644 index 0000000..958157e --- /dev/null +++ b/src/assets/svg/IcQuesitonmark.tsx @@ -0,0 +1,3 @@ +import type { SVGProps } from "react"; +const SvgIcQuesitonmark = (props: SVGProps) => ; +export default SvgIcQuesitonmark; \ No newline at end of file diff --git a/src/assets/svg/index.ts b/src/assets/svg/index.ts index dca399d..d313cee 100644 --- a/src/assets/svg/index.ts +++ b/src/assets/svg/index.ts @@ -11,4 +11,8 @@ export { default as IcSearchBlack24 } from './IcSearchBlack24' export { default as IcSearchGray } from './IcSearchGray' export { default as IcSearchWhite24 } from './IcSearchWhite24' export { default as IcSpeaker3D } from './IcSpeaker3D' -export { default as IcXCircleGray } from './IcXCircleGray' \ No newline at end of file +export { default as IcXCircleGray } from './IcXCircleGray' + + + +export { default as IcQuesitonmark } from './IcQuesitonmark' \ No newline at end of file diff --git a/src/components/Flex/index.tsx b/src/components/Flex/index.tsx index efee356..afbcbf7 100644 --- a/src/components/Flex/index.tsx +++ b/src/components/Flex/index.tsx @@ -25,8 +25,10 @@ interface FlexProps extends HTMLAttributes { paddingLeft?: string; border?: string; borderRadius?: string; - borderTop?: string; + borderColor?: string; + borderWidth?: string; borderBottom?: string; + borderTop?: string; } const Flex = ({ @@ -52,8 +54,10 @@ const Flex = ({ paddingLeft, border, borderRadius, - borderTop, + borderColor, + borderWidth, borderBottom, + borderTop, children, className, ...props @@ -77,8 +81,10 @@ const Flex = ({ paddingLeft, border, borderRadius, - borderTop, + borderColor, + borderWidth, borderBottom, + borderTop, }; return ( diff --git a/src/components/Head/index.tsx b/src/components/Head/index.tsx index 5deabdf..b2319ce 100644 --- a/src/components/Head/index.tsx +++ b/src/components/Head/index.tsx @@ -1,4 +1,3 @@ -import clsx from 'clsx'; import { ComponentPropsWithoutRef } from 'react'; import { headStyle } from '@/components/Head/index.css'; diff --git a/src/components/Tab/TabPanel.tsx b/src/components/Tab/TabPanel.tsx index 0ec95e0..ba82c00 100644 --- a/src/components/Tab/TabPanel.tsx +++ b/src/components/Tab/TabPanel.tsx @@ -1,4 +1,5 @@ import { ReactNode } from 'react'; +import { tabPanelStyle } from '@/components/Tab/index.css'; interface TabPanelProps { children: ReactNode; @@ -9,7 +10,7 @@ interface TabPanelProps { const TabPanel = ({ children, isSelected, className }: TabPanelProps) => { if (!isSelected) return null; return ( -
+
{children}
); diff --git a/src/components/Tab/index.css.ts b/src/components/Tab/index.css.ts index a0762e2..7c1426d 100644 --- a/src/components/Tab/index.css.ts +++ b/src/components/Tab/index.css.ts @@ -87,3 +87,6 @@ export const tabListStyle = style({ export const tabRootStyle = style({ width: '100%', }); +export const tabPanelStyle = style({ + width: '100%', +}); diff --git a/src/constants/mocks/mockLessonData.ts b/src/constants/mocks/mockLessonData.ts new file mode 100644 index 0000000..d947234 --- /dev/null +++ b/src/constants/mocks/mockLessonData.ts @@ -0,0 +1,34 @@ +export const LESSON_DATA = { + lessonImageUrl: "https://hankki-prod-bucket.s3.ap-northeast-2.amazonaws.com/dummy/%E1%84%80%E1%85%A9%E1%84%85%E1%85%A7%E1%84%83%E1%85%A2+%E1%84%86%E1%85%B5%E1%86%AF%E1%84%91%E1%85%B3%E1%86%AF%E1%84%85%E1%85%A2%E1%86%AB%E1%84%87%E1%85%B5.jpeg", + lessonGenre: "힙합", + lessonName: "힙합 댄스 기초", + teacherId: 101, + teacherNickname: "김태훈", + teacherImageUrl: "https://hankki-prod-bucket.s3.ap-northeast-2.amazonaws.com/dummy/%E1%84%80%E1%85%A9%E1%84%85%E1%85%A7%E1%84%83%E1%85%A2+%E1%84%86%E1%85%B5%E1%86%AF%E1%84%91%E1%85%B3%E1%86%AF%E1%84%85%E1%85%A2%E1%86%AB%E1%84%87%E1%85%B5.jpeg", + reservationCount: 25, + maxReservationCount: 30, + individualPrice: 15000, + lessonDetail: "안녕하세요, 안무가 김태훈입니다.\n\n저는 인기 아이돌의 안무가로서 다양한 작업에 참여해왔고 현재는 안무 작업 뿐만 아니라 강습도 함께 진행하고 있습니다.\n\n이번 강의에서는 기본기와 프리스타일 위주로 진행하려고 합니다.\n\n💜먼저, 연결이라는 주제로 움직여보면서 자신만의 무브를 찾아가 볼 예정이고 익숙해진 후에는 컨트롤을 주제로 다양한 루틴을 시도해 볼 예정입니다.", + lessonRecommendation: "초급자에게 적합한 수업입니다.\n김태훈만의 트렌디한 힙합 베이스를 배우고 싶은 분\n멋있다ㄷ", + lessonLevel: "초급", + lessonLevelDetail: "기초적인 동작과 리듬을 익힐 ", + lessonRound: [ + { + lessonStartDateTime: "2023-12-10T10:00:00", + lessonEndDateTime: "2023-12-10T12:30:00", + }, + { + lessonStartDateTime: "2024-12-10T10:00:00", + lessonEndDateTime: "2024-12-10T12:00:00", + }, + { + lessonStartDateTime: "2023-12-11T10:00:00", + lessonEndDateTime: "2023-12-12T22:00:00", + }, + ], + lessonLocation: "로제이 댄스홀 합정점", + lessonStreetAddress: "서울특별시 송파구 올림픽로 240\n잠실종합운동장제2경기장 동문 앞 주차장", + lessonOldStreetAddress: "서교동 395-124", + favoriteCount: 120, + favoriteStatus: true, +}; diff --git a/src/pages/class/Card/index.css.ts b/src/pages/class/Card/index.css.ts new file mode 100644 index 0000000..bab17bf --- /dev/null +++ b/src/pages/class/Card/index.css.ts @@ -0,0 +1,16 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '@/styles/theme.css'; + +export const cardStyle = style({ + display: 'inline-flex', + alignItems: 'center', + + width: '100%', + padding: '1.6rem 2rem', + gap: '0.8rem', + + borderRadius: '4px', + border: `1px solid ${vars.colors.gray02}`, + borderColor: vars.colors.gray02, + backgroundColor: vars.colors.white, +}); diff --git a/src/pages/class/Card/index.tsx b/src/pages/class/Card/index.tsx new file mode 100644 index 0000000..c1074fc --- /dev/null +++ b/src/pages/class/Card/index.tsx @@ -0,0 +1,14 @@ +import type { ComponentProps } from 'react'; +import { cardStyle } from './index.css'; + +type CardProps = ComponentProps<'div'>; + +const Card = ({ children, ...props }: CardProps) => { + return ( +
+ {children} +
+ ); +}; + +export default Card; diff --git a/src/pages/class/TabWrapper/TabIntro/index.css.ts b/src/pages/class/TabWrapper/TabIntro/index.css.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/class/TabWrapper/TabIntro/index.tsx b/src/pages/class/TabWrapper/TabIntro/index.tsx new file mode 100644 index 0000000..5b90164 --- /dev/null +++ b/src/pages/class/TabWrapper/TabIntro/index.tsx @@ -0,0 +1,19 @@ +import Flex from '@/components/Flex'; +import Text from '@/components/Text'; +import { LESSON_DATA } from '@/constants/mocks/mockLessonData'; + +const Intro = () => { + const { lessonDetail } = LESSON_DATA; + + return ( + <> + + + {lessonDetail} + + + + ); +}; + +export default Intro; diff --git a/src/pages/class/TabWrapper/TabLevel/index.css.ts b/src/pages/class/TabWrapper/TabLevel/index.css.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/class/TabWrapper/TabLevel/index.tsx b/src/pages/class/TabWrapper/TabLevel/index.tsx new file mode 100644 index 0000000..0dffd52 --- /dev/null +++ b/src/pages/class/TabWrapper/TabLevel/index.tsx @@ -0,0 +1,50 @@ +import Card from '@/pages/class/Card'; +import Flex from '@/components/Flex'; +import Head from '@/components/Head'; +import Text from '@/components/Text'; +import { LESSON_DATA } from '@/constants/mocks/mockLessonData'; +import { IcClose, IcQuesitonmark } from '@/assets/svg'; + +const Level = () => { + const { lessonLevel, lessonLevelDetail, lessonRecommendation } = LESSON_DATA; + + return ( + + + + + + + {lessonLevel} + + + {lessonLevelDetail} + + + + + + 클래스 난이도는 이렇게 설정되어있어요! + + + + + + + + + 이런 분들에게 해당 클래스를 추천해요! + + + + + {lessonRecommendation} + + + + ); +}; + +export default Level; diff --git a/src/pages/class/TabWrapper/TabLocation/index.css.ts b/src/pages/class/TabWrapper/TabLocation/index.css.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/class/TabWrapper/TabLocation/index.tsx b/src/pages/class/TabWrapper/TabLocation/index.tsx new file mode 100644 index 0000000..280c299 --- /dev/null +++ b/src/pages/class/TabWrapper/TabLocation/index.tsx @@ -0,0 +1,52 @@ +import Card from '@/pages/class/Card'; +import Flex from '@/components/Flex'; +import Text from '@/components/Text'; +import { LESSON_DATA } from '@/constants/mocks/mockLessonData'; +import { IcClose } from '@/assets/svg'; + +const LocationInfo = () => { + const { lessonLocation, lessonStreetAddress, lessonOldStreetAddress } = LESSON_DATA; + + return ( + + + + {/* 왼쪽 */} + + + {lessonLocation} + + + + + + 주소 + + + + {lessonStreetAddress} + + + + + + + 지번 + + + + {lessonOldStreetAddress} + + + + + + {/* 오른쪽 */} + + + + + ); +}; + +export default LocationInfo; diff --git a/src/pages/class/TabWrapper/TabPeriod/index.css.ts b/src/pages/class/TabWrapper/TabPeriod/index.css.ts new file mode 100644 index 0000000..237c8f8 --- /dev/null +++ b/src/pages/class/TabWrapper/TabPeriod/index.css.ts @@ -0,0 +1,14 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from "@/styles/theme.css"; + +export const roundBoxStyle = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + + marginRight: '1rem', + padding: '0.6rem 1.2rem', + + borderRadius: '4px', + backgroundColor: vars.colors.main04, +}); diff --git a/src/pages/class/TabWrapper/TabPeriod/index.tsx b/src/pages/class/TabWrapper/TabPeriod/index.tsx new file mode 100644 index 0000000..0220dc2 --- /dev/null +++ b/src/pages/class/TabWrapper/TabPeriod/index.tsx @@ -0,0 +1,43 @@ +import Card from '@/pages/class/Card'; +import { roundBoxStyle } from '@/pages/class/TabWrapper/TabPeriod/index.css'; +import Flex from '@/components/Flex'; +import Text from '@/components/Text'; +import { LESSON_DATA } from '@/constants/mocks/mockLessonData'; +import { calculatePeriod, formatDate } from '@/utils/dateCalculate'; + +const Period = () => { + const { lessonRound } = LESSON_DATA; + + return ( + + {lessonRound.map((item, index) => { + const { lessonStartDateTime, lessonEndDateTime } = item; + const { startTime, formattedEndTime, durationString } = calculatePeriod(lessonStartDateTime, lessonEndDateTime); + + return ( + +
+ +
+ + {index + 1}회차 + +
+
+ + {formatDate(lessonStartDateTime)} + + + {startTime} - {formattedEndTime} ({durationString}) + +
+
+
+
+ ); + })} +
+ ); +}; + +export default Period; diff --git a/src/pages/class/TabWrapper/index.css.ts b/src/pages/class/TabWrapper/index.css.ts new file mode 100644 index 0000000..e46e890 --- /dev/null +++ b/src/pages/class/TabWrapper/index.css.ts @@ -0,0 +1,9 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '@/styles/theme.css'; + +export const tabPanelStyle = style({ + padding: '2.4rem 2rem', + borderTop: '1px solid', + + borderColor: vars.colors.gray01, +}); diff --git a/src/pages/class/TabWrapper/index.tsx b/src/pages/class/TabWrapper/index.tsx new file mode 100644 index 0000000..f0a245b --- /dev/null +++ b/src/pages/class/TabWrapper/index.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; +import Intro from '@/pages/class/TabWrapper/TabIntro'; +import Level from '@/pages/class/TabWrapper/TabLevel'; +import LocationInfo from '@/pages/class/TabWrapper/TabLocation'; +import Period from '@/pages/class/TabWrapper/TabPeriod'; +import Flex from '@/components/Flex'; +import { TabRoot, TabList, TabButton, TabPanel } from '@/components/Tab'; +import { vars } from '@/styles/theme.css'; + +interface TabWrapperProps { + colorScheme: 'primary' | 'secondary'; +} + +const TabWrapper = ({ colorScheme }: TabWrapperProps) => { + const [selectedTab, setSelectedTab] = useState(0); + + const tabs = [ + { id: 1, label: '소개', component: }, + { id: 2, label: '난이도', component: }, + { id: 3, label: '기간', component: }, + { id: 4, label: '위치', component: }, + ]; + + return ( + + + + {tabs.map((tab) => ( + setSelectedTab(tab.id - 1)} + colorScheme={colorScheme} + > + {tab.label} + + ))} + + + + + {tabs.map((tab) => ( + + {tab.component} + + ))} + + + ); +}; + +export default TabWrapper; \ No newline at end of file diff --git a/src/pages/class/index.css.ts b/src/pages/class/index.css.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/class/index.tsx b/src/pages/class/index.tsx index c38576e..c0373fe 100644 --- a/src/pages/class/index.tsx +++ b/src/pages/class/index.tsx @@ -1,5 +1,11 @@ +import TabWrapper from "@/pages/class/TabWrapper"; + const Class = () => { - return
수업 정보 상세
; + return( + <> + + + ); }; export default Class; diff --git a/src/utils/dateCalculate.ts b/src/utils/dateCalculate.ts new file mode 100644 index 0000000..9445bb2 --- /dev/null +++ b/src/utils/dateCalculate.ts @@ -0,0 +1,32 @@ +// 시간을 계산, "익일" 여부를 판단 +export const calculatePeriod = (start: string, end: string) => { + const startDate = new Date(start); + const endDate = new Date(end); + + const startTime = `${startDate.getHours().toString().padStart(2, '0')}:${startDate + .getMinutes() + .toString() + .padStart(2, '0')}`; + const endTime = `${endDate.getHours().toString().padStart(2, '0')}:${endDate + .getMinutes() + .toString() + .padStart(2, '0')}`; + + const isNextDay = endDate.getDate() !== startDate.getDate(); + const formattedEndTime = isNextDay ? `익일 ${endTime}` : endTime; + + const totalMinutes = Math.abs(endDate.getTime() - startDate.getTime()) / 1000 / 60; + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + const durationString = minutes > 0 ? `총 ${hours}시간 ${minutes}분` : `총 ${hours}시간`; + + return { startTime, formattedEndTime, durationString }; +}; + +// 날짜를 포맷팅 (ex. "2025년 1월 8일 수요일") +export const formatDate = (dateString: string) => { + const date = new Date(dateString); + const days = ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일']; + return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일 ${days[date.getDay()]}`; +}; \ No newline at end of file