-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 공통 캘린더 컴포넌트 구현 #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
78abe01
0c11d89
3104604
37e7bdb
40c18d5
1d2bb2c
297fa69
c76827a
087b40d
4e0e1ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { useState } from "react"; | ||
|
|
||
| import { Calendar } from "./Calendar"; | ||
|
|
||
| import type { CalendarProps } from "./Calendar"; | ||
| import type { Meta, StoryObj } from "@storybook/react"; | ||
|
|
||
| const meta = { | ||
| title: "Components/Calendar", | ||
| component: Calendar, | ||
| parameters: { | ||
| layout: "centered", | ||
| }, | ||
| } satisfies Meta<typeof Calendar>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| const CalendarDemo = (args: CalendarProps) => { | ||
| const [selectedDate, setSelectedDate] = useState<Date | undefined>( | ||
| new Date(2026, 6, 1), | ||
| ); | ||
|
|
||
| return ( | ||
| <Calendar | ||
| {...args} | ||
| value={selectedDate} | ||
| defaultMonth={new Date(2026, 6, 1)} | ||
| onChange={setSelectedDate} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| export const Default: Story = { | ||
| render: (args) => <CalendarDemo {...args} />, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| import { useState } from "react"; | ||
|
|
||
| import { cn } from "../../lib"; | ||
| import { ChevronButton } from "../button/chevron-button/ChevronButton"; | ||
|
|
||
| const WEEKDAYS = ["S", "M", "T", "W", "T", "F", "S"]; | ||
|
|
||
| const MONTH_LABELS = [ | ||
| "January", | ||
| "February", | ||
| "March", | ||
| "April", | ||
| "May", | ||
| "June", | ||
| "July", | ||
| "August", | ||
| "September", | ||
| "October", | ||
| "November", | ||
| "December", | ||
| ]; | ||
|
yumin-kim2 marked this conversation as resolved.
|
||
|
|
||
| interface CalendarDate { | ||
| date: Date; | ||
| day: number; | ||
| isCurrentMonth: boolean; | ||
| } | ||
|
|
||
| const getCalendarDates = (month: Date): CalendarDate[] => { | ||
| const year = month.getFullYear(); | ||
| const monthIndex = month.getMonth(); | ||
| const firstDate = new Date(year, monthIndex, 1); | ||
| const firstDay = firstDate.getDay(); | ||
| const lastDate = new Date(year, monthIndex + 1, 0); | ||
| const lastDay = lastDate.getDate(); | ||
|
|
||
| const calendarCellCount = firstDay + lastDay > 35 ? 42 : 35; | ||
|
|
||
| const startDate = new Date(year, monthIndex, 1 - firstDay); | ||
|
|
||
| return Array.from({ length: calendarCellCount }, (_, index) => { | ||
| const date = new Date( | ||
| startDate.getFullYear(), | ||
| startDate.getMonth(), | ||
| startDate.getDate() + index, | ||
| ); | ||
|
|
||
| return { | ||
| date, | ||
| day: date.getDate(), | ||
| isCurrentMonth: date.getMonth() === monthIndex, | ||
| }; | ||
| }); | ||
| }; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| export interface CalendarProps { | ||
| value?: Date; | ||
| defaultMonth?: Date; | ||
| onChange?: (date: Date) => void; | ||
| className?: string; | ||
| } | ||
|
|
||
| export const Calendar = ({ | ||
| value, | ||
| defaultMonth, | ||
| onChange, | ||
| className, | ||
| }: CalendarProps) => { | ||
| const [visibleMonth, setVisibleMonth] = useState( | ||
| defaultMonth ?? value ?? new Date(), | ||
| ); | ||
|
Comment on lines
+69
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🔄 value 변경 시 visibleMonth 동기화 제안+import { useEffect, useState } from "react";
-import { useState } from "react";
export const Calendar = ({ value, defaultMonth, onChange, className }: CalendarProps) => {
const [visibleMonth, setVisibleMonth] = useState(
defaultMonth ?? value ?? new Date(),
);
+ useEffect(() => {
+ if (value) {
+ setVisibleMonth(value);
+ }
+ }, [value]);🤖 Prompt for AI Agents |
||
| const calendarDates = getCalendarDates(visibleMonth); | ||
| const today = new Date(); | ||
| const isSameDate = (a: Date, b: Date) => { | ||
| return ( | ||
| a.getFullYear() === b.getFullYear() && | ||
| a.getMonth() === b.getMonth() && | ||
| a.getDate() === b.getDate() | ||
| ); | ||
| }; | ||
|
|
||
| const handleMoveMonth = (amount: number) => { | ||
| setVisibleMonth( | ||
| (prev) => new Date(prev.getFullYear(), prev.getMonth() + amount, 1), | ||
| ); | ||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| className={cn("h-66 w-67 rounded-[4px] bg-white px-6 py-4", className)} | ||
| > | ||
| <div className="relative mb-4 h-8"> | ||
| <ChevronButton | ||
| variant="left" | ||
| onClick={() => handleMoveMonth(-1)} | ||
| className="absolute top-0 left-0" | ||
| /> | ||
|
|
||
| <div className="flex h-full items-center justify-center gap-1"> | ||
| <span className="typo-headline-b-16 text-timo-gray-700"> | ||
| {MONTH_LABELS[visibleMonth.getMonth()]} | ||
| </span> | ||
| <span className="typo-headline-b-16 text-timo-gray-700"> | ||
| {visibleMonth.getFullYear()} | ||
| </span> | ||
| </div> | ||
|
|
||
| <ChevronButton | ||
| variant="right" | ||
| onClick={() => handleMoveMonth(1)} | ||
| className="absolute top-0 right-0" | ||
| /> | ||
| </div> | ||
|
|
||
| <div className={cn("grid grid-cols-7 grid-rows-6 gap-x-1 gap-y-1.5")}> | ||
| {WEEKDAYS.map((weekday, index) => ( | ||
| <div | ||
| key={`${weekday}-${index}`} | ||
| className="typo-headline-b-16 text-timo-gray-900 flex items-center justify-center" | ||
| > | ||
| {weekday} | ||
| </div> | ||
| ))} | ||
|
|
||
| {calendarDates.map((calendarDate) => { | ||
| const isSelected = value | ||
| ? isSameDate(calendarDate.date, value) | ||
| : false; | ||
| const isToday = isSameDate(calendarDate.date, today); | ||
|
|
||
| return ( | ||
| <button | ||
| key={calendarDate.date.toISOString()} | ||
| type="button" | ||
| onClick={() => { | ||
| if (!calendarDate.isCurrentMonth) { | ||
| setVisibleMonth( | ||
| new Date( | ||
| calendarDate.date.getFullYear(), | ||
| calendarDate.date.getMonth(), | ||
| 1, | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| onChange?.(calendarDate.date); | ||
| }} | ||
| className={cn( | ||
| "flex h-6.5 w-7 items-center justify-center", | ||
| isToday ? "typo-headline-b-14" : "typo-headline-r-14", | ||
| calendarDate.isCurrentMonth | ||
| ? "text-timo-gray-700" | ||
| : "text-timo-gray-500", | ||
| isToday && "text-timo-gray-900", | ||
| isSelected && "bg-timo-blue-300 rounded-[4px] text-white", | ||
| )} | ||
| > | ||
| {calendarDate.day} | ||
| </button> | ||
| ); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| })} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.