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

- 일정 상세 조회 기능 구현 중
This commit is contained in:
geonhee-min
2025-12-15 17:34:52 +09:00
parent 9173556204
commit 4a3896a313
17 changed files with 334 additions and 99 deletions

8
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "scheduler",
"version": "0.0.0",
"dependencies": {
"@baekyangdan/core-utils": "^1.0.4",
"@baekyangdan/core-utils": "^1.0.9",
"@diceui/mention": "^0.8.0",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
@@ -373,9 +373,9 @@
}
},
"node_modules/@baekyangdan/core-utils": {
"version": "1.0.4",
"resolved": "https://gitea.bkdhome.p-e.kr/api/packages/baekyangdan/npm/%40baekyangdan%2Fcore-utils/-/1.0.4/core-utils-1.0.4.tgz",
"integrity": "sha512-0++RXd6eg3IkS5xygRzv19p174wPtSh/tQYSGjKtlKP5GZnhRO8NYNDT1IciornLmFfVHAnuC/cnmwUqcFPX4A==",
"version": "1.0.9",
"resolved": "https://gitea.bkdhome.p-e.kr/api/packages/baekyangdan/npm/%40baekyangdan%2Fcore-utils/-/1.0.9/core-utils-1.0.9.tgz",
"integrity": "sha512-zeXQPXJlwpO2/PzmQJQrXP9A6/maZmWWISmEuW82R42fkgXwaxavT/xlI1MsYLg1tqibwNBLSgnjXd0H9vR0KQ==",
"license": "ISC",
"dependencies": {
"date-fns": "^4.1.0",

View File

@@ -11,7 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"@baekyangdan/core-utils": "^1.0.4",
"@baekyangdan/core-utils": "^1.0.9",
"@diceui/mention": "^0.8.0",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",

View File

@@ -14,7 +14,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
className="toaster group select-none"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,

View File

@@ -0,0 +1 @@
export type SchedulePopoverMode = 'list' | 'create' | 'detail' | 'update';

View File

@@ -9,4 +9,5 @@ export * from './account/ResetPasswordResponse';
export * from './schedule/CreateScheduleResponse';
export * from './schedule/UpdateScheduleResponse';
export * from './schedule/ScheduleListResponse';
export * from './schedule/ScheduleListResponse';
export * from './schedule/ScheduleDetailResponse';

View File

@@ -0,0 +1,24 @@
import type { ScheduleStatus } from "@/const/schedule/ScheduleStatus";
import { BaseResponse } from "../BaseResponse";
import type { ScheduleType } from "@/const/schedule/ScheduleType";
export class ScheduleDetailData {
id!: string;
name!: string;
startDate!: string;
endDate!: string;
status!: ScheduleStatus;
content?: string | null;
type!: ScheduleType;
createdAt!: string;
owner!: string;
style!: string;
startTime!: string;
endTime!: string;
dayList?: string | null;
participantList?: string[];
}
export class ScheduleDetailResponse extends BaseResponse {
data?: ScheduleDetailData;
}

View File

@@ -31,6 +31,7 @@ export default function Layout() {
<>
<Toaster
position="top-center"
visibleToasts={1}
icons={{
success: <CircleCheckIcon className="size-4" fill="#15b815" color="white" />,
error: <OctagonXIcon className="size-4" fill="#f14e4e" color="white" />,

View File

@@ -19,15 +19,17 @@ import {
ResetPasswordResponse
} from "@/data/response";
import { BaseNetwork } from "./BaseNetwork";
import { HttpApiUrl } from "@baekyangdan/core-utils";
const AccountApi = HttpApiUrl.Account;
export class AccountNetwork extends BaseNetwork {
private baseUrl = "/account";
private baseUrl = AccountApi.base;
async checkDuplication(data: CheckDuplicationRequest) {
const { type, value } = data;
return await this.get<CheckDuplicationResponse>(
`${this.baseUrl}/check-duplication?type=${type}&value=${value}`
`${this.baseUrl}${AccountApi.checkDuplication}?type=${type}&value=${value}`
, {
authPass: true
}
@@ -36,7 +38,7 @@ export class AccountNetwork extends BaseNetwork {
async sendVerificationCode(data: SendVerificationCodeRequest) {
return await this.post<SendVerificationCodeResponse>(
this.baseUrl + "/send-email-verification-code"
`${this.baseUrl}${AccountApi.sendEmailVerificationCode}`
, data
, {
authPass: true
@@ -46,7 +48,7 @@ export class AccountNetwork extends BaseNetwork {
async verifyCode(data: VerifyCodeRequest) {
return await this.post<VerifyCodeResponse>(
this.baseUrl + "/verify-email-verification-code"
`${this.baseUrl}${AccountApi.verifyEmailVerificationCode}`
, data
, {
authPass: true
@@ -56,7 +58,7 @@ export class AccountNetwork extends BaseNetwork {
async signup(data: SignupRequest) {
return await this.post<SignupResponse>(
this.baseUrl + "/signup"
`${this.baseUrl}${AccountApi.signup}`
, data
, {
authPass: true
@@ -66,7 +68,7 @@ export class AccountNetwork extends BaseNetwork {
async login(data: LoginRequest) {
return await this.post<LoginResponse>(
this.baseUrl + "/login"
`${this.baseUrl}${AccountApi.login}`
, data
, {
authPass: true
@@ -76,21 +78,21 @@ export class AccountNetwork extends BaseNetwork {
async sendResetPasswordCode(data: SendResetPasswordCodeRequest) {
return await this.post<SendResetPasswordCodeResponse>(
this.baseUrl + '/send-reset-password-code',
`${this.baseUrl}${AccountApi.sendResetPasswordCode}`,
data
);
}
async verifyResetPasswordCode(data: VerifyResetPasswordCodeRequest) {
return await this.post<VerifyResetPasswordCodeResponse>(
this.baseUrl + '/verify-reset-password-code',
`${this.baseUrl}${AccountApi.verifyResetPasswordCode}`,
data
);
}
async resetPassword(data: ResetPasswordRequest) {
return await this.post<ResetPasswordResponse>(
this.baseUrl + '/reset-password',
`${this.baseUrl}${AccountApi.resetPassword}`,
data
);
}

View File

@@ -9,7 +9,7 @@ import type {
import { useAuthStore } from '@/store/authStore';
import { RefreshAccessTokenResponse } from '@/data/response/account/RefreshAccessTokenResponse';
import type { AuthData } from '@/data/AuthData';
import { UnauthorizedCode, UnauthorizedMessage } from '@baekyangdan/core-utils';
import { HttpApiUrl, UnauthorizedCode, UnauthorizedMessage } from '@baekyangdan/core-utils';
export class BaseNetwork {
protected instance: AxiosInstance;
@@ -92,14 +92,16 @@ export class BaseNetwork {
}
private async handleRefreshToken(originalRequest: AxiosRequestConfig) {
const authData = useAuthStore.getState().authData;
if (!authData) {
useAuthStore.getState().logout();
return Promise.reject(UnauthorizedMessage.INVALID_TOKEN);
const autoLogin = localStorage.getItem('autoLogin') === 'true';
if (autoLogin) {
const authData = useAuthStore.getState().authData;
if (!authData) {
useAuthStore.getState().logout();
return Promise.reject(UnauthorizedMessage.INVALID_TOKEN);
}
}
if (this.isRefreshing) {
return new Promise((resolve) => {
this.refreshQueue.push((newToken: string) => {
@@ -148,22 +150,25 @@ export class BaseNetwork {
}
public async refreshToken() {
const storedAuth = localStorage.getItem('auth-storage');
const autoLogin = localStorage.getItem('autoLogin') === 'true';
if (autoLogin) {
const storedAuth = localStorage.getItem('auth-storage');
if (!storedAuth) {
localStorage.setItem('autoLogin', 'false');
throw new Error;
}
const authData: AuthData = JSON.parse(storedAuth).state;
if (!storedAuth) {
localStorage.setItem('autoLogin', 'false');
throw new Error;
}
const authData: AuthData = JSON.parse(storedAuth).state;
if (!authData) {
localStorage.setItem('autoLogin', 'false');
throw new Error;
if (!authData) {
localStorage.setItem('autoLogin', 'false');
throw new Error;
}
}
const result = await this.get<RefreshAccessTokenResponse>(
'/account/refresh-access-token',
`${HttpApiUrl.Account.base}${HttpApiUrl.Account.refreshAccessToken}`,
{
withCredentials: true
}

View File

@@ -7,11 +7,14 @@ import {
} from '@/data/request';
import {
CreateScheduleResponse,
ScheduleDetailResponse,
ScheduleListResponse
} from "@/data/response";
import { HttpApiUrl } from "@baekyangdan/core-utils";
const ScheduleApi = HttpApiUrl.Schedule;
export class ScheduleNetwork extends BaseNetwork {
private baseUrl = "/schedule";
private baseUrl = ScheduleApi.base;
async getList(data: ScheduleListRequest) {
return await this.post<ScheduleListResponse>(
@@ -21,28 +24,28 @@ export class ScheduleNetwork extends BaseNetwork {
}
async getDetail(id: string) {
return await this.get(
return await this.get<ScheduleDetailResponse>(
`${this.baseUrl}/${id}`
);
}
async create(data: CreateScheduleRequest) {
return await this.post<CreateScheduleResponse>(
`${this.baseUrl}/create`,
`${this.baseUrl}${ScheduleApi.create}`,
data
);
}
async update(data: UpdateScheduleRequest) {
return await this.post(
`${this.baseUrl}/update`,
`${this.baseUrl}${ScheduleApi.update}`,
data
);
}
async del(data: DeleteScheduleRequest) {
return await this.post(
`${this.baseUrl}/delete`,
`${this.baseUrl}${ScheduleApi.delete}`,
data
);
}

View File

@@ -9,6 +9,7 @@ import { ScheduleListData } from "@/data/response";
import { CustomCalendarCN } from "./CustomCalendarCN";
import { toast } from "sonner";
import { Converter } from "@/util/Converter";
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
interface CustomCalendarProps {
data?: any;
@@ -27,13 +28,16 @@ const DATE_FORMAT_ARIA = 'EEEE, MMMM do, yyyy';
const DATE_FORMAT_KEY = 'yyyyMMdd';
export const CustomCalendar = ({ data }: CustomCalendarProps) => {
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 [popoverSide, setPopoverSide] = useState<'right' | 'left'>('right');
const [popoverAlign, setPopoverAlign] = useState<'start' | 'end'>('end');
const [popoverMode, setPopoverMode] = useState<SchedulePopoverMode>('list');
const [popoverDetailId, setPopoverDetailId] = useState('');
const [month, setMonth] = useState(new Date());
const [currentDataMonth, setCurrentDataMonth] = useState(month);
const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight });
const [maxVisibleEvents, setMaxVisibleEvents] = useState(3);
const [overflowTrackIndex, setOverflowTrackIndex] = useState(maxVisibleEvents + 1);
@@ -41,7 +45,8 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
const [barPositions, setBarPositions] = useState<Array<EventBarPosition>>([]);
const scheduleNetwork = new ScheduleNetwork();
const containerRef = useRef<HTMLDivElement>(null);
const cellInfoMapRef = useRef<Map<string, { cell: HTMLElement, rect: DOMRect }>>(new Map());
const updateWeekCount = () => {
if (containerRef === null) return;
if (!containerRef.current) return;
@@ -73,11 +78,12 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
}
}, []);
// 화면 상의 달이 바뀌면 req 하는 로직
useLayoutEffect(() => {
updateWeekCount();
const reqList = async () => {
setIsLoading(true);
const requestedMonth = month;
const monthStart = startOfMonth(month);
@@ -96,12 +102,13 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
if (result.data.success) {
if (result.data.data) {
if (isSameMonth(requestedMonth, month)) {
// setCurrentDataMonth(month);
setScheduleList(result.data.data!);
setCurrentDataMonth(month);
}
setScheduleList(result.data.data);
// setScheduleList(result.data.data);
}
}
setIsLoading(false);
}
requestAnimationFrame(() => {
@@ -118,19 +125,25 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
)
updateWeekCount();
});
}, [month]);
}, [month, refetchTrigger]);
const refetchList = () => {
setRefetchTrigger(prev => prev + 1);
}
// 이벤트 bar 그리는 로직
useLayoutEffect(() => {
if (!isSameMonth(month, currentDataMonth)) {
setBarPositions([]);
return;
}
// if (!isSameMonth(month, currentDataMonth)) {
// setBarPositions([]);
// return;
// }
if (!containerRef.current || scheduleList.length === 0) {
setBarPositions([]);
// setBarPositions([]);
return;
}
// setBarPositions([]);
const containerRect = containerRef.current.getBoundingClientRect();
const dayCells = containerRef.current.querySelectorAll('.rdp-day button');
@@ -138,15 +151,20 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
dayCells.forEach((cell) => {
const dayButton = cell as HTMLButtonElement;
const ariaLabel = dayButton.getAttribute('aria-label');
let ariaLabel = dayButton.getAttribute('aria-label');
if (ariaLabel) {
try {
if (ariaLabel.startsWith('Today, ')) {
ariaLabel = ariaLabel.replace('Today, ', '');
}
if (ariaLabel.endsWith(', selected')) {
ariaLabel = ariaLabel.replace(', selected', '');
}
const parsedDate = parse(ariaLabel, DATE_FORMAT_ARIA, new Date());
if (!isNaN(parsedDate.getTime())) {
const dateKey = format(parsedDate, DATE_FORMAT_KEY);
console.log(dateKey);
cellInfoMap.set(dateKey, {
cell: dayButton,
rect: dayButton.getBoundingClientRect()
@@ -156,6 +174,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
} catch (e) {}
}
});
cellInfoMapRef.current = cellInfoMap;
const scheduleListWithTrack: (ScheduleListData & { trackIndex: number })[] = [];
const occupiedTrackList = new Map<string, number[]>();
@@ -292,7 +311,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
}
});
setBarPositions([...regularPositions, ...overflowPositions]);
}, [scheduleList, month, currentDataMonth, windowSize]);
}, [scheduleList, month, windowSize]);
const createAndPushPosition = (
positions: EventBarPosition[],
@@ -334,6 +353,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
}
const handleMonthChange = async (month: Date) => {
if (isLoading) return;
setMonth(month);
}
@@ -351,6 +371,8 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
setPopoverOpen(false);
setTimeout(() => {
setSelectedDate(undefined);
setPopoverDetailId('');
setPopoverMode('list');
}, 150);
return;
}
@@ -385,6 +407,51 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
}
}
const findDateFromClick = (
clientX: number,
clientY: number
): string | null => {
if (!cellInfoMapRef.current) {
return null;
}
const cellInfoMap = cellInfoMapRef.current;
for (const [dateKey, info] of cellInfoMap.entries()) {
const rect = info.rect;
if (
clientX >= rect.left
&& clientX < rect.right
&& clientY >= rect.top
&& clientY < rect.bottom
) {
return dateKey;
}
}
return null;
}
const handleEventBarClick = (e: React.MouseEvent<HTMLDivElement>, bar: EventBarPosition) => {
const clientX = e.clientX;
const clientY = e.clientY;
const eventId = bar.id;
const dateKey = findDateFromClick(clientX, clientY);
if (dateKey && !selectedDate) {
const clickedDate = parse(dateKey, DATE_FORMAT_KEY, new Date());
if (!(eventId.includes('overflow'))) {
setPopoverMode('detail');
setPopoverDetailId(eventId);
}
handleDaySelect(clickedDate);
} else {
handleDaySelect(undefined);
}
}
return (
<div
className="w-full h-full relative"
@@ -433,24 +500,32 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
/>
{
barPositions.map(pos => (
<div
key={pos.segmentId}
className={cn(
`flex flex-row justify-start items-center absolute`,
"py-0.5 px-2 rounded-sm text-xs text-white overflow-hidden"
)}
style={{...pos.positionStyle, backgroundColor: pos.style}}
>
{pos.name}
</div>
<div
onClick={(e: React.MouseEvent<HTMLDivElement>) => handleEventBarClick(e, pos)}
key={pos.segmentId}
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"
)}
style={{...pos.positionStyle, backgroundColor: pos.style}}
>
{pos.name}
</div>
))
}
<SchedulePopover
date={selectedDate}
open={popoverOpen}
mode={popoverMode}
setMode={setPopoverMode}
detailId={popoverDetailId}
setDetailId={setPopoverDetailId}
popoverSide={popoverSide}
popoverAlign={popoverAlign}
onScheduleCreated={refetchList}
/>
</Popover>
</div>
)

View File

@@ -10,7 +10,7 @@ export const CustomCalendarCN = {
),
nav: cn(
defaultCN.nav,
"flex w-full item-center gap-1 justify-around absolute top-0 inset-x-0"
"flex w-full item-center gap-1 justify-center gap-30 absolute top-0 inset-x-0"
),
month: cn(
defaultCN.month,

View File

@@ -3,17 +3,22 @@ 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';
interface ScheduleSheetProps {
date: Date | undefined;
open: boolean;
popoverSide: 'left' | 'right';
popoverAlign: 'start' | 'end';
mode: SchedulePopoverMode;
setMode: (mode: SchedulePopoverMode) => void;
detailId: string;
setDetailId: (id: string) => void;
onScheduleCreated: () => void;
}
export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: ScheduleSheetProps) => {
const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list');
const [detailId, setDetailId] = useState('');
export const SchedulePopover = ({ date, open, mode, setMode, detailId, setDetailId, popoverSide, popoverAlign, onScheduleCreated }: ScheduleSheetProps) => {
useEffect(() => {
if (!open) {
@@ -57,6 +62,7 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
popoverAlign={popoverAlign}
popoverSide={popoverSide}
open={open}
refetchList={onScheduleCreated}
/>
case 'detail':
return <ScheduleDetailContent

View File

@@ -1,13 +1,15 @@
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
interface BaseProps {
date: Date | undefined;
open: boolean;
popoverSide: 'left' | 'right';
popoverAlign: 'start' | 'end';
setMode: (mode: 'list' | 'create' | 'detail' | 'update') => void;
setMode: (mode: SchedulePopoverMode) => void;
}
export interface ScheduleCreateContentProps extends BaseProps {
refetchList: () => void;
}
export interface ScheduleListContentProps extends BaseProps {

View File

@@ -28,7 +28,7 @@ import { TypePickPopover } from '../popover/TypePickPopover';
import type { ScheduleCreateContentProps } from './ContentProps';
import { Converter } from '@/util/Converter';
export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign }: ScheduleCreateContentProps) => {
export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign, refetchList }: ScheduleCreateContentProps) => {
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { getPaletteByKey } = usePalette();
@@ -74,6 +74,7 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
const reqCreate = async () => {
if (isLoading) return;
const data = {
name,
startDate: Converter.dateToUTC9(startDate),
@@ -87,28 +88,35 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
style
};
const createPromise = scheduleNetwork.create(data);
setIsLoading(true);
toast.promise(
createPromise,
{
loading: '일정 생성 중입니다',
success: (res) => {
setIsLoading(false);
if (res.data.success) {
setMode('list');
return '일정이 생성되었습니다'
} else {
throw new Error(res.data.error);
}
},
error: (err: Error) => {
setIsLoading(false);
return err.message || "에러 발생";
}
const createPromise = scheduleNetwork.create(data);
toast.promise(createPromise, {
loading: '일정 생성 중입니다',
});
try {
const res = await createPromise;
if (!res.data.success) {
throw new Error(res.data.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) => {
createScheduleForm.setValue('style', color.style);

View File

@@ -1,11 +1,117 @@
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 { cn } from '@/lib/utils';
import { ScrollArea } from '@/components/ui/scroll-area';
export const ScheduleDetailContent = ({ setMode, popoverSide, popoverAlign, id }: ScheduleDetailContentProps) => {
const scheduleNetwork = new ScheduleNetwork();
const [data, setData] = useState<ScheduleDetailData>();
const [commentFold, setCommentFold] = useState(true);
useEffect(() => {
if (!id || id.trim().length === 0) {
return;
}
reqDetail();
}, [id]);
const reqDetail = async () => {
const result = await scheduleNetwork.getDetail(id);
if (result.data.success && result.data) {
setData(result.data.data!);
}
}
const moveToList = () => {
setMode('list');
}
export const ScheduleDetailContent = ({ date, setMode, popoverSide, popoverAlign, id }: ScheduleDetailContentProps) => {
return (
<div
className="w-full h-full flex flex-col justify-start items-start gap-4"
className="relative w-full h-full flex flex-col justify-start items-start"
>
{id}
<div className="relative w-full h-10 flex flex-row justify-center items-center border-b border-b-indigo-300">
<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>
<span className="text-indigo-400 select-none">{data?.name}</span>
</div>
<div
className={cn(
"mt-2 w-full flex flex-col transition-all duration-300 border",
"h-[calc(100%-86px)]"
)}
>
<div
className="w-full border-b border-b-indigo-100 flex flex-col items-end text-xs text-indigo-200"
>
<span className="flex flex-row justify-start items-center gap-1"><Clock size={12}/> : {data?.createdAt}</span>
</div>
<ScrollArea
className="max-h-30 w-full border"
>
<span
className="whitespace-pre-wrap text-indigo-500 select-none"
>
{data?.content && data?.content}
</span>
</ScrollArea>
</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"
)}
>
<div
className={cn(
"w-full h-10 px-2 flex flex-row justify-between items-center",
commentFold ? "" : "border-b border-b-indigo-200"
)}
>
<span
className="text-indigo-400 text-sm"
>
<span className="text-indigo-200 text-xs">[12]</span>
</span>
<div
className="h-10 text-indigo-400 flex flex-row justify-center items-end pb-2 select-none"
onClick={() => setCommentFold(prev => !prev)}
>
<ChevronUp
className={cn(
"transition-all duration-300 stroke-indigo-400 mr-1",
commentFold ? "" : "rotate-180"
)}
size={14}
/>
<span className="text-indigo-300 text-xs">{ commentFold ? "열기" : "닫기" }</span>
</div>
</div>
<ScrollArea
className="flex-1"
>
<div>
a
<br/>
b
<br/>
c
<br/>
d
</div>
</ScrollArea>
</div>
</div>
)
}

View File

@@ -1,9 +1,10 @@
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
import { ScheduleTypeLabel } from "@/const/schedule/ScheduleType";
import { ScheduleListData } from "@/data/response";
import { Converter } from "@/util/Converter";
interface ScheduleListTileProps {
setMode: (mode: 'list' | 'create' | 'detail' | 'update') => void;
setMode: (mode: SchedulePopoverMode) => void;
data: ScheduleListData;
onClick: (id: string) => void;
}
@@ -31,12 +32,12 @@ export const ScheduleListTile = ({ setMode, data, onClick }: ScheduleListTilePro
>
<div className="flex-6 h-full flex flex-row justify-end items-start">
<span
className="text-lg font-semibold text-gray-700"
className="text-lg font-semibold text-indigo-300"
>
{data.name}
</span>
</div>
<div className="flex-4 w-full flex flex-row text-xs font-light items-center text-gray-300">
<div className="flex-4 w-full flex flex-row text-xs font-light items-center text-indigo-200">
{formatter(data.startDate)} - {formatter(data.endDate)}
</div>
</div>