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
20 changes: 20 additions & 0 deletions src/app/mobile/main/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { WelfareItemData, Item } from '@/types/welfareItemType';
import IconSearch from 'public/assets/icons/icon-search.svg';
import { useRouter } from 'next/navigation';
import { requestNotificationPermission } from '@/utils/pushNotification';
import PopUp from '@/components/mobile/PopUp';

export default function MobileMain() {
const [isBottomSheetOpen, setIsBottomSheetOpen] = useState(false);
Expand All @@ -19,6 +20,7 @@ export default function MobileMain() {
items: [],
});
const [searchQuery, setSearchQuery] = useState('');
const [showPopUp, setShowPopUp] = useState<boolean>(false);
const router = useRouter();

const fetchWelfareItems = async () => {
Expand Down Expand Up @@ -47,6 +49,11 @@ export default function MobileMain() {
fetchWelfareItems();

requestNotificationPermission();

// "다시 보지 않기" 플래그가 없으면 팝업 표시
if (!localStorage.getItem('popUpDismissed')) {
setShowPopUp(true);
}
}, []);

const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -103,6 +110,19 @@ export default function MobileMain() {
</section>
</div>

{showPopUp && (
<PopUp
title="🚨 변경사항 안내 🚨"
content={`물품 대여 신청은 현재 시간으로부터
5분 후에 가능합니다.\n(16시 55분부터는 즉시 신청이 가능합니다.)`}
onClickCta={() => setShowPopUp(false)}
onClickOther={() => {
localStorage.setItem('popUpDismissed', 'true');
setShowPopUp(false);
}}
/>
)}

{/* Bottom Sheet */}
<BottomSheet
isOpen={isBottomSheetOpen}
Expand Down
122 changes: 85 additions & 37 deletions src/components/mobile/BottomSheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,6 @@ export default function BottomSheet({
const isLoadingRef = useRef(false);
const reRender = useReRenderer();

// 현재 시간 가져오기 (현재 시간 이후로만 입력 가능하도록 하기 위함)
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();

const maxQuantity = item?.count || 0;

// 모달이 열릴 때마다 입력값 초기화
Expand Down Expand Up @@ -92,49 +87,102 @@ export default function BottomSheet({
setErrors((prevErrors) => ({ ...prevErrors, quantity: errorMsg }));
};

// 시간 입력 시 검증
const handleHourChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setHour(value);
// 통합 시간 검증 함수
const validateTime = (hourStr: string, minuteStr: string): string => {
const numHour = parseInt(hourStr, 10);
const numMinute = parseInt(minuteStr, 10);

const numHour = parseInt(value, 10);
let errorMsg = '';
if (Number.isNaN(numHour) || Number.isNaN(numMinute)) {
return '올바른 시간을 입력해주세요.';
}

if (Number.isNaN(numHour)) {
errorMsg = '올바른 시간을 입력해주세요.';
} else if (numHour < 10 || numHour >= 17) {
errorMsg = '대여 가능 시간은 10:00 ~ 17:00입니다.'; // 10시 ~ 17시 사이가 아닐 경우
} else if (numHour < currentHour) {
errorMsg = '대여는 현재 시간 이후로만 가능합니다.'; // 현재 시간보다 이전이면 무조건 오류
} else if (
numHour === currentHour &&
parseInt(minute || '0', 10) <= currentMinute
) {
errorMsg = '대여는 현재 시간 이후로만 가능합니다.'; // 현재 시각과 같다면, 분이 현재 분보다 커야 함
const now = new Date();
const inputTime = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
numHour,
numMinute,
);
const openTime = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
10,
0,
);
const closeTime = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
17,
0,
);

// 점심 시간
const lunchOpenTime = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
12,
0,
);
const lunchCloseTime = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
12,
59,
);

// 운영시간: 10:00 ~ 17:00
if (inputTime < openTime || inputTime > closeTime) {
return '대여 가능한 시간은 10:00 ~ 17:00입니다.';
}

// 점심시간: 12:00 ~ 12:59
if (inputTime >= lunchOpenTime && inputTime < lunchCloseTime) {
return '12:00 ~ 12:59은 점심시간입니다.';
}

// 입력 시간이 현재 시간보다 이후인지 체크
if (inputTime <= now) {
return '대여는 현재 시간 이후로만 가능합니다.';
}

// 현재 시각을 기준으로 5분 후 체크 (현재 시간이 16:55 이전일 때만 적용)
const threshold = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
16,
55,
);
if (now < threshold) {
const fiveMinutesLater = new Date(now.getTime() + 5 * 60 * 1000);
if (inputTime < fiveMinutesLater) {
return '대여는 현재 시간으로부터 5분 후에 가능합니다.';
}
}

return '';
};

// 시간 입력 시 검증 (시)
const handleHourChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setHour(value);

const errorMsg = validateTime(value, minute);
setErrors((prevErrors) => ({ ...prevErrors, time: errorMsg }));
};

// 입력 시 검증
// 시간 입력 시 검증 (분)
const handleMinuteChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setMinute(value);

const numHour = parseInt(hour, 10);
const numMinute = parseInt(value, 10);
let errorMsg = '';

if (Number.isNaN(numHour) || Number.isNaN(numMinute)) {
errorMsg = '올바른 시간을 입력해주세요.';
} else if (numHour < 10 || numHour >= 17) {
errorMsg = '대여 가능 시간은 10:00 ~ 17:00입니다.';
} else if (numHour < currentHour) {
errorMsg = '대여는 현재 시간 이후로만 가능합니다.';
} else if (numHour === currentHour && numMinute <= currentMinute) {
errorMsg = '대여는 현재 시간 이후로만 가능합니다.';
}

const errorMsg = validateTime(hour, value);
setErrors((prevErrors) => ({ ...prevErrors, time: errorMsg }));
};

Expand Down
66 changes: 66 additions & 0 deletions src/components/mobile/PopUp/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { handleTouchStart, handleTouchEnd } from '@/utils/handleTouch';

interface PopUpProps {
title: string; // PopUp창 제목
content: string; // PopUp창 내용
ctaButtonText?: string; // 오른쪽 버튼에 들어갈 문구
otherButtonText?: string; // 왼쪽 버튼에 들어갈 문구
onClickCta?: () => void; // 오른쪽 버튼을 눌렀을 때 실행될 함수
onClickOther?: () => void; // 왼쪽 버튼을 눌렀을 때 실행될 함수
}

export default function PopUp({
title,
content,
ctaButtonText = '확인',
otherButtonText = '다시 보지 않기',
onClickCta,
onClickOther,
}: PopUpProps) {
const defalutButtonClass =
'text-body-1-normal_semi w-[108px] rounded-[10px] py-[9px] font-medium outline-none ';

return (
<div className="fixed inset-0 z-20 flex items-center justify-center">
{/* 반투명한 검정 배경 */}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
<div
className="absolute inset-0 bg-black bg-opacity-50"
onClick={onClickCta} // 배경 클릭 시 닫히도록 설정
/>

<div className="relative flex w-[275px] flex-col gap-2.5 rounded-[20px] bg-white-primary p-5">
<div className="flex flex-col gap-2.5 py-5 text-center">
{/* 제목 */}
<div className="text-body-1-normal_semi font-semibold">{title}</div>
{/* 문구 */}
<div className="whitespace-pre-line text-caption-1_midi font-medium text-gray-primary">
{content}
</div>
</div>

{/* 버튼 2개(서브 버튼 / 메인 버튼) */}
<div className="flex justify-between gap-5">
<button
type="button"
onClick={onClickOther}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
className={`${defalutButtonClass} bg-gray-tertiary text-gray-secondary transition-all`}
>
{otherButtonText}
</button>
<button
type="button"
onClick={onClickCta}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
className={` ${defalutButtonClass} bg-return-blue text-white-primary`}
>
{ctaButtonText}
</button>
</div>
</div>
</div>
);
}