-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feat] class 상세 내 탭 뷰 구현 #82
base: develop
Are you sure you want to change the base?
Changes from 19 commits
21d2d96
ca5484e
08e0ca8
58ee383
e8c234a
bacfdaf
21b4c06
dea1dad
57d4a74
82390fa
e86a017
e5b25ba
421d47a
ee559fe
0b86049
fc969f0
357e265
5c8956b
935f4e5
4c12c61
f3595a2
e0bce8d
b0e8334
31b1585
fb7a688
ac518b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import type { SVGProps } from "react"; | ||
const SvgIcQuesitonmark = (props: SVGProps<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14" {...props}><rect width={14} height={14} fill="#CFD3DE" rx={7} /><path fill="#fff" d="M6.082 7.996a.07.07 0 0 1-.068-.068c0-1.025.532-1.382.957-1.663.3-.193.541-.358.541-.677 0-.328-.222-.522-.56-.522-.3 0-.561.194-.561.57 0 .038-.03.068-.068.068h-1.18a.126.126 0 0 1-.125-.125c0-1.141.841-1.866 1.982-1.866 1.15 0 1.982.705 1.982 1.866 0 .986-.522 1.286-.976 1.547-.339.203-.639.377-.639.802 0 .038-.03.068-.067.068zm.628 2.291a.905.905 0 0 1-.928-.928c0-.532.396-.928.928-.928s.928.396.928.928a.905.905 0 0 1-.928.928" /></svg>; | ||
export default SvgIcQuesitonmark; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export { default as IcBack } from './IcBack' | ||
export { default as IcClose } from './IcClose' | ||
export { default as IcClose } from './IcClose' | ||
export { default as IcQuesitonmark } from './IcQuesitonmark' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
export const lessonData = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저희 상수파일 네이밍은 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋습니다! 추가로 이후 API 개발 이후에 서버 데이터 활용하는 constants는 constants폴더 밑에 mocks 폴더를 두고 따로 관리해도 좋을 것 같다고 생각합니다! |
||
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, | ||
}; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,29 @@ | ||||||
import React from "react"; | ||||||
import Text from '@/components/Text' | ||||||
import Flex from "@/components/Flex"; | ||||||
import { lessonData } from "@/constants/LessonData"; | ||||||
|
||||||
interface LessonDataProps { | ||||||
lessonDetail: string; | ||||||
} | ||||||
|
||||||
const Intro = () => { | ||||||
const { lessonDetail }:LessonDataProps = lessonData; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
|
||||||
return ( | ||||||
<> | ||||||
<Flex padding="0.8rem 0 0"> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
으로 사용하는 것이 어떨까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 padding top 넣기 전에 했던 거라 수정해두겠습니다..!! |
||||||
<Text tag="b3" color="gray8"> | ||||||
{lessonDetail.split('\n').map((line, idx) => ( | ||||||
<React.Fragment key={idx}> | ||||||
{line} | ||||||
<br /> | ||||||
</React.Fragment> | ||||||
))} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 물론 해당 데이터는 이후 서버에서 받을 데이터이긴 하지만 만약 서버에서 string 문자열을 줄 때 공백 문자를 포함해서 주고 그걸 클라이언트에서 줄바꿈 처리를 해야 한다면 해당 로직이 아닌 |
||||||
</Text> | ||||||
</Flex> | ||||||
</> | ||||||
); | ||||||
}; | ||||||
|
||||||
export default Intro; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import React from 'react'; | ||
import Card from '@/pages/class/Card'; | ||
import Flex from '@/components/Flex'; | ||
import Head from '@/components/Head'; | ||
import Text from '@/components/Text'; | ||
import { lessonData } from '@/constants/LessonData'; | ||
import { IcClose, IcQuesitonmark } from '@/assets/svg'; | ||
|
||
interface LessonDataProps { | ||
lessonLevel: string; | ||
lessonLevelDetail: string; | ||
lessonRecommendation: string; | ||
} | ||
|
||
const Level = () => { | ||
const { lessonLevel, lessonLevelDetail, lessonRecommendation }: LessonDataProps = lessonData; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 부분도 다른 코드 리뷰에서 작성한 것처럼 타입 추론이 되기 때문에 타입 지정을 안 해도 될 것 같습니다!! 👍 |
||
|
||
return ( | ||
<Flex direction="column" gap="3.6rem"> | ||
<Flex width="100%" align="flexEnd" direction="column" gap="0.6rem"> | ||
<Card> | ||
<Flex gap="0.8rem" align="center"> | ||
<IcClose width={36} /> | ||
<Head level="h6" tag="h6"> | ||
{lessonLevel} | ||
</Head> | ||
<Text tag="b8" color="gray8"> | ||
{lessonLevelDetail} | ||
</Text> | ||
</Flex> | ||
</Card> | ||
<Flex justify="flexEnd" align="center" gap="0.6rem"> | ||
<Text tag="c3" color="gray7"> | ||
클래스 난이도는 이렇게 설정되어있어요! | ||
</Text> | ||
<button> | ||
<IcQuesitonmark width={14} /> | ||
</button> | ||
</Flex> | ||
</Flex> | ||
<Flex direction="column" gap="1.2rem"> | ||
<Flex justify="flexStart" align="center" gap="0.8rem"> | ||
<IcClose width={24} /> | ||
<Head level="h5" tag="h6"> | ||
이런 분들에게 해당 클래스를 추천해요! | ||
</Head> | ||
</Flex> | ||
|
||
{/* 줄바꿈 처리된 텍스트 */} | ||
<Text tag="b3" color="gray8"> | ||
{lessonRecommendation.split('\n').map((line, idx) => ( | ||
<React.Fragment key={idx}> | ||
{line} | ||
<br /> | ||
</React.Fragment> | ||
))} | ||
</Text> | ||
</Flex> | ||
</Flex> | ||
); | ||
}; | ||
|
||
export default Level; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import React from 'react'; | ||
import Card from '@/pages/class/Card'; | ||
import Flex from '@/components/Flex'; | ||
import Text from '@/components/Text'; | ||
import { lessonData } from '@/constants/LessonData'; | ||
import { IcClose } from '@/assets/svg'; | ||
|
||
interface LessonDataProps { | ||
lessonLocation: string; | ||
lessonStreetAddress: string; | ||
lessonOldStreetAddress: string; | ||
} | ||
|
||
const LocationInfo = () => { | ||
const { lessonLocation, lessonStreetAddress, lessonOldStreetAddress }: LessonDataProps = lessonData; | ||
|
||
return ( | ||
<Flex direction="column" justify="center" gap="1.2rem"> | ||
<Card> | ||
<Flex align="center" justify="spaceBetween" gap="1.6rem" width="100%"> | ||
{/* 왼쪽 */} | ||
<Flex direction="column" gap="0.6rem"> | ||
<Text tag="b4" color="black"> | ||
{lessonLocation} | ||
</Text> | ||
<Flex direction="column" gap="0.4rem"> | ||
<Flex justify="spaceBetween"> | ||
<Flex marginRight="0.4rem"> | ||
<Text tag="b7" color="gray6"> | ||
주소 | ||
</Text> | ||
</Flex> | ||
<Text tag="b7" color="gray7"> | ||
{lessonStreetAddress.split('\n').map((line, idx) => ( | ||
<React.Fragment key={idx}> | ||
{line} | ||
<br /> | ||
</React.Fragment> | ||
))} | ||
</Text> | ||
</Flex> | ||
|
||
<Flex justify="spaceBetween"> | ||
<Flex marginRight="0.4rem"> | ||
<Text tag="b7" color="gray6"> | ||
지번 | ||
</Text> | ||
</Flex> | ||
<Text tag="b7" color="gray7"> | ||
{lessonOldStreetAddress} | ||
</Text> | ||
</Flex> | ||
</Flex> | ||
</Flex> | ||
|
||
{/* 오른쪽 */} | ||
<IcClose width={41} /> | ||
</Flex> | ||
</Card> | ||
</Flex> | ||
); | ||
}; | ||
|
||
export default LocationInfo; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { style } from '@vanilla-extract/css'; | ||
import { vars } from "@/styles/theme.css"; | ||
|
||
export const roundBox = style({ | ||
display: 'flex', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
backgroundColor: vars.colors.main04, | ||
borderRadius: '4px', | ||
marginRight: '1rem', | ||
padding: '0.6rem 1.2rem', | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. roundBoxStyle로 이름 변경해주세요! |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import Card from '@/pages/class/Card'; | ||
import { roundBox } from '@/pages/class/BottomWrapper/TabPeriod/index.css'; | ||
import Flex from '@/components/Flex'; | ||
import Text from '@/components/Text'; | ||
import { lessonData } from '@/constants/LessonData'; | ||
|
||
const Period = () => { | ||
const { lessonRound } = lessonData; | ||
|
||
// 시간을 계산, "익일" 여부를 판단 | ||
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일 수요일") | ||
const formatDate = (dateString: string) => { | ||
const date = new Date(dateString); | ||
const days = ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일']; | ||
return `${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일 ${days[date.getDay()]}`; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 시간 변환해서 사용하는 페이지가 많은 것 같은데, utils 폴더에 시간 관련 함수 파일 하나 만들어서 분리해주시면 좋을 것 같아요! 제 페이지에서도 사용되는 똑같은 로직이 있어서 !! |
||
|
||
return ( | ||
<Flex direction="column" justify="center" gap="1.2rem"> | ||
{lessonRound.map((item, index) => { | ||
const { lessonStartDateTime, lessonEndDateTime } = item; | ||
const { startTime, formattedEndTime, durationString } = calculatePeriod(lessonStartDateTime, lessonEndDateTime); | ||
|
||
return ( | ||
<Card key={index}> | ||
<Flex align="center"> | ||
<div className={roundBox}> | ||
<Text tag="b10" color="white"> | ||
{index + 1}회차 | ||
</Text> | ||
</div> | ||
<div> | ||
<Text tag="b4" color="black"> | ||
{formatDate(lessonStartDateTime)} | ||
</Text> | ||
<Text tag="b7" color="gray7"> | ||
{startTime} - {formattedEndTime} ({durationString}) | ||
</Text> | ||
</div> | ||
</Flex> | ||
</Card> | ||
); | ||
})} | ||
</Flex> | ||
); | ||
}; | ||
|
||
export default Period; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
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, | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style 이름 시작 소문자로 해주세요! |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 ~ 넵 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { useState } from 'react'; | ||
import { TabPanelStyle } from '@/pages/class/BottomWrapper/index.css'; | ||
import Intro from '@/pages/class/BottomWrapper/TabIntro'; | ||
import Level from '@/pages/class/BottomWrapper/TabLevel'; | ||
import LocationInfo from '@/pages/class/BottomWrapper/TabLocation'; | ||
import Period from '@/pages/class/BottomWrapper/TabPeriod'; | ||
import Flex from '@/components/Flex'; | ||
import Head from '@/components/Head'; | ||
import { TabRoot, TabList, TabButton, TabPanel } from '@/components/Tab'; | ||
|
||
interface BottomComponentProps { | ||
colorScheme: 'primary' | 'secondary'; | ||
} | ||
|
||
const BottomWrapper = ({ colorScheme }: BottomComponentProps) => { | ||
const [selectedTab, setSelectedTab] = useState(0); | ||
|
||
return ( | ||
<> | ||
<TabRoot> | ||
<Flex paddingTop="1.6rem" paddingLeft="2rem"> | ||
<TabList> | ||
<TabButton isSelected={selectedTab === 0} onClick={() => setSelectedTab(0)} colorScheme={colorScheme}> | ||
<Head level="h5" tag="h5"> | ||
소개 | ||
</Head> | ||
</TabButton> | ||
<TabButton isSelected={selectedTab === 1} onClick={() => setSelectedTab(1)} colorScheme={colorScheme}> | ||
<Head level="h5" tag="h5"> | ||
난이도 | ||
</Head> | ||
</TabButton> | ||
<TabButton isSelected={selectedTab === 2} onClick={() => setSelectedTab(2)} colorScheme={colorScheme}> | ||
<Head level="h5" tag="h5"> | ||
기간 | ||
</Head> | ||
</TabButton> | ||
<TabButton isSelected={selectedTab === 3} onClick={() => setSelectedTab(3)} colorScheme={colorScheme}> | ||
<Head level="h5" tag="h5"> | ||
위치 | ||
</Head> | ||
</TabButton> | ||
</TabList> | ||
</Flex> | ||
|
||
<div className={TabPanelStyle}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
<TabPanel isSelected={selectedTab === 0}> | ||
<Intro /> | ||
</TabPanel> | ||
<TabPanel isSelected={selectedTab === 1}> | ||
<Level /> | ||
</TabPanel> | ||
<TabPanel isSelected={selectedTab === 2}> | ||
<Period /> | ||
</TabPanel> | ||
<TabPanel isSelected={selectedTab === 3}> | ||
<LocationInfo /> | ||
</TabPanel> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 물론 지금 코드도 잘못된 것은 아니지만 확장성을 고려해서 필요한 데이터를 상수화하고 map을 돌리는 것도 좋은 방법이 될 것 같습니다! 그렇게 되면 상수(배열 형식)에 데이터를 추가만 하면 확장이 편하게 되기 때문에 해당 설계도 고려해보면 좋을 것 같습니다! 예시 export const THEATER_TABS = [
{
id: 1,
name: '일반관',
},
{
id: 2,
name: '스페셜관',
},
]; {THEATER_TABS.map(({ id, name }) => (
<S.Tab key={id} $isActive={activeTab === id} onClick={() => handleTabClick(id)}>
{name}
</S.Tab>
))} |
||
</div> | ||
</TabRoot> | ||
</> | ||
); | ||
}; | ||
|
||
export default BottomWrapper; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { style } from '@vanilla-extract/css'; | ||
import { vars } from '@/styles/theme.css'; | ||
|
||
export const cardStyle = style({ | ||
width: '100%', | ||
display: 'inline-flex', | ||
padding: '1.6rem 2rem', | ||
alignItems: 'center', | ||
borderRadius: '4px', | ||
border: '0.5px solid', | ||
borderColor: vars.colors.gray02, | ||
backgroundColor: vars.colors.white, | ||
gap: '0.8rem', | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기 border랑 borderColor 따로 되어있는데, border: |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { PropsWithChildren } from 'react'; | ||
import { cardStyle } from '@/pages/class/Card/index.css'; | ||
|
||
type CardProps = PropsWithChildren<{}>; | ||
|
||
const Card = ({ children }: CardProps) => { | ||
return <div className={cardStyle}>{children}</div>; | ||
}; | ||
|
||
export default Card; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
목업 데이터 네임 형식 맞추면 좋을 것 같아요 ! mockLessonData로 파일이름이랑 데이터 이름 변경해주세요 !!