Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 32 additions & 14 deletions src/pages/class/Class.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import { useParams } from 'react-router-dom';
import { useGetLessonDetail } from '@/pages/class/apis/queries';
import * as styles from '@/pages/class/class.css';
import ClassButtonWrapper from '@/pages/class/components/ClassButtonWrapper/ClassButtonWrapper';
import ClassInfoWrapper from '@/pages/class/components/ClassInfoWrapper/ClassInfoWrapper';
import LimitedChip from '@/pages/class/components/LimitedChip/LimitedChip';
import TabWrapper from '@/pages/class/components/TabWrapper/TabWrapper';
import { LOW_SEAT_THRESHOLD } from '@/pages/class/constants';
import ErrorPage from '@/pages/error/ErrorPage';
import IcCircleCautionFilled from '@/shared/assets/svg/IcCircleCautionFilled';
import Divider from '@/shared/components/Divider/Divider';
import Text from '@/shared/components/Text/Text';
import { chipWrapperStyle, topImgStyle, withdrawIconStyle, withdrawImgStyle } from './class.css';

const Class = () => {
const { id } = useParams<{ id: string }>();
const lessonId = Number(id);

if (!id) {
const isValidLessonId = Number.isInteger(lessonId) && lessonId > 0;

const { data, isPending, isError } = useGetLessonDetail(lessonId, {
enabled: Boolean(isValidLessonId),
});

if (!isValidLessonId) {
return <ErrorPage />;
}

// eslint-disable-next-line react-hooks/rules-of-hooks
const { data, isError, isLoading } = useGetLessonDetail(+id);

if (isLoading) {
if (isPending) {
return <></>;
}

Expand All @@ -28,20 +34,32 @@ const Class = () => {
}

const imageUrl = data.imageUrl;
const isWithdrawTeacher = !imageUrl;
const remainingSeats = data.maxReservationCount - data.reservationCount;
const shouldShowChip = data.status === 'OPEN' && remainingSeats < LOW_SEAT_THRESHOLD;

return (
<main>
<section
className={styles.topImgStyle}
style={{
backgroundImage: `url(${imageUrl})`,
}}>
{shouldShowChip && (
<div className={styles.chipWrapperStyle}>
<LimitedChip lessonData={data} />
<section className={topImgStyle}>
{isWithdrawTeacher ? (
<div className={withdrawImgStyle}>
<IcCircleCautionFilled width={54} height={54} className={withdrawIconStyle} />
<Text tag="b1_sb" color="gray6">
탈퇴한 유저의 클래스입니다.
</Text>
<Text tag="b1_sb" color="gray6">
새로운 클래스를 탐색해 보세요!
</Text>
</div>
) : (
<>
<div className={topImgStyle} style={{ backgroundImage: `url(${imageUrl})` }} />
{shouldShowChip && (
<div className={chipWrapperStyle}>
<LimitedChip lessonData={data} />
</div>
)}
</>
)}
</section>

Expand Down
15 changes: 15 additions & 0 deletions src/pages/class/class.css.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { style } from '@vanilla-extract/css';
import { vars } from '@/shared/styles/theme.css';

export const withdrawImgStyle = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '0.8rem',
width: '100%',
height: '37.5rem',
backgroundColor: vars.colors.gray01,
});

export const withdrawIconStyle = style({
marginBottom: '2rem',
});

export const topImgStyle = style({
position: 'relative',

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@ import IcHeartFilledGray07 from '@/shared/assets/svg/IcHeartFilledGray07';
import IcHeartOutlinedGray07 from '@/shared/assets/svg/IcHeartOutlinedGray07';
import BlurButton from '@/shared/components/BlurButton/BlurButton';
import BoxButton from '@/shared/components/BoxButton/BoxButton';
import { WITHDRAW_USER_NAME } from '@/shared/constants/withdrawUser';

const ClassButtonWrapper = ({ lessonData }: { lessonData: LessonDetailResponseTypes }) => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const isMyLesson = lessonData.isMyLesson;

const { isHeartFilled, toggleHeart } = useHeartToggle();
const isDeletedTeacher = lessonData.teacherNickname === WITHDRAW_USER_NAME && lessonData.imageUrl === null;

const { buttonText, isDisabled } = useClassButtonState(lessonData.status, lessonData.bookStatus);

const finalButtonText = isDeletedTeacher ? '신청불가' : buttonText;
const finalIsDisabled = isDeletedTeacher || isDisabled || isMyLesson;

const handleApplyClick = () => {
if (!isDisabled && id) {
const path = ROUTES_CONFIG.reservation.path(id);
Expand All @@ -30,8 +36,8 @@ const ClassButtonWrapper = ({ lessonData }: { lessonData: LessonDetailResponseTy
{isHeartFilled ? <IcHeartFilledGray07 width={28} /> : <IcHeartOutlinedGray07 width={28} />}
</BoxButton>

<BoxButton variant="primary" isDisabled={isDisabled || isMyLesson} onClick={handleApplyClick}>
{buttonText}
<BoxButton variant="primary" isDisabled={finalIsDisabled} onClick={handleApplyClick}>
{finalButtonText}
</BoxButton>
</BlurButton>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import type { LessonDetailResponseTypes } from '@/pages/class/types/api';
import { getDDayLabel } from '@/pages/class/utils/dDay';
import type { GenreTypes } from '@/pages/onboarding/types/genreTypes';
import { ROUTES_CONFIG } from '@/routes/routesConfig';
import IcCircleCautionFilled from '@/shared/assets/svg/IcCircleCautionFilled';
import Head from '@/shared/components/Head/Head';
import Tag from '@/shared/components/Tag/Tag';
import Text from '@/shared/components/Text/Text';
import { levelMapping, genreMapping } from '@/shared/constants/index';
import { genreMapping, levelMapping } from '@/shared/constants/index';
import { WITHDRAW_USER_NAME } from '@/shared/constants/withdrawUser';

const ClassInfoWrapper = ({ lessonData }: { lessonData: LessonDetailResponseTypes }) => {
const {
Expand All @@ -47,6 +49,7 @@ const ClassInfoWrapper = ({ lessonData }: { lessonData: LessonDetailResponseType
};

const MAX_DISPLAY_RESERVATION_COUNT = 999;
const isWithdrawTeacher = teacherNickname === WITHDRAW_USER_NAME;

return (
<section className={sectionContainer} aria-label={`${name} 클래스 정보`}>
Expand All @@ -68,9 +71,16 @@ const ClassInfoWrapper = ({ lessonData }: { lessonData: LessonDetailResponseType
</Head>

<div>
<button className={teacherWrapper} onClick={() => handleTeacherClick(teacherId)}>
<img src={teacherImageUrl} alt={`${teacherNickname} 프로필`} className={profileStyle} />
<Text as="span" tag="b1_sb" color="gray9">
<button
className={teacherWrapper}
onClick={isWithdrawTeacher ? undefined : () => handleTeacherClick(teacherId)}
disabled={isWithdrawTeacher}>
{isWithdrawTeacher ? (
<IcCircleCautionFilled width={40} height={40} />
) : (
teacherImageUrl && <img src={teacherImageUrl} alt={`${teacherNickname} 프로필`} className={profileStyle} />
)}
<Text as="span" tag="b1_sb" color={isWithdrawTeacher ? 'gray6' : 'gray9'}>
{teacherNickname}
</Text>
</button>
Expand Down
17 changes: 12 additions & 5 deletions src/pages/dancer/Dancer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useParams } from 'react-router-dom';
import { useGetDancerDetail } from '@/pages/dancer/apis/queries';
import DancerInfo from '@/pages/dancer/components/DancerInfo/DancerInfo';
import TabWrapper from '@/pages/dancer/components/TabWrapper/TabWrapper';
import { topImgStyle, gradientOverlayStyle, textWrapperStyle, genresWrapperStyle } from '@/pages/dancer/dancer.css';
import { genresWrapperStyle, gradientOverlayStyle, textWrapperStyle, topImgStyle } from '@/pages/dancer/dancer.css';
import ErrorPage from '@/pages/error/ErrorPage';
import Head from '@/shared/components/Head/Head';
import Tag from '@/shared/components/Tag/Tag';
Expand All @@ -11,16 +11,23 @@ import { genreMapping } from '@/shared/constants/index';

const Dancer = () => {
const { id } = useParams<{ id: string }>();
const dancerId = Number(id);

const { data, isError, isPending } = useGetDancerDetail(id ?? '', {
enabled: Boolean(id),
const isValidDancerId = Number.isInteger(dancerId) && dancerId > 0;

const { data, isPending, isError } = useGetDancerDetail(dancerId, {
enabled: Boolean(isValidDancerId),
});

if (isPending || !id) {
if (!isValidDancerId) {
return <ErrorPage />;
}

if (isPending) {
return <></>;
}

if (isError || !data) {
if (isError || !data || data.detail === '탈퇴한 회원입니다.') {
Copy link
Member

Choose a reason for hiding this comment

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

해당 조건 문자열 비교 이외에 다른 방법은 없을까요??

Copy link
Member Author

Choose a reason for hiding this comment

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

현재 response 기준으로 탈퇴 여부를 구분할 수 있는 값이
nickname: "알 수 없음", profileImage: null, detail: "탈퇴한 회원입니다." 이렇게 세 가지가 있습니다.

이 중에서 "탈퇴한 회원입니다."가 의미상 가장 명확하게 탈퇴 여부를 표현하고 있다고 판단해서 현재는 detail 값을 기준으로 분기 처리했습니다.

다만 다른 곳에서는 nickname을 기준으로 처리를 하고 있어서 일관성을 위해 이 로직도 nickname 기준으로 통일하는 것이 더 나을지 고민이 되네요 ..
이 부분에 대해 의견 주시면 그에 맞춰 수정하겠습니다 ㅎㅎ

return <ErrorPage />;
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/dancer/apis/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { DancerDetailResponseTypes } from '@/pages/dancer/types/api';
import { instance } from '@/shared/apis/instance';
import { API_URL } from '@/shared/constants/apiURL';

export const getDancerDetail = async (teacherId: string): Promise<DancerDetailResponseTypes> => {
export const getDancerDetail = async (teacherId: number): Promise<DancerDetailResponseTypes> => {
const url = `${API_URL.TEACHER_DETAIL}/${teacherId}`;

const { data } = await instance.get(url);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/dancer/apis/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getDancerDetail } from '@/pages/dancer/apis/axios';
import type { DancerDetailResponseTypes } from '@/pages/dancer/types/api';
import { teacherKeys } from '@/shared/constants/queryKey';

export const useGetDancerDetail = (teacherId: string, options?: { enabled?: boolean }) => {
export const useGetDancerDetail = (teacherId: number, options?: { enabled?: boolean }) => {
return useQuery<DancerDetailResponseTypes, AxiosError>({
queryKey: teacherKeys.me._ctx.profile(Number(teacherId)).queryKey,
queryFn: () => getDancerDetail(teacherId),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Head from '@/shared/components/Head/Head';
import Text from '@/shared/components/Text/Text';
import { notify } from '@/shared/components/Toast/Toast';
import { teacherKeys } from '@/shared/constants/queryKey';
import { WITHDRAW_USER_NAME } from '@/shared/constants/withdrawUser';
import { formatDateTime } from '@/shared/utils/timeUtils';

const STATUS_BUTTON_MAP: Record<
Expand All @@ -40,7 +41,7 @@ const StudentCard = ({ studentData, index, lessonId, selectedTab }: StudentCardP
const { text: buttonText, variant: buttonVariant } = STATUS_BUTTON_MAP[studentData.reservationStatus];

const status = studentData.reservationStatus;

const isWithdrawStudent = studentData.name === WITHDRAW_USER_NAME;
const { mutate: approveMutate, isPending: successPending } = useLessonApproveMutation();
const { mutate: cancelMutate, isPending: cancelPending } = useLessonCancelMutation();

Expand Down Expand Up @@ -107,13 +108,13 @@ const StudentCard = ({ studentData, index, lessonId, selectedTab }: StudentCardP
return (
<section className={styles.cardContainerStyle}>
<section className={styles.leftWrapper}>
<Head level="h2" tag="b1_sb">
<Head level="h2" tag="b1_sb" className={styles.indexStyle}>
{index + 1}
</Head>

<div className={styles.infoWrapper}>
<div className={styles.nameWrapper}>
<Head level="h2" tag="b1_sb">
<Head level="h2" tag="b1_sb" color={isWithdrawStudent ? 'gray6' : 'black'}>
{studentData.name}
</Head>
<ApplyTag variant={studentData.reservationStatus}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export const infoWrapper = style({
gap: '0.9rem',
});

export const indexStyle = style({
lineHeight: '2rem',
});

export const nameWrapper = style({
display: 'flex',
alignItems: 'center',
Expand Down
36 changes: 36 additions & 0 deletions src/routes/guards/reservationGuard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Navigate, Outlet, useParams } from 'react-router-dom';
import { useGetLessonDetail } from '@/pages/class/apis/queries';
import { ROUTES_CONFIG } from '@/routes/routesConfig';
import { WITHDRAW_USER_NAME } from '@/shared/constants/withdrawUser';

export const ReservationGuard = () => {
const { id } = useParams<{ id: string }>();
const lessonId = Number(id);

const isValidLessonId = Number.isInteger(lessonId) && lessonId > 0;

const { data, isPending, isError } = useGetLessonDetail(lessonId, {
enabled: isValidLessonId,
});

if (!isValidLessonId) {
return <Navigate to={ROUTES_CONFIG.error.path} replace />;
}

if (isPending) return null;

if (isError || !data) {
return <Navigate to={ROUTES_CONFIG.error.path} replace />;
}
Comment on lines +16 to +24
Copy link
Member

Choose a reason for hiding this comment

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

isValidLessonId와 error/data 조건 처리에 대한 return 값이 같은데 한번에 처리하는 것은 너무 가독성이 별로일까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

isValidLessonId와 isError / !data는 의미상 역할이 다르다고 생각해서 분리해두는 쪽이 의도를 읽기에는 더 낫다고 판단했습니다!
isValidLessonId는 라우팅 파라미터 자체가 잘못된 경우에 대한 입력 검증 단계이고,
isError / !data는 유효한 ID로 요청했지만 서버/데이터 조회에 실패한 경우라서
현 단계에서는 의도 드러내기 측면에서 분리 유지가 더 적절하다고 생각했습니다.

다만 가드 관련 로직이 점점 필요한 부분에 추가되는 형태가 되고 있어서,
추후에 한 번 전체적으로 정리하거나 구조를 개선할 필요는 있을 것 같습니당 ..


const { status, bookStatus, teacherNickname, isMyLesson } = data;

const isButtonEnabled =
status === 'OPEN' && isMyLesson === false && bookStatus === false && teacherNickname !== WITHDRAW_USER_NAME;

if (!isButtonEnabled) {
return <Navigate to={ROUTES_CONFIG.class.path(lessonId.toString())} replace />;
}

return <Outlet />;
};
8 changes: 7 additions & 1 deletion src/routes/router.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { lazy } from 'react';
import { createBrowserRouter } from 'react-router-dom';
import Withdraw from '@/pages/mypage/components/Withdraw/Withdraw';
import Reservation from '@/pages/reservation/Reservation';
import AuthGuard from '@/routes/guards/authGuard';
import GuestGuard from '@/routes/guards/guestGuard';
import OnboardingGuard from '@/routes/guards/onboardingGuard';
import { ReservationGuard } from '@/routes/guards/reservationGuard';
import WithdrawGuard from '@/routes/guards/withdrawGuard';
import { guestRoutes } from '@/routes/modules/guestRoutes';
import { protectedRoutes } from '@/routes/modules/protectedRoutes';
Expand Down Expand Up @@ -38,7 +40,11 @@ export const router = createBrowserRouter([
element: <WithdrawGuard />,
children: [{ index: true, element: <Withdraw /> }],
},

{
path: ROUTES_CONFIG.reservation.path(':id'),
element: <ReservationGuard />,
children: [{ index: true, element: <Reservation /> }],
},
{ path: '*', element: <Error /> },
],
},
Expand Down
16 changes: 9 additions & 7 deletions src/shared/components/ClassCard/ClassCardBody.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import clsx from 'clsx';
import type { GenreTypes } from '@/pages/onboarding/types/genreTypes';
import type { LevelTypes } from '@/pages/onboarding/types/levelTypes';
import IcCircleCautionFilled from '@/shared/assets/svg/IcCircleCautionFilled';
import * as styles from '@/shared/components/ClassCard/style.css';
import Head from '@/shared/components/Head/Head';
import Tag from '@/shared/components/Tag/Tag';
Expand Down Expand Up @@ -31,13 +32,14 @@ const ClassCardBody = ({
onClick,
}: ClassCardBodyProps) => {
return (
<div
className={sprinkles({
display: 'flex',
gap: 12,
})}
onClick={onClick}>
<img src={imageUrl} className={styles.cardImageStyle} alt={`${name}`} />
<div className={styles.cardStyle} onClick={onClick}>
{imageUrl ? (
<img src={imageUrl} className={styles.cardImageStyle} alt={`${name}`} />
) : (
<div className={styles.cardImageStyle}>
<IcCircleCautionFilled width={36} height={36} />
</div>
)}
<div
className={clsx(
sprinkles({
Expand Down
10 changes: 9 additions & 1 deletion src/shared/components/ClassCard/style.css.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { style } from '@vanilla-extract/css';
import { vars } from '@/shared/styles/theme.css';

export const cardStyle = style({
display: 'flex',
gap: '1.2rem',
});

export const cardContainerStyle = style({
width: '100%',

Expand All @@ -19,7 +24,10 @@ export const cardImageStyle = style({

borderRadius: 3.4,

backgroundColor: vars.colors.gray04,
backgroundColor: vars.colors.gray01,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
});

export const cardContentStyle = style({
Expand Down
1 change: 1 addition & 0 deletions src/shared/constants/withdrawUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const WITHDRAW_USER_NAME = '알 수 없음';