From b164289b4e34b962bd392f64826dec0d94f5e62c Mon Sep 17 00:00:00 2001 From: jieunsse Date: Sat, 7 Feb 2026 21:31:25 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Calender/CalenderModal.module.css | 224 +++++++++++++++ .../components/Calender/CalenderModal.tsx | 268 ++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 src/components/Modal/domain/components/Calender/CalenderModal.module.css create mode 100644 src/components/Modal/domain/components/Calender/CalenderModal.tsx diff --git a/src/components/Modal/domain/components/Calender/CalenderModal.module.css b/src/components/Modal/domain/components/Calender/CalenderModal.module.css new file mode 100644 index 0000000..b3b7d03 --- /dev/null +++ b/src/components/Modal/domain/components/Calender/CalenderModal.module.css @@ -0,0 +1,224 @@ +section > .modalContent { + width: 384px; + height: 664px; + display: inline-flex; + padding: 0 16px 32px 16px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 24px; + background: var(--Background-Primary, #fff); + box-shadow: 4px 4px 10px 0 rgba(36, 36, 36, 0.25); + box-sizing: border-box; +} + +section > .modalContentDateOpen { + height: 930px; +} + +section > .modalContentTimeOpen { + height: 848px; +} + +section > .modalContentWeekdayOpen { + height: 759px; +} + +.container { + width: 100%; + height: 100%; + margin-top: 24px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.header { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.title { + margin: 0; + color: var(--Text-Primary, #1e293b); + font-family: Pretendard, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 19px; +} + +.description { + margin: 0; + color: var(--Text-Default, #64748b); + text-align: center; + font-family: Pretendard, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 17px; + word-break: keep-all; +} + +.form { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + gap: 16px; +} + +.fieldGroup { + display: flex; + flex-direction: column; + gap: 8px; +} + +.label { + margin: 0; + color: var(--Text-Primary, #1e293b); + font-family: Pretendard, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 19px; +} + +.todoTitleInput { + display: flex; + width: 100%; + height: 48px; + padding: 16px; + align-items: center; + gap: 10px; + border-radius: 12px; + border: 1px solid var(--Border-Primary, #e2e8f0); + background: var(--Background-Primary, #fff); + color: var(--Text-Primary, #1e293b); +} + +.todoTitleInput::placeholder { + color: var(--Text-Default, #64748b); + font-family: Pretendard, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 19px; +} + +.dateTimeInputRow { + display: flex; + align-items: center; + gap: 8px; +} + +.dateInput, +.timeInput { + display: flex; + height: 48px; + padding: 16px; + align-items: center; + gap: 10px; + border-radius: 12px; + border: 1px solid var(--Border-Primary, #e2e8f0); + background: var(--Background-Primary, #fff); + color: var(--Text-Primary, #1e293b); + cursor: pointer; +} + +.dateInput { + width: 204px; +} + +.timeInput { + width: 124px; +} + +.dateInput::placeholder, +.timeInput::placeholder { + color: var(--Text-Default, #64748b); + font-family: Pretendard, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 19px; +} + +.activePickerInput { + border-color: var(--Interaction-Hover, #416ec8); +} + +.calendarPanel, +.timePickerPanel { + display: flex; + justify-content: center; +} + +.repeatDropdownButton { + width: 120px; + height: 40px; +} + +.repeatDropdownMenu { + z-index: 20; +} + +.weekdayButtonGroup { + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; +} + +.memoInput { + display: flex; + height: 75px; + padding: 12px 16px; + align-items: flex-start; + gap: 10px; + align-self: stretch; + border-radius: 12px; + border: 1px solid var(--Border-Primary, #e2e8f0); + background: var(--Background-Primary, #fff); +} + +.memoInput::placeholder { + color: var(--Text-Default, #64748b); +} + +.footer { + margin-top: auto; +} + +.submitButton { + display: flex; + width: 100%; + height: 48px; + padding: 14px 0; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 12px; + background: var(--Color-Brand-Primary, #5189fa); + color: var(--Text-inverse, #fff); + text-align: center; + font-family: Pretendard, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 19px; +} + +@media (max-width: 480px) { + section > .modalContent, + section > .modalContentDateOpen, + section > .modalContentTimeOpen, + section > .modalContentWeekdayOpen { + width: 100%; + max-width: 384px; + border-radius: 24px 24px 0 0; + } +} diff --git a/src/components/Modal/domain/components/Calender/CalenderModal.tsx b/src/components/Modal/domain/components/Calender/CalenderModal.tsx new file mode 100644 index 0000000..4ae03f9 --- /dev/null +++ b/src/components/Modal/domain/components/Calender/CalenderModal.tsx @@ -0,0 +1,268 @@ +'use client'; + +import clsx from 'clsx'; +import type { FormEvent, KeyboardEvent } from 'react'; +import { useCallback, useId } from 'react'; + +import BaseButton from '@/components/Button/base/BaseButton'; +import DatePickerButton from '@/components/Button/domain/DatePickerButton/DatePickerButton'; +import Calendar from '@/components/calendar/Calendar'; +import CalendarTime from '@/components/calendar/time/CalendarTime'; +import Dropdown from '@/components/dropdown/Dropdown'; +import Input from '@/components/input/Input'; +import TextArea from '@/components/input/TextArea'; +import Modal from '../../../Modal'; +import styles from './CalenderModal.module.css'; +import { + DESCRIPTION_ID, + REPEAT_OPTIONS, + START_DATE_INPUT_ID, + START_TIME_INPUT_ID, + TITLE_ID, + TODO_MEMO_NAME, + TODO_TITLE_NAME, + WEEKDAY_OPTIONS, +} from './constants/CalenderModal.constants'; +import { useCalenderModalForm } from './hooks/useCalenderModalForm'; +import type { CalenderModalProps } from './types/CalenderModal.types'; +import { + formatDateLabel, + isPickerToggleKey, + resolveContentHeightClassNames, + resolveCalenderModalText, + resolveCloseOptions, +} from './utils/CalenderModal.utils'; +export type { CalenderModalProps } from './types/CalenderModal.types'; + +/** + * @param props.isOpen 모달 표시 여부를 boolean으로 전달합니다. + * @param props.onClose 모달을 닫을 때 실행할 함수를 전달합니다. + * @param props.onSubmit 할 일 생성 제출 시 실행할 함수를 전달합니다. + * @param props.text 제목과 라벨, 플레이스홀더 같은 텍스트 옵션을 객체로 전달합니다. + * @param props.input 입력창에 적용할 옵션을 객체로 전달합니다. + * @param props.initialValues 초기 입력 값을 객체로 전달합니다. + * @param props.closeOptions 오버레이 클릭과 Escape 닫힘 옵션을 객체로 전달합니다. + */ +export default function CalenderModal({ + isOpen, + onClose, + onSubmit, + text, + input, + initialValues, + closeOptions, +}: CalenderModalProps) { + const textOptions = resolveCalenderModalText(text); + const { closeOnOverlayClick, closeOnEscape } = resolveCloseOptions(closeOptions); + const todoTitleInputId = useId(); + const todoMemoInputId = useId(); + + const { + todoTitle, + startDate, + startTime, + repeatType, + repeatDays, + memo, + activePicker, + isWeekdaySelectorVisible, + setTodoTitle, + setStartDate, + setStartTime, + setMemo, + handleRepeatTypeChange, + handleToggleWeekday, + handleDatePickerToggle, + handleTimePickerToggle, + resetForm, + } = useCalenderModalForm({ + initialValues, + }); + + const handleDateInputKeyDown = useCallback( + (event: KeyboardEvent) => { + if (!isPickerToggleKey(event.key)) return; + event.preventDefault(); + handleDatePickerToggle(); + }, + [handleDatePickerToggle], + ); + + const handleTimeInputKeyDown = useCallback( + (event: KeyboardEvent) => { + if (!isPickerToggleKey(event.key)) return; + event.preventDefault(); + handleTimePickerToggle(); + }, + [handleTimePickerToggle], + ); + + const contentHeightClassNames = resolveContentHeightClassNames( + activePicker, + isWeekdaySelectorVisible, + { + modalContentDateOpen: styles.modalContentDateOpen, + modalContentTimeOpen: styles.modalContentTimeOpen, + modalContentWeekdayOpen: styles.modalContentWeekdayOpen, + }, + ); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + onSubmit({ + todoTitle: todoTitle.trim(), + startDate, + startTime, + repeatType, + repeatDays: isWeekdaySelectorVisible ? repeatDays : [], + memo: memo.trim(), + }); + }; + + const handleClose = () => { + resetForm(); + onClose(); + }; + + return ( + +
+
+

+ {textOptions.title} +

+

+ {textOptions.description} +

+
+ +
+
+ + setTodoTitle(event.target.value)} + /> +
+ +
+

{textOptions.startDateTimeLabel}

+
+ + +
+ + {activePicker === 'date' ? ( +
+ +
+ ) : null} + {activePicker === 'time' ? ( +
+ +
+ ) : null} +
+ +
+

{textOptions.repeatSettingLabel}

+ +
+ + {isWeekdaySelectorVisible ? ( +
+

{textOptions.repeatWeekdayLabel}

+
+ {WEEKDAY_OPTIONS.map((weekday) => ( + + ))} +
+
+ ) : null} + +
+ +