-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/71 예약 내역 페이지, 후기 작성 구현(UI 및 API 연동) #94
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
Changes from 9 commits
2ee457b
4e27143
9c83f59
ca01044
a04a6b4
9fd63c0
624e574
87b488d
b659067
e808e82
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,23 @@ | ||||||||||||||||||||||||||||||||||||||
| import React from 'react'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const EmptyDocumentIcon = ({ size = 24, ...props }) => ( | ||||||||||||||||||||||||||||||||||||||
| <svg | ||||||||||||||||||||||||||||||||||||||
| xmlns='http://www.w3.org/2000/svg' | ||||||||||||||||||||||||||||||||||||||
| width={size} | ||||||||||||||||||||||||||||||||||||||
| height={size} | ||||||||||||||||||||||||||||||||||||||
| fill='none' | ||||||||||||||||||||||||||||||||||||||
| viewBox='0 0 131 178' | ||||||||||||||||||||||||||||||||||||||
| {...props} | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+11
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. 접근성을 위한 대체 텍스트가 필요합니다. 스크린 리더 사용자를 위해 SVG에 대체 텍스트를 제공해야 합니다. <svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
viewBox='0 0 131 178'
+ role='img'
+ aria-label='빈 문서 아이콘'
{...props}
>📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (2.1.2)[error] 4-11: Alternative text title element cannot be empty For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute. (lint/a11y/noSvgWithoutTitle) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| <path | ||||||||||||||||||||||||||||||||||||||
| fill='#DDD' | ||||||||||||||||||||||||||||||||||||||
| d='M106.397 0c13.255 0 24 10.745 24 24v129.259c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24V44.974L44.974 0z' | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| <path | ||||||||||||||||||||||||||||||||||||||
| fill='#79747E' | ||||||||||||||||||||||||||||||||||||||
| d='M41.752 2.108c1.895-1.873 5.11-.53 5.11 2.134v18.62c0 13.254-10.746 24-24 24H3.781c-2.68 0-4.015-3.25-2.109-5.134z' | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export default EmptyDocumentIcon; | ||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { SvgProps } from '@/types/svgType'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const StarEmpty = ({ size = 20, color = '#DDDDDD', ...props }: SvgProps) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <svg | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| xmlns='http://www.w3.org/2000/svg' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width={size} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height={size} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| viewBox='0 0 20 20' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fill="none" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stroke={color} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| strokeWidth="1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {...props} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| d='M12.3122 15.0005C12.2069 15.0009 12.1042 14.9681 12.0187 14.9067L7.99966 11.993L3.9806 14.9067C3.89474 14.969 3.79129 15.0024 3.68522 15.002C3.57915 15.0016 3.47595 14.9675 3.39054 14.9046C3.30513 14.8417 3.24193 14.7532 3.21009 14.6521C3.17825 14.5509 3.17942 14.4422 3.21341 14.3417L4.78091 9.69891L0.718413 6.91298C0.630416 6.8527 0.564002 6.76586 0.528872 6.66515C0.493743 6.56444 0.491741 6.45513 0.523158 6.3532C0.554575 6.25127 0.617764 6.16206 0.703494 6.0986C0.789224 6.03514 0.893001 6.00076 0.999663 6.00048H6.01154L7.52404 1.34579C7.55662 1.2453 7.6202 1.15771 7.70564 1.09558C7.79109 1.03346 7.89402 1 7.99966 1C8.10531 1 8.20823 1.03346 8.29368 1.09558C8.37913 1.15771 8.44271 1.2453 8.47529 1.34579L9.98779 6.00204H14.9997C15.1065 6.00199 15.2105 6.03613 15.2964 6.09947C15.3824 6.16281 15.4459 6.25201 15.4775 6.35402C15.5091 6.45603 15.5072 6.56548 15.4721 6.66634C15.437 6.76721 15.3706 6.85419 15.2825 6.91454L11.2184 9.69891L12.785 14.3405C12.8104 14.4156 12.8175 14.4957 12.8058 14.5742C12.7941 14.6526 12.7639 14.7272 12.7177 14.7917C12.6715 14.8561 12.6107 14.9087 12.5401 14.945C12.4696 14.9813 12.3915 15.0003 12.3122 15.0005Z' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+19
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. 🛠️ Refactor suggestion 접근성 개선을 위한 title 요소 추가 필요 정적 분석 도구가 지적한 대로, SVG 요소에 대체 텍스트가 없어 스크린 리더 사용자의 접근성이 떨어집니다. 다음과 같이 title 요소를 추가하여 접근성을 개선하세요: const StarEmpty = ({ size = 20, color = '#DDDDDD', ...props }: SvgProps) => (
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
viewBox='0 0 20 20'
fill="none"
stroke={color}
strokeWidth="1"
{...props}
>
+ <title>빈 별</title>
<path
d='M12.3122 15.0005C12.2069 15.0009 12.1042 14.9681 12.0187 14.9067L7.99966 11.993L3.9806 14.9067C3.89474 14.969 3.79129 15.0024 3.68522 15.002C3.57915 15.0016 3.47595 14.9675 3.39054 14.9046C3.30513 14.8417 3.24193 14.7532 3.21009 14.6521C3.17825 14.5509 3.17942 14.4422 3.21341 14.3417L4.78091 9.69891L0.718413 6.91298C0.630416 6.8527 0.564002 6.76586 0.528872 6.66515C0.493743 6.56444 0.491741 6.45513 0.523158 6.3532C0.554575 6.25127 0.617764 6.16206 0.703494 6.0986C0.789224 6.03514 0.893001 6.00076 0.999663 6.00048H6.01154L7.52404 1.34579C7.55662 1.2453 7.6202 1.15771 7.70564 1.09558C7.79109 1.03346 7.89402 1 7.99966 1C8.10531 1 8.20823 1.03346 8.29368 1.09558C8.37913 1.15771 8.44271 1.2453 8.47529 1.34579L9.98779 6.00204H14.9997C15.1065 6.00199 15.2105 6.03613 15.2964 6.09947C15.3824 6.16281 15.4459 6.25201 15.4775 6.35402C15.5091 6.45603 15.5072 6.56548 15.4721 6.66634C15.437 6.76721 15.3706 6.85419 15.2825 6.91454L11.2184 9.69891L12.785 14.3405C12.8104 14.4156 12.8175 14.4957 12.8058 14.5742C12.7941 14.6526 12.7639 14.7272 12.7177 14.7917C12.6715 14.8561 12.6107 14.9087 12.5401 14.945C12.4696 14.9813 12.3915 15.0003 12.3122 15.0005Z'
/>
</svg>
);📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (2.1.2)[error] 5-14: Alternative text title element cannot be empty For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute. (lint/a11y/noSvgWithoutTitle) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default StarEmpty; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||||||||||||||||||||||||||||||
| import { privateInstance } from './privateInstance'; | ||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||
| MyReservationsResponse, | ||||||||||||||||||||||||||||||||||
| GetMyReservationsParams, | ||||||||||||||||||||||||||||||||||
| UpdateReservationRequest, | ||||||||||||||||||||||||||||||||||
| Reservation, | ||||||||||||||||||||||||||||||||||
| CreateReviewRequest, | ||||||||||||||||||||||||||||||||||
| ReviewResponse, | ||||||||||||||||||||||||||||||||||
| } from '@/types/reservationTypes'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * 내 예약 리스트 조회 | ||||||||||||||||||||||||||||||||||
| * GET /api/reservations | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export const getMyReservations = async ( | ||||||||||||||||||||||||||||||||||
| params: GetMyReservationsParams, | ||||||||||||||||||||||||||||||||||
| ): Promise<MyReservationsResponse> => { | ||||||||||||||||||||||||||||||||||
| const queryParams = new URLSearchParams(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (params.cursorId) { | ||||||||||||||||||||||||||||||||||
| queryParams.append('cursorId', params.cursorId.toString()); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if (params.size) { | ||||||||||||||||||||||||||||||||||
| queryParams.append('size', params.size.toString()); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if (params.status) { | ||||||||||||||||||||||||||||||||||
| queryParams.append('status', params.status); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+29
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. 🧹 Nitpick (assertive) 쿼리 파라미터 생성 로직 간소화 가능 조건문을 제거하고 더 간결하게 작성할 수 있습니다. - const queryParams = new URLSearchParams();
-
- if (params.cursorId) {
- queryParams.append('cursorId', params.cursorId.toString());
- }
- if (params.size) {
- queryParams.append('size', params.size.toString());
- }
- if (params.status) {
- queryParams.append('status', params.status);
- }
+ const queryParams = new URLSearchParams(
+ Object.entries(params)
+ .filter(([_, value]) => value !== undefined)
+ .map(([key, value]) => [key, String(value)])
+ );📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| const response = await privateInstance.get( | ||||||||||||||||||||||||||||||||||
| `/reservations?${queryParams.toString()}`, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * 내 예약 수정(취소) | ||||||||||||||||||||||||||||||||||
| * PATCH /api/reservations/{reservationId} | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export const updateMyReservation = async ( | ||||||||||||||||||||||||||||||||||
| reservationId: number, | ||||||||||||||||||||||||||||||||||
| data: UpdateReservationRequest, | ||||||||||||||||||||||||||||||||||
| ): Promise<Reservation> => { | ||||||||||||||||||||||||||||||||||
| const response = await privateInstance.patch( | ||||||||||||||||||||||||||||||||||
| `/reservations/${reservationId}`, | ||||||||||||||||||||||||||||||||||
| data, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * 내 예약 리뷰 작성 | ||||||||||||||||||||||||||||||||||
| * POST /api/reservations/{reservationId}/reviews | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export const createReview = async ( | ||||||||||||||||||||||||||||||||||
| reservationId: number, | ||||||||||||||||||||||||||||||||||
| data: CreateReviewRequest, | ||||||||||||||||||||||||||||||||||
| ): Promise<ReviewResponse> => { | ||||||||||||||||||||||||||||||||||
| const response = await privateInstance.post( | ||||||||||||||||||||||||||||||||||
| `/reservations/${reservationId}/reviews`, | ||||||||||||||||||||||||||||||||||
| data, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| return response.data; | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+64
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. 🧹 Nitpick (assertive) API 함수에 에러 처리 추가 권장 모든 API 함수에서 에러를 catch하고 의미 있는 에러 메시지로 변환하면 좋겠습니다. 에러 처리 래퍼 함수 추가: const handleApiError = (error: any, context: string) => {
if (error.response?.data?.message) {
throw new Error(error.response.data.message);
}
throw new Error(`${context} 중 오류가 발생했습니다.`);
};
export const getMyReservations = async (
params: GetMyReservationsParams,
): Promise<MyReservationsResponse> => {
try {
// 기존 코드
} catch (error) {
handleApiError(error, '예약 목록 조회');
}
};🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import Modal from '@/components/Modal'; | ||||||||||||||||||||||||
| import Button from '@/components/Button'; | ||||||||||||||||||||||||
| import CheckIcon from '@assets/svg/check'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| interface CancelReservationModalProps { | ||||||||||||||||||||||||
| isOpen: boolean; | ||||||||||||||||||||||||
| onCancel: () => void; | ||||||||||||||||||||||||
| onConfirm: () => void; | ||||||||||||||||||||||||
| isLoading?: boolean; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| export default function CancelReservationModal({ | ||||||||||||||||||||||||
| isOpen, | ||||||||||||||||||||||||
| onCancel, | ||||||||||||||||||||||||
| onConfirm, | ||||||||||||||||||||||||
| isLoading = false, | ||||||||||||||||||||||||
| }: CancelReservationModalProps) { | ||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||
| <Modal isOpen={isOpen} onOpenChange={(open) => !open && onCancel()}> | ||||||||||||||||||||||||
| <Modal.Content className='!h-[184px] !w-[298px] !max-w-none !min-w-0 !rounded-xl !p-0'> | ||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||
| className='flex h-full w-full flex-col items-center justify-center gap-24 bg-white p-16' | ||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||
| borderRadius: '12px', | ||||||||||||||||||||||||
| background: '#FFFFFF', | ||||||||||||||||||||||||
| boxShadow: '0px 4px 16px 0px rgba(17, 34, 17, 0.05)', | ||||||||||||||||||||||||
| overflow: 'hidden', | ||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| <Modal.Content className='!h-[184px] !w-[298px] !max-w-none !min-w-0 !rounded-xl !p-0'> | |
| <div | |
| className='flex h-full w-full flex-col items-center justify-center gap-24 bg-white p-16' | |
| style={{ | |
| borderRadius: '12px', | |
| background: '#FFFFFF', | |
| boxShadow: '0px 4px 16px 0px rgba(17, 34, 17, 0.05)', | |
| overflow: 'hidden', | |
| }} | |
| <Modal.Content className='!h-[184px] !w-[298px] !max-w-none !min-w-0 !rounded-xl !p-0 shadow-[0px_4px_16px_0px_rgba(17,34,17,0.05)]'> | |
| <div className='flex h-full w-full flex-col items-center justify-center gap-24 rounded-xl bg-white p-16'> |
🤖 Prompt for AI Agents
In
src/app/(with-header)/mypage/reservations/components/CancelReservationModal.tsx
around lines 22 to 30, the styles borderRadius, background, and boxShadow are
redundantly defined both in the className and the style attribute. To fix this,
consolidate the styling by either converting all these styles into appropriate
Tailwind CSS classes or by removing them from className and keeping them only in
the style object, ensuring no duplication and consistent styling approach.
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.
이 부분은 팝업 컴포넌트 이용하시면 좋을 것 같습니다! 이미 만들어 둔 부분이라 확인 해보세요!
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.
Popup이 아닌 Modal을 선택해서 사용한 이유가 isLoading 상태 처리 및 동적 텍스트가 필요하기에 Popup에는 props에 isLoading?:boolean이 존재하지 않는 반면 Modal 컴포넌트의 경우 isLoading?:boolean이 존재하여 Modal 컴포넌트를 선택해서 사용했습니다.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,17 @@ | ||||||||||||||||||||||||||||||
| import EmptyDocumentIcon from '@assets/svg/empty-document'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export default function EmptyReservations() { | ||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
| <div className='flex flex-col items-center justify-center py-[120px]'> | ||||||||||||||||||||||||||||||
| {/* 빈 상태 아이콘 */} | ||||||||||||||||||||||||||||||
| <div className='mb-[24px]'> | ||||||||||||||||||||||||||||||
| <EmptyDocumentIcon size={131} /> | ||||||||||||||||||||||||||||||
|
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. 🧹 Nitpick (assertive) 아이콘 크기 상수화 제안. 하드코딩된 아이콘 크기(131)를 상수로 분리하면 유지보수성이 향상됩니다. 다음과 같이 상수로 분리할 수 있습니다: +const EMPTY_ICON_SIZE = 131;
+
export default function EmptyReservations() {
return (
<div className='flex flex-col items-center justify-center py-120'>
{/* 빈 상태 아이콘 */}
<div className='mb-24'>
- <EmptyDocumentIcon size={131} />
+ <EmptyDocumentIcon size={EMPTY_ICON_SIZE} />
</div>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {/* 빈 상태 메시지 */} | ||||||||||||||||||||||||||||||
| <p className='text-2xl font-normal text-gray-700'> | ||||||||||||||||||||||||||||||
| 아직 등록한 체험이 없어요 | ||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| 'use client'; | ||
|
|
||
| import Image from 'next/image'; | ||
| import Button from '@/components/Button'; | ||
| import { Reservation } from '@/types/reservationTypes'; | ||
| import { STATUS_LABELS, STATUS_COLORS } from '@/constants/reservationConstants'; | ||
| import cn from '@/lib/cn'; | ||
|
|
||
| interface ReservationCardProps { | ||
| reservation: Reservation; | ||
| onCancel?: (reservationId: number) => void; | ||
| onReview?: (reservationId: number) => void; | ||
| } | ||
|
|
||
| // 체험 완료 여부 확인 (현재 시간이 체험 종료 시간을 지났는지) | ||
| const isExperienceCompleted = (date: string, endTime: string): boolean => { | ||
| const experienceEndDateTime = new Date(`${date}T${endTime}+09:00`); | ||
| return new Date() > experienceEndDateTime; | ||
| }; | ||
|
|
||
| export default function ReservationCard({ | ||
| reservation, | ||
| onCancel, | ||
| onReview, | ||
| }: ReservationCardProps) { | ||
| const { | ||
| id, | ||
| activity, | ||
| status, | ||
| reviewSubmitted, | ||
| totalPrice, | ||
| headCount, | ||
| date, | ||
| startTime, | ||
| endTime, | ||
| } = reservation; | ||
|
|
||
| const isCompleted = isExperienceCompleted(date, endTime); | ||
| const showCancelButton = status === 'pending'; | ||
| const showReviewButton = isCompleted && !reviewSubmitted; | ||
| const showReviewCompleted = isCompleted && reviewSubmitted; | ||
|
|
||
| return ( | ||
| <div className='rounded-24 flex h-204 w-792 overflow-hidden border border-gray-300 bg-white'> | ||
| {/* 이미지 영역 */} | ||
| <div className='relative h-204 w-204 flex-shrink-0'> | ||
| <Image | ||
| src={activity.bannerImageUrl} | ||
| alt={activity.title} | ||
| fill | ||
| className='rounded-l-24 object-cover' | ||
| /> | ||
| </div> | ||
|
|
||
| {/* 콘텐츠 영역 */} | ||
| <div className='flex flex-1 flex-col justify-start py-21 pr-24 pl-24'> | ||
| {/* 상태 라벨 */} | ||
| <div> | ||
| <span className={cn('text-lg font-bold', STATUS_COLORS[status])}> | ||
| {STATUS_LABELS[status]} | ||
| </span> | ||
| </div> | ||
|
|
||
| {/* 제목 */} | ||
| <div className='mt-8'> | ||
| <h3 className='text-nomad text-xl font-bold'>{activity.title}</h3> | ||
| </div> | ||
|
|
||
| {/* 날짜 및 인원 정보 */} | ||
| <div className='mt-12'> | ||
| <p className='text-2lg text-nomad'> | ||
| {date} · {startTime} - {endTime} · {headCount}명 | ||
| </p> | ||
| </div> | ||
|
|
||
| {/* 가격 + 버튼 */} | ||
| <div className='mt-21 flex items-center justify-between'> | ||
| {/* 가격 */} | ||
| <p className='text-2xl font-bold text-black'> | ||
| ₩{totalPrice.toLocaleString()} | ||
| </p> | ||
|
|
||
| {/* 버튼/상태 */} | ||
| <div className='flex h-43 w-144 items-center justify-center'> | ||
| {showCancelButton && ( | ||
| <Button | ||
| variant='secondary' | ||
| className='h-43 w-144 rounded-md text-lg font-bold' | ||
| onClick={() => onCancel?.(id)} | ||
| > | ||
| 예약 취소 | ||
| </Button> | ||
| )} | ||
| {showReviewButton && ( | ||
| <Button | ||
| variant='primary' | ||
| className='bg-nomad h-43 w-144 rounded-md text-lg font-bold' | ||
| onClick={() => onReview?.(id)} | ||
| > | ||
| 후기 작성 | ||
| </Button> | ||
| )} | ||
| {showReviewCompleted && ( | ||
| <div className='text-lg font-bold text-gray-500'>후기 완료</div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import Dropdown from '@/components/Dropdown'; | ||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||
| FilterOption, | ||||||||||||||||||||||||||||||||||||||
| FILTER_OPTIONS, | ||||||||||||||||||||||||||||||||||||||
| FILTER_LABELS, | ||||||||||||||||||||||||||||||||||||||
| } from '@/constants/reservationConstants'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| interface ReservationFilterProps { | ||||||||||||||||||||||||||||||||||||||
| value: FilterOption; | ||||||||||||||||||||||||||||||||||||||
| onChange: (value: FilterOption) => void; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export default function ReservationFilter({ | ||||||||||||||||||||||||||||||||||||||
| value, | ||||||||||||||||||||||||||||||||||||||
| onChange, | ||||||||||||||||||||||||||||||||||||||
| }: ReservationFilterProps) { | ||||||||||||||||||||||||||||||||||||||
| // 표시용 라벨 옵션 배열 | ||||||||||||||||||||||||||||||||||||||
| const labelOptions = FILTER_OPTIONS.map((option) => FILTER_LABELS[option]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // 라벨 선택 시 실제 값으로 변환 | ||||||||||||||||||||||||||||||||||||||
| const handleChange = (selectedLabel: string) => { | ||||||||||||||||||||||||||||||||||||||
| const selectedOption = FILTER_OPTIONS.find( | ||||||||||||||||||||||||||||||||||||||
| (option) => FILTER_LABELS[option] === selectedLabel, | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| if (selectedOption !== undefined) { | ||||||||||||||||||||||||||||||||||||||
| onChange(selectedOption); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+30
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. 🧹 Nitpick (assertive) 라벨-옵션 변환 로직에 안전 장치를 추가해보세요. 현재 const handleChange = (selectedLabel: string) => {
const selectedOption = FILTER_OPTIONS.find(
(option) => FILTER_LABELS[option] === selectedLabel,
);
- if (selectedOption !== undefined) {
+ if (selectedOption) {
onChange(selectedOption);
+ } else {
+ console.warn(`Unknown filter label: ${selectedLabel}`);
}
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| <Dropdown | ||||||||||||||||||||||||||||||||||||||
| options={labelOptions} | ||||||||||||||||||||||||||||||||||||||
| value={FILTER_LABELS[value]} | ||||||||||||||||||||||||||||||||||||||
| onChange={handleChange} | ||||||||||||||||||||||||||||||||||||||
| placeholder='필터' | ||||||||||||||||||||||||||||||||||||||
| className='h-[53px] w-[160px]' | ||||||||||||||||||||||||||||||||||||||
| disableScroll={true} | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
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.
🛠️ Refactor suggestion
TypeScript 타입 정의가 필요합니다.
props에 대한 TypeScript 타입이 정의되지 않았습니다. 타입 안전성을 위해 인터페이스를 추가해주세요.
📝 Committable suggestion
🤖 Prompt for AI Agents