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">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/scheduler_favicon__1_.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>scheduler</title>
</head>

9
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "scheduler",
"version": "0.0.0",
"dependencies": {
"@baekyangdan/core-utils": "^1.0.21",
"@baekyangdan/core-utils": "^1.0.23",
"@diceui/mention": "^0.8.0",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
@@ -57,6 +57,7 @@
"react-resizable-panels": "^3.0.6",
"react-router-dom": "^7.9.5",
"recharts": "^2.15.4",
"reflect-metadata": "^0.2.2",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"vaul": "^1.1.2",
@@ -375,9 +376,9 @@
}
},
"node_modules/@baekyangdan/core-utils": {
"version": "1.0.21",
"resolved": "https://gitea.bkdhome.p-e.kr/api/packages/baekyangdan/npm/%40baekyangdan%2Fcore-utils/-/1.0.21/core-utils-1.0.21.tgz",
"integrity": "sha512-LYkzavYnforDtXm/icOg6rQkRAQAgpdwlC6w8dpWAz/N7ynIHdUHJZSPRRsQc9Jy3hQp7+vtKjZI4LP3NKC0UA==",
"version": "1.0.23",
"resolved": "https://gitea.bkdhome.p-e.kr/api/packages/baekyangdan/npm/%40baekyangdan%2Fcore-utils/-/1.0.23/core-utils-1.0.23.tgz",
"integrity": "sha512-PmxaMqMOpLKiUD5+gmC/uslSpcGzGWzaIVCTJgsHnucsPvGY+cyrUFldJaeCsJ5UeCy8eZf1hVWF3DdbKFCCVA==",
"license": "ISC",
"dependencies": {
"@swc/core": "^1.15.5",

View File

@@ -11,7 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"@baekyangdan/core-utils": "^1.0.21",
"@baekyangdan/core-utils": "^1.0.23",
"@diceui/mention": "^0.8.0",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
@@ -60,6 +60,7 @@
"react-resizable-panels": "^3.0.6",
"react-router-dom": "^7.9.5",
"recharts": "^2.15.4",
"reflect-metadata": "^0.2.2",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"vaul": "^1.1.2",

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 'reflect-metadata';
import SignUpPage from './ui/page/account/signup/SignUpPage';
import Layout from './layouts/Layout';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';

View File

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

View File

@@ -5,20 +5,27 @@ export const UpdateScheduleSchema = z.object({
.string()
, name: z
.string()
.nonempty()
, startDate: z
.date()
, endDate: z
.date()
, status: z
.string()
.default("yet")
, content: z
.string()
, type: z
.string()
.default("once")
, style: z
.string()
, startTime: z
.string()
, endTime: z
.string()
, dayList: z
.string()
.optional()
, participantList: z
.array(z.string())
.optional()
});

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 "tw-animate-css";
@@ -115,10 +116,43 @@
@apply border-border outline-ring/50;
}
body {
font-family: "Wanted Sans Variable", "Wanted Sans", -apple-system,BlinkMacSystemFont, system-ui, "Segeo UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segeo UI Emoji", "Segeo UI Symbol", sans-serif;
@apply bg-background text-foreground;
}
}
@layer utilities {
/* Tailwind의 굵기 유틸리티를 @apply로 재정의 */
/* 일반적인 가변 폰트의 wght 축 값을 사용 (400~700 사이) */
.font-thin {
font-variation-settings: 'wght' 100;
}
.font-extralight {
font-variation-settings: 'wght' 200;
}
.font-light {
font-variation-settings: 'wght' 300;
}
.font-normal, .font-regular { /* font-normal 및 font-regular 모두 400 */
font-variation-settings: 'wght' 400;
}
.font-medium {
font-variation-settings: 'wght' 500;
}
.font-semibold {
font-variation-settings: 'wght' 600;
}
.font-bold {
font-variation-settings: 'wght' 700;
}
.font-extrabold {
font-variation-settings: 'wght' 800;
}
.font-black {
font-variation-settings: 'wght' 900;
}
}
html, body, #root {
width: 100%;
height: 100%;

View File

@@ -175,7 +175,18 @@ export class BaseNetwork {
if (rawData) {
const instance = plainToInstance(dtoClass, rawData);
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;
}
}

View File

@@ -1,36 +1,33 @@
import { BaseNetwork } from "./BaseNetwork"
import {
ScheduleListRequest,
CreateScheduleRequest,
UpdateScheduleRequest,
DeleteScheduleRequest
} from '@/data/request';
import {
CreateScheduleResponse,
ScheduleDetailResponse,
ScheduleListResponse
} from "@/data/response";
import { HttpApiUrl } from "@baekyangdan/core-utils";
import { SchedulerDTO as DTO } from "@baekyangdan/core-utils";
import { SchedulerDTO } from "@baekyangdan/core-utils";
const ScheduleApi = HttpApiUrl.Schedule;
export class ScheduleNetwork extends BaseNetwork {
private baseUrl = ScheduleApi.base;
async getList(data: DTO.ScheduleListRequest) {
return await this.post<DTO.ScheduleListResponse>(
async getList(data: SchedulerDTO.ScheduleListRequest) {
return await this.post<SchedulerDTO.ScheduleListResponse, SchedulerDTO.ScheduleList>(
this.baseUrl,
data
data,
undefined,
SchedulerDTO.ScheduleList
);
}
async getDetail(id: string) {
return await this.get<DTO.ScheduleDetailResponse>(
`${this.baseUrl}/${id}`
return await this.get<SchedulerDTO.ScheduleDetailResponse, SchedulerDTO.ScheduleDetail>(
`${this.baseUrl}/${id}`,
undefined,
SchedulerDTO.ScheduleDetail
);
}
async create(data: DTO.ScheduleCreateRequest) {
return await this.post<DTO.ScheduleCreateResponse>(
async create(data: SchedulerDTO.ScheduleCreateRequest) {
return await this.post<SchedulerDTO.ScheduleCreateResponse>(
`${this.baseUrl}${ScheduleApi.create}`,
data
);

View File

@@ -10,9 +10,6 @@ import { CustomCalendarCN } from "./CustomCalendarCN";
import { toast } from "sonner";
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
import { SchedulerDTO as DTO, Type } from '@baekyangdan/core-utils';
interface CustomCalendarProps {
data?: any;
}
interface EventBarPosition extends DTO.ScheduleList {
positionStyle: React.CSSProperties;
@@ -26,12 +23,13 @@ const TOP_OFFSET_FROM_CELL = 35;
const DATE_FORMAT_ARIA = 'EEEE, MMMM do, yyyy';
const DATE_FORMAT_KEY = 'yyyyMMdd';
export const CustomCalendar = ({ data }: CustomCalendarProps) => {
export const CustomCalendar = () => {
const [isLoading, setIsLoading] = useState(false);
const [refetchTrigger, setRefetchTrigger] = useState(0);
const [weekCount, setWeekCount] = useState(5);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
const [popoverOpen, setPopoverOpen] = useState(false);
const [popoverOpenCoolDown, setPopoverOpenCoolDown] = useState(false);
const [popoverSide, setPopoverSide] = useState<'right' | 'left'>('right');
const [popoverAlign, setPopoverAlign] = useState<'start' | 'end'>('end');
const [popoverMode, setPopoverMode] = useState<SchedulePopoverMode>('list');
@@ -163,7 +161,6 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
if (!isNaN(parsedDate.getTime())) {
const dateKey = format(parsedDate, DATE_FORMAT_KEY);
console.log(dateKey);
cellInfoMap.set(dateKey, {
cell: dayButton,
rect: dayButton.getBoundingClientRect()
@@ -251,8 +248,8 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
const renderStartDate = startDateObj > calendarStart ? startDateObj : calendarStart;
const renderEndDate = endDateObj < calendarEnd ? endDateObj : calendarEnd;
const renderStartKey = format(renderStartDate, DATE_FORMAT_KEY);
const renderEndKey = format(renderEndDate, DATE_FORMAT_KEY);
// const renderStartKey = format(renderStartDate, DATE_FORMAT_KEY);
// const renderEndKey = format(renderEndDate, DATE_FORMAT_KEY);
const allRenderDays = eachDayOfInterval({ start: renderStartDate, end: renderEndDate });
@@ -366,24 +363,29 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
const handleOpenChange = (open: boolean) => {
setPopoverOpen(open);
setPopoverDetailId('');
if (!open) {
setTimeout(() => {
setSelectedDate(undefined);
setPopoverMode('list');
}, 150);
}
}
const handleDaySelect = (date: Date | undefined) => {
if (popoverOpenCoolDown) return;
if (!date) {
setPopoverOpenCoolDown(true);
setPopoverOpen(false);
setTimeout(() => {
setSelectedDate(undefined);
setPopoverDetailId('');
setTimeout(() => {
setPopoverOpenCoolDown(false);
setSelectedDate(undefined);
setPopoverMode('list');
}, 150);
return;
}
if (date) {
setSelectedDate(date);
const dayOfWeek = date.getDay();
@@ -410,8 +412,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
requestAnimationFrame(() => {
setPopoverOpen(true);
})
}
});
}
const findDateFromClick = (
@@ -455,7 +456,6 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
handleDaySelect(clickedDate);
} else {
handleDaySelect(undefined);
}
}
@@ -513,7 +513,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
id={pos.segmentId}
className={cn(
`flex flex-row justify-start items-center absolute select-none`,
"py-0.5 px-2 rounded-sm text-xs text-white overflow-hidden"
"py-0.5 px-2 rounded-sm text-xs font-thin text-white overflow-hidden"
)}
style={{...pos.positionStyle, backgroundColor: pos.style}}
>
@@ -530,7 +530,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
setDetailId={setPopoverDetailId}
popoverSide={popoverSide}
popoverAlign={popoverAlign}
onScheduleCreated={refetchList}
reqRefetchList={refetchList}
/>
</Popover>

View File

@@ -1,9 +1,10 @@
import { PopoverContent } from '@/components/ui/popover';
import { useEffect, useState } from 'react';
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
import { ScheduleListContent } from './content/ScheduleListContent';
import { ScheduleDetailContent } from './content/ScheduleDetailContent';
import type { SchedulePopoverMode } from '@/const/schedule/SchedulePopoverMode';
import { ScheduleUpdateContent } from './content/ScheduleUpdateContent';
import { memo } from 'react';
interface ScheduleSheetProps {
date: Date | undefined;
@@ -14,37 +15,22 @@ interface ScheduleSheetProps {
setMode: (mode: SchedulePopoverMode) => void;
detailId: string;
setDetailId: (id: string) => void;
onScheduleCreated: () => void;
reqRefetchList: () => void;
}
export const SchedulePopover = ({ date, open, mode, setMode, detailId, setDetailId, popoverSide, popoverAlign, onScheduleCreated }: ScheduleSheetProps) => {
export const SchedulePopover = memo(({ mode, ...props}: ScheduleSheetProps) => {
useEffect(() => {
if (!open) {
setTimeout(() => {
setMode('list');
}, 150);
}
}, [open]);
const DetailContent = () => {
return (
<div>
Detail
</div>
<PopoverContent
className="p-0 rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[384px] min-h-[125px] h-[calc(100vh/2.2)]"
align={props.popoverAlign} side={props.popoverSide}
>
{<SchedulePopoverContent mode={mode} { ...props} />}
</PopoverContent>
)
}
});
const UpdateContent = () => {
return (
<div>
Update
</div>
)
}
const SchedulePopoverContent = () => {
const SchedulePopoverContent = memo(({ mode, setMode, setDetailId, date, popoverAlign, popoverSide, open, reqRefetchList, detailId }: ScheduleSheetProps) => {
switch(mode) {
case 'list':
return <ScheduleListContent
@@ -62,7 +48,7 @@ export const SchedulePopover = ({ date, open, mode, setMode, detailId, setDetail
popoverAlign={popoverAlign}
popoverSide={popoverSide}
open={open}
refetchList={onScheduleCreated}
refetchList={reqRefetchList}
/>
case 'detail':
return <ScheduleDetailContent
@@ -74,16 +60,16 @@ export const SchedulePopover = ({ date, open, mode, setMode, detailId, setDetail
id={detailId}
/>
case 'update':
return <UpdateContent />
}
return <ScheduleUpdateContent
setMode={setMode}
date={date}
popoverAlign={popoverAlign}
popoverSide={popoverSide}
open={open}
id={detailId}
refetchList={reqRefetchList}
/>
}
});
return (
<PopoverContent
className="rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[384px] min-h-[125px] h-[calc(100vh/2.3)]"
align={popoverAlign} side={popoverSide}
>
{<SchedulePopoverContent />}
</PopoverContent>
)
}
SchedulePopover.displayName = "SchedulePopover";

View File

@@ -19,3 +19,8 @@ export interface ScheduleListContentProps extends BaseProps {
export interface ScheduleDetailContentProps extends BaseProps {
id: string;
}
export interface ScheduleUpdateContentProps extends BaseProps {
refetchList: () => void;
id: string;
}

View File

@@ -24,7 +24,6 @@ import { DatePickPopover } from '../popover/DatePickPopover';
import { TimePickPopover } from '../popover/TimePickPopover';
import { TypePickPopover } from '../popover/TypePickPopover';
import type { ScheduleCreateContentProps } from './ContentProps';
import { Converter } from '@/util/Converter';
import { SchedulerDTO as DTO } from '@baekyangdan/core-utils';
export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign, refetchList }: ScheduleCreateContentProps) => {
@@ -52,8 +51,7 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
type: "once",
status: "yet",
style: getPaletteByKey('SerenityBlue').style,
dayList: "",
participantList: []
dayList: ""
}
});
@@ -73,18 +71,6 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
const reqCreate = async () => {
if (isLoading) return;
// const data = {
// name,
// startDate: Converter.dateToUTC9(startDate),
// endDate: Converter.dateToUTC9(endDate),
// content,
// startTime: standardTimeToContinentalTime(startTime),
// endTime: standardTimeToContinentalTime(endTime),
// type: type as Type.Type,
// status: status as Type.Status,
// style
// } as DTO.ScheduleCreateRequest;
const data = {
name,
startDate: startDate,
@@ -212,7 +198,8 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
}
return (
<div className="w-full h-full flex flex-col justify-start items-start gap-4">
<div className="flex flex-col w-full h-full justify-between items-center">
<div className="p-4 w-full h-[calc(100%-40px)] flex flex-col justify-start items-start">
<div className="w-full flex flex-row justify-between items-center gap-4">
<div
onClick={() => setMode('list')}
@@ -228,7 +215,8 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
{...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"
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'
}}
@@ -261,6 +249,7 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
className={
cn(
"min-h-[125px] h-[calc(100vh/2.3-40px)]! w-full",
"[&>div>div:last-child]:block"
)
}
>
@@ -296,7 +285,8 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
{...field}
rows={2}
placeholder="일정 상세 사항"
className="placeholder-indigo-200! focus-visible:placeholder-indigo-300! border-indigo-100 focus-visible:border-indigo-300 resize-none focus-visible:ring-0"
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'
}}
@@ -309,14 +299,16 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
/> */}
</div>
</ScrollArea>
</div>
<div
className="absolute w-full border-b border-l border-r rounded-b-md left-0 bottom-0 h-10 flex flex-row justify-start items-end p-0"
className="w-full h-10 flex flex-row justify-start items-end p-0"
>
<Button
className={cn(
"h-full flex-5 rounded-none rounded-bl-md flex justify-center items-center",
"text-indigo-300 bg-white",
"hover:text-white hover:bg-indigo-300"
"hover:text-white hover:bg-indigo-300",
"rounded-none rounded-bl-xl"
)}
type="button"
onClick={reqCreate}
@@ -325,9 +317,10 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
</Button>
<Button
className={cn(
"h-full flex-5 rounded-none rounded-br-md flex justify-center items-center",
"h-full flex-5 flex justify-center items-center",
"bg-white text-red-400",
"hover:text-white hover:bg-red-400"
"hover:text-white hover:bg-red-400",
"rounded-none rounded-br-xl"
)}
onClick={() => setMode('list')}
>

View File

@@ -1,38 +1,90 @@
import { ScheduleNetwork } from '@/network/ScheduleNetwork';
import type { ScheduleDetailContentProps } from './ContentProps';
import { useEffect, useState } from 'react';
import { ArrowLeft, ChevronUp, Clock } from 'lucide-react';
import type { ScheduleDetailData } from '@/data/response';
import { useCallback, useEffect, useState } from 'react';
import { ArrowLeft, ChevronUp, LucideCalendarDays, LucideClock, LucidePen } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ScrollArea } from '@/components/ui/scroll-area';
import { SchedulerDTO as DTO } from '@baekyangdan/core-utils'
import { Converter, DateFormat, SchedulerDTO as DTO } from '@baekyangdan/core-utils'
import { toast } from 'sonner';
import { Skeleton } from '@/components/ui/skeleton';
export const ScheduleDetailContent = ({ setMode, popoverSide, popoverAlign, id }: ScheduleDetailContentProps) => {
export const ScheduleDetailContent = ({ setMode, id }: ScheduleDetailContentProps) => {
const scheduleNetwork = new ScheduleNetwork();
const [data, setData] = useState<DTO.ScheduleDetail>();
const [isLoading, setIsLoading] = useState(false);
const [commentFold, setCommentFold] = useState(true);
const converter = new Converter();
useEffect(() => {
if (!id || id.trim().length === 0) {
if (!id || id === '' || id.trim().length === 0) {
return;
}
reqDetail();
}, [id]);
const reqDetail = async () => {
setIsLoading(true);
try {
const result = await scheduleNetwork.getDetail(id);
if (result.success) {
setData(result.data);
} else {
throw new Error;
}
}
const moveToList = () => {
} catch (e) {
toast.error('일정을 불러오는 데에 실패하였습니다.\n잠시 후 다시 시도해주십시오.');
setMode('list');
}
setTimeout(() => {
setIsLoading(false);
}, 1000);
}
const moveToList = useCallback(() => {
setMode('list');
}, []);
const moveToUpdate = useCallback(() => {
setMode('update');
}, []);
return (
isLoading || !data
? <div
className="p-4 relative w-full h-full flex flex-col justify-start tiems-start"
>
<div
className="relative w-full h-full flex flex-col justify-start items-start"
className="relative w-full h-10 flex flex-row justify-center items-center"
>
<div
className="absolute top-1.5 left-0.5"
>
<ArrowLeft
className="stroke-indigo-200 hover:stroke-indigo-400 transition-all duration-300"
onClick={moveToList}
/>
</div>
<Skeleton className="h-full w-3/5 bg-indigo-200" />
</div>
<div
className="mt-2 w-full h-[calc(100%-86px)] flex flex-col gap-2"
>
<div
className="w-full h-5 flex flex-col items-end"
>
<Skeleton className="w-3/5 h-full bg-indigo-200 rounded-sm" />
</div>
<Skeleton className="w-full h-30 bg-indigo-200" />
<div
className="w-full h-12 flex flex-row gap-3.5"
>
<Skeleton className="flex-1 h-full bg-indigo-200" />
<Skeleton className="flex-1 h-full bg-indigo-200" />
</div>
<Skeleton className="w-4/5 h-10 bg-indigo-200" />
</div>
</div>
: <div
className="p-4 relative w-full h-full flex flex-col justify-start items-start"
>
<div className="relative w-full h-10 flex flex-row justify-center items-center border-b border-b-indigo-300">
<div
@@ -43,21 +95,30 @@ export const ScheduleDetailContent = ({ setMode, popoverSide, popoverAlign, id }
onClick={moveToList}
/>
</div>
<span className="text-indigo-400 select-none">{data?.name}</span>
<span className="text-indigo-400 text-lg font-bold select-none">{data?.name}</span>
<div
className="absolute top-3.5 right-0.5"
>
<LucidePen
size={16}
className="stroke-indigo-200 hover:stroke-indigo-400 transition-all duration-300"
onClick={moveToUpdate}
/>
</div>
</div>
<div
className={cn(
"mt-2 w-full flex flex-col transition-all duration-300 border",
"mt-2 w-full flex flex-col transition-all duration-300",
"h-[calc(100%-86px)]"
)}
>
<div
className="w-full border-b border-b-indigo-100 flex flex-col items-end text-xs text-indigo-200"
className="w-full flex flex-col items-end text-xs text-indigo-300"
>
<span className="flex flex-row justify-start items-center gap-1"><Clock size={12}/> : {data?.createdAt}</span>
<span className="flex flex-row justify-start items-center text-sm font-regular gap-1"><LucideClock size={12}/> : {data?.createdAt}</span>
</div>
<ScrollArea
className="max-h-30 w-full border"
className="h-25 w-full"
>
<span
className="whitespace-pre-wrap text-indigo-500 select-none"
@@ -65,28 +126,60 @@ export const ScheduleDetailContent = ({ setMode, popoverSide, popoverAlign, id }
{data?.content && data?.content}
</span>
</ScrollArea>
<div className="w-full h-12 flex flex-row justify-between items-center">
<div className="flex-1 flex flex-col">
<div className="flex flex-row gap-1 items-center">
<LucideCalendarDays size={14} className="stroke-indigo-300" />
<span className="font-normal text-[12px] text-indigo-300"></span>
</div>
<span className="text-sm font-normal text-indigo-400">{converter.dateToFormattedString(data.startDate, DateFormat.KOREAN)}</span>
</div>
<div className="flex-1 flex flex-col">
<div className="flex flex-row gap-1 items-center">
<LucideCalendarDays size={14} className="stroke-indigo-300" />
<span className="font-normal text-[12px] text-indigo-300"></span>
</div>
<span className="text-sm font-normal text-indigo-400">{converter.dateToFormattedString(data.endDate, DateFormat.KOREAN)}</span>
</div>
</div>
<div className="w-full h-12 flex flex-row justify-between items-center">
<div className="flex-1 flex flex-col">
<div className="flex flex-row gap-1 items-center">
<LucideClock size={14} className="stroke-indigo-300" />
<span className="font-normal text-[12px] text-indigo-300"> </span>
</div>
<span className="text-sm font-normal text-indigo-400">{data.startTime}</span>
</div>
<div className="flex-1 flex flex-col">
<div className="flex flex-row gap-1 items-center">
<LucideClock size={14} className="stroke-indigo-300" />
<span className="font-normal text-[12px] text-indigo-300"> </span>
</div>
<span className="text-sm font-normal text-indigo-400">{data.endTime}</span>
</div>
</div>
</div>
<div
className={cn(
"absolute left-0 bottom-0 w-full flex flex-col transition-all overflow-hidden duration-300 bg-white",
"border-t border-l border-r border-b-0 border-indigo-100 rounded-t-lg",
commentFold? "h-10" : "h-[calc(100%-44px)] border-b"
"absolute px-1 left-2 bottom-4 w-[calc(100%-16px)] flex flex-col transition-all overflow-hidden duration-300 bg-white",
"border-t border-l border-r border-b border-indigo-100 rounded-t-lg",
commentFold? "h-10" : "h-[calc(100%-44px)]"
)}
>
<div
className={cn(
"w-full h-10 px-2 flex flex-row justify-between items-center",
commentFold ? "" : "border-b border-b-indigo-200"
"border-b border-b-indigo-200"
)}
>
<span
className="text-indigo-400 text-sm"
>
<span className="text-indigo-200 text-xs">[12]</span>
<span className="text-indigo-300 text-xs">[12]</span>
</span>
<div
className="h-10 text-indigo-400 flex flex-row justify-center items-end pb-2 select-none"
className="h-10 text-indigo-400 flex flex-row justify-center items-center select-none"
onClick={() => setCommentFold(prev => !prev)}
>
<ChevronUp

View File

@@ -1,21 +1,19 @@
import { ScrollArea } from "@/components/ui/scroll-area";
import { ListScheduleSchema } from "@/data/form/schedule/listSchedule.schema";
import { format } from "date-fns";
import { List, PenSquare } from "lucide-react";
import { PenSquare } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import type { ScheduleListContentProps } from "./ContentProps";
import { zodResolver } from "@hookform/resolvers/zod";
import { ScheduleNetwork } from "@/network/ScheduleNetwork";
import type { ScheduleStatus } from "@/const/schedule/ScheduleStatus";
import type { ScheduleType } from "@/const/schedule/ScheduleType";
import { ScheduleListData } from "@/data/response";
import { Converter } from "@/util/Converter";
import { ScheduleListTile } from "../tile/ScheduleListTile";
import { SchedulerDTO as DTO, Type } from "@baekyangdan/core-utils";
import { Skeleton } from "@/components/ui/skeleton";
import { toast } from "sonner";
export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide, open, setId }: ScheduleListContentProps) => {
export const ScheduleListContent = ({ date, setMode, open, setId }: ScheduleListContentProps) => {
const [isLoading, setIsLoading] = useState(false);
const [scheduleList, setScheduleList] = useState<Array<DTO.ScheduleList>>([]);
const scheduleNetwork = new ScheduleNetwork();
@@ -59,11 +57,19 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
styleList: styleList
} as DTO.ScheduleListRequest;
setIsLoading(true);
try {
const result = await scheduleNetwork.getList(data);
if (result.success) {
setScheduleList(result.data!);
if (!result.success) throw new Error(result.error);
setScheduleList(result.data);
setIsLoading(false);
} catch (e) {
setIsLoading(false);
toast.error('일정을 불러오는 데에 실패하였습니다.\n잠시 후 다시 시도해주십시오.');
}
}
const moveToDetail = (id: string) => {
@@ -72,7 +78,7 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
}
return (
<div className="w-full h-full flex flex-col gap-4">
<div className="p-4 w-full h-full flex flex-col gap-4">
<div className="relative w-full h-10 border-b border-b-indigo-300 flex flex-row items-center justify-center">
<span className="text-indigo-400">{date && format(date, "yyyy년 MM월 dd일")}</span>
<div className="absolute top-3 right-0.5 group">
@@ -84,7 +90,14 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
</div>
</div>
<div className="w-full h-[calc(100%-40px)]">
<ScrollArea
{
isLoading
? <div className="w-full h-full flex flex-col justify-start items-start gap-3">
{[1,2,3,4].map((_) => (
<Skeleton className="w-full h-10 bg-indigo-200"/>
))}
</div>
: <ScrollArea
className="w-full h-full"
>
<div className="w-full h-full flex flex-col justify-start items-start gap-3">
@@ -97,6 +110,7 @@ export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide,
)) }
</div>
</ScrollArea>
}
</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(
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
"bg-white text-indigo-300",
"hover:bg-indigo-300 hover:text-white"
"hover:bg-indigo-300 hover:text-white",
"rounded-none rounded-bl-md"
)}
onClick={applyList}
>
@@ -151,7 +152,8 @@ export const ParticipantPopover = ({ participantList, setParticipantList }: Part
className={cn(
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
"bg-white text-red-400",
"hover:bg-red-400 hover:text-white"
"hover:bg-red-400 hover:text-white",
"rounded-none rounded-br-md"
)}
onClick={cancelList}
>

View File

@@ -1,6 +1,4 @@
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
import { ScheduleTypeLabel } from "@/const/schedule/ScheduleType";
import { ScheduleListData } from "@/data/response";
import { Converter } from "@/util/Converter";
import { SchedulerDTO as DTO } from "@baekyangdan/core-utils";
interface ScheduleListTileProps {
@@ -8,7 +6,7 @@ interface ScheduleListTileProps {
data: DTO.ScheduleList;
onClick: (id: string) => void;
}
export const ScheduleListTile = ({ setMode, data, onClick }: ScheduleListTileProps) => {
export const ScheduleListTile = ({ data, onClick }: ScheduleListTileProps) => {
const formatter = Converter.isoStringToFormattedString;
const handleOnClickTile = (id: string) => {

23
tailwind.config.js Normal file
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"
]
}
}
}
}