issue #60
All checks were successful
Test CI / build (push) Successful in 27s

- 일정 상세 조회 화면 및 기능 1차 구현 완료
This commit is contained in:
geonhee-min
2025-12-17 17:02:00 +09:00
parent 60e9d2a631
commit 8b085107f6
20 changed files with 1494 additions and 291 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -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';

View File

@@ -22,6 +22,5 @@ export const CreateScheduleSchema = z.object({
.string() .string()
, dayList: z , dayList: z
.string() .string()
, participantList: z .optional()
.array(z.string())
}); });

View File

@@ -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()
}); });

View File

@@ -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%;

View File

@@ -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;
} }
} }

View File

@@ -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
); );

View File

@@ -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>

View File

@@ -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";

View File

@@ -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;
} }

View File

@@ -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')}
> >

View File

@@ -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

View File

@@ -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>
) )

View File

@@ -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>
)
}

View File

@@ -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}
> >

View File

@@ -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
View 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"
]
}
}
}
}