- 일정 목록 조회 1차 구현 - 일정 당일 목록 조회 1차 구현
This commit is contained in:
@@ -214,7 +214,7 @@ function CalendarDayButton({
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-day={day.date.toISOString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
|
||||
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="card"
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -30,7 +30,7 @@ function PopoverContent({
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export type ScheduleType = 'once' | 'daily' | 'weekly' | 'monthly' | 'annual';
|
||||
|
||||
export const ScheduleTypeLabel: Record<ScheduleType, string> = {
|
||||
'once': '한 번만',
|
||||
'once': '반복없음',
|
||||
'daily': '매일',
|
||||
'weekly': '매주',
|
||||
'monthly': '매월',
|
||||
|
||||
@@ -4,12 +4,8 @@ export const ListScheduleSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.optional()
|
||||
, startDate: z
|
||||
, date: z
|
||||
.date()
|
||||
.optional()
|
||||
, endDate: z
|
||||
.date()
|
||||
.optional()
|
||||
, status: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { ScheduleType } from "@/const/schedule/ScheduleType";
|
||||
export class CreateScheduleRequest {
|
||||
name: string;
|
||||
content: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
status: ScheduleStatus;
|
||||
type: ScheduleType;
|
||||
startTime: string;
|
||||
@@ -16,8 +16,8 @@ export class CreateScheduleRequest {
|
||||
constructor (
|
||||
name: string,
|
||||
content: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
status: ScheduleStatus,
|
||||
type: ScheduleType,
|
||||
startTime: string,
|
||||
|
||||
@@ -2,8 +2,9 @@ import type { ScheduleStatus } from "@/const/schedule/ScheduleStatus";
|
||||
import type { ScheduleType } from "@/const/schedule/ScheduleType";
|
||||
|
||||
export class ScheduleListRequest {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
date?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
typeList?: ScheduleType[];
|
||||
styleList?: string[];
|
||||
status?: ScheduleStatus;
|
||||
|
||||
@@ -144,9 +144,9 @@ input[type="number"] {
|
||||
}
|
||||
|
||||
.custom-rdp-week:not(:first-child) {
|
||||
@apply border-t!;
|
||||
@apply border-t! border-indigo-200!;
|
||||
}
|
||||
|
||||
.custom-rdp-day:not(:first-child) {
|
||||
@apply border-l!;
|
||||
@apply border-l! border-indigo-200!;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export default function Header() {
|
||||
}
|
||||
|
||||
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">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
||||
|
||||
@@ -1,37 +1,261 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { getDefaultClassNames } from "react-day-picker";
|
||||
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/schedule/SchedulePopover";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { Popover, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { isSameDay, getWeeksInMonth, getWeekOfMonth, eachDayOfInterval, parse, startOfMonth, startOfWeek, endOfWeek, endOfMonth, format, isSameMonth } from "date-fns";
|
||||
import { SchedulePopover } from "../schedule/SchedulePopover";
|
||||
import { ScheduleNetwork } from "@/network/ScheduleNetwork";
|
||||
import { ScheduleListData } from "@/data/response";
|
||||
import { CustomCalendarCN } from "./CustomCalendarCN";
|
||||
import { toast } from "sonner";
|
||||
import { Converter } from "@/util/Converter";
|
||||
|
||||
interface CustomCalendarProps {
|
||||
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) => {
|
||||
const [weekCount, setWeekCount] = useState(5);
|
||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const [popoverSide, setPopoverSide] = useState<'right' | 'left'>('right');
|
||||
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 updateWeekCount = () => {
|
||||
if (containerRef === null) return;
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const weeks = containerRef.current.querySelectorAll('.rdp-week');
|
||||
|
||||
if (weeks?.length) setWeekCount(weeks.length);
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
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) => {
|
||||
setPopoverOpen(open);
|
||||
if (!open) {
|
||||
@@ -82,7 +306,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full"
|
||||
className="w-full h-full relative"
|
||||
ref={containerRef}
|
||||
>
|
||||
<Popover
|
||||
@@ -91,66 +315,12 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
||||
>
|
||||
<Calendar
|
||||
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}
|
||||
onSelect={handleDaySelect}
|
||||
onMonthChange={() => {
|
||||
// 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`,
|
||||
'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"
|
||||
),
|
||||
|
||||
}}
|
||||
month={month}
|
||||
onMonthChange={handleMonthChange}
|
||||
classNames={CustomCalendarCN}
|
||||
styles={{
|
||||
day: {
|
||||
height: `calc(100%/${weekCount})`
|
||||
@@ -174,13 +344,26 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
||||
DayButton: ({ day, ...props}) => (
|
||||
<button
|
||||
{...props}
|
||||
disabled={day.outside}
|
||||
>
|
||||
{props.children}
|
||||
</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
|
||||
date={selectedDate}
|
||||
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 { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useState } from 'react';
|
||||
import { PenSquare } from 'lucide-react';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScheduleNetwork } from '@/network/ScheduleNetwork';
|
||||
import { ScheduleListContent } from './content/ScheduleListContent';
|
||||
|
||||
interface ScheduleSheetProps {
|
||||
@@ -19,6 +13,14 @@ interface ScheduleSheetProps {
|
||||
export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: ScheduleSheetProps) => {
|
||||
const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list');
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setTimeout(() => {
|
||||
setMode('list');
|
||||
}, 150);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const DetailContent = () => {
|
||||
return (
|
||||
<div>
|
||||
@@ -21,11 +21,12 @@ import { useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import * as z from 'zod';
|
||||
import { ColorPickPopover } from '../ColorPickPopover';
|
||||
import { DatePickPopover } from '../DatePickPopover';
|
||||
import { TimePickPopover } from '../TimePickPopover';
|
||||
import { TypePickPopover } from '../TypePickPopover';
|
||||
import { ColorPickPopover } from '../popover/ColorPickPopover';
|
||||
import { DatePickPopover } from '../popover/DatePickPopover';
|
||||
import { TimePickPopover } from '../popover/TimePickPopover';
|
||||
import { TypePickPopover } from '../popover/TypePickPopover';
|
||||
import type { ScheduleCreateContentProps } from './ContentProps';
|
||||
import { Converter } from '@/util/Converter';
|
||||
|
||||
export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign }: ScheduleCreateContentProps) => {
|
||||
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
|
||||
@@ -75,8 +76,8 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
||||
if (isLoading) return;
|
||||
const data = {
|
||||
name,
|
||||
startDate,
|
||||
endDate,
|
||||
startDate: Converter.dateToUTC9(startDate),
|
||||
endDate: Converter.dateToUTC9(endDate),
|
||||
content,
|
||||
startTime: standardTimeToContinentalTime(startTime),
|
||||
endTime: standardTimeToContinentalTime(endTime),
|
||||
@@ -226,7 +227,7 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
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={{
|
||||
backgroundColor: `${style}`,
|
||||
@@ -11,6 +11,8 @@ import { ScheduleNetwork } from "@/network/ScheduleNetwork";
|
||||
import type { ScheduleStatus } from "@/const/schedule/ScheduleStatus";
|
||||
import type { ScheduleType } from "@/const/schedule/ScheduleType";
|
||||
import { ScheduleListData } from "@/data/response";
|
||||
import { Converter } from "@/util/Converter";
|
||||
import { ScheduleListTile } from "../tile/ScheduleListTile";
|
||||
|
||||
export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide, open }: ScheduleListContentProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -21,8 +23,7 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
||||
resolver: zodResolver(ListScheduleSchema),
|
||||
defaultValues: {
|
||||
name: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
date: date,
|
||||
status: undefined,
|
||||
typeList: undefined,
|
||||
styleList: undefined
|
||||
@@ -31,8 +32,7 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
||||
|
||||
const {
|
||||
name,
|
||||
startDate,
|
||||
endDate,
|
||||
date: searchDate,
|
||||
status,
|
||||
typeList,
|
||||
styleList
|
||||
@@ -52,8 +52,7 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
||||
const reqList = async () => {
|
||||
const data = {
|
||||
name,
|
||||
startDate: date,
|
||||
endDate: date,
|
||||
date: Converter.dateToUTC9(searchDate),
|
||||
status: status as ScheduleStatus | undefined,
|
||||
styleList,
|
||||
typeList: typeList as ScheduleType[] | undefined
|
||||
@@ -80,9 +79,16 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
||||
</div>
|
||||
<div className="w-full h-[calc(100%-40px)]">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,7 +81,7 @@ export const ColorPickPopover = ({ setColor }: ColorPickPopoverProps) => {
|
||||
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",
|
||||
"group-hover:border-b-white trnasition-all duration-150",
|
||||
seeMore && "rotate-180"
|
||||
!seeMore && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</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