이번 프로젝트에서는 모달을 열어 사용자가 선택할 수 있는 날짜와 시간을 제공해야 했습니다. 이 과정에서 셀렉트 박스와 달력을 통해 사용자에게 직관적인 인터페이스를 제공하고, 선택된 값들을 함께 처리하는 기능을 구현했습니다.
import 부분
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import dayjs from "dayjs"; // 날짜 처리 라이브러리
import "dayjs/locale/ko"; // 한국어 로케일
import { format } from "date-fns"
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { timeHour, timeMinute } from "../data"; // 시간 관련 데이터
import useTimePicker from "../hooks/useTimePicker"; // 시간 선택 관련 훅
함수 호출 부분
// 폼 데이터 유효성 검사를 위한 Zod 스키마 정의
const formSchema = z.object({
total: z.string().min(1), // total은 필수값
data: z.string().min(1), // data도 필수값
});
function SyncModal() {
// 한국어 로케일 설정
dayjs.locale("ko");
// React Hook Form 초기화
const form = useForm({
resolver: zodResolver(formSchema), // 유효성 검사 resolver 설정
defaultValues: {
total: "", // 기본값 설정
data: "",
},
});
// 날짜 및 시간 선택에 대한 상태 관리
const [total, setTotal] = useState<string | undefined>(undefined); // 전체 시간 상태
const [date, setDate] = useState<Date>(); // 선택한 날짜 상태
const [time, setTime] = useState<Time>({
hour: "00", // 시
minute: "00", // 분
});
// 확인 및 취소 모달 관련 상태
const [loading, setLoading] = useState(false); // 로딩 상태
const [error, setError] = useState(""); // 에러 메시지 상태
const [isCalendarOpen, setCalendarOpen] = useState(false); // 캘린더 열림 상태
const [isTimeOpen, setIsTimeOpen] = useState(false); // 시간 선택 열림 상태
// 캘린더에서 날짜 선택 함수
const onSelect = (selectedDate: Date | undefined) => {
setDate(selectedDate); // 선택된 날짜 설정
setTotal(addTime(selectedDate, time)); // 선택된 날짜와 시간으로 total 업데이트
setCalendarOpen(false); // 캘린더 닫기
};
// 시간 선택 관련 훅 사용
const { onHourClickHandle, onMinuteClickHandle, convertToKST, addTime } =
useTimePicker({ time, setTime, setTotal, date, isTimeOpen, setIsTimeOpen });
// 화면에 표시될 데이터 포맷
const totalInvaild = dayjs(total).format("YYYY. MM. DD / HH: mm");
const queryClient = useQueryClient(); // 쿼리 클라이언트 초기화
// 동기화 요청을 위한 mutation 정의
const mutation = useMutation(() => postSyncList(total, data), {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["sync"] }); // 쿼리 무효화
},
onError: () => {
setError("서버와의 통신 중 문제가 발생했습니다."); // 에러 메시지 설정
},
});
// 확인 버튼 클릭 시 수행되는 함수
const onConfirmBtn = async () => {
try {
if (total === undefined) {
setError("모두 필수값입니다."); // 필수값 체크
} else if (total?.endsWith("00:00:00+09:00") === true) {
setError("모두 필수값입니다."); // 잘못된 데이터 체크
} else if (totalInvaild === "Invalid Date") {
setError("모두 필수값입니다."); // 잘못된 날짜 체크
} else {
// mutation.mutate({ values: { total: total, data: data } });
const kstTime = convertToKST(time.hour, time.minute); // 선택된 시간을 KST로 변환
setLoading(true); // 로딩 시작
setTimeout(async () => {
setLoading(false); // 로딩 종료
}, 3000);
setTotal(kstTime); // 총 시간 설정
toast({
description:
"동기화 요청을 보냈습니다. (api연결이 아직 되지 않았습니다.)", // 토스트 메시지
});
onClose(); // 모달 닫기
// 상태 초기화
setTotal("");
setError("");
setDate(undefined);
setTime({
hour: "00",
minute: "00",
});
}
} catch (error) {
setError("서버와의 통신 중 문제가 발생했습니다."); // 에러 메시지 설정
}
};
// 취소 버튼 클릭 시 수행되는 함수
const onCancleBtn = () => {
onClose(); // 모달 닫기
// 상태 초기화
setTotal("");
setError("");
setDate(undefined);
setTime({
hour: "00",
minute: "00",
});
};
// 제어 관련 훅 사용
const { onClickControlButton } = useControl({
setCalendarOpen,
setIsTimeOpen,
});
jsx 부분
return (
<div onClick={onClickControlButton}>
<AlertDialog open={isModalOpen}>
<AlertDialogContent className="md:w-[355px] h-[400px] bg-white rounded-lg shadow">
<AlertDialogHeader>
<AlertDialogTitle>동기화 예약</AlertDialogTitle>
<AlertDialogDescription>
언제 동기화하시겠습니까?
</AlertDialogDescription>
</AlertDialogHeader>
<Form {...form}>
<form onSubmit={(e) => e.preventDefault()}>
{/* 날짜,시간 픽커 */}
<p className="text-slate-600 text-xs font-normal mb-2">날짜</p>
<div className="relative flex flex-col gap-3 ">
<div>
<Button
variant="outline"
className={cn(
"w-[278px] justify-start text-left font-normal",
!date && "text-muted-foreground"
)}
onClick={(e) => {
e.stopPropagation();
setCalendarOpen(!isCalendarOpen); // 캘린더 열림/닫힘 토글
setIsTimeOpen(false); // 시간 선택 닫기
}}
>
<div className="w-full flex items-center justify-between">
{date ? (
format(date, "yyyy. MM. dd") // 선택된 날짜 포맷
) : (
<span className="text-black">----. --. --</span>
)}
<CalendarIcon
id="calendar"
color="black"
className="mr-[2px] h-3.5 w-3.5"
/>
</div>
</Button>
</div>
{isCalendarOpen && (
<div onClick={(e) => e.stopPropagation()}>
<div className="absolute top-[45px] bg-white shadow-lg border">
<Calendar
mode="single"
selected={date} // 선택된 날짜
onSelect={onSelect} // 날짜 선택 핸들러
initialFocus
disabledDays={[new Date()]} // 오늘 날짜 비활성화
/>
</div>
</div>
)}
<p className="text-slate-600 text-xs font-normal mb-2">시간</p>
<div>
<Button
variant="outline"
className={cn(
"w-[278px] justify-start text-left font-normal",
!time.hour && "text-muted-foreground"
)}
onClick={(e) => {
e.stopPropagation();
setIsTimeOpen(!isTimeOpen); // 시간 선택 열림/닫힘 토글
setCalendarOpen(false); // 캘린더 닫기
}}
>
<div className="w-full flex items-center justify-between">
{time.hour && time.minute ? (
`${time.hour}시 ${time.minute}분` // 선택된 시간 포맷
) : (
<span className="text-black">00시 00분</span>
)}
<AlarmClock
id="clock"
color="black"
className="mr-[2px] h-3.5 w-3.5"
/>
</div>
</Button>
</div>
{isTimeOpen && (
<div className="absolute top-[45px] w-[278px] bg-white border shadow-lg z-10">
<div className="flex justify-between">
<div className="flex flex-col border-r">
{timeHour.map((hour) => (
<Button
key={hour.value}
variant="outline"
className="h-8 text-xs font-normal w-full"
onClick={() => {
onHourClickHandle(hour.value); // 시 클릭 핸들러
}}
>
{hour.label} {/* 시 표시 */}
</Button>
))}
</div>
<div className="flex flex-col">
{timeMinute.map((minute) => (
<Button
key={minute.value}
variant="outline"
className="h-8 text-xs font-normal w-full"
onClick={() => {
onMinuteClickHandle(minute.value); // 분 클릭 핸들러
}}
>
{minute.label} {/* 분 표시 */}
</Button>
))}
</div>
</div>
</div>
)}
</div>
{/* 필수 입력 항목 확인 및 오류 메시지 출력 */}
{error && (
<p className="text-red-500 text-xs mt-2">{error}</p>
)}
<div className="flex justify-between mt-2">
<Button
variant="outline"
onClick={onCancleBtn} // 취소 버튼 클릭 핸들러
>
취소
</Button>
<Button
variant="default"
onClick={onConfirmBtn} // 확인 버튼 클릭 핸들러
>
{loading ? <Loader2 className="mr-2 animate-spin" /> : "확인"}
</Button>
</div>
</form>
</Form>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
export default SyncModal;
숫자 설정 관련 설명
- 시간과 분 설정: timeHour와 timeMinute는 각각 시간과 분을 나타내는 데이터입니다. 이 데이터는 사용자가 선택할 수 있는 시와 분의 목록을 제공합니다.
- 시간 선택: 사용자가 시간 버튼을 클릭하면, onHourClickHandle이나 onMinuteClickHandle 함수가 호출되어 선택된 시간을 time 상태로 업데이트합니다.
- 초기값 설정: 초기값으로 시와 분은 각각 "00"으로 설정되어 있으며, 이는 선택하지 않은 경우 기본적으로 표시됩니다.
- 날짜 및 시간 입력 처리: 사용자가 날짜와 시간을 선택한 후, 이 값들이 올바른 형식으로 변환되어 total 상태에 저장됩니다. total은 최종적으로 동기화 요청에 사용되는 값입니다.
이를 위해 로딩 상태, 에러 메시지 상태, 캘린더 열림 상태, 시간 선택 열림 상태 등 여러 가지 상태를 분리하여 관리했습니다. 이러한 상태 관리 방식을 통해 각 요소의 동작을 명확히 하고, 사용자 경험을 개선할 수 있었습니다.
'TIL' 카테고리의 다른 글
설계 단계_아키텍처 설계 (0) | 2024.05.10 |
---|---|
React Query (0) | 2023.11.13 |
TypeScript를 활용한 안전한 클라이언트-서버 통신 및 인증 관리 (0) | 2023.11.10 |
마크다운 사용 (0) | 2023.11.10 |
UTC와 KST (0) | 2023.11.10 |