- 일정 목록 조회 1차 구현 - 일정 당일 목록 조회 1차 구현
This commit is contained in:
@@ -214,7 +214,7 @@ function CalendarDayButton({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
data-day={day.date.toLocaleDateString()}
|
data-day={day.date.toISOString()}
|
||||||
data-selected-single={
|
data-selected-single={
|
||||||
modifiers.selected &&
|
modifiers.selected &&
|
||||||
!modifiers.range_start &&
|
!modifiers.range_start &&
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
<div
|
<div
|
||||||
data-slot="card"
|
data-slot="card"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-indigo-200 py-6 shadow-sm shadow-indigo-200",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function PopoverContent({
|
|||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-indigo-200 p-4 shadow-md shadow-indigo-200 outline-hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export type ScheduleType = 'once' | 'daily' | 'weekly' | 'monthly' | 'annual';
|
export type ScheduleType = 'once' | 'daily' | 'weekly' | 'monthly' | 'annual';
|
||||||
|
|
||||||
export const ScheduleTypeLabel: Record<ScheduleType, string> = {
|
export const ScheduleTypeLabel: Record<ScheduleType, string> = {
|
||||||
'once': '한 번만',
|
'once': '반복없음',
|
||||||
'daily': '매일',
|
'daily': '매일',
|
||||||
'weekly': '매주',
|
'weekly': '매주',
|
||||||
'monthly': '매월',
|
'monthly': '매월',
|
||||||
|
|||||||
@@ -4,12 +4,8 @@ export const ListScheduleSchema = z.object({
|
|||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
, startDate: z
|
, date: z
|
||||||
.date()
|
.date()
|
||||||
.optional()
|
|
||||||
, endDate: z
|
|
||||||
.date()
|
|
||||||
.optional()
|
|
||||||
, status: z
|
, status: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import type { ScheduleType } from "@/const/schedule/ScheduleType";
|
|||||||
export class CreateScheduleRequest {
|
export class CreateScheduleRequest {
|
||||||
name: string;
|
name: string;
|
||||||
content: string;
|
content: string;
|
||||||
startDate: Date;
|
startDate: string;
|
||||||
endDate: Date;
|
endDate: string;
|
||||||
status: ScheduleStatus;
|
status: ScheduleStatus;
|
||||||
type: ScheduleType;
|
type: ScheduleType;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
@@ -16,8 +16,8 @@ export class CreateScheduleRequest {
|
|||||||
constructor (
|
constructor (
|
||||||
name: string,
|
name: string,
|
||||||
content: string,
|
content: string,
|
||||||
startDate: Date,
|
startDate: string,
|
||||||
endDate: Date,
|
endDate: string,
|
||||||
status: ScheduleStatus,
|
status: ScheduleStatus,
|
||||||
type: ScheduleType,
|
type: ScheduleType,
|
||||||
startTime: string,
|
startTime: string,
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import type { ScheduleStatus } from "@/const/schedule/ScheduleStatus";
|
|||||||
import type { ScheduleType } from "@/const/schedule/ScheduleType";
|
import type { ScheduleType } from "@/const/schedule/ScheduleType";
|
||||||
|
|
||||||
export class ScheduleListRequest {
|
export class ScheduleListRequest {
|
||||||
startDate?: Date;
|
date?: string;
|
||||||
endDate?: Date;
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
typeList?: ScheduleType[];
|
typeList?: ScheduleType[];
|
||||||
styleList?: string[];
|
styleList?: string[];
|
||||||
status?: ScheduleStatus;
|
status?: ScheduleStatus;
|
||||||
|
|||||||
@@ -144,9 +144,9 @@ input[type="number"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.custom-rdp-week:not(:first-child) {
|
.custom-rdp-week:not(:first-child) {
|
||||||
@apply border-t!;
|
@apply border-t! border-indigo-200!;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-rdp-day:not(:first-child) {
|
.custom-rdp-day:not(:first-child) {
|
||||||
@apply border-l!;
|
@apply border-l! border-indigo-200!;
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ export default function Header() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="w-full flex shrink-0 flex-row justify-between items-center border-b px-4 h-12">
|
<header className="w-full flex shrink-0 flex-row justify-between items-center border-b border-b-indigo-200 px-4 h-12">
|
||||||
<div className="flex flex-row gap-2 items-center">
|
<div className="flex flex-row gap-2 items-center">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
||||||
|
|||||||
@@ -1,37 +1,261 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { useLayoutEffect, useRef, useState } from "react";
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { getDefaultClassNames } from "react-day-picker";
|
import { Popover, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { isSameDay, getWeeksInMonth, getWeekOfMonth, eachDayOfInterval, parse, startOfMonth, startOfWeek, endOfWeek, endOfMonth, format, isSameMonth } from "date-fns";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { SchedulePopover } from "../schedule/SchedulePopover";
|
||||||
import { isSameDay, getWeeksInMonth, getWeekOfMonth } from "date-fns";
|
import { ScheduleNetwork } from "@/network/ScheduleNetwork";
|
||||||
import { SchedulePopover } from "../popover/schedule/SchedulePopover";
|
import { ScheduleListData } from "@/data/response";
|
||||||
|
import { CustomCalendarCN } from "./CustomCalendarCN";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Converter } from "@/util/Converter";
|
||||||
|
|
||||||
interface CustomCalendarProps {
|
interface CustomCalendarProps {
|
||||||
data?: any;
|
data?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EventBarPosition extends ScheduleListData {
|
||||||
|
positionStyle: React.CSSProperties;
|
||||||
|
trackIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_VISIBLE_EVENTS = 3;
|
||||||
|
const OVERFLOW_TRACK_INDEX = MAX_VISIBLE_EVENTS + 1;
|
||||||
|
const TRACK_HEIGHT = 20;
|
||||||
|
const TOP_OFFSET_FROM_CELL = 35;
|
||||||
|
const DATE_FORMAT_ARIA = 'EEEE, MMMM do, yyyy';
|
||||||
|
const DATE_FORMAT_KEY = 'yyyyMMdd';
|
||||||
|
|
||||||
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 [popoverOpen, setPopoverOpen] = useState(false);
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
const [popoverSide, setPopoverSide] = useState<'right' | 'left'>('right');
|
const [popoverSide, setPopoverSide] = useState<'right' | 'left'>('right');
|
||||||
const [popoverAlign, setPopoverAlign] = useState<'start' | 'end'>('end');
|
const [popoverAlign, setPopoverAlign] = useState<'start' | 'end'>('end');
|
||||||
const defaultClassNames = getDefaultClassNames();
|
const [month, setMonth] = useState(new Date());
|
||||||
|
const [currentDataMonth, setCurrentDataMonth] = useState(month);
|
||||||
|
const [scheduleList, setScheduleList] = useState<Array<ScheduleListData>>([]);
|
||||||
|
const [barPositions, setBarPositions] = useState<Array<EventBarPosition>>([]);
|
||||||
|
const scheduleNetwork = new ScheduleNetwork();
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const updateWeekCount = () => {
|
const updateWeekCount = () => {
|
||||||
if (containerRef === null) return;
|
if (containerRef === null) return;
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
const weeks = containerRef.current.querySelectorAll('.rdp-week');
|
const weeks = containerRef.current.querySelectorAll('.rdp-week');
|
||||||
|
|
||||||
if (weeks?.length) setWeekCount(weeks.length);
|
if (weeks?.length) setWeekCount(weeks.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
updateWeekCount();
|
updateWeekCount();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
updateWeekCount();
|
||||||
|
|
||||||
|
const reqList = async () => {
|
||||||
|
const requestedMonth = month;
|
||||||
|
|
||||||
|
const monthStart = startOfMonth(month);
|
||||||
|
const monthEnd = endOfMonth(month);
|
||||||
|
|
||||||
|
const startDate = startOfWeek(monthStart, { weekStartsOn: 0 });
|
||||||
|
const endDate = endOfWeek(monthEnd, { weekStartsOn: 0 });
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
startDate: Converter.dateToUTC9(startDate),
|
||||||
|
endDate: Converter.dateToUTC9(endDate)
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await scheduleNetwork.getList(data);
|
||||||
|
|
||||||
|
if (result.data.success) {
|
||||||
|
if (result.data.data) {
|
||||||
|
if (isSameMonth(requestedMonth, month)) {
|
||||||
|
setScheduleList(result.data.data!);
|
||||||
|
setCurrentDataMonth(month);
|
||||||
|
}
|
||||||
|
setScheduleList(result.data.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const reqListPromise = reqList;
|
||||||
|
|
||||||
|
toast.promise(
|
||||||
|
reqListPromise,
|
||||||
|
{
|
||||||
|
loading: `${month.getFullYear()}년 ${month.getMonth() + 1}월 일정을 불러오는 중입니다`,
|
||||||
|
success: `일정을 불러왔습니다.`,
|
||||||
|
error: `일정을 불러오는 데에 실패하였습니다.\n잠시 후 다시 시도해주십시오.`,
|
||||||
|
duration: 1000
|
||||||
|
},
|
||||||
|
)
|
||||||
|
updateWeekCount();
|
||||||
|
});
|
||||||
|
}, [month]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!isSameMonth(month, currentDataMonth)) {
|
||||||
|
setBarPositions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!containerRef.current || scheduleList.length === 0) {
|
||||||
|
setBarPositions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerRect = containerRef.current.getBoundingClientRect();
|
||||||
|
const dayCells = containerRef.current.querySelectorAll('.rdp-day button');
|
||||||
|
|
||||||
|
const cellInfoMap = new Map<string, { cell: HTMLElement, rect: DOMRect }>();
|
||||||
|
|
||||||
|
dayCells.forEach((cell) => {
|
||||||
|
const dayButton = cell as HTMLButtonElement;
|
||||||
|
const ariaLabel = dayButton.getAttribute('aria-label');
|
||||||
|
|
||||||
|
if (ariaLabel) {
|
||||||
|
try {
|
||||||
|
const parsedDate = parse(ariaLabel, DATE_FORMAT_ARIA, new Date());
|
||||||
|
|
||||||
|
if (!isNaN(parsedDate.getTime())) {
|
||||||
|
const dateKey = format(parsedDate, DATE_FORMAT_KEY);
|
||||||
|
|
||||||
|
cellInfoMap.set(dateKey, {
|
||||||
|
cell: dayButton,
|
||||||
|
rect: dayButton.getBoundingClientRect()
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const scheduleListWithTrack: (ScheduleListData & { trackIndex: number })[] = [];
|
||||||
|
const occupiedTrackList = new Map<string, number[]>();
|
||||||
|
const overflowCountMap = new Map<string, number>();
|
||||||
|
|
||||||
|
const sortedScheduleList = [...scheduleList].sort((a, b) =>
|
||||||
|
new Date(b.endDate).getTime() - new Date(b.startDate).getTime() -
|
||||||
|
(new Date(a.endDate).getTime() - new Date(a.startDate).getTime())
|
||||||
|
);
|
||||||
|
|
||||||
|
sortedScheduleList.forEach(schedule => {
|
||||||
|
const startDateObj = new Date(schedule.startDate);
|
||||||
|
const endDateObj = new Date(schedule.endDate);
|
||||||
|
|
||||||
|
const eventDays = eachDayOfInterval({ start: startDateObj, end: endDateObj });
|
||||||
|
|
||||||
|
let assignedTrack = -1;
|
||||||
|
|
||||||
|
for (let track = 0; track < MAX_VISIBLE_EVENTS; track++) {
|
||||||
|
let isAvailable = true;
|
||||||
|
for (const day of eventDays) {
|
||||||
|
const dayKey = format(day, DATE_FORMAT_KEY);
|
||||||
|
if (cellInfoMap.has(dayKey)) {
|
||||||
|
const occupied = occupiedTrackList.get(dayKey) || [];
|
||||||
|
|
||||||
|
if (occupied.includes(track)) {
|
||||||
|
isAvailable = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAvailable) {
|
||||||
|
assignedTrack = track;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assignedTrack !== -1) {
|
||||||
|
for (const day of eventDays) {
|
||||||
|
const dayKey = format(day, DATE_FORMAT_KEY);
|
||||||
|
if (cellInfoMap.has(dayKey)) {
|
||||||
|
const occupied = occupiedTrackList.get(dayKey) || [];
|
||||||
|
occupiedTrackList.set(dayKey, [...occupied, assignedTrack]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduleListWithTrack.push({ ...schedule, trackIndex: assignedTrack });
|
||||||
|
} else {
|
||||||
|
for (const day of eventDays) {
|
||||||
|
const dayKey = format(day, DATE_FORMAT_KEY);
|
||||||
|
if (cellInfoMap.has(dayKey)) {
|
||||||
|
overflowCountMap.set(dayKey, (overflowCountMap.get(dayKey) || 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const regularPositions: EventBarPosition[] = [];
|
||||||
|
|
||||||
|
scheduleListWithTrack.forEach(schedule => {
|
||||||
|
const startKey = format(new Date(schedule.startDate), DATE_FORMAT_KEY);
|
||||||
|
const endKey = format(new Date(schedule.endDate), DATE_FORMAT_KEY);
|
||||||
|
|
||||||
|
const startInfo = cellInfoMap.get(startKey);
|
||||||
|
const endInfo = cellInfoMap.get(endKey);
|
||||||
|
|
||||||
|
if (startInfo && endInfo) {
|
||||||
|
const startRect = startInfo.rect;
|
||||||
|
const endRect = endInfo.rect;
|
||||||
|
|
||||||
|
const baseTop = startRect.top - containerRect.top;
|
||||||
|
const top = baseTop + TOP_OFFSET_FROM_CELL + (schedule.trackIndex * (TRACK_HEIGHT + 5));
|
||||||
|
const left = startRect.left - containerRect.left + 5;
|
||||||
|
const width = endRect.right - startRect.left - 10;
|
||||||
|
|
||||||
|
regularPositions.push({
|
||||||
|
...schedule,
|
||||||
|
trackIndex: schedule.trackIndex,
|
||||||
|
positionStyle: {
|
||||||
|
top: `${top}px`,
|
||||||
|
left: `${left}px`,
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${TRACK_HEIGHT}px`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const overflowPositions: EventBarPosition[] = [];
|
||||||
|
overflowCountMap.forEach((count, dayKey) => {
|
||||||
|
const dayInfo = cellInfoMap.get(dayKey);
|
||||||
|
|
||||||
|
if (dayInfo) {
|
||||||
|
const dayRect = dayInfo.rect;
|
||||||
|
|
||||||
|
const baseTop = dayRect.top - containerRect.top;
|
||||||
|
const top = baseTop + TOP_OFFSET_FROM_CELL + ((OVERFLOW_TRACK_INDEX - 1) * (TRACK_HEIGHT + 5));
|
||||||
|
const left = dayRect.left - containerRect.left + 5;
|
||||||
|
const width = dayInfo.cell.clientWidth - 10;
|
||||||
|
|
||||||
|
overflowPositions.push({
|
||||||
|
id: `overflow-${dayKey}`,
|
||||||
|
name: `${count} more`,
|
||||||
|
startDate: Converter.dateToUTC9(new Date()),
|
||||||
|
endDate: Converter.dateToUTC9(new Date()),
|
||||||
|
style: '#9CA3AF',
|
||||||
|
trackIndex: OVERFLOW_TRACK_INDEX,
|
||||||
|
positionStyle: {
|
||||||
|
top: `${top}px`,
|
||||||
|
left: `${left}px`,
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${TRACK_HEIGHT}px`
|
||||||
|
}
|
||||||
|
} as EventBarPosition)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setBarPositions([...regularPositions, ...overflowPositions]);
|
||||||
|
}, [scheduleList, month, currentDataMonth]);
|
||||||
|
|
||||||
|
const handleMonthChange = async (month: Date) => {
|
||||||
|
setMonth(month);
|
||||||
|
}
|
||||||
|
|
||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
setPopoverOpen(open);
|
setPopoverOpen(open);
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@@ -82,7 +306,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full h-full"
|
className="w-full h-full relative"
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
<Popover
|
<Popover
|
||||||
@@ -91,66 +315,12 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
>
|
>
|
||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
className="h-full w-full border rounded-lg"
|
className="h-full w-full border border-indigo-200 rounded-lg shadow-sm shadow-indigo-200"
|
||||||
selected={selectedDate}
|
selected={selectedDate}
|
||||||
onSelect={handleDaySelect}
|
onSelect={handleDaySelect}
|
||||||
onMonthChange={() => {
|
month={month}
|
||||||
// month 바뀐 직후 DOM 변화가 생기므로 다음 프레임에서 계산
|
onMonthChange={handleMonthChange}
|
||||||
requestAnimationFrame(() => {
|
classNames={CustomCalendarCN}
|
||||||
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`,
|
|
||||||
'custom-rdp-week'
|
|
||||||
),
|
|
||||||
day: cn(
|
|
||||||
defaultClassNames.day,
|
|
||||||
`w-[calc(100%/7)] rounded-none`,
|
|
||||||
'custom-rdp-day'
|
|
||||||
),
|
|
||||||
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={{
|
styles={{
|
||||||
day: {
|
day: {
|
||||||
height: `calc(100%/${weekCount})`
|
height: `calc(100%/${weekCount})`
|
||||||
@@ -174,13 +344,26 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
DayButton: ({ day, ...props}) => (
|
DayButton: ({ day, ...props}) => (
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
disabled={day.outside}
|
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
barPositions.map(pos => (
|
||||||
|
<div
|
||||||
|
key={pos.id}
|
||||||
|
className={cn(
|
||||||
|
`flex flex-row justify-start items-center absolute`,
|
||||||
|
"py-0.5 px-2 rounded-sm text-xs text-white overflow-hidden pointer-events-none"
|
||||||
|
)}
|
||||||
|
style={{...pos.positionStyle, backgroundColor: pos.style}}
|
||||||
|
>
|
||||||
|
{pos.name}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
<SchedulePopover
|
<SchedulePopover
|
||||||
date={selectedDate}
|
date={selectedDate}
|
||||||
open={popoverOpen}
|
open={popoverOpen}
|
||||||
|
|||||||
55
src/ui/component/calendar/CustomCalendarCN.ts
Normal file
55
src/ui/component/calendar/CustomCalendarCN.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { getDefaultClassNames } from "react-day-picker";
|
||||||
|
|
||||||
|
const defaultCN = getDefaultClassNames();
|
||||||
|
|
||||||
|
export const CustomCalendarCN = {
|
||||||
|
months: cn(
|
||||||
|
defaultCN.months,
|
||||||
|
"w-full h-full relative"
|
||||||
|
),
|
||||||
|
nav: cn(
|
||||||
|
defaultCN.nav,
|
||||||
|
"flex w-full item-center gap-1 justify-around absolute top-0 inset-x-0"
|
||||||
|
),
|
||||||
|
month: cn(
|
||||||
|
defaultCN.month,
|
||||||
|
"h-full w-full flex flex-col"
|
||||||
|
),
|
||||||
|
month_grid: cn(
|
||||||
|
defaultCN.month_grid,
|
||||||
|
"w-full h-full flex-1"
|
||||||
|
),
|
||||||
|
weeks: cn(
|
||||||
|
defaultCN.weeks,
|
||||||
|
"w-full h-full"
|
||||||
|
),
|
||||||
|
weekdays: cn(
|
||||||
|
defaultCN.weekdays,
|
||||||
|
"w-full"
|
||||||
|
),
|
||||||
|
week: cn(
|
||||||
|
defaultCN.week,
|
||||||
|
`w-full`,
|
||||||
|
'custom-rdp-week'
|
||||||
|
),
|
||||||
|
day: cn(
|
||||||
|
defaultCN.day,
|
||||||
|
`w-[calc(100%/7)] rounded-none`,
|
||||||
|
'custom-rdp-day'
|
||||||
|
),
|
||||||
|
day_button: cn(
|
||||||
|
defaultCN.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(
|
||||||
|
defaultCN.selected,
|
||||||
|
"h-full border-0 fill-transparent"
|
||||||
|
),
|
||||||
|
today: cn(
|
||||||
|
defaultCN.today,
|
||||||
|
"h-full"
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
import { PopoverContent } from '@/components/ui/popover';
|
import { PopoverContent } from '@/components/ui/popover';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { useEffect, useState } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { PenSquare } from 'lucide-react';
|
|
||||||
|
|
||||||
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
|
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { ScheduleNetwork } from '@/network/ScheduleNetwork';
|
|
||||||
import { ScheduleListContent } from './content/ScheduleListContent';
|
import { ScheduleListContent } from './content/ScheduleListContent';
|
||||||
|
|
||||||
interface ScheduleSheetProps {
|
interface ScheduleSheetProps {
|
||||||
@@ -19,6 +13,14 @@ interface ScheduleSheetProps {
|
|||||||
export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: ScheduleSheetProps) => {
|
export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: ScheduleSheetProps) => {
|
||||||
const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list');
|
const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setMode('list');
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
const DetailContent = () => {
|
const DetailContent = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -21,11 +21,12 @@ import { useState } from 'react';
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { ColorPickPopover } from '../ColorPickPopover';
|
import { ColorPickPopover } from '../popover/ColorPickPopover';
|
||||||
import { DatePickPopover } from '../DatePickPopover';
|
import { DatePickPopover } from '../popover/DatePickPopover';
|
||||||
import { TimePickPopover } from '../TimePickPopover';
|
import { TimePickPopover } from '../popover/TimePickPopover';
|
||||||
import { TypePickPopover } from '../TypePickPopover';
|
import { TypePickPopover } from '../popover/TypePickPopover';
|
||||||
import type { ScheduleCreateContentProps } from './ContentProps';
|
import type { ScheduleCreateContentProps } from './ContentProps';
|
||||||
|
import { Converter } from '@/util/Converter';
|
||||||
|
|
||||||
export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign }: ScheduleCreateContentProps) => {
|
export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign }: ScheduleCreateContentProps) => {
|
||||||
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
|
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
|
||||||
@@ -75,8 +76,8 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
|||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
const data = {
|
const data = {
|
||||||
name,
|
name,
|
||||||
startDate,
|
startDate: Converter.dateToUTC9(startDate),
|
||||||
endDate,
|
endDate: Converter.dateToUTC9(endDate),
|
||||||
content,
|
content,
|
||||||
startTime: standardTimeToContinentalTime(startTime),
|
startTime: standardTimeToContinentalTime(startTime),
|
||||||
endTime: standardTimeToContinentalTime(endTime),
|
endTime: standardTimeToContinentalTime(endTime),
|
||||||
@@ -226,7 +227,7 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-full w-5 h-5 border-2 border-gray-300',
|
'rounded-full w-5 h-5 border-2 border-gray-300 hover:border-indigo-300 transition-all duration-150',
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${style}`,
|
backgroundColor: `${style}`,
|
||||||
@@ -11,6 +11,8 @@ import { ScheduleNetwork } from "@/network/ScheduleNetwork";
|
|||||||
import type { ScheduleStatus } from "@/const/schedule/ScheduleStatus";
|
import type { ScheduleStatus } from "@/const/schedule/ScheduleStatus";
|
||||||
import type { ScheduleType } from "@/const/schedule/ScheduleType";
|
import type { ScheduleType } from "@/const/schedule/ScheduleType";
|
||||||
import { ScheduleListData } from "@/data/response";
|
import { ScheduleListData } from "@/data/response";
|
||||||
|
import { Converter } from "@/util/Converter";
|
||||||
|
import { ScheduleListTile } from "../tile/ScheduleListTile";
|
||||||
|
|
||||||
export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide, open }: ScheduleListContentProps) => {
|
export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide, open }: ScheduleListContentProps) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -21,8 +23,7 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
|||||||
resolver: zodResolver(ListScheduleSchema),
|
resolver: zodResolver(ListScheduleSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
startDate: undefined,
|
date: date,
|
||||||
endDate: undefined,
|
|
||||||
status: undefined,
|
status: undefined,
|
||||||
typeList: undefined,
|
typeList: undefined,
|
||||||
styleList: undefined
|
styleList: undefined
|
||||||
@@ -31,8 +32,7 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
startDate,
|
date: searchDate,
|
||||||
endDate,
|
|
||||||
status,
|
status,
|
||||||
typeList,
|
typeList,
|
||||||
styleList
|
styleList
|
||||||
@@ -52,8 +52,7 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
|||||||
const reqList = async () => {
|
const reqList = async () => {
|
||||||
const data = {
|
const data = {
|
||||||
name,
|
name,
|
||||||
startDate: date,
|
date: Converter.dateToUTC9(searchDate),
|
||||||
endDate: date,
|
|
||||||
status: status as ScheduleStatus | undefined,
|
status: status as ScheduleStatus | undefined,
|
||||||
styleList,
|
styleList,
|
||||||
typeList: typeList as ScheduleType[] | undefined
|
typeList: typeList as ScheduleType[] | undefined
|
||||||
@@ -80,9 +79,16 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full h-[calc(100%-40px)]">
|
<div className="w-full h-[calc(100%-40px)]">
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
className="w-full h-full flex flex-col justify-start items-center"
|
className="w-full h-full"
|
||||||
>
|
>
|
||||||
|
<div className="w-full h-full flex flex-col justify-start items-start gap-3">
|
||||||
|
{ scheduleList.map(schedule => (
|
||||||
|
<ScheduleListTile
|
||||||
|
data={schedule}
|
||||||
|
setMode={setMode}
|
||||||
|
/>
|
||||||
|
)) }
|
||||||
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +81,7 @@ export const ColorPickPopover = ({ setColor }: ColorPickPopoverProps) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"w-0 h-0 border-l-8 border-l-transparent border-r-8 border-r-transparent border-b-14 border-b-indigo-300",
|
"w-0 h-0 border-l-8 border-l-transparent border-r-8 border-r-transparent border-b-14 border-b-indigo-300",
|
||||||
"group-hover:border-b-white trnasition-all duration-150",
|
"group-hover:border-b-white trnasition-all duration-150",
|
||||||
seeMore && "rotate-180"
|
!seeMore && "rotate-180"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
34
src/ui/component/schedule/tile/ScheduleListTile.tsx
Normal file
34
src/ui/component/schedule/tile/ScheduleListTile.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { ScheduleTypeLabel } from "@/const/schedule/ScheduleType";
|
||||||
|
import { ScheduleListData } from "@/data/response";
|
||||||
|
import { Converter } from "@/util/Converter";
|
||||||
|
|
||||||
|
interface ScheduleListTileProps {
|
||||||
|
setMode: (mode: 'list' | 'create' | 'detail' | 'update') => void;
|
||||||
|
data: ScheduleListData;
|
||||||
|
}
|
||||||
|
export const ScheduleListTile = ({ setMode, data }: ScheduleListTileProps) => {
|
||||||
|
const formatter = Converter.isoStringToFormattedString;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full h-15 rounded-sm border flex flex-row items-center cursor-default group"
|
||||||
|
>
|
||||||
|
<div className={`w-6 h-full rounded-l-xs group-hover:w-10 transition-all duration-150`} style={{backgroundColor: `${data.style}CC`}} />
|
||||||
|
<div
|
||||||
|
className="w-[calc(100%-24px)] px-2 h-full flex flex-col justify-center items-start"
|
||||||
|
>
|
||||||
|
<div className="flex-6 h-full flex flex-row justify-end items-start">
|
||||||
|
<span
|
||||||
|
className="text-lg font-semibold text-gray-700"
|
||||||
|
>
|
||||||
|
{data.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-4 w-full flex flex-row text-xs font-light items-center text-gray-300">
|
||||||
|
{formatter(data.startDate)} - {formatter(data.endDate)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
src/util/Converter.ts
Normal file
16
src/util/Converter.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
|
export class Converter {
|
||||||
|
static dateToUTC9(date: Date) {
|
||||||
|
const utc9Date = new Date(date);
|
||||||
|
utc9Date.setHours(9);
|
||||||
|
|
||||||
|
return utc9Date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static isoStringToFormattedString(isoString: string) {
|
||||||
|
const isoDate = new Date(isoString);
|
||||||
|
const dateFormatter = "yyyy년 MM월 dd일";
|
||||||
|
return format(isoDate, dateFormatter);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user