- 일정 상세 조회 화면 및 기능 1차 구현 완료
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/scheduler_favicon__1_.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>scheduler</title>
|
||||
</head>
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"name": "scheduler",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@baekyangdan/core-utils": "^1.0.21",
|
||||
"@baekyangdan/core-utils": "^1.0.23",
|
||||
"@diceui/mention": "^0.8.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
@@ -57,6 +57,7 @@
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"recharts": "^2.15.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"vaul": "^1.1.2",
|
||||
@@ -375,9 +376,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@baekyangdan/core-utils": {
|
||||
"version": "1.0.21",
|
||||
"resolved": "https://gitea.bkdhome.p-e.kr/api/packages/baekyangdan/npm/%40baekyangdan%2Fcore-utils/-/1.0.21/core-utils-1.0.21.tgz",
|
||||
"integrity": "sha512-LYkzavYnforDtXm/icOg6rQkRAQAgpdwlC6w8dpWAz/N7ynIHdUHJZSPRRsQc9Jy3hQp7+vtKjZI4LP3NKC0UA==",
|
||||
"version": "1.0.23",
|
||||
"resolved": "https://gitea.bkdhome.p-e.kr/api/packages/baekyangdan/npm/%40baekyangdan%2Fcore-utils/-/1.0.23/core-utils-1.0.23.tgz",
|
||||
"integrity": "sha512-PmxaMqMOpLKiUD5+gmC/uslSpcGzGWzaIVCTJgsHnucsPvGY+cyrUFldJaeCsJ5UeCy8eZf1hVWF3DdbKFCCVA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@swc/core": "^1.15.5",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@baekyangdan/core-utils": "^1.0.21",
|
||||
"@baekyangdan/core-utils": "^1.0.23",
|
||||
"@diceui/mention": "^0.8.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
@@ -60,6 +60,7 @@
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-router-dom": "^7.9.5",
|
||||
"recharts": "^2.15.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"vaul": "^1.1.2",
|
||||
|
||||
673
public/scheduler_favicon__1_.svg
Normal file
673
public/scheduler_favicon__1_.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 113 KiB |
@@ -1,4 +1,5 @@
|
||||
import './App.css';
|
||||
import 'reflect-metadata';
|
||||
import SignUpPage from './ui/page/account/signup/SignUpPage';
|
||||
import Layout from './layouts/Layout';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
|
||||
@@ -22,6 +22,5 @@ export const CreateScheduleSchema = z.object({
|
||||
.string()
|
||||
, dayList: z
|
||||
.string()
|
||||
, participantList: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
});
|
||||
@@ -5,20 +5,27 @@ export const UpdateScheduleSchema = z.object({
|
||||
.string()
|
||||
, name: z
|
||||
.string()
|
||||
.nonempty()
|
||||
, startDate: z
|
||||
.date()
|
||||
, endDate: z
|
||||
.date()
|
||||
, status: z
|
||||
.string()
|
||||
.default("yet")
|
||||
, content: z
|
||||
.string()
|
||||
, type: z
|
||||
.string()
|
||||
.default("once")
|
||||
, style: z
|
||||
.string()
|
||||
, startTime: z
|
||||
.string()
|
||||
, endTime: z
|
||||
.string()
|
||||
, dayList: z
|
||||
.string()
|
||||
.optional()
|
||||
, participantList: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
@import url("https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.3/packages/wanted-sans/fonts/webfonts/variable/split/WantedSansVariable.min.css");
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@@ -115,10 +116,43 @@
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
font-family: "Wanted Sans Variable", "Wanted Sans", -apple-system,BlinkMacSystemFont, system-ui, "Segeo UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segeo UI Emoji", "Segeo UI Symbol", sans-serif;
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Tailwind의 굵기 유틸리티를 @apply로 재정의 */
|
||||
/* 일반적인 가변 폰트의 wght 축 값을 사용 (400~700 사이) */
|
||||
.font-thin {
|
||||
font-variation-settings: 'wght' 100;
|
||||
}
|
||||
.font-extralight {
|
||||
font-variation-settings: 'wght' 200;
|
||||
}
|
||||
.font-light {
|
||||
font-variation-settings: 'wght' 300;
|
||||
}
|
||||
.font-normal, .font-regular { /* font-normal 및 font-regular 모두 400 */
|
||||
font-variation-settings: 'wght' 400;
|
||||
}
|
||||
.font-medium {
|
||||
font-variation-settings: 'wght' 500;
|
||||
}
|
||||
.font-semibold {
|
||||
font-variation-settings: 'wght' 600;
|
||||
}
|
||||
.font-bold {
|
||||
font-variation-settings: 'wght' 700;
|
||||
}
|
||||
.font-extrabold {
|
||||
font-variation-settings: 'wght' 800;
|
||||
}
|
||||
.font-black {
|
||||
font-variation-settings: 'wght' 900;
|
||||
}
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -175,7 +175,18 @@ export class BaseNetwork {
|
||||
|
||||
if (rawData) {
|
||||
const instance = plainToInstance(dtoClass, rawData);
|
||||
await validateOrReject(instance);
|
||||
|
||||
try {
|
||||
if (Array.isArray(instance)) {
|
||||
for (const item of instance) {
|
||||
await validateOrReject(item);
|
||||
}
|
||||
} else {
|
||||
await validateOrReject(instance);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
(result.data as any).data = instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
import { BaseNetwork } from "./BaseNetwork"
|
||||
import {
|
||||
ScheduleListRequest,
|
||||
CreateScheduleRequest,
|
||||
UpdateScheduleRequest,
|
||||
DeleteScheduleRequest
|
||||
} from '@/data/request';
|
||||
import {
|
||||
CreateScheduleResponse,
|
||||
ScheduleDetailResponse,
|
||||
ScheduleListResponse
|
||||
} from "@/data/response";
|
||||
import { HttpApiUrl } from "@baekyangdan/core-utils";
|
||||
import { SchedulerDTO as DTO } from "@baekyangdan/core-utils";
|
||||
import { SchedulerDTO } from "@baekyangdan/core-utils";
|
||||
const ScheduleApi = HttpApiUrl.Schedule;
|
||||
export class ScheduleNetwork extends BaseNetwork {
|
||||
private baseUrl = ScheduleApi.base;
|
||||
|
||||
async getList(data: DTO.ScheduleListRequest) {
|
||||
return await this.post<DTO.ScheduleListResponse>(
|
||||
async getList(data: SchedulerDTO.ScheduleListRequest) {
|
||||
return await this.post<SchedulerDTO.ScheduleListResponse, SchedulerDTO.ScheduleList>(
|
||||
this.baseUrl,
|
||||
data
|
||||
data,
|
||||
undefined,
|
||||
SchedulerDTO.ScheduleList
|
||||
);
|
||||
}
|
||||
|
||||
async getDetail(id: string) {
|
||||
return await this.get<DTO.ScheduleDetailResponse>(
|
||||
`${this.baseUrl}/${id}`
|
||||
return await this.get<SchedulerDTO.ScheduleDetailResponse, SchedulerDTO.ScheduleDetail>(
|
||||
`${this.baseUrl}/${id}`,
|
||||
undefined,
|
||||
SchedulerDTO.ScheduleDetail
|
||||
);
|
||||
}
|
||||
|
||||
async create(data: DTO.ScheduleCreateRequest) {
|
||||
return await this.post<DTO.ScheduleCreateResponse>(
|
||||
async create(data: SchedulerDTO.ScheduleCreateRequest) {
|
||||
return await this.post<SchedulerDTO.ScheduleCreateResponse>(
|
||||
`${this.baseUrl}${ScheduleApi.create}`,
|
||||
data
|
||||
);
|
||||
|
||||
@@ -10,9 +10,6 @@ import { CustomCalendarCN } from "./CustomCalendarCN";
|
||||
import { toast } from "sonner";
|
||||
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
|
||||
import { SchedulerDTO as DTO, Type } from '@baekyangdan/core-utils';
|
||||
interface CustomCalendarProps {
|
||||
data?: any;
|
||||
}
|
||||
|
||||
interface EventBarPosition extends DTO.ScheduleList {
|
||||
positionStyle: React.CSSProperties;
|
||||
@@ -26,12 +23,13 @@ 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 = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [refetchTrigger, setRefetchTrigger] = useState(0);
|
||||
const [weekCount, setWeekCount] = useState(5);
|
||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const [popoverOpenCoolDown, setPopoverOpenCoolDown] = useState(false);
|
||||
const [popoverSide, setPopoverSide] = useState<'right' | 'left'>('right');
|
||||
const [popoverAlign, setPopoverAlign] = useState<'start' | 'end'>('end');
|
||||
const [popoverMode, setPopoverMode] = useState<SchedulePopoverMode>('list');
|
||||
@@ -163,7 +161,6 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
||||
|
||||
if (!isNaN(parsedDate.getTime())) {
|
||||
const dateKey = format(parsedDate, DATE_FORMAT_KEY);
|
||||
console.log(dateKey);
|
||||
cellInfoMap.set(dateKey, {
|
||||
cell: dayButton,
|
||||
rect: dayButton.getBoundingClientRect()
|
||||
@@ -251,8 +248,8 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
||||
const renderStartDate = startDateObj > calendarStart ? startDateObj : calendarStart;
|
||||
const renderEndDate = endDateObj < calendarEnd ? endDateObj : calendarEnd;
|
||||
|
||||
const renderStartKey = format(renderStartDate, DATE_FORMAT_KEY);
|
||||
const renderEndKey = format(renderEndDate, DATE_FORMAT_KEY);
|
||||
// const renderStartKey = format(renderStartDate, DATE_FORMAT_KEY);
|
||||
// const renderEndKey = format(renderEndDate, DATE_FORMAT_KEY);
|
||||
|
||||
const allRenderDays = eachDayOfInterval({ start: renderStartDate, end: renderEndDate });
|
||||
|
||||
@@ -366,52 +363,56 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setPopoverOpen(open);
|
||||
setPopoverDetailId('');
|
||||
if (!open) {
|
||||
setTimeout(() => {
|
||||
setSelectedDate(undefined);
|
||||
setPopoverMode('list');
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDaySelect = (date: Date | undefined) => {
|
||||
if (popoverOpenCoolDown) return;
|
||||
if (!date) {
|
||||
setPopoverOpenCoolDown(true);
|
||||
setPopoverOpen(false);
|
||||
setPopoverDetailId('');
|
||||
setTimeout(() => {
|
||||
setPopoverOpenCoolDown(false);
|
||||
setSelectedDate(undefined);
|
||||
setPopoverDetailId('');
|
||||
setPopoverMode('list');
|
||||
}, 150);
|
||||
return;
|
||||
}
|
||||
if (date) {
|
||||
setSelectedDate(date);
|
||||
|
||||
const dayOfWeek = date.getDay();
|
||||
setSelectedDate(date);
|
||||
|
||||
if (0 <= dayOfWeek && dayOfWeek < 4) {
|
||||
setPopoverSide('right');
|
||||
} else {
|
||||
setPopoverSide('left');
|
||||
}
|
||||
const dayOfWeek = date.getDay();
|
||||
|
||||
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);
|
||||
})
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
const findDateFromClick = (
|
||||
@@ -455,7 +456,6 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
||||
handleDaySelect(clickedDate);
|
||||
} else {
|
||||
handleDaySelect(undefined);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,7 +513,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
||||
id={pos.segmentId}
|
||||
className={cn(
|
||||
`flex flex-row justify-start items-center absolute select-none`,
|
||||
"py-0.5 px-2 rounded-sm text-xs text-white overflow-hidden"
|
||||
"py-0.5 px-2 rounded-sm text-xs font-thin text-white overflow-hidden"
|
||||
)}
|
||||
style={{...pos.positionStyle, backgroundColor: pos.style}}
|
||||
>
|
||||
@@ -530,7 +530,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
||||
setDetailId={setPopoverDetailId}
|
||||
popoverSide={popoverSide}
|
||||
popoverAlign={popoverAlign}
|
||||
onScheduleCreated={refetchList}
|
||||
reqRefetchList={refetchList}
|
||||
/>
|
||||
|
||||
</Popover>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { PopoverContent } from '@/components/ui/popover';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
|
||||
import { ScheduleListContent } from './content/ScheduleListContent';
|
||||
import { ScheduleDetailContent } from './content/ScheduleDetailContent';
|
||||
import type { SchedulePopoverMode } from '@/const/schedule/SchedulePopoverMode';
|
||||
import { ScheduleUpdateContent } from './content/ScheduleUpdateContent';
|
||||
import { memo } from 'react';
|
||||
|
||||
interface ScheduleSheetProps {
|
||||
date: Date | undefined;
|
||||
@@ -14,76 +15,61 @@ interface ScheduleSheetProps {
|
||||
setMode: (mode: SchedulePopoverMode) => void;
|
||||
detailId: string;
|
||||
setDetailId: (id: string) => void;
|
||||
onScheduleCreated: () => void;
|
||||
reqRefetchList: () => void;
|
||||
}
|
||||
|
||||
export const SchedulePopover = ({ date, open, mode, setMode, detailId, setDetailId, popoverSide, popoverAlign, onScheduleCreated }: ScheduleSheetProps) => {
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setTimeout(() => {
|
||||
setMode('list');
|
||||
}, 150);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const DetailContent = () => {
|
||||
return (
|
||||
<div>
|
||||
Detail
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const UpdateContent = () => {
|
||||
return (
|
||||
<div>
|
||||
Update
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SchedulePopoverContent = () => {
|
||||
switch(mode) {
|
||||
case 'list':
|
||||
return <ScheduleListContent
|
||||
setMode={setMode}
|
||||
setId={setDetailId}
|
||||
date={date}
|
||||
popoverAlign={popoverAlign}
|
||||
popoverSide={popoverSide}
|
||||
open={open}
|
||||
/>
|
||||
case 'create':
|
||||
return <ScheduleCreateContent
|
||||
setMode={setMode}
|
||||
date={date}
|
||||
popoverAlign={popoverAlign}
|
||||
popoverSide={popoverSide}
|
||||
open={open}
|
||||
refetchList={onScheduleCreated}
|
||||
/>
|
||||
case 'detail':
|
||||
return <ScheduleDetailContent
|
||||
setMode={setMode}
|
||||
date={date}
|
||||
popoverAlign={popoverAlign}
|
||||
popoverSide={popoverSide}
|
||||
open={open}
|
||||
id={detailId}
|
||||
/>
|
||||
case 'update':
|
||||
return <UpdateContent />
|
||||
}
|
||||
}
|
||||
export const SchedulePopover = memo(({ mode, ...props}: ScheduleSheetProps) => {
|
||||
|
||||
return (
|
||||
<PopoverContent
|
||||
className="rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[384px] min-h-[125px] h-[calc(100vh/2.3)]"
|
||||
align={popoverAlign} side={popoverSide}
|
||||
className="p-0 rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[384px] min-h-[125px] h-[calc(100vh/2.2)]"
|
||||
align={props.popoverAlign} side={props.popoverSide}
|
||||
>
|
||||
{<SchedulePopoverContent />}
|
||||
{<SchedulePopoverContent mode={mode} { ...props} />}
|
||||
</PopoverContent>
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
const SchedulePopoverContent = memo(({ mode, setMode, setDetailId, date, popoverAlign, popoverSide, open, reqRefetchList, detailId }: ScheduleSheetProps) => {
|
||||
switch(mode) {
|
||||
case 'list':
|
||||
return <ScheduleListContent
|
||||
setMode={setMode}
|
||||
setId={setDetailId}
|
||||
date={date}
|
||||
popoverAlign={popoverAlign}
|
||||
popoverSide={popoverSide}
|
||||
open={open}
|
||||
/>
|
||||
case 'create':
|
||||
return <ScheduleCreateContent
|
||||
setMode={setMode}
|
||||
date={date}
|
||||
popoverAlign={popoverAlign}
|
||||
popoverSide={popoverSide}
|
||||
open={open}
|
||||
refetchList={reqRefetchList}
|
||||
/>
|
||||
case 'detail':
|
||||
return <ScheduleDetailContent
|
||||
setMode={setMode}
|
||||
date={date}
|
||||
popoverAlign={popoverAlign}
|
||||
popoverSide={popoverSide}
|
||||
open={open}
|
||||
id={detailId}
|
||||
/>
|
||||
case 'update':
|
||||
return <ScheduleUpdateContent
|
||||
setMode={setMode}
|
||||
date={date}
|
||||
popoverAlign={popoverAlign}
|
||||
popoverSide={popoverSide}
|
||||
open={open}
|
||||
id={detailId}
|
||||
refetchList={reqRefetchList}
|
||||
/>
|
||||
}
|
||||
});
|
||||
|
||||
SchedulePopover.displayName = "SchedulePopover";
|
||||
@@ -19,3 +19,8 @@ export interface ScheduleListContentProps extends BaseProps {
|
||||
export interface ScheduleDetailContentProps extends BaseProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ScheduleUpdateContentProps extends BaseProps {
|
||||
refetchList: () => void;
|
||||
id: string;
|
||||
}
|
||||
@@ -24,7 +24,6 @@ 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';
|
||||
import { SchedulerDTO as DTO } from '@baekyangdan/core-utils';
|
||||
|
||||
export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign, refetchList }: ScheduleCreateContentProps) => {
|
||||
@@ -52,8 +51,7 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
||||
type: "once",
|
||||
status: "yet",
|
||||
style: getPaletteByKey('SerenityBlue').style,
|
||||
dayList: "",
|
||||
participantList: []
|
||||
dayList: ""
|
||||
}
|
||||
});
|
||||
|
||||
@@ -73,18 +71,6 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
||||
const reqCreate = async () => {
|
||||
if (isLoading) return;
|
||||
|
||||
// const data = {
|
||||
// name,
|
||||
// startDate: Converter.dateToUTC9(startDate),
|
||||
// endDate: Converter.dateToUTC9(endDate),
|
||||
// content,
|
||||
// startTime: standardTimeToContinentalTime(startTime),
|
||||
// endTime: standardTimeToContinentalTime(endTime),
|
||||
// type: type as Type.Type,
|
||||
// status: status as Type.Status,
|
||||
// style
|
||||
// } as DTO.ScheduleCreateRequest;
|
||||
|
||||
const data = {
|
||||
name,
|
||||
startDate: startDate,
|
||||
@@ -212,111 +198,117 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col justify-start items-start gap-4">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-4">
|
||||
<div
|
||||
onClick={() => setMode('list')}
|
||||
>
|
||||
<ArrowLeft className="stroke-indigo-100 hover:stroke-indigo-300 transition-all duration-150" />
|
||||
</div>
|
||||
<Controller
|
||||
name="name"
|
||||
control={createScheduleForm.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Input
|
||||
{...field}
|
||||
id="form-create-schedule-name"
|
||||
placeholder="제목"
|
||||
className="placeholder-indigo-200! font-bold border-t-0 border-r-0 border-l-0 p-0 border-b-2 rounded-none shadow-none border-indigo-100 focus-visible:ring-0 focus-visible:border-b-indigo-300"
|
||||
style={{
|
||||
fontSize: '20px'
|
||||
}}
|
||||
tabIndex={1}
|
||||
arai-invalid={fieldState.invalid}
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
|
||||
<div className="w-6 h-6 flex justify-center items-center">
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-full w-5 h-5 border-2 border-gray-300 hover:border-indigo-300 transition-all duration-150',
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: `${style}`,
|
||||
}}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
</div>
|
||||
<ColorPickPopover
|
||||
setColor={selectColor}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
<ScrollArea
|
||||
className={
|
||||
cn(
|
||||
"min-h-[125px] h-[calc(100vh/2.3-40px)]! w-full",
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="w-full h-full flex! flex-col! gap-4!"
|
||||
>
|
||||
<TypePickPopover
|
||||
type={type as Type.Type}
|
||||
setType={selectType}
|
||||
popoverSide={popoverSide}
|
||||
/>
|
||||
<div className="flex flex-col w-full h-full justify-between items-center">
|
||||
<div className="p-4 w-full h-[calc(100%-40px)] flex flex-col justify-start items-start">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-4">
|
||||
<div
|
||||
className="w-full h-10"
|
||||
onClick={() => setMode('list')}
|
||||
>
|
||||
{renderContent()}
|
||||
</div>
|
||||
<div className="w-full h-10">
|
||||
<TimePickPopover
|
||||
mode='range'
|
||||
popoverAlign={popoverAlign}
|
||||
disabled={false}
|
||||
startTime={startTime}
|
||||
setStartTime={(time: string | undefined) => selectTime('startTime', time ?? '')}
|
||||
endTime={endTime}
|
||||
setEndTime={(time: string | undefined) => selectTime('endTime', time ?? '')}
|
||||
/>
|
||||
<ArrowLeft className="stroke-indigo-100 hover:stroke-indigo-300 transition-all duration-150" />
|
||||
</div>
|
||||
<Controller
|
||||
name="content"
|
||||
name="name"
|
||||
control={createScheduleForm.control}
|
||||
render={({ field }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
rows={2}
|
||||
placeholder="일정 상세 사항"
|
||||
className="placeholder-indigo-200! focus-visible:placeholder-indigo-300! border-indigo-100 focus-visible:border-indigo-300 resize-none focus-visible:ring-0"
|
||||
style={{
|
||||
'scrollbarWidth': 'none'
|
||||
}}
|
||||
/>
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Input
|
||||
{...field}
|
||||
id="form-create-schedule-name"
|
||||
placeholder="제목"
|
||||
maxLength={10}
|
||||
className="placeholder-indigo-200! text-indigo-400 font-bold border-t-0 border-r-0 border-l-0 p-0 border-b-2 rounded-none shadow-none border-indigo-100 focus-visible:ring-0 focus-visible:border-b-indigo-300"
|
||||
style={{
|
||||
fontSize: '20px'
|
||||
}}
|
||||
tabIndex={1}
|
||||
arai-invalid={fieldState.invalid}
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
{/* <ParticipantPopover
|
||||
participantList={participantList}
|
||||
setParticipantList={selectParticipant}
|
||||
/> */}
|
||||
<Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
|
||||
<div className="w-6 h-6 flex justify-center items-center">
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-full w-5 h-5 border-2 border-gray-300 hover:border-indigo-300 transition-all duration-150',
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: `${style}`,
|
||||
}}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
</div>
|
||||
<ColorPickPopover
|
||||
setColor={selectColor}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div
|
||||
className="absolute w-full border-b border-l border-r rounded-b-md left-0 bottom-0 h-10 flex flex-row justify-start items-end p-0"
|
||||
<ScrollArea
|
||||
className={
|
||||
cn(
|
||||
"min-h-[125px] h-[calc(100vh/2.3-40px)]! w-full",
|
||||
"[&>div>div:last-child]:block"
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="w-full h-full flex! flex-col! gap-4!"
|
||||
>
|
||||
<TypePickPopover
|
||||
type={type as Type.Type}
|
||||
setType={selectType}
|
||||
popoverSide={popoverSide}
|
||||
/>
|
||||
<div
|
||||
className="w-full h-10"
|
||||
>
|
||||
{renderContent()}
|
||||
</div>
|
||||
<div className="w-full h-10">
|
||||
<TimePickPopover
|
||||
mode='range'
|
||||
popoverAlign={popoverAlign}
|
||||
disabled={false}
|
||||
startTime={startTime}
|
||||
setStartTime={(time: string | undefined) => selectTime('startTime', time ?? '')}
|
||||
endTime={endTime}
|
||||
setEndTime={(time: string | undefined) => selectTime('endTime', time ?? '')}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
name="content"
|
||||
control={createScheduleForm.control}
|
||||
render={({ field }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
rows={2}
|
||||
placeholder="일정 상세 사항"
|
||||
spellCheck={false}
|
||||
className="placeholder-indigo-200! text-indigo-400 focus-visible:placeholder-indigo-300! border-indigo-100 focus-visible:border-indigo-300 resize-none focus-visible:ring-0"
|
||||
style={{
|
||||
'scrollbarWidth': 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{/* <ParticipantPopover
|
||||
participantList={participantList}
|
||||
setParticipantList={selectParticipant}
|
||||
/> */}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<div
|
||||
className="w-full h-10 flex flex-row justify-start items-end p-0"
|
||||
>
|
||||
<Button
|
||||
className={cn(
|
||||
"h-full flex-5 rounded-none rounded-bl-md flex justify-center items-center",
|
||||
"text-indigo-300 bg-white",
|
||||
"hover:text-white hover:bg-indigo-300"
|
||||
"hover:text-white hover:bg-indigo-300",
|
||||
"rounded-none rounded-bl-xl"
|
||||
)}
|
||||
type="button"
|
||||
onClick={reqCreate}
|
||||
@@ -325,9 +317,10 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(
|
||||
"h-full flex-5 rounded-none rounded-br-md flex justify-center items-center",
|
||||
"h-full flex-5 flex justify-center items-center",
|
||||
"bg-white text-red-400",
|
||||
"hover:text-white hover:bg-red-400"
|
||||
"hover:text-white hover:bg-red-400",
|
||||
"rounded-none rounded-br-xl"
|
||||
)}
|
||||
onClick={() => setMode('list')}
|
||||
>
|
||||
|
||||
@@ -1,38 +1,90 @@
|
||||
import { ScheduleNetwork } from '@/network/ScheduleNetwork';
|
||||
import type { ScheduleDetailContentProps } from './ContentProps';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ArrowLeft, ChevronUp, Clock } from 'lucide-react';
|
||||
import type { ScheduleDetailData } from '@/data/response';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ArrowLeft, ChevronUp, LucideCalendarDays, LucideClock, LucidePen } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { SchedulerDTO as DTO } from '@baekyangdan/core-utils'
|
||||
import { Converter, DateFormat, SchedulerDTO as DTO } from '@baekyangdan/core-utils'
|
||||
import { toast } from 'sonner';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
export const ScheduleDetailContent = ({ setMode, popoverSide, popoverAlign, id }: ScheduleDetailContentProps) => {
|
||||
export const ScheduleDetailContent = ({ setMode, id }: ScheduleDetailContentProps) => {
|
||||
const scheduleNetwork = new ScheduleNetwork();
|
||||
const [data, setData] = useState<DTO.ScheduleDetail>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [commentFold, setCommentFold] = useState(true);
|
||||
|
||||
const converter = new Converter();
|
||||
useEffect(() => {
|
||||
if (!id || id.trim().length === 0) {
|
||||
if (!id || id === '' || id.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
reqDetail();
|
||||
}, [id]);
|
||||
|
||||
const reqDetail = async () => {
|
||||
const result = await scheduleNetwork.getDetail(id);
|
||||
if (result.success) {
|
||||
setData(result.data);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await scheduleNetwork.getDetail(id);
|
||||
if (result.success) {
|
||||
setData(result.data);
|
||||
} else {
|
||||
throw new Error;
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('일정을 불러오는 데에 실패하였습니다.\n잠시 후 다시 시도해주십시오.');
|
||||
setMode('list');
|
||||
}
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
const moveToList = () => {
|
||||
const moveToList = useCallback(() => {
|
||||
setMode('list');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const moveToUpdate = useCallback(() => {
|
||||
setMode('update');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full h-full flex flex-col justify-start items-start"
|
||||
isLoading || !data
|
||||
? <div
|
||||
className="p-4 relative w-full h-full flex flex-col justify-start tiems-start"
|
||||
>
|
||||
<div
|
||||
className="relative w-full h-10 flex flex-row justify-center items-center"
|
||||
>
|
||||
<div
|
||||
className="absolute top-1.5 left-0.5"
|
||||
>
|
||||
<ArrowLeft
|
||||
className="stroke-indigo-200 hover:stroke-indigo-400 transition-all duration-300"
|
||||
onClick={moveToList}
|
||||
/>
|
||||
</div>
|
||||
<Skeleton className="h-full w-3/5 bg-indigo-200" />
|
||||
</div>
|
||||
<div
|
||||
className="mt-2 w-full h-[calc(100%-86px)] flex flex-col gap-2"
|
||||
>
|
||||
<div
|
||||
className="w-full h-5 flex flex-col items-end"
|
||||
>
|
||||
<Skeleton className="w-3/5 h-full bg-indigo-200 rounded-sm" />
|
||||
</div>
|
||||
<Skeleton className="w-full h-30 bg-indigo-200" />
|
||||
<div
|
||||
className="w-full h-12 flex flex-row gap-3.5"
|
||||
>
|
||||
<Skeleton className="flex-1 h-full bg-indigo-200" />
|
||||
<Skeleton className="flex-1 h-full bg-indigo-200" />
|
||||
</div>
|
||||
<Skeleton className="w-4/5 h-10 bg-indigo-200" />
|
||||
</div>
|
||||
</div>
|
||||
: <div
|
||||
className="p-4 relative w-full h-full flex flex-col justify-start items-start"
|
||||
>
|
||||
<div className="relative w-full h-10 flex flex-row justify-center items-center border-b border-b-indigo-300">
|
||||
<div
|
||||
@@ -43,21 +95,30 @@ export const ScheduleDetailContent = ({ setMode, popoverSide, popoverAlign, id }
|
||||
onClick={moveToList}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-indigo-400 select-none">{data?.name}</span>
|
||||
<span className="text-indigo-400 text-lg font-bold select-none">{data?.name}</span>
|
||||
<div
|
||||
className="absolute top-3.5 right-0.5"
|
||||
>
|
||||
<LucidePen
|
||||
size={16}
|
||||
className="stroke-indigo-200 hover:stroke-indigo-400 transition-all duration-300"
|
||||
onClick={moveToUpdate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-2 w-full flex flex-col transition-all duration-300 border",
|
||||
"mt-2 w-full flex flex-col transition-all duration-300",
|
||||
"h-[calc(100%-86px)]"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="w-full border-b border-b-indigo-100 flex flex-col items-end text-xs text-indigo-200"
|
||||
className="w-full flex flex-col items-end text-xs text-indigo-300"
|
||||
>
|
||||
<span className="flex flex-row justify-start items-center gap-1"><Clock size={12}/>생성일 : {data?.createdAt}</span>
|
||||
<span className="flex flex-row justify-start items-center text-sm font-regular gap-1"><LucideClock size={12}/>생성일 : {data?.createdAt}</span>
|
||||
</div>
|
||||
<ScrollArea
|
||||
className="max-h-30 w-full border"
|
||||
className="h-25 w-full"
|
||||
>
|
||||
<span
|
||||
className="whitespace-pre-wrap text-indigo-500 select-none"
|
||||
@@ -65,28 +126,60 @@ export const ScheduleDetailContent = ({ setMode, popoverSide, popoverAlign, id }
|
||||
{data?.content && data?.content}
|
||||
</span>
|
||||
</ScrollArea>
|
||||
<div className="w-full h-12 flex flex-row justify-between items-center">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<LucideCalendarDays size={14} className="stroke-indigo-300" />
|
||||
<span className="font-normal text-[12px] text-indigo-300">시작일</span>
|
||||
</div>
|
||||
<span className="text-sm font-normal text-indigo-400">{converter.dateToFormattedString(data.startDate, DateFormat.KOREAN)}</span>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<LucideCalendarDays size={14} className="stroke-indigo-300" />
|
||||
<span className="font-normal text-[12px] text-indigo-300">종료일</span>
|
||||
</div>
|
||||
<span className="text-sm font-normal text-indigo-400">{converter.dateToFormattedString(data.endDate, DateFormat.KOREAN)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-12 flex flex-row justify-between items-center">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<LucideClock size={14} className="stroke-indigo-300" />
|
||||
<span className="font-normal text-[12px] text-indigo-300">시작 시간</span>
|
||||
</div>
|
||||
<span className="text-sm font-normal text-indigo-400">{data.startTime}</span>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<LucideClock size={14} className="stroke-indigo-300" />
|
||||
<span className="font-normal text-[12px] text-indigo-300">종료 시간</span>
|
||||
</div>
|
||||
<span className="text-sm font-normal text-indigo-400">{data.endTime}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-0 bottom-0 w-full flex flex-col transition-all overflow-hidden duration-300 bg-white",
|
||||
"border-t border-l border-r border-b-0 border-indigo-100 rounded-t-lg",
|
||||
commentFold? "h-10" : "h-[calc(100%-44px)] border-b"
|
||||
"absolute px-1 left-2 bottom-4 w-[calc(100%-16px)] flex flex-col transition-all overflow-hidden duration-300 bg-white",
|
||||
"border-t border-l border-r border-b border-indigo-100 rounded-t-lg",
|
||||
commentFold? "h-10" : "h-[calc(100%-44px)]"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"w-full h-10 px-2 flex flex-row justify-between items-center",
|
||||
commentFold ? "" : "border-b border-b-indigo-200"
|
||||
"border-b border-b-indigo-200"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className="text-indigo-400 text-sm"
|
||||
>
|
||||
댓글 <span className="text-indigo-200 text-xs">[12]</span>
|
||||
댓글 <span className="text-indigo-300 text-xs">[12]</span>
|
||||
</span>
|
||||
|
||||
<div
|
||||
className="h-10 text-indigo-400 flex flex-row justify-center items-end pb-2 select-none"
|
||||
className="h-10 text-indigo-400 flex flex-row justify-center items-center select-none"
|
||||
onClick={() => setCommentFold(prev => !prev)}
|
||||
>
|
||||
<ChevronUp
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { ListScheduleSchema } from "@/data/form/schedule/listSchedule.schema";
|
||||
import { format } from "date-fns";
|
||||
import { List, PenSquare } from "lucide-react";
|
||||
import { PenSquare } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
import type { ScheduleListContentProps } from "./ContentProps";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
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";
|
||||
import { SchedulerDTO as DTO, Type } from "@baekyangdan/core-utils";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide, open, setId }: ScheduleListContentProps) => {
|
||||
export const ScheduleListContent = ({ date, setMode, open, setId }: ScheduleListContentProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [scheduleList, setScheduleList] = useState<Array<DTO.ScheduleList>>([]);
|
||||
const scheduleNetwork = new ScheduleNetwork();
|
||||
@@ -59,11 +57,19 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
||||
styleList: styleList
|
||||
} as DTO.ScheduleListRequest;
|
||||
|
||||
const result = await scheduleNetwork.getList(data);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await scheduleNetwork.getList(data);
|
||||
|
||||
if (result.success) {
|
||||
setScheduleList(result.data!);
|
||||
if (!result.success) throw new Error(result.error);
|
||||
|
||||
setScheduleList(result.data);
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
toast.error('일정을 불러오는 데에 실패하였습니다.\n잠시 후 다시 시도해주십시오.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const moveToDetail = (id: string) => {
|
||||
@@ -72,7 +78,7 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-4">
|
||||
<div className="p-4 w-full h-full flex flex-col gap-4">
|
||||
<div className="relative w-full h-10 border-b border-b-indigo-300 flex flex-row items-center justify-center">
|
||||
<span className="text-indigo-400">{date && format(date, "yyyy년 MM월 dd일")}</span>
|
||||
<div className="absolute top-3 right-0.5 group">
|
||||
@@ -84,19 +90,27 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-[calc(100%-40px)]">
|
||||
<ScrollArea
|
||||
className="w-full h-full"
|
||||
>
|
||||
<div className="w-full h-full flex flex-col justify-start items-start gap-3">
|
||||
{ scheduleList.map(schedule => (
|
||||
<ScheduleListTile
|
||||
onClick={moveToDetail}
|
||||
data={schedule}
|
||||
setMode={setMode}
|
||||
/>
|
||||
)) }
|
||||
{
|
||||
isLoading
|
||||
? <div className="w-full h-full flex flex-col justify-start items-start gap-3">
|
||||
{[1,2,3,4].map((_) => (
|
||||
<Skeleton className="w-full h-10 bg-indigo-200"/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
: <ScrollArea
|
||||
className="w-full h-full"
|
||||
>
|
||||
<div className="w-full h-full flex flex-col justify-start items-start gap-3">
|
||||
{ scheduleList.map(schedule => (
|
||||
<ScheduleListTile
|
||||
onClick={moveToDetail}
|
||||
data={schedule}
|
||||
setMode={setMode}
|
||||
/>
|
||||
)) }
|
||||
</div>
|
||||
</ScrollArea>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Field, FieldError } from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Popover, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import type { ColorPaletteType } from '@/const/ColorPalette';
|
||||
import { Type } from '@baekyangdan/core-utils';
|
||||
import { usePalette } from '@/hooks/use-palette';
|
||||
import { useRecord } from '@/hooks/use-record';
|
||||
import { useTime } from '@/hooks/use-time';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ScheduleNetwork } from '@/network/ScheduleNetwork';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import * as z from 'zod';
|
||||
import { ColorPickPopover } from '../popover/ColorPickPopover';
|
||||
import { DatePickPopover } from '../popover/DatePickPopover';
|
||||
import { TimePickPopover } from '../popover/TimePickPopover';
|
||||
import { TypePickPopover } from '../popover/TypePickPopover';
|
||||
import type { ScheduleUpdateContentProps } from './ContentProps';
|
||||
import { SchedulerDTO as DTO } from '@baekyangdan/core-utils';
|
||||
import { UpdateScheduleSchema } from '@/data/form/schedule/updateSchedule.schema';
|
||||
import { ParticipantPopover } from '../popover/ParticipantPopover';
|
||||
|
||||
export const ScheduleUpdateContent = ({ date, setMode, popoverSide, popoverAlign, refetchList, id }: ScheduleUpdateContentProps) => {
|
||||
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { getPaletteByKey } = usePalette();
|
||||
const { getCurrentTimeString, standardTimeToContinentalTime } = useTime();
|
||||
const dayLabelList = useRecord(Type.Day).keys.map((key) => {
|
||||
return {
|
||||
day: key,
|
||||
label: Type.Day[key as keyof typeof Type.Day]
|
||||
} as { day: string, label: string };
|
||||
});
|
||||
const scheduleNetwork = new ScheduleNetwork();
|
||||
|
||||
const updateScheduleForm = useForm<z.infer<typeof UpdateScheduleSchema>>({
|
||||
resolver: zodResolver(UpdateScheduleSchema),
|
||||
defaultValues: {
|
||||
id: id,
|
||||
name: "",
|
||||
startDate: date || new Date(),
|
||||
endDate: date || new Date(),
|
||||
content: "",
|
||||
startTime: getCurrentTimeString('standard'),
|
||||
endTime: getCurrentTimeString('standard'),
|
||||
type: "once",
|
||||
status: "yet",
|
||||
style: getPaletteByKey('SerenityBlue').style,
|
||||
dayList: "",
|
||||
participantList: []
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
name,
|
||||
startDate,
|
||||
endDate,
|
||||
content,
|
||||
startTime,
|
||||
endTime,
|
||||
type,
|
||||
style,
|
||||
dayList,
|
||||
participantList
|
||||
} = updateScheduleForm.watch();
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, [id]);
|
||||
|
||||
const init = async () => {
|
||||
if (isLoading) return;
|
||||
if (!id || id.trim().length === 0) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const initData = await scheduleNetwork.getDetail(id);
|
||||
if (!initData.success) throw new Error(initData.error);
|
||||
const data = initData.data;
|
||||
updateScheduleForm.reset({
|
||||
...data,
|
||||
content: data.content ? data.content : '',
|
||||
dayList: data.dayList ? data.dayList : '',
|
||||
participantList: data.participantList ? data.participantList : []
|
||||
});
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
console.error((e as Error).message);
|
||||
toast.error('일정을 불러오는 데에 실패하였습니다.\n잠시 후 다시 시도해주십시오.');
|
||||
setMode('detail');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
const reqUpate = async () => {
|
||||
if (isLoading) return;
|
||||
|
||||
const data = {
|
||||
name,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
type: type as Type.Type,
|
||||
style: style,
|
||||
startTime: standardTimeToContinentalTime(startTime),
|
||||
endTime: standardTimeToContinentalTime(endTime),
|
||||
dayList,
|
||||
content
|
||||
} as DTO.ScheduleCreateRequest;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const createPromise = scheduleNetwork.create(data);
|
||||
|
||||
toast.promise(createPromise, {
|
||||
loading: '일정 생성 중입니다',
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await createPromise;
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
toast.success('일정이 생성되었습니다');
|
||||
|
||||
// ✅ 기존 동작 그대로
|
||||
setMode('list');
|
||||
refetchList();
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : '에러 발생';
|
||||
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectColor = (color: ColorPaletteType) => {
|
||||
updateScheduleForm.setValue('style', color.style);
|
||||
setColorPopoverOpen(false);
|
||||
}
|
||||
|
||||
const selectType = (type: Type.Type) => {
|
||||
updateScheduleForm.setValue('type', type);
|
||||
}
|
||||
|
||||
const selectDayList = (newValues: string[]) => {
|
||||
const sortedValues = newValues.sort();
|
||||
const newDayList = sortedValues.join('');
|
||||
updateScheduleForm.setValue('dayList', newDayList);
|
||||
}
|
||||
|
||||
const selectDate = (type: 'startDate' | 'endDate', date: Date | undefined) => {
|
||||
if (!date) return;
|
||||
updateScheduleForm.setValue(type, date);
|
||||
}
|
||||
|
||||
const selectTime = (type: 'startTime' | 'endTime', time: string) => {
|
||||
updateScheduleForm.setValue(type, time);
|
||||
}
|
||||
|
||||
const selectParticipant = (participantList: string[]) => {
|
||||
updateScheduleForm.setValue('participantList', participantList);
|
||||
}
|
||||
|
||||
const OnceContent = () => {
|
||||
return (
|
||||
<div
|
||||
className="w-full h-10"
|
||||
>
|
||||
<DatePickPopover
|
||||
disabled={false}
|
||||
mode={'range'}
|
||||
popoverAlign={popoverAlign}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
setStartDate={(date: Date | undefined) => selectDate('startDate', date)}
|
||||
setEndDate={(date: Date | undefined) => selectDate('endDate', date)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DailyContent = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const DefaultContent = () => {
|
||||
return (
|
||||
(
|
||||
<ToggleGroup
|
||||
type="multiple"
|
||||
className="w-full h-10 flex flex-row justify-center items-center"
|
||||
onValueChange={selectDayList}
|
||||
>
|
||||
{
|
||||
dayLabelList.map((day) => (
|
||||
<ToggleGroupItem
|
||||
value={`${day.day}`}
|
||||
className="border w-[calc(100%/7)] h-10 rounded-none not-last:border-r-0"
|
||||
>
|
||||
{day.label}
|
||||
</ToggleGroupItem>
|
||||
))
|
||||
}
|
||||
</ToggleGroup>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
switch (type) {
|
||||
case 'once':
|
||||
return <OnceContent />;
|
||||
case 'daily':
|
||||
return <DailyContent />;
|
||||
default:
|
||||
return <DefaultContent />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col justify-between items-center">
|
||||
<div className="px-4 pt-4 w-full h-[calc(100%-40px)] flex flex-col justify-start items-start gap-4">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-4">
|
||||
<div
|
||||
onClick={() => setMode('list')}
|
||||
>
|
||||
<ArrowLeft className="stroke-indigo-100 hover:stroke-indigo-300 transition-all duration-150" />
|
||||
</div>
|
||||
<Controller
|
||||
name="name"
|
||||
control={updateScheduleForm.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<Input
|
||||
{...field}
|
||||
id="form-create-schedule-name"
|
||||
placeholder="제목"
|
||||
maxLength={10}
|
||||
className="placeholder-indigo-200! text-indigo-400 font-bold border-t-0 border-r-0 border-l-0 p-0 border-b-2 rounded-none shadow-none border-indigo-100 focus-visible:ring-0 focus-visible:border-b-indigo-300"
|
||||
style={{
|
||||
fontSize: '20px'
|
||||
}}
|
||||
tabIndex={1}
|
||||
arai-invalid={fieldState.invalid}
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
|
||||
<div className="w-6 h-6 flex justify-center items-center">
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-full w-5 h-5 border-2 border-gray-300 hover:border-indigo-300 transition-all duration-150',
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: `${style}`,
|
||||
}}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
</div>
|
||||
<ColorPickPopover
|
||||
setColor={selectColor}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
<ScrollArea
|
||||
className={
|
||||
cn(
|
||||
"min-h-[120px] h-[calc(78%)] w-full",
|
||||
"[&>div>div:last-child]:block *:data-radix-scroll-area-thumb:bg-indigo-200"
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="w-full h-full flex! flex-col! gap-4!"
|
||||
>
|
||||
<TypePickPopover
|
||||
type={type as Type.Type}
|
||||
setType={selectType}
|
||||
popoverSide={popoverSide}
|
||||
/>
|
||||
<div
|
||||
className="w-full h-10"
|
||||
>
|
||||
{renderContent()}
|
||||
</div>
|
||||
<div className="w-full h-10">
|
||||
<TimePickPopover
|
||||
mode='range'
|
||||
popoverAlign={popoverAlign}
|
||||
disabled={false}
|
||||
startTime={startTime}
|
||||
setStartTime={(time: string | undefined) => selectTime('startTime', time ?? '')}
|
||||
endTime={endTime}
|
||||
setEndTime={(time: string | undefined) => selectTime('endTime', time ?? '')}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
name="content"
|
||||
control={updateScheduleForm.control}
|
||||
render={({ field }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
rows={2}
|
||||
placeholder="일정 상세 사항"
|
||||
spellCheck={false}
|
||||
className="placeholder-indigo-200! text-indigo-400 focus-visible:placeholder-indigo-300! border-indigo-100 focus-visible:border-indigo-300 resize-none focus-visible:ring-0"
|
||||
style={{
|
||||
'scrollbarWidth': 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ParticipantPopover
|
||||
participantList={participantList || []}
|
||||
setParticipantList={selectParticipant}
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<div
|
||||
className="w-full h-10 flex flex-row justify-start items-start"
|
||||
>
|
||||
<Button
|
||||
className={cn(
|
||||
"h-full flex-5 flex justify-center items-center",
|
||||
"text-indigo-300 bg-white",
|
||||
"hover:text-white hover:bg-indigo-300",
|
||||
"rounded-none rounded-bl-xl"
|
||||
)}
|
||||
type="button"
|
||||
onClick={reqUpate}
|
||||
>
|
||||
추가
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(
|
||||
"h-full flex-5 flex justify-center items-center",
|
||||
"bg-white text-red-400",
|
||||
"hover:text-white hover:bg-red-400",
|
||||
"rounded-none rounded-br-xl"
|
||||
)}
|
||||
onClick={() => setMode('list')}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -141,7 +141,8 @@ export const ParticipantPopover = ({ participantList, setParticipantList }: Part
|
||||
className={cn(
|
||||
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
|
||||
"bg-white text-indigo-300",
|
||||
"hover:bg-indigo-300 hover:text-white"
|
||||
"hover:bg-indigo-300 hover:text-white",
|
||||
"rounded-none rounded-bl-md"
|
||||
)}
|
||||
onClick={applyList}
|
||||
>
|
||||
@@ -151,7 +152,8 @@ export const ParticipantPopover = ({ participantList, setParticipantList }: Part
|
||||
className={cn(
|
||||
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
|
||||
"bg-white text-red-400",
|
||||
"hover:bg-red-400 hover:text-white"
|
||||
"hover:bg-red-400 hover:text-white",
|
||||
"rounded-none rounded-br-md"
|
||||
)}
|
||||
onClick={cancelList}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
|
||||
import { ScheduleTypeLabel } from "@/const/schedule/ScheduleType";
|
||||
import { ScheduleListData } from "@/data/response";
|
||||
import { Converter } from "@/util/Converter";
|
||||
import { SchedulerDTO as DTO } from "@baekyangdan/core-utils";
|
||||
interface ScheduleListTileProps {
|
||||
@@ -8,7 +6,7 @@ interface ScheduleListTileProps {
|
||||
data: DTO.ScheduleList;
|
||||
onClick: (id: string) => void;
|
||||
}
|
||||
export const ScheduleListTile = ({ setMode, data, onClick }: ScheduleListTileProps) => {
|
||||
export const ScheduleListTile = ({ data, onClick }: ScheduleListTileProps) => {
|
||||
const formatter = Converter.isoStringToFormattedString;
|
||||
|
||||
const handleOnClickTile = (id: string) => {
|
||||
|
||||
23
tailwind.config.js
Normal file
23
tailwind.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: [
|
||||
"Wanted Sans Variable",
|
||||
"Wanted Sans",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
"system-ui",
|
||||
"Segoe UI",
|
||||
"Apple SD Gothic Neo",
|
||||
"Noto Sans KR",
|
||||
"Malgun Gothic",
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Symbol",
|
||||
"sans-serif"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user