Skip to content
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

Open
wants to merge 26 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
21d2d96
feat: 클래스 상세 Tab 구현 초기 (#52)
hansoojeongsj Jan 15, 2025
ca5484e
Merge branch 'develop' into feat/#52/class
hansoojeongsj Jan 15, 2025
08e0ca8
feat: 위치탭 카드 내부 gap 잡는중 (#52)
hansoojeongsj Jan 15, 2025
58ee383
Merge branch 'develop' into feat/#52/class
hansoojeongsj Jan 15, 2025
e8c234a
feat: TabList아래 구분선 추가
hansoojeongsj Jan 15, 2025
bacfdaf
feat: class 상세 탭 중 소개 구현 중 (#52)
hansoojeongsj Jan 15, 2025
21b4c06
feat: text 공컴 사용해보기 (#52)
hansoojeongsj Jan 15, 2025
dea1dad
Merge branch 'develop' into feat/#52/class
hansoojeongsj Jan 15, 2025
57d4a74
feat: 난이도 탭 기능 (#52)
hansoojeongsj Jan 15, 2025
82390fa
feat: class 상세 탭 세부 구조 잡는 중 (#52)
hansoojeongsj Jan 15, 2025
e86a017
Merge branch 'develop' into feat/#52/class
hansoojeongsj Jan 15, 2025
e5b25ba
Merge branch 'develop' into feat/#52/class
hansoojeongsj Jan 15, 2025
421d47a
feat: Flex 컴포넌트로 변경 (#52)
hansoojeongsj Jan 15, 2025
ee559fe
feat: 클래스 상세 탭 뷰 (#52)
hansoojeongsj Jan 15, 2025
0b86049
feat: 클래스 상세 탭 뷰 폴더 위치, 이름 변경 (#52)
hansoojeongsj Jan 15, 2025
fc969f0
feat: 버튼 태그 추가 (#52)
hansoojeongsj Jan 15, 2025
357e265
feat: 상수 데이터에 실제 이미지 url 변경 (#52)
hansoojeongsj Jan 15, 2025
5c8956b
feat: icquestionmark에 import 제거 (#52)
hansoojeongsj Jan 16, 2025
935f4e5
chore: 이름에 props 추가 (#52)
hansoojeongsj Jan 16, 2025
4c12c61
code review: 코드리뷰 1차 반영 (#52)
hansoojeongsj Jan 16, 2025
f3595a2
code review: 코드리뷰 2차 반영 (#52)
hansoojeongsj Jan 16, 2025
e0bce8d
code review: 코드리뷰 2차 반영 (#52)
hansoojeongsj Jan 16, 2025
b0e8334
Merge branch 'develop' into feat/#52/class
hansoojeongsj Jan 16, 2025
31b1585
code review: 코드리뷰 4차 반영 (#52)
hansoojeongsj Jan 16, 2025
fb7a688
chore: App.tsx 수정 (#52)
hansoojeongsj Jan 16, 2025
ac518b5
chore: 파일명 변경 (#52)
hansoojeongsj Jan 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions public/svg/ic_quesitonmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/svg/IcQuesitonmark.tsx
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;
3 changes: 2 additions & 1 deletion src/assets/svg/index.ts
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'
34 changes: 34 additions & 0 deletions src/constants/LessonData.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

목업 데이터 네임 형식 맞추면 좋을 것 같아요 ! mockLessonData로 파일이름이랑 데이터 이름 변경해주세요 !!

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const lessonData = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희 상수파일 네이밍은 BIG_SNAKE_CASE 방식을 사용하기로 해서, LESSON_DATA로 수정해주세요!

Copy link
Collaborator

Choose a reason for hiding this comment

The 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,
};
Empty file.
29 changes: 29 additions & 0 deletions src/pages/class/BottomWrapper/TabIntro/index.tsx
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 lessonData(상수)는 타입 추론이 되기 때문에 따로 타입 지정을 해주지 않아도 될 것 같습니다!
image


return (
<>
<Flex padding="0.8rem 0 0">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Flex padding="0.8rem 0 0">
<Flex paddingTop="0.8rem">

으로 사용하는 것이 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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>
))}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

물론 해당 데이터는 이후 서버에서 받을 데이터이긴 하지만 만약 서버에서 string 문자열을 줄 때 공백 문자를 포함해서 주고 그걸 클라이언트에서 줄바꿈 처리를 해야 한다면 해당 로직이 아닌 whiteSpace: 'pre-line' 속성을 쓰면 될 것 같습니다!
whiteSpace: 'pre-line'는 줄바꿈 문자를 처리 해주기 때문에 split으로 확인하지 않고도 가능할 것 같습니다!

</Text>
</Flex>
</>
);
};

export default Intro;
Empty file.
63 changes: 63 additions & 0 deletions src/pages/class/BottomWrapper/TabLevel/index.tsx
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
Empty file.
64 changes: 64 additions & 0 deletions src/pages/class/BottomWrapper/TabLocation/index.tsx
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;
13 changes: 13 additions & 0 deletions src/pages/class/BottomWrapper/TabPeriod/index.css.ts
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',
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

roundBoxStyle로 이름 변경해주세요!


73 changes: 73 additions & 0 deletions src/pages/class/BottomWrapper/TabPeriod/index.tsx
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()]}`;
};
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
8 changes: 8 additions & 0 deletions src/pages/class/BottomWrapper/index.css.ts
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,
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style 이름 시작 소문자로 해주세요! tabPanelStyle

65 changes: 65 additions & 0 deletions src/pages/class/BottomWrapper/index.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BottomWrapper보다는 Tab에 관련된 컴포넌트들이 담기기 때문에 TabWrapper를 사용하는 것이 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

div에 스타일을 적용시켜주기보다는 Flex컴포넌트를 사용하는 것이 어떨까요?

<TabPanel isSelected={selectedTab === 0}>
<Intro />
</TabPanel>
<TabPanel isSelected={selectedTab === 1}>
<Level />
</TabPanel>
<TabPanel isSelected={selectedTab === 2}>
<Period />
</TabPanel>
<TabPanel isSelected={selectedTab === 3}>
<LocationInfo />
</TabPanel>
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
14 changes: 14 additions & 0 deletions src/pages/class/Card/index.css.ts
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',
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 border랑 borderColor 따로 되어있는데, border: 0.5px solid ${vars.colors.gray02} 로 한번에 적어도 될 것 같아요!

10 changes: 10 additions & 0 deletions src/pages/class/Card/index.tsx
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;
Empty file added src/pages/class/index.css.ts
Empty file.
Loading