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
2 changes: 1 addition & 1 deletion src/components/dateCourse/dateCourseSearchFilterOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export default function DateCourseSearchFilterOption({
onChange={(e) => {
const val = e.target.value;
setDate(val);
if (time) onChange(`${val}T${time}:00`);
if (time) onChange(`${val}T${time}:00.000Z`);
}}
className="absolute top-0 left-0 w-full h-full opacity-0"
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/dateCourse/keywordButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function KeywordButton({ tag, selected = false, onClick, isButton
return (
<div
onClick={isButton ? onClick : undefined}
className={`flex flex-nowrap rounding-32 px-[16px] py-[8px] font-body2 min-w-fit w-fit select-none
className={`flex whitespace-nowrap rounding-32 px-[16px] py-[8px] font-body2 min-w-fit w-fit select-none
${selected ? 'bg-primary-700 text-white' : 'bg-default-gray-400 text-default-gray-700'}
${isButton && 'cursor-pointer'}
`}
Expand Down
19 changes: 10 additions & 9 deletions src/components/dateCourse/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,20 @@ function Timeline({ end = false, image, name, placeCategoryResponseList, roadNam

{open && (
<div className="flex lg:items-start self-stretch w-full gap-[9px] flex-col lg:flex-row items-center">
<div className="flex flex-col gap-[8px] lg:w-[50%] max-h-fit justify-around items-start">
{signatureDish && (
<div className="flex w-full flex-col ">
<div className="flex gap-[8px] font-body1 select-none text-center items-center h-[24px] w-full">
<div className="flex text-center h-full">WithTime Pick</div>
{signatureDish && (
<div className="flex flex-col gap-[8px] lg:w-[50%] w-full max-h-fit justify-around items-start">
<div className="flex w-full flex-row self-start lg:flex-col h-fit lg:gap-0 gap-[8px]">
<div className="flex gap-[8px] font-body1 select-none text-center items-center h-[35.6px] lg:w-full">
<div className="flex text-center h-full items-center">WithTime Pick</div>
<CheckSuccess stroke={'#000000'} />
</div>
<KeywordButton tag={signatureDish.name || ''} />
</div>
)}
{image && <img src={image} className="w-[80%] self-start" />}
</div>
<div className="flex flex-col h-full lg:w-[50%] gap-[16px]">

{image && <img src={image} className="w-[80%] self-center lg:self-start" />}
</div>
)}
<div className={`flex flex-col h-full gap-[16px] self-start ${signatureDish == null ? 'w-full' : 'lg:w-[50%]'}`}>
<div className="flex gap-[16px] font-body2 text-default-gray-800 break-keep lg:items-start items-center">
<Location stroke="#000000" className="min-w-[24px]" />
{roadNameAddress}
Expand Down
105 changes: 45 additions & 60 deletions src/components/modal/dateCourseSearchFilterModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import ClipLoader from 'react-spinners/ClipLoader';

import type { IQuestion } from '@/types/dateCourse/dateCourse';
import { DateCourseQuestion } from '@/constants/dateCourseQuestion';

import {
Expand Down Expand Up @@ -64,6 +65,13 @@ function computeErrors(f: { budget: any; dateDurationTime: any; mealTypes?: any[

return e;
}
const TOTAL_QUESTIONS = 8;
const Questions: IQuestion[] = Array.isArray(DateCourseQuestion)
? DateCourseQuestion.slice(0, TOTAL_QUESTIONS - 1).map((q) => ({
...q,
type: q.type as IQuestion['type'],
}))
: [];

export default function DateCourseSearchFilterModal({ onClose }: TProps) {
const location = useLocation();
Expand All @@ -89,36 +97,41 @@ export default function DateCourseSearchFilterModal({ onClose }: TProps) {

const data = isBookmarked ? bookmarkedData : courseData;

const Questions = useMemo(
() =>
(Array.isArray(DateCourseQuestion) ? DateCourseQuestion.slice(0, 7) : [])
.map((q) => ({
...q,
type: q.type as 'choice' | 'search' | 'time' | 'choices' | 'keyword',
}))
.filter((q) => q.filterTitle !== ''),
[],
);
const stepFieldMap = {
0: 'budget',
1: 'datePlaces',
2: 'dateDurationTime',
3: 'mealTypes',
4: 'transportation',
5: 'userPreferredKeywords',
6: 'startTime',
} as const;

const fieldValues = {
budget,
datePlaces,
dateDurationTime,
mealTypes,
transportation,
userPreferredKeywords,
startTime,
};

const valueByStep = (idx: number): string | string[] | null => {
const step = idx; // ★ 중요
const fieldName = stepFieldMap[step as keyof typeof stepFieldMap];
return fieldName ? fieldValues[fieldName] : null;
};

const valueByIndex = (idx: number) => {
switch (idx) {
case 0:
return budget;
case 1:
return datePlaces;
case 2:
return dateDurationTime;
case 3:
return mealTypes;
case 4:
return transportation;
case 5:
return startTime;
case 6:
return userPreferredKeywords;
default:
return null;
const updateByStep = (idx: number, v: any) => {
const step = idx; // ★ 중요
const fieldName = stepFieldMap[step as keyof typeof stepFieldMap];
if (!fieldName) return;

if ([1, 3, 5].includes(step) && !Array.isArray(v)) {
v = [];
}
setField(fieldName, v ?? null);
};

const errorMessages = useMemo(
Expand All @@ -133,35 +146,6 @@ export default function DateCourseSearchFilterModal({ onClose }: TProps) {
[budget, dateDurationTime, mealTypes, userPreferredKeywords, startTime],
);

const updateByIndex = (idx: number, raw: any) => {
let v = raw;
if ([1, 3, 6].includes(idx) && !Array.isArray(v)) v = [];

switch (idx) {
case 0:
setField('budget', v ?? null);
break;
case 1:
setField('datePlaces', v);
break;
case 2:
setField('dateDurationTime', v ?? null);
break;
case 3:
setField('mealTypes', v);
break;
case 4:
setField('transportation', v ?? null);
break;
case 5:
setField('startTime', v ?? null);
break;
case 6:
setField('userPreferredKeywords', v);
break;
}
};

if (bookmarkDataError || courseDataError) {
return <Navigate to="/error" replace={true} />;
}
Expand All @@ -172,14 +156,15 @@ export default function DateCourseSearchFilterModal({ onClose }: TProps) {
{Questions.map((q, idx) => (
<DateCourseSearchFilterOption
key={q.id}
title={q.filterTitle}
title={q.title}
subTitle={q.subTitle}
options={q.options}
value={valueByIndex(idx)}
onChange={(v) => updateByIndex(idx, v)}
value={valueByStep(idx)}
onChange={(v) => updateByStep(idx, v)}
type={q.type}
apiRequestValue={q.apiRequestValue}
errorMessage={errorMessages[idx] ?? ''}
autoInit={q.type === 'time' && idx === 7}
/>
))}

Expand Down
11 changes: 2 additions & 9 deletions src/utils/dateCourseValidation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,15 @@ const keywordGroups: Record<string, string[]> = {
export const mealKeyword = ['양식', '한식', '중식', '이자카야/펍', '퓨전 음식점', '브런치 카페', '디저트 카페', '루프탑 카페'];

export function MealTimeValidation({ meal, time, totalTime }: { meal: string[]; time: string; totalTime: string }): string | null {
if (!time || meal.length === 0 || !totalTime) return null;
if (!time || meal.length === 0 || !totalTime || time == null) return null;
const timeStr = time.split('T')[1]; // 'HH:mm'
const toMinutes = (t: string) => {
const [h, m] = t.split(':').map(Number);
return h * 60 + m;
};

const start = toMinutes(timeStr); // 데이트 시작 시간
const duration = timeMap[totalTime]; // 소요 시간(시간 단위)
const end = start + duration * 60; // 종료 시간 (분)

const isOverlapping = (start1: number, end1: number, start2: number, end2: number) => {
if (start1 < start2) {
// 데이트 시작시간이 식사 시작시간보다 빠를 경우
Expand All @@ -50,25 +48,20 @@ export function MealTimeValidation({ meal, time, totalTime }: { meal: string[];
return false;
}
};

// 선택한 식사 중 하나라도 겹치면 통과
for (const m of meal) {
const mealRange = mealTimeRanges[m];

if (!mealRange) continue; // 존재하지 않으면 skip

const [mealStartStr, mealEndStr] = mealRange;
const mealStart = toMinutes(mealStartStr);
const mealEnd = toMinutes(mealEndStr);
if (isOverlapping(start, end, mealStart, mealEnd)) {
return null;
}
}

// 아무것도 안겹치면 첫 번째 식사를 기준으로 안내
const first = meal[0];
const [mealStartStr, mealEndStr] = mealTimeRanges[first];

return `선택하신 시간에는 ${mealTimeKorean[first]} 식사를 하기 어려워요. (가능 시간: ${mealStartStr}~${mealEndStr})`;
}

Expand All @@ -78,7 +71,7 @@ type TDateTimeStartValidationInput = {
};

export function DateTimeStartValidation({ totalTime, time }: TDateTimeStartValidationInput): string | null {
if (!totalTime || !time) return null;
if (!totalTime || !time || time == null) return null;
const duration = timeMap[totalTime];
if (duration == null) return null;
const timeStr = time.split('T')[1];
Expand Down