- 일정 상세 조회 화면 및 기능 1차 구현 완료
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>scheduler</title>
|
<title>scheduler</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "scheduler",
|
"name": "scheduler",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@baekyangdan/core-utils": "^1.0.21",
|
"@baekyangdan/core-utils": "^1.0.23",
|
||||||
"@diceui/mention": "^0.8.0",
|
"@diceui/mention": "^0.8.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"react-router-dom": "^7.9.5",
|
"react-router-dom": "^7.9.5",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
@@ -375,9 +376,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@baekyangdan/core-utils": {
|
"node_modules/@baekyangdan/core-utils": {
|
||||||
"version": "1.0.21",
|
"version": "1.0.23",
|
||||||
"resolved": "https://gitea.bkdhome.p-e.kr/api/packages/baekyangdan/npm/%40baekyangdan%2Fcore-utils/-/1.0.21/core-utils-1.0.21.tgz",
|
"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-LYkzavYnforDtXm/icOg6rQkRAQAgpdwlC6w8dpWAz/N7ynIHdUHJZSPRRsQc9Jy3hQp7+vtKjZI4LP3NKC0UA==",
|
"integrity": "sha512-PmxaMqMOpLKiUD5+gmC/uslSpcGzGWzaIVCTJgsHnucsPvGY+cyrUFldJaeCsJ5UeCy8eZf1hVWF3DdbKFCCVA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/core": "^1.15.5",
|
"@swc/core": "^1.15.5",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@baekyangdan/core-utils": "^1.0.21",
|
"@baekyangdan/core-utils": "^1.0.23",
|
||||||
"@diceui/mention": "^0.8.0",
|
"@diceui/mention": "^0.8.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"react-router-dom": "^7.9.5",
|
"react-router-dom": "^7.9.5",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"vaul": "^1.1.2",
|
"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 './App.css';
|
||||||
|
import 'reflect-metadata';
|
||||||
import SignUpPage from './ui/page/account/signup/SignUpPage';
|
import SignUpPage from './ui/page/account/signup/SignUpPage';
|
||||||
import Layout from './layouts/Layout';
|
import Layout from './layouts/Layout';
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
|||||||
@@ -22,6 +22,5 @@ export const CreateScheduleSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
, dayList: z
|
, dayList: z
|
||||||
.string()
|
.string()
|
||||||
, participantList: z
|
.optional()
|
||||||
.array(z.string())
|
|
||||||
});
|
});
|
||||||
@@ -5,20 +5,27 @@ export const UpdateScheduleSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
, name: z
|
, name: z
|
||||||
.string()
|
.string()
|
||||||
|
.nonempty()
|
||||||
, startDate: z
|
, startDate: z
|
||||||
.date()
|
.date()
|
||||||
, endDate: z
|
, endDate: z
|
||||||
.date()
|
.date()
|
||||||
, status: z
|
, status: z
|
||||||
.string()
|
.string()
|
||||||
.default("yet")
|
, content: z
|
||||||
|
.string()
|
||||||
, type: z
|
, type: z
|
||||||
.string()
|
.string()
|
||||||
.default("once")
|
|
||||||
, style: z
|
, style: z
|
||||||
.string()
|
.string()
|
||||||
, startTime: z
|
, startTime: z
|
||||||
.string()
|
.string()
|
||||||
, endTime: z
|
, endTime: z
|
||||||
.string()
|
.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 "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@@ -115,10 +116,43 @@
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
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;
|
@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 {
|
html, body, #root {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -175,7 +175,18 @@ export class BaseNetwork {
|
|||||||
|
|
||||||
if (rawData) {
|
if (rawData) {
|
||||||
const instance = plainToInstance(dtoClass, 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;
|
(result.data as any).data = instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,33 @@
|
|||||||
import { BaseNetwork } from "./BaseNetwork"
|
import { BaseNetwork } from "./BaseNetwork"
|
||||||
import {
|
import {
|
||||||
ScheduleListRequest,
|
|
||||||
CreateScheduleRequest,
|
|
||||||
UpdateScheduleRequest,
|
UpdateScheduleRequest,
|
||||||
DeleteScheduleRequest
|
DeleteScheduleRequest
|
||||||
} from '@/data/request';
|
} from '@/data/request';
|
||||||
import {
|
|
||||||
CreateScheduleResponse,
|
|
||||||
ScheduleDetailResponse,
|
|
||||||
ScheduleListResponse
|
|
||||||
} from "@/data/response";
|
|
||||||
import { HttpApiUrl } from "@baekyangdan/core-utils";
|
import { HttpApiUrl } from "@baekyangdan/core-utils";
|
||||||
import { SchedulerDTO as DTO } from "@baekyangdan/core-utils";
|
import { SchedulerDTO } from "@baekyangdan/core-utils";
|
||||||
const ScheduleApi = HttpApiUrl.Schedule;
|
const ScheduleApi = HttpApiUrl.Schedule;
|
||||||
export class ScheduleNetwork extends BaseNetwork {
|
export class ScheduleNetwork extends BaseNetwork {
|
||||||
private baseUrl = ScheduleApi.base;
|
private baseUrl = ScheduleApi.base;
|
||||||
|
|
||||||
async getList(data: DTO.ScheduleListRequest) {
|
async getList(data: SchedulerDTO.ScheduleListRequest) {
|
||||||
return await this.post<DTO.ScheduleListResponse>(
|
return await this.post<SchedulerDTO.ScheduleListResponse, SchedulerDTO.ScheduleList>(
|
||||||
this.baseUrl,
|
this.baseUrl,
|
||||||
data
|
data,
|
||||||
|
undefined,
|
||||||
|
SchedulerDTO.ScheduleList
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDetail(id: string) {
|
async getDetail(id: string) {
|
||||||
return await this.get<DTO.ScheduleDetailResponse>(
|
return await this.get<SchedulerDTO.ScheduleDetailResponse, SchedulerDTO.ScheduleDetail>(
|
||||||
`${this.baseUrl}/${id}`
|
`${this.baseUrl}/${id}`,
|
||||||
|
undefined,
|
||||||
|
SchedulerDTO.ScheduleDetail
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: DTO.ScheduleCreateRequest) {
|
async create(data: SchedulerDTO.ScheduleCreateRequest) {
|
||||||
return await this.post<DTO.ScheduleCreateResponse>(
|
return await this.post<SchedulerDTO.ScheduleCreateResponse>(
|
||||||
`${this.baseUrl}${ScheduleApi.create}`,
|
`${this.baseUrl}${ScheduleApi.create}`,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ import { CustomCalendarCN } from "./CustomCalendarCN";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
|
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
|
||||||
import { SchedulerDTO as DTO, Type } from '@baekyangdan/core-utils';
|
import { SchedulerDTO as DTO, Type } from '@baekyangdan/core-utils';
|
||||||
interface CustomCalendarProps {
|
|
||||||
data?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventBarPosition extends DTO.ScheduleList {
|
interface EventBarPosition extends DTO.ScheduleList {
|
||||||
positionStyle: React.CSSProperties;
|
positionStyle: React.CSSProperties;
|
||||||
@@ -26,12 +23,13 @@ const TOP_OFFSET_FROM_CELL = 35;
|
|||||||
const DATE_FORMAT_ARIA = 'EEEE, MMMM do, yyyy';
|
const DATE_FORMAT_ARIA = 'EEEE, MMMM do, yyyy';
|
||||||
const DATE_FORMAT_KEY = 'yyyyMMdd';
|
const DATE_FORMAT_KEY = 'yyyyMMdd';
|
||||||
|
|
||||||
export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
export const CustomCalendar = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [refetchTrigger, setRefetchTrigger] = useState(0);
|
const [refetchTrigger, setRefetchTrigger] = useState(0);
|
||||||
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 [popoverOpenCoolDown, setPopoverOpenCoolDown] = 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 [popoverMode, setPopoverMode] = useState<SchedulePopoverMode>('list');
|
const [popoverMode, setPopoverMode] = useState<SchedulePopoverMode>('list');
|
||||||
@@ -97,7 +95,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
} as DTO.ScheduleListRequest;
|
} as DTO.ScheduleListRequest;
|
||||||
|
|
||||||
const result = await scheduleNetwork.getList(data);
|
const result = await scheduleNetwork.getList(data);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
if (isSameMonth(requestedMonth, month)) {
|
if (isSameMonth(requestedMonth, month)) {
|
||||||
@@ -163,7 +161,6 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
|
|
||||||
if (!isNaN(parsedDate.getTime())) {
|
if (!isNaN(parsedDate.getTime())) {
|
||||||
const dateKey = format(parsedDate, DATE_FORMAT_KEY);
|
const dateKey = format(parsedDate, DATE_FORMAT_KEY);
|
||||||
console.log(dateKey);
|
|
||||||
cellInfoMap.set(dateKey, {
|
cellInfoMap.set(dateKey, {
|
||||||
cell: dayButton,
|
cell: dayButton,
|
||||||
rect: dayButton.getBoundingClientRect()
|
rect: dayButton.getBoundingClientRect()
|
||||||
@@ -251,8 +248,8 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
const renderStartDate = startDateObj > calendarStart ? startDateObj : calendarStart;
|
const renderStartDate = startDateObj > calendarStart ? startDateObj : calendarStart;
|
||||||
const renderEndDate = endDateObj < calendarEnd ? endDateObj : calendarEnd;
|
const renderEndDate = endDateObj < calendarEnd ? endDateObj : calendarEnd;
|
||||||
|
|
||||||
const renderStartKey = format(renderStartDate, DATE_FORMAT_KEY);
|
// const renderStartKey = format(renderStartDate, DATE_FORMAT_KEY);
|
||||||
const renderEndKey = format(renderEndDate, DATE_FORMAT_KEY);
|
// const renderEndKey = format(renderEndDate, DATE_FORMAT_KEY);
|
||||||
|
|
||||||
const allRenderDays = eachDayOfInterval({ start: renderStartDate, end: renderEndDate });
|
const allRenderDays = eachDayOfInterval({ start: renderStartDate, end: renderEndDate });
|
||||||
|
|
||||||
@@ -366,52 +363,56 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
|
|
||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
setPopoverOpen(open);
|
setPopoverOpen(open);
|
||||||
|
setPopoverDetailId('');
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSelectedDate(undefined);
|
setSelectedDate(undefined);
|
||||||
|
setPopoverMode('list');
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDaySelect = (date: Date | undefined) => {
|
const handleDaySelect = (date: Date | undefined) => {
|
||||||
|
if (popoverOpenCoolDown) return;
|
||||||
if (!date) {
|
if (!date) {
|
||||||
|
setPopoverOpenCoolDown(true);
|
||||||
setPopoverOpen(false);
|
setPopoverOpen(false);
|
||||||
|
setPopoverDetailId('');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
setPopoverOpenCoolDown(false);
|
||||||
setSelectedDate(undefined);
|
setSelectedDate(undefined);
|
||||||
setPopoverDetailId('');
|
|
||||||
setPopoverMode('list');
|
setPopoverMode('list');
|
||||||
}, 150);
|
}, 150);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (date) {
|
|
||||||
setSelectedDate(date);
|
|
||||||
|
|
||||||
const dayOfWeek = date.getDay();
|
setSelectedDate(date);
|
||||||
|
|
||||||
if (0 <= dayOfWeek && dayOfWeek < 4) {
|
const dayOfWeek = date.getDay();
|
||||||
setPopoverSide('right');
|
|
||||||
} else {
|
|
||||||
setPopoverSide('left');
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = { weekStartsOn: 0 as 0 };
|
if (0 <= dayOfWeek && dayOfWeek < 4) {
|
||||||
|
setPopoverSide('right');
|
||||||
const totalWeeks = getWeeksInMonth(date, options);
|
} else {
|
||||||
|
setPopoverSide('left');
|
||||||
const currentWeekNumber = getWeekOfMonth(date, options);
|
|
||||||
|
|
||||||
const threshold = Math.ceil(totalWeeks / 2);
|
|
||||||
|
|
||||||
if (currentWeekNumber <= threshold) {
|
|
||||||
setPopoverAlign('start');
|
|
||||||
} else {
|
|
||||||
setPopoverAlign('end');
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setPopoverOpen(true);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = (
|
const findDateFromClick = (
|
||||||
@@ -455,7 +456,6 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
handleDaySelect(clickedDate);
|
handleDaySelect(clickedDate);
|
||||||
} else {
|
} else {
|
||||||
handleDaySelect(undefined);
|
handleDaySelect(undefined);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,7 +513,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
id={pos.segmentId}
|
id={pos.segmentId}
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex flex-row justify-start items-center absolute select-none`,
|
`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}}
|
style={{...pos.positionStyle, backgroundColor: pos.style}}
|
||||||
>
|
>
|
||||||
@@ -530,7 +530,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
setDetailId={setPopoverDetailId}
|
setDetailId={setPopoverDetailId}
|
||||||
popoverSide={popoverSide}
|
popoverSide={popoverSide}
|
||||||
popoverAlign={popoverAlign}
|
popoverAlign={popoverAlign}
|
||||||
onScheduleCreated={refetchList}
|
reqRefetchList={refetchList}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { PopoverContent } from '@/components/ui/popover';
|
import { PopoverContent } from '@/components/ui/popover';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
|
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
|
||||||
import { ScheduleListContent } from './content/ScheduleListContent';
|
import { ScheduleListContent } from './content/ScheduleListContent';
|
||||||
import { ScheduleDetailContent } from './content/ScheduleDetailContent';
|
import { ScheduleDetailContent } from './content/ScheduleDetailContent';
|
||||||
import type { SchedulePopoverMode } from '@/const/schedule/SchedulePopoverMode';
|
import type { SchedulePopoverMode } from '@/const/schedule/SchedulePopoverMode';
|
||||||
|
import { ScheduleUpdateContent } from './content/ScheduleUpdateContent';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
interface ScheduleSheetProps {
|
interface ScheduleSheetProps {
|
||||||
date: Date | undefined;
|
date: Date | undefined;
|
||||||
@@ -14,76 +15,61 @@ interface ScheduleSheetProps {
|
|||||||
setMode: (mode: SchedulePopoverMode) => void;
|
setMode: (mode: SchedulePopoverMode) => void;
|
||||||
detailId: string;
|
detailId: string;
|
||||||
setDetailId: (id: string) => void;
|
setDetailId: (id: string) => void;
|
||||||
onScheduleCreated: () => void;
|
reqRefetchList: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SchedulePopover = ({ date, open, mode, setMode, detailId, setDetailId, popoverSide, popoverAlign, onScheduleCreated }: ScheduleSheetProps) => {
|
export const SchedulePopover = memo(({ mode, ...props}: 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 />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[384px] min-h-[125px] h-[calc(100vh/2.3)]"
|
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={popoverAlign} side={popoverSide}
|
align={props.popoverAlign} side={props.popoverSide}
|
||||||
>
|
>
|
||||||
{<SchedulePopoverContent />}
|
{<SchedulePopoverContent mode={mode} { ...props} />}
|
||||||
</PopoverContent>
|
</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";
|
||||||
@@ -18,4 +18,9 @@ export interface ScheduleListContentProps extends BaseProps {
|
|||||||
|
|
||||||
export interface ScheduleDetailContentProps extends BaseProps {
|
export interface ScheduleDetailContentProps extends BaseProps {
|
||||||
id: string;
|
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 { TimePickPopover } from '../popover/TimePickPopover';
|
||||||
import { TypePickPopover } from '../popover/TypePickPopover';
|
import { TypePickPopover } from '../popover/TypePickPopover';
|
||||||
import type { ScheduleCreateContentProps } from './ContentProps';
|
import type { ScheduleCreateContentProps } from './ContentProps';
|
||||||
import { Converter } from '@/util/Converter';
|
|
||||||
import { SchedulerDTO as DTO } from '@baekyangdan/core-utils';
|
import { SchedulerDTO as DTO } from '@baekyangdan/core-utils';
|
||||||
|
|
||||||
export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign, refetchList }: ScheduleCreateContentProps) => {
|
export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign, refetchList }: ScheduleCreateContentProps) => {
|
||||||
@@ -52,8 +51,7 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
|||||||
type: "once",
|
type: "once",
|
||||||
status: "yet",
|
status: "yet",
|
||||||
style: getPaletteByKey('SerenityBlue').style,
|
style: getPaletteByKey('SerenityBlue').style,
|
||||||
dayList: "",
|
dayList: ""
|
||||||
participantList: []
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,18 +71,6 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
|||||||
const reqCreate = async () => {
|
const reqCreate = async () => {
|
||||||
if (isLoading) return;
|
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 = {
|
const data = {
|
||||||
name,
|
name,
|
||||||
startDate: startDate,
|
startDate: startDate,
|
||||||
@@ -212,111 +198,117 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col justify-start items-start gap-4">
|
<div className="flex flex-col w-full h-full justify-between items-center">
|
||||||
<div className="w-full flex flex-row justify-between items-center gap-4">
|
<div className="p-4 w-full h-[calc(100%-40px)] flex flex-col justify-start items-start">
|
||||||
<div
|
<div className="w-full flex flex-row justify-between items-center gap-4">
|
||||||
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
|
<div
|
||||||
className="w-full h-10"
|
onClick={() => setMode('list')}
|
||||||
>
|
>
|
||||||
{renderContent()}
|
<ArrowLeft className="stroke-indigo-100 hover:stroke-indigo-300 transition-all duration-150" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-10">
|
<Controller
|
||||||
<TimePickPopover
|
name="name"
|
||||||
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}
|
control={createScheduleForm.control}
|
||||||
render={({ field }) => (
|
render={({ field, fieldState }) => (
|
||||||
<Textarea
|
<Field data-invalid={fieldState.invalid}>
|
||||||
{...field}
|
<Input
|
||||||
rows={2}
|
{...field}
|
||||||
placeholder="일정 상세 사항"
|
id="form-create-schedule-name"
|
||||||
className="placeholder-indigo-200! focus-visible:placeholder-indigo-300! border-indigo-100 focus-visible:border-indigo-300 resize-none focus-visible:ring-0"
|
placeholder="제목"
|
||||||
style={{
|
maxLength={10}
|
||||||
'scrollbarWidth': 'none'
|
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
|
<Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
|
||||||
participantList={participantList}
|
<div className="w-6 h-6 flex justify-center items-center">
|
||||||
setParticipantList={selectParticipant}
|
<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>
|
</div>
|
||||||
</ScrollArea>
|
<ScrollArea
|
||||||
<div
|
className={
|
||||||
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"
|
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
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full flex-5 rounded-none rounded-bl-md flex justify-center items-center",
|
"h-full flex-5 rounded-none rounded-bl-md flex justify-center items-center",
|
||||||
"text-indigo-300 bg-white",
|
"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"
|
type="button"
|
||||||
onClick={reqCreate}
|
onClick={reqCreate}
|
||||||
@@ -325,9 +317,10 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
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",
|
"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')}
|
onClick={() => setMode('list')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,38 +1,90 @@
|
|||||||
import { ScheduleNetwork } from '@/network/ScheduleNetwork';
|
import { ScheduleNetwork } from '@/network/ScheduleNetwork';
|
||||||
import type { ScheduleDetailContentProps } from './ContentProps';
|
import type { ScheduleDetailContentProps } from './ContentProps';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { ArrowLeft, ChevronUp, Clock } from 'lucide-react';
|
import { ArrowLeft, ChevronUp, LucideCalendarDays, LucideClock, LucidePen } from 'lucide-react';
|
||||||
import type { ScheduleDetailData } from '@/data/response';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
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 scheduleNetwork = new ScheduleNetwork();
|
||||||
const [data, setData] = useState<DTO.ScheduleDetail>();
|
const [data, setData] = useState<DTO.ScheduleDetail>();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [commentFold, setCommentFold] = useState(true);
|
const [commentFold, setCommentFold] = useState(true);
|
||||||
|
const converter = new Converter();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id || id.trim().length === 0) {
|
if (!id || id === '' || id.trim().length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reqDetail();
|
reqDetail();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const reqDetail = async () => {
|
const reqDetail = async () => {
|
||||||
const result = await scheduleNetwork.getDetail(id);
|
setIsLoading(true);
|
||||||
if (result.success) {
|
try {
|
||||||
setData(result.data);
|
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');
|
setMode('list');
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
|
const moveToUpdate = useCallback(() => {
|
||||||
|
setMode('update');
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
isLoading || !data
|
||||||
className="relative w-full h-full flex flex-col justify-start items-start"
|
? <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 className="relative w-full h-10 flex flex-row justify-center items-center border-b border-b-indigo-300">
|
||||||
<div
|
<div
|
||||||
@@ -43,21 +95,30 @@ export const ScheduleDetailContent = ({ setMode, popoverSide, popoverAlign, id }
|
|||||||
onClick={moveToList}
|
onClick={moveToList}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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)]"
|
"h-[calc(100%-86px)]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<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>
|
</div>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
className="max-h-30 w-full border"
|
className="h-25 w-full"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="whitespace-pre-wrap text-indigo-500 select-none"
|
className="whitespace-pre-wrap text-indigo-500 select-none"
|
||||||
@@ -65,28 +126,60 @@ export const ScheduleDetailContent = ({ setMode, popoverSide, popoverAlign, id }
|
|||||||
{data?.content && data?.content}
|
{data?.content && data?.content}
|
||||||
</span>
|
</span>
|
||||||
</ScrollArea>
|
</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>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute left-0 bottom-0 w-full flex flex-col transition-all overflow-hidden duration-300 bg-white",
|
"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-0 border-indigo-100 rounded-t-lg",
|
"border-t border-l border-r border-b border-indigo-100 rounded-t-lg",
|
||||||
commentFold? "h-10" : "h-[calc(100%-44px)] border-b"
|
commentFold? "h-10" : "h-[calc(100%-44px)]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full h-10 px-2 flex flex-row justify-between items-center",
|
"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
|
<span
|
||||||
className="text-indigo-400 text-sm"
|
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>
|
</span>
|
||||||
|
|
||||||
<div
|
<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)}
|
onClick={() => setCommentFold(prev => !prev)}
|
||||||
>
|
>
|
||||||
<ChevronUp
|
<ChevronUp
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { ListScheduleSchema } from "@/data/form/schedule/listSchedule.schema";
|
import { ListScheduleSchema } from "@/data/form/schedule/listSchedule.schema";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { List, PenSquare } from "lucide-react";
|
import { PenSquare } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
import type { ScheduleListContentProps } from "./ContentProps";
|
import type { ScheduleListContentProps } from "./ContentProps";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { ScheduleNetwork } from "@/network/ScheduleNetwork";
|
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 { ScheduleListTile } from "../tile/ScheduleListTile";
|
||||||
import { SchedulerDTO as DTO, Type } from "@baekyangdan/core-utils";
|
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 [isLoading, setIsLoading] = useState(false);
|
||||||
const [scheduleList, setScheduleList] = useState<Array<DTO.ScheduleList>>([]);
|
const [scheduleList, setScheduleList] = useState<Array<DTO.ScheduleList>>([]);
|
||||||
const scheduleNetwork = new ScheduleNetwork();
|
const scheduleNetwork = new ScheduleNetwork();
|
||||||
@@ -59,11 +57,19 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
|||||||
styleList: styleList
|
styleList: styleList
|
||||||
} as DTO.ScheduleListRequest;
|
} as DTO.ScheduleListRequest;
|
||||||
|
|
||||||
const result = await scheduleNetwork.getList(data);
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await scheduleNetwork.getList(data);
|
||||||
|
|
||||||
if (result.success) {
|
if (!result.success) throw new Error(result.error);
|
||||||
setScheduleList(result.data!);
|
|
||||||
|
setScheduleList(result.data);
|
||||||
|
setIsLoading(false);
|
||||||
|
} catch (e) {
|
||||||
|
setIsLoading(false);
|
||||||
|
toast.error('일정을 불러오는 데에 실패하였습니다.\n잠시 후 다시 시도해주십시오.');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveToDetail = (id: string) => {
|
const moveToDetail = (id: string) => {
|
||||||
@@ -72,7 +78,7 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<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>
|
<span className="text-indigo-400">{date && format(date, "yyyy년 MM월 dd일")}</span>
|
||||||
<div className="absolute top-3 right-0.5 group">
|
<div className="absolute top-3 right-0.5 group">
|
||||||
@@ -84,19 +90,27 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-[calc(100%-40px)]">
|
<div className="w-full h-[calc(100%-40px)]">
|
||||||
<ScrollArea
|
{
|
||||||
className="w-full h-full"
|
isLoading
|
||||||
>
|
? <div className="w-full h-full flex flex-col justify-start items-start gap-3">
|
||||||
<div className="w-full h-full flex flex-col justify-start items-start gap-3">
|
{[1,2,3,4].map((_) => (
|
||||||
{ scheduleList.map(schedule => (
|
<Skeleton className="w-full h-10 bg-indigo-200"/>
|
||||||
<ScheduleListTile
|
))}
|
||||||
onClick={moveToDetail}
|
|
||||||
data={schedule}
|
|
||||||
setMode={setMode}
|
|
||||||
/>
|
|
||||||
)) }
|
|
||||||
</div>
|
</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>
|
||||||
</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(
|
className={cn(
|
||||||
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
|
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
|
||||||
"bg-white text-indigo-300",
|
"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}
|
onClick={applyList}
|
||||||
>
|
>
|
||||||
@@ -151,7 +152,8 @@ export const ParticipantPopover = ({ participantList, setParticipantList }: Part
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
|
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
|
||||||
"bg-white text-red-400",
|
"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}
|
onClick={cancelList}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
|
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
|
||||||
import { ScheduleTypeLabel } from "@/const/schedule/ScheduleType";
|
|
||||||
import { ScheduleListData } from "@/data/response";
|
|
||||||
import { Converter } from "@/util/Converter";
|
import { Converter } from "@/util/Converter";
|
||||||
import { SchedulerDTO as DTO } from "@baekyangdan/core-utils";
|
import { SchedulerDTO as DTO } from "@baekyangdan/core-utils";
|
||||||
interface ScheduleListTileProps {
|
interface ScheduleListTileProps {
|
||||||
@@ -8,7 +6,7 @@ interface ScheduleListTileProps {
|
|||||||
data: DTO.ScheduleList;
|
data: DTO.ScheduleList;
|
||||||
onClick: (id: string) => void;
|
onClick: (id: string) => void;
|
||||||
}
|
}
|
||||||
export const ScheduleListTile = ({ setMode, data, onClick }: ScheduleListTileProps) => {
|
export const ScheduleListTile = ({ data, onClick }: ScheduleListTileProps) => {
|
||||||
const formatter = Converter.isoStringToFormattedString;
|
const formatter = Converter.isoStringToFormattedString;
|
||||||
|
|
||||||
const handleOnClickTile = (id: string) => {
|
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