datemonthPicker 추가

This commit is contained in:
leedj312
2025-06-28 15:00:43 +09:00
parent 381d95dd75
commit a5aeeb3d40
3 changed files with 209 additions and 1 deletions
@@ -0,0 +1,82 @@
'use client';
import { cn } from "../../lib/utils";
import {
Popover,
PopoverTrigger,
PopoverContent,
Button,
} from "@common/components/ui";
import { ko } from "date-fns/locale";
import { format } from "date-fns";
import { useState } from "react";
import { MonthPicker } from "./mothPicker";
import { CalendarIcon } from "lucide-react";
type MonthPickerProps = {
field: { value: string; onChange: (date: string) => void }; // yyyyMM 문자열
selectClass?: string;
readOnly?: boolean;
};
// yyyyMM → Date 객체
const parseYYYYMM = (value: string) => {
const year = Number(value.substring(0, 4));
const month = Number(value.substring(4, 6)) - 1;
return new Date(year, month);
};
// Date 객체 → yyyyMM 문자열
const formatDateToYYYYMM = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
return `${year}${month}`;
};
export const DateMonthPicker = ({
field,
selectClass,
readOnly = false,
}: MonthPickerProps) => {
const [open, setOpen] = useState(false);
const parsedDate = field?.value ? parseYYYYMM(field.value) : new Date();
const handleSelect = (
date: Date | undefined,
shouldClosePopover?: boolean
) => {
if (!readOnly && date) {
field.onChange(formatDateToYYYYMM(date));
if (shouldClosePopover) setOpen(false);
}
};
return (
<Popover open={open && !readOnly} onOpenChange={readOnly ? undefined : setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
color="white"
disabled={readOnly}
className={cn(
selectClass ?? "w-[240px] justify-between pl-3 text-left font-normal",
!field?.value && "text-muted-foreground",
readOnly && "opacity-60 cursor-not-allowed"
)}
>
{parsedDate
?format(parsedDate, "yyyy년 M월", { locale: ko })
: "월을 선택하세요"}
<CalendarIcon className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<MonthPicker
currentMonth={parsedDate}
onMonthChange={handleSelect}
/>
</PopoverContent>
</Popover>
);
};
@@ -0,0 +1,125 @@
import { cn } from "../../lib/utils";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components/ui";
import {
eachMonthOfInterval,
endOfYear,
format,
isEqual,
isFuture,
parse,
startOfMonth,
startOfToday,
} from "date-fns";
import { ko } from "date-fns/locale";
function getStartOfCurrentMonth() {
return startOfMonth(startOfToday());
}
export type MonthPickerProps = {
currentMonth: Date;
onMonthChange: (
newMonth: Date | undefined,
shouldClosePopover?: boolean
) => void;
};
export function MonthPicker({ currentMonth, onMonthChange }: MonthPickerProps) {
const today = startOfToday();
const currentYear = today.getFullYear();
const yearFromProp = format(currentMonth, "yyyy");
const selectedYear = parseInt(yearFromProp);
// 현재 월의 시작 날짜
const fullYearStart = parse(yearFromProp, "yyyy", new Date());
const yearEnd =
selectedYear === currentYear ? today : endOfYear(fullYearStart);
const yearRange = Array.from({ length: 21 }, (_, i) => currentYear - 20 + i);
const months = eachMonthOfInterval({ start: fullYearStart, end: yearEnd });
const getMonthButtonClass = (month: Date) =>
cn(
"inline-flex h-9 w-16 items-center justify-center rounded-md p-0 text-sm font-normal ring-offset-white transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-selected:opacity-100 dark:ring-offset-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50 dark:focus-visible:ring-slate-800",
isEqual(month, currentMonth) &&
"bg-slate-900 text-slate-50 hover:bg-slate-900 hover:text-slate-50 focus:bg-slate-900 focus:text-slate-50 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50 dark:hover:text-slate-900 dark:focus:bg-slate-50 dark:focus:text-slate-900",
!isEqual(month, currentMonth) &&
isEqual(month, getStartOfCurrentMonth()) &&
"bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-50"
);
return (
<div className="p-3">
<div className="flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
<div className="space-y-4">
{/* 년도 셀렉트 */}
<div className="flex justify-center pt-1">
<Select
value={yearFromProp}
onValueChange={(selectedYearStr) => {
const selectedYearNum = parseInt(selectedYearStr);
const newDate = new Date(
selectedYearNum,
currentMonth.getMonth(),
1
);
onMonthChange(newDate, false);
}}
>
<SelectTrigger className="w-[120px] h-7">
<SelectValue placeholder="년도" />
</SelectTrigger>
<SelectContent>
{yearRange.map((year) => (
<SelectItem key={year} value={String(year)}>
{year}년
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 월 그리드 */}
<div
className="grid w-full grid-cols-3 gap-2"
role="grid"
aria-labelledby="month-picker"
>
{months.map((month) => (
<div
key={month.toString()}
className="relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-slate-100 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md dark:[&:has([aria-selected])]:bg-slate-800"
role="presentation"
>
<button
name="month"
className={getMonthButtonClass(month)}
disabled={isFuture(month)}
role="gridcell"
tabIndex={-1}
type="button"
onClick={() => onMonthChange(month, true)}
>
<time dateTime={format(month, "yyyy-MM-dd")}>
{format(month, 'MMM', { locale: ko })}
</time>
</button>
</div>
))}
</div>
</div>
</div>
</div>
);
}
@@ -20,4 +20,5 @@ export * from "./table";
export * from "./table_div";
export * from "./title";
export * from "./sdmInsertForm";
export * from "./popover_ui"
export * from "./popover_ui";
export * from "./date_month_picker";