- 일정 상세 조회 기능 구현 중
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />,
|
||||
|
||||
1
src/const/schedule/SchedulePopoverMode.ts
Normal file
1
src/const/schedule/SchedulePopoverMode.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type SchedulePopoverMode = 'list' | 'create' | 'detail' | 'update';
|
||||
@@ -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';
|
||||
24
src/data/response/schedule/ScheduleDetailResponse.ts
Normal file
24
src/data/response/schedule/ScheduleDetailResponse.ts
Normal 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;
|
||||
}
|
||||
@@ -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" />,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user