From 4a8e761b3d80c2e01abaaf23c7a965baf1df84af Mon Sep 17 00:00:00 2001 From: geonhee-min Date: Fri, 5 Dec 2025 17:10:58 +0900 Subject: [PATCH] =?UTF-8?q?issue=20#60=20-=20=EB=82=A0=EC=A7=9C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EB=B0=8F=20=ED=95=B4=EB=8B=B9=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EC=9D=BC=EC=A0=95=20=EC=A1=B0=ED=9A=8C=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EA=B5=AC=ED=98=84=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.css | 2 +- src/const/ColorPalette.ts | 80 ++++++ src/hooks/use-palette.ts | 65 +++++ src/hooks/use-viewport.ts | 27 ++ src/index.css | 8 + src/ui/component/calendar/CustomCalendar.tsx | 230 ++++++++++++------ src/ui/component/popover/ColorPickPopover.tsx | 65 +++++ src/ui/component/popover/SchedulePopover.tsx | 66 ++++- 8 files changed, 456 insertions(+), 87 deletions(-) create mode 100644 src/const/ColorPalette.ts create mode 100644 src/hooks/use-palette.ts create mode 100644 src/hooks/use-viewport.ts create mode 100644 src/ui/component/popover/ColorPickPopover.tsx diff --git a/src/App.css b/src/App.css index f4c1e9b..7550e24 100644 --- a/src/App.css +++ b/src/App.css @@ -117,4 +117,4 @@ body { @apply bg-background text-foreground; } -} +} \ No newline at end of file diff --git a/src/const/ColorPalette.ts b/src/const/ColorPalette.ts new file mode 100644 index 0000000..0c50cfe --- /dev/null +++ b/src/const/ColorPalette.ts @@ -0,0 +1,80 @@ +export type ColorPaletteType = { + style: string; + main: boolean; +} + + +export const ColorPalette: Record = { + Black: { + style: '#000000', + main: true + }, + White: { + style: '#FFFFFF', + main: true + }, + PeachCream: { + style: '#FFDAB9', + main: false + }, + CoralPink: { + style: '#F08080', + main: true + }, + MintIcing: { + style: '#C1E1C1', + main: false + }, + Vanilla: { + style: '#FFFACD', + main: true + }, + Wheat: { + style: '#F5DEB3', + main: false + }, + AliceBlue: { + style: '#F0F8FF', + main: true + }, + Lavender: { + style: '#E6E6FA', + main: false + }, + LightAqua: { + style: '#A8E6CF', + main: true + }, + CloudWhite: { + style: '#F0F8FF', + main: false + }, + LightGray: { + style: '#D3D3D3', + main: true + }, + LightKhakki: { + style: '#F0F8E6', + main: false + }, + DustyRose: { + style: '#D8BFD8', + main: false + }, + CreamBeige: { + style: '#FAF0E6', + main: true, + }, + Oatmeal: { + style: '#FDF5E6', + main: false + }, + CharcoalLight: { + style: '#A9A9A9', + main: true + }, + custom: { + style: 'transprent', + main: true + }, +} \ No newline at end of file diff --git a/src/hooks/use-palette.ts b/src/hooks/use-palette.ts new file mode 100644 index 0000000..3afed53 --- /dev/null +++ b/src/hooks/use-palette.ts @@ -0,0 +1,65 @@ +import { ColorPalette, type ColorPaletteType } from "@/const/ColorPalette"; + +export function usePalette() { + const ColorPaletteType = typeof ColorPalette; + + const getPaletteNameList = () => { + return Object.keys(ColorPalette); + } + + const getMainPaletteList = () => { + const paletteKeys = Object.keys(ColorPalette); + const paletteList: ColorPaletteType[] = []; + paletteKeys.forEach((paletteKey) => { + const key = paletteKey as keyof typeof ColorPalette; + const palette: ColorPaletteType = ColorPalette[key]; + if (palette.main) { + paletteList.push(palette); + } + }); + + return paletteList; + } + + const getExtraPaletteList = () => { + const paletteKeys = Object.keys(ColorPalette); + const paletteList: ColorPaletteType[] = []; + paletteKeys.forEach((paletteKey) => { + const key = paletteKey as keyof typeof ColorPalette; + const palette: ColorPaletteType = ColorPalette[key]; + if (!palette.main) { + paletteList.push(palette); + } + }); + + return paletteList; + } + + const getAllPaletteList = [...getMainPaletteList(), ...getExtraPaletteList()]; + + const getPaletteByKey = (key: keyof typeof ColorPalette) => { + return ColorPalette[key]; + } + + const getCustomColor = (style: string) => { + return { + style: `#${style}`, + main: false + } as ColorPaletteType; + } + + const getStyle = (palette: ColorPaletteType) => { + return palette.style; + } + + return { + ColorPaletteType, + getPaletteNameList, + getMainPaletteList, + getExtraPaletteList, + getAllPaletteList, + getPaletteByKey, + getCustomColor, + getStyle + } +} \ No newline at end of file diff --git a/src/hooks/use-viewport.ts b/src/hooks/use-viewport.ts new file mode 100644 index 0000000..1e24635 --- /dev/null +++ b/src/hooks/use-viewport.ts @@ -0,0 +1,27 @@ +import { useState, useEffect } from 'react'; + +const useViewport = () => { + const [width, setWidth] = useState( + typeof window !== 'undefined' ? window.innerWidth : 0 + ); + const [height, setHeight] = useState( + typeof window !== 'undefined' ? window.innerHeight : 0 + ); + + useEffect(() => { + const handleResize = () => { + setWidth(window.innerWidth); + setHeight(window.innerHeight); + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + } + }, []); + + return { width, height }; +} + +export default useViewport; \ No newline at end of file diff --git a/src/index.css b/src/index.css index 68589cb..b2f3297 100644 --- a/src/index.css +++ b/src/index.css @@ -141,4 +141,12 @@ input[type="number"]::-webkit-outer-spin-button { /* Firefox */ input[type="number"] { -moz-appearance: textfield; +} + +.rdp-week:not(:first-child) { + @apply border-t; +} + +.rdp-day:not(:first-child) { + @apply border-l; } \ No newline at end of file diff --git a/src/ui/component/calendar/CustomCalendar.tsx b/src/ui/component/calendar/CustomCalendar.tsx index 5822664..779075e 100644 --- a/src/ui/component/calendar/CustomCalendar.tsx +++ b/src/ui/component/calendar/CustomCalendar.tsx @@ -1,8 +1,11 @@ import { cn } from "@/lib/utils"; import { Calendar } from "@/components/ui/calendar"; -import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { useLayoutEffect, useRef, useState } from "react"; import { getDefaultClassNames } from "react-day-picker"; -import { ScheduleSheet } from "../popover/SchedulePopover"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { isSameDay, getWeeksInMonth, getWeekOfMonth } from "date-fns"; +import { SchedulePopover } from "../popover/SchedulePopover"; interface CustomCalendarProps { data?: any; @@ -11,7 +14,9 @@ interface CustomCalendarProps { export const CustomCalendar = ({ data }: CustomCalendarProps) => { const [weekCount, setWeekCount] = useState(5); const [selectedDate, setSelectedDate] = useState(undefined); - const [sheetOpen, setSheetOpen] = useState(false); + const [popoverOpen, setPopoverOpen] = useState(false); + const [popoverSide, setPopoverSide] = useState<'right' | 'left'>('right'); + const [popoverAlign, setPopoverAlign] = useState<'start' | 'end'>('end'); const defaultClassNames = getDefaultClassNames(); const containerRef = useRef(null); const updateWeekCount = () => { @@ -26,85 +31,160 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => { useLayoutEffect(() => { updateWeekCount(); }, []); + + const handleOpenChange = (open: boolean) => { + setPopoverOpen(open); + if (!open) { + setTimeout(() => { + setSelectedDate(undefined); + }, 150); + } + } - useEffect(() => { - setSheetOpen(!!selectedDate); - }, [selectedDate]); + const handleDaySelect = (date: Date | undefined) => { + if (!date) { + setPopoverOpen(false); + setTimeout(() => { + setSelectedDate(undefined); + }, 150); + return; + } + if (date) { + setSelectedDate(date); + + const dayOfWeek = date.getDay(); + + if (0 <= dayOfWeek && dayOfWeek < 4) { + setPopoverSide('right'); + } else { + setPopoverSide('left'); + } + + const options = { weekStartsOn: 0 as 0 }; + + const totalWeeks = getWeeksInMonth(date, options); + + const currentWeekNumber = getWeekOfMonth(date, options); + + const threshold = Math.ceil(totalWeeks / 2); + + if (currentWeekNumber <= threshold) { + setPopoverAlign('start'); + } else { + setPopoverAlign('end'); + } + + requestAnimationFrame(() => { + setPopoverOpen(true); + }) + } + } return (
- - { - // month 바뀐 직후 DOM 변화가 생기므로 다음 프레임에서 계산 - requestAnimationFrame(() => { - updateWeekCount(); - }); - }} - classNames={{ - months: cn( - defaultClassNames.months, - "w-full h-full relative" - ), - nav: cn( - defaultClassNames.nav, - "flex w-full item-center gap-1 justify-around absolute top-0 inset-x-0" - ), - month: cn( - defaultClassNames.month, - "h-full w-full flex flex-col" - ), - month_grid: cn( - defaultClassNames.month_grid, - "w-full h-full flex-1" - ), - weeks: cn( - defaultClassNames.weeks, - "w-full h-full" - ), - weekdays: cn( - defaultClassNames.weekdays, - "w-full" - ), - week: cn( - defaultClassNames.week, - `w-full` - ), - day: cn( - defaultClassNames.day, - `w-[calc(100%/7)]` - ), - day_button: cn( - defaultClassNames.day_button, - "h-full w-full flex p-2 justify-start items-start", - "hover:bg-transparent", - "data-[selected-single=true]:bg-transparent data-[selected-single=true]:text-black" - ), - selected: cn( - defaultClassNames.selected, - "h-full border-0 fill-transparent" - ), - today: cn( - defaultClassNames.today, - "h-full" - ), - - }} - styles={{ - day: { - height: `calc(100%/${weekCount})` - }, - }} - /> + + { + // month 바뀐 직후 DOM 변화가 생기므로 다음 프레임에서 계산 + requestAnimationFrame(() => { + updateWeekCount(); + }); + }} + classNames={{ + months: cn( + defaultClassNames.months, + "w-full h-full relative" + ), + nav: cn( + defaultClassNames.nav, + "flex w-full item-center gap-1 justify-around absolute top-0 inset-x-0" + ), + month: cn( + defaultClassNames.month, + "h-full w-full flex flex-col" + ), + month_grid: cn( + defaultClassNames.month_grid, + "w-full h-full flex-1" + ), + weeks: cn( + defaultClassNames.weeks, + "w-full h-full" + ), + weekdays: cn( + defaultClassNames.weekdays, + "w-full" + ), + week: cn( + defaultClassNames.week, + `w-full` + ), + day: cn( + defaultClassNames.day, + `w-[calc(100%/7)] rounded-none` + ), + day_button: cn( + defaultClassNames.day_button, + "h-full w-full flex p-2 justify-start items-start", + "hover:bg-transparent", + "data-[selected-single=true]:bg-transparent data-[selected-single=true]:text-black" + ), + selected: cn( + defaultClassNames.selected, + "h-full border-0 fill-transparent" + ), + today: cn( + defaultClassNames.today, + "h-full" + ), + + }} + styles={{ + day: { + height: `calc(100%/${weekCount})` + }, + }} + components={{ + Day: ({ day, ...props }) => { + const date = day.date; + const isSelected = selectedDate && isSameDay(selectedDate, date); + return ( + + { isSelected + ? + {props.children} + + : props.children + } + + ) + }, + DayButton: ({ day, ...props}) => ( + + ) + }} + /> + +
) } \ No newline at end of file diff --git a/src/ui/component/popover/ColorPickPopover.tsx b/src/ui/component/popover/ColorPickPopover.tsx new file mode 100644 index 0000000..2595c6d --- /dev/null +++ b/src/ui/component/popover/ColorPickPopover.tsx @@ -0,0 +1,65 @@ +import { PopoverContent } from "@/components/ui/popover" +import type { ColorPaletteType } from "@/const/ColorPalette" +import { usePalette } from "@/hooks/use-palette"; +import { useState } from "react"; + +interface ColorPickPopoverProps { + setColor: (color: ColorPaletteType) => void; +} + +export const ColorPickPopover = ({ setColor }: ColorPickPopoverProps) => { + const [seeMore, setSeeMore] = useState(false); + const { + getMainPaletteList, + getExtraPaletteList, + getCustomColor + } = usePalette(); + const mainPaletteList = getMainPaletteList(); + const extraPaletteList = getExtraPaletteList(); + + const getSlicedList = (paletteList: ColorPaletteType[], length: number) => { + const slicedList: ColorPaletteType[][] = []; + let index = 0; + while (index < paletteList.length) { + slicedList.push(paletteList.slice(index, index + length)); + index += length; + } + return slicedList; + } + + return ( + + + {getSlicedList(mainPaletteList, 5).map((list) => ( +
+ {list.map((palette) => ( +
setColor(palette)} + /> + ))} +
+ ))} + { + !seeMore + ?
setSeeMore(true)}>더 보기
+ : <> + {getSlicedList(extraPaletteList, 5).map((list) => ( +
+ {list.map((palette) => ( +
setColor(palette)} + /> + ))} +
+ ))} + + } + + ) +} \ No newline at end of file diff --git a/src/ui/component/popover/SchedulePopover.tsx b/src/ui/component/popover/SchedulePopover.tsx index c3cb335..953eafe 100644 --- a/src/ui/component/popover/SchedulePopover.tsx +++ b/src/ui/component/popover/SchedulePopover.tsx @@ -1,22 +1,66 @@ +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { ScrollArea } from '@/components/ui/scroll-area'; import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet'; +import { cn } from '@/lib/utils'; import { useEffect, useState } from 'react'; +import { usePalette } from '@/hooks/use-palette'; +import { type ColorPaletteType } from '@/const/ColorPalette'; +import { ColorPickPopover } from './ColorPickPopover'; interface ScheduleSheetProps { date: Date | undefined; - open: boolean; - setOpen: (open: boolean) => void; + popoverSide: 'left' | 'right'; + popoverAlign: 'start' | 'end'; } -export const ScheduleSheet = ({ date, open, setOpen }: ScheduleSheetProps) => { +export const SchedulePopover = ({ date, popoverSide, popoverAlign }: ScheduleSheetProps) => { + const { + ColorPaletteType, + getPaletteNameList, + getMainPaletteList, + getAllPaletteList, + getCustomColor, + getPaletteByKey, + getStyle + } = usePalette(); + const defaultColor = getPaletteByKey('Black'); + const [scheduleColor, setScheduleColor] = useState(defaultColor); + const [colorPopoverOpen, setColorPopoverOpen] = useState(false); + const selectColor = (color: ColorPaletteType) => { + setScheduleColor(color); + setColorPopoverOpen(false); + } + return ( - - + div>div:last-child]:hidden min-h-[125px] h-[calc(100vh/2)] p-2.5 w-full flex flex-col", + ) + } > - - - +
+ + +
+ + + +
+ + ) } \ No newline at end of file