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

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

@@ -142,3 +142,11 @@ input[type="number"]::-webkit-outer-spin-button {
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 = () => {
@@ -27,23 +32,68 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
updateWeekCount(); updateWeekCount();
}, []); }, []);
useEffect(() => { const handleOpenChange = (open: boolean) => {
setSheetOpen(!!selectedDate); setPopoverOpen(open);
}, [selectedDate]); if (!open) {
setTimeout(() => {
setSelectedDate(undefined);
}, 150);
}
}
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 ( 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 <Calendar
mode="single" mode="single"
className="h-full w-full border rounded-lg" className="h-full w-full border rounded-lg"
selected={selectedDate} selected={selectedDate}
onSelect={setSelectedDate} onSelect={handleDaySelect}
onMonthChange={() => { onMonthChange={() => {
// month 바뀐 직후 DOM 변화가 생기므로 다음 프레임에서 계산 // month 바뀐 직후 DOM 변화가 생기므로 다음 프레임에서 계산
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -81,7 +131,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
), ),
day: cn( day: cn(
defaultClassNames.day, defaultClassNames.day,
`w-[calc(100%/7)]` `w-[calc(100%/7)] rounded-none`
), ),
day_button: cn( day_button: cn(
defaultClassNames.day_button, defaultClassNames.day_button,
@@ -104,7 +154,37 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
height: `calc(100%/${weekCount})` 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) => {
return ( const {
<Sheet open={open} onOpenChange={setOpen}> ColorPaletteType,
<SheetContent getPaletteNameList,
className="md:w-[500px] sm:w-[400px] lg:w-[600px]" getMainPaletteList,
side="right" 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 (
<PopoverContent
className="rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[320px]"
align={popoverAlign} side={popoverSide}
> >
<SheetHeader></SheetHeader> <ScrollArea
</SheetContent> className={
</Sheet> cn(
"[&>div>div:last-child]:hidden min-h-[125px] h-[calc(100vh/2)] p-2.5 w-full flex flex-col",
)
}
>
<div className="w-full flex flex-row space-between gap-1.5">
<Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
<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>
) )
} }