issue #60
All checks were successful
Test CI / build (push) Successful in 18s

- 날짜 선택 및 해당 날짜 일정 조회 화면 구현 중
This commit is contained in:
geonhee-min
2025-12-05 17:10:58 +09:00
parent 0c8e0893c7
commit 4a8e761b3d
8 changed files with 456 additions and 87 deletions

View File

@@ -117,4 +117,4 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }

80
src/const/ColorPalette.ts Normal file
View File

@@ -0,0 +1,80 @@
export type ColorPaletteType = {
style: string;
main: boolean;
}
export const ColorPalette: Record<any, ColorPaletteType> = {
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
},
}

65
src/hooks/use-palette.ts Normal file
View File

@@ -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
}
}

27
src/hooks/use-viewport.ts Normal file
View File

@@ -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;

View File

@@ -141,4 +141,12 @@ input[type="number"]::-webkit-outer-spin-button {
/* Firefox */ /* Firefox */
input[type="number"] { input[type="number"] {
-moz-appearance: textfield; -moz-appearance: textfield;
}
.rdp-week:not(:first-child) {
@apply border-t;
}
.rdp-day:not(:first-child) {
@apply border-l;
} }

View File

@@ -1,8 +1,11 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Calendar } from "@/components/ui/calendar"; 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 { 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 { interface CustomCalendarProps {
data?: any; data?: any;
@@ -11,7 +14,9 @@ interface CustomCalendarProps {
export const CustomCalendar = ({ data }: CustomCalendarProps) => { export const CustomCalendar = ({ data }: CustomCalendarProps) => {
const [weekCount, setWeekCount] = useState(5); const [weekCount, setWeekCount] = useState(5);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined); const [selectedDate, setSelectedDate] = useState<Date | undefined>(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 defaultClassNames = getDefaultClassNames();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const updateWeekCount = () => { const updateWeekCount = () => {
@@ -26,85 +31,160 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
useLayoutEffect(() => { useLayoutEffect(() => {
updateWeekCount(); updateWeekCount();
}, []); }, []);
const handleOpenChange = (open: boolean) => {
setPopoverOpen(open);
if (!open) {
setTimeout(() => {
setSelectedDate(undefined);
}, 150);
}
}
useEffect(() => { const handleDaySelect = (date: Date | undefined) => {
setSheetOpen(!!selectedDate); if (!date) {
}, [selectedDate]); 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 ( return (
<div <div
className="w-full h-full" className="w-full h-full"
ref={containerRef} ref={containerRef}
> >
<ScheduleSheet <Popover
open={sheetOpen} setOpen={setSheetOpen} date={selectedDate} open={popoverOpen}
/> onOpenChange={handleOpenChange}
<Calendar >
mode="single" <Calendar
className="h-full w-full border rounded-lg" mode="single"
selected={selectedDate} className="h-full w-full border rounded-lg"
onSelect={setSelectedDate} selected={selectedDate}
onMonthChange={() => { onSelect={handleDaySelect}
// month 바뀐 직후 DOM 변화가 생기므로 다음 프레임에서 계산 onMonthChange={() => {
requestAnimationFrame(() => { // month 바뀐 직후 DOM 변화가 생기므로 다음 프레임에서 계산
updateWeekCount(); requestAnimationFrame(() => {
}); updateWeekCount();
}} });
classNames={{ }}
months: cn( classNames={{
defaultClassNames.months, months: cn(
"w-full h-full relative" defaultClassNames.months,
), "w-full h-full relative"
nav: cn( ),
defaultClassNames.nav, nav: cn(
"flex w-full item-center gap-1 justify-around absolute top-0 inset-x-0" defaultClassNames.nav,
), "flex w-full item-center gap-1 justify-around absolute top-0 inset-x-0"
month: cn( ),
defaultClassNames.month, month: cn(
"h-full w-full flex flex-col" defaultClassNames.month,
), "h-full w-full flex flex-col"
month_grid: cn( ),
defaultClassNames.month_grid, month_grid: cn(
"w-full h-full flex-1" defaultClassNames.month_grid,
), "w-full h-full flex-1"
weeks: cn( ),
defaultClassNames.weeks, weeks: cn(
"w-full h-full" defaultClassNames.weeks,
), "w-full h-full"
weekdays: cn( ),
defaultClassNames.weekdays, weekdays: cn(
"w-full" defaultClassNames.weekdays,
), "w-full"
week: cn( ),
defaultClassNames.week, week: cn(
`w-full` defaultClassNames.week,
), `w-full`
day: cn( ),
defaultClassNames.day, day: cn(
`w-[calc(100%/7)]` defaultClassNames.day,
), `w-[calc(100%/7)] rounded-none`
day_button: cn( ),
defaultClassNames.day_button, day_button: cn(
"h-full w-full flex p-2 justify-start items-start", defaultClassNames.day_button,
"hover:bg-transparent", "h-full w-full flex p-2 justify-start items-start",
"data-[selected-single=true]:bg-transparent data-[selected-single=true]:text-black" "hover:bg-transparent",
), "data-[selected-single=true]:bg-transparent data-[selected-single=true]:text-black"
selected: cn( ),
defaultClassNames.selected, selected: cn(
"h-full border-0 fill-transparent" defaultClassNames.selected,
), "h-full border-0 fill-transparent"
today: cn( ),
defaultClassNames.today, today: cn(
"h-full" defaultClassNames.today,
), "h-full"
),
}}
styles={{ }}
day: { styles={{
height: `calc(100%/${weekCount})` day: {
}, height: `calc(100%/${weekCount})`
}} },
/> }}
components={{
Day: ({ day, ...props }) => {
const date = day.date;
const isSelected = selectedDate && isSameDay(selectedDate, date);
return (
<td {...props}>
{ isSelected
? <PopoverTrigger asChild>
{props.children}
</PopoverTrigger>
: props.children
}
</td>
)
},
DayButton: ({ day, ...props}) => (
<button
{...props}
disabled={day.outside}
>
{props.children}
</button>
)
}}
/>
<SchedulePopover
date={selectedDate}
popoverSide={popoverSide}
popoverAlign={popoverAlign}
/>
</Popover>
</div> </div>
) )
} }

View File

@@ -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 (
<PopoverContent
className="flex flex-col gap-1.5 w-fit"
>
{getSlicedList(mainPaletteList, 5).map((list) => (
<div className="flex flex-row gap-2.5">
{list.map((palette) => (
<div
className="rounded-full w-5 h-5 border border-gray-300"
style={{ backgroundColor: `${palette.style}` }}
onClick={() => setColor(palette)}
/>
))}
</div>
))}
{
!seeMore
? <div className="w-full" onClick={() => setSeeMore(true)}> </div>
: <>
{getSlicedList(extraPaletteList, 5).map((list) => (
<div className="flex flex-row gap-2.5">
{list.map((palette) => (
<div
className="rounded-full w-5 h-5 border border-gray-300"
style={{ backgroundColor: `${palette.style}` }}
onClick={() => setColor(palette)}
/>
))}
</div>
))}
</>
}
</PopoverContent>
)
}

View File

@@ -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 { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet';
import { cn } from '@/lib/utils';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { usePalette } from '@/hooks/use-palette';
import { type ColorPaletteType } from '@/const/ColorPalette';
import { ColorPickPopover } from './ColorPickPopover';
interface ScheduleSheetProps { interface ScheduleSheetProps {
date: Date | undefined; date: Date | undefined;
open: boolean; popoverSide: 'left' | 'right';
setOpen: (open: boolean) => void; 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 ( return (
<Sheet open={open} onOpenChange={setOpen}> <PopoverContent
<SheetContent className="rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[320px]"
className="md:w-[500px] sm:w-[400px] lg:w-[600px]" align={popoverAlign} side={popoverSide}
side="right" >
<ScrollArea
className={
cn(
"[&>div>div:last-child]:hidden min-h-[125px] h-[calc(100vh/2)] p-2.5 w-full flex flex-col",
)
}
> >
<SheetHeader></SheetHeader> <div className="w-full flex flex-row space-between gap-1.5">
</SheetContent> <Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
</Sheet> <PopoverTrigger asChild>
<div
className={cn(
'rounded-full w-5 h-5 border border-gray-300',
)}
style={{
backgroundColor: `${scheduleColor.style}`
}}
/>
</PopoverTrigger>
<ColorPickPopover
setColor={selectColor}
/>
</Popover>
</div>
</ScrollArea>
</PopoverContent>
) )
} }