- 일정 상세 조회 기능 구현 중
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "scheduler",
|
"name": "scheduler",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@baekyangdan/core-utils": "^1.0.4",
|
"@baekyangdan/core-utils": "^1.0.9",
|
||||||
"@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",
|
||||||
@@ -373,9 +373,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@baekyangdan/core-utils": {
|
"node_modules/@baekyangdan/core-utils": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.9",
|
||||||
"resolved": "https://gitea.bkdhome.p-e.kr/api/packages/baekyangdan/npm/%40baekyangdan%2Fcore-utils/-/1.0.4/core-utils-1.0.4.tgz",
|
"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-0++RXd6eg3IkS5xygRzv19p174wPtSh/tQYSGjKtlKP5GZnhRO8NYNDT1IciornLmFfVHAnuC/cnmwUqcFPX4A==",
|
"integrity": "sha512-zeXQPXJlwpO2/PzmQJQrXP9A6/maZmWWISmEuW82R42fkgXwaxavT/xlI1MsYLg1tqibwNBLSgnjXd0H9vR0KQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@baekyangdan/core-utils": "^1.0.4",
|
"@baekyangdan/core-utils": "^1.0.9",
|
||||||
"@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",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||||||
return (
|
return (
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={theme as ToasterProps["theme"]}
|
theme={theme as ToasterProps["theme"]}
|
||||||
className="toaster group"
|
className="toaster group select-none"
|
||||||
icons={{
|
icons={{
|
||||||
success: <CircleCheckIcon className="size-4" />,
|
success: <CircleCheckIcon className="size-4" />,
|
||||||
info: <InfoIcon 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';
|
||||||
@@ -10,3 +10,4 @@ export * from './account/ResetPasswordResponse';
|
|||||||
export * from './schedule/CreateScheduleResponse';
|
export * from './schedule/CreateScheduleResponse';
|
||||||
export * from './schedule/UpdateScheduleResponse';
|
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
|
<Toaster
|
||||||
position="top-center"
|
position="top-center"
|
||||||
|
visibleToasts={1}
|
||||||
icons={{
|
icons={{
|
||||||
success: <CircleCheckIcon className="size-4" fill="#15b815" color="white" />,
|
success: <CircleCheckIcon className="size-4" fill="#15b815" color="white" />,
|
||||||
error: <OctagonXIcon className="size-4" fill="#f14e4e" color="white" />,
|
error: <OctagonXIcon className="size-4" fill="#f14e4e" color="white" />,
|
||||||
|
|||||||
@@ -19,15 +19,17 @@ import {
|
|||||||
ResetPasswordResponse
|
ResetPasswordResponse
|
||||||
} from "@/data/response";
|
} from "@/data/response";
|
||||||
import { BaseNetwork } from "./BaseNetwork";
|
import { BaseNetwork } from "./BaseNetwork";
|
||||||
|
import { HttpApiUrl } from "@baekyangdan/core-utils";
|
||||||
|
|
||||||
|
const AccountApi = HttpApiUrl.Account;
|
||||||
export class AccountNetwork extends BaseNetwork {
|
export class AccountNetwork extends BaseNetwork {
|
||||||
private baseUrl = "/account";
|
private baseUrl = AccountApi.base;
|
||||||
|
|
||||||
async checkDuplication(data: CheckDuplicationRequest) {
|
async checkDuplication(data: CheckDuplicationRequest) {
|
||||||
const { type, value } = data;
|
const { type, value } = data;
|
||||||
|
|
||||||
return await this.get<CheckDuplicationResponse>(
|
return await this.get<CheckDuplicationResponse>(
|
||||||
`${this.baseUrl}/check-duplication?type=${type}&value=${value}`
|
`${this.baseUrl}${AccountApi.checkDuplication}?type=${type}&value=${value}`
|
||||||
, {
|
, {
|
||||||
authPass: true
|
authPass: true
|
||||||
}
|
}
|
||||||
@@ -36,7 +38,7 @@ export class AccountNetwork extends BaseNetwork {
|
|||||||
|
|
||||||
async sendVerificationCode(data: SendVerificationCodeRequest) {
|
async sendVerificationCode(data: SendVerificationCodeRequest) {
|
||||||
return await this.post<SendVerificationCodeResponse>(
|
return await this.post<SendVerificationCodeResponse>(
|
||||||
this.baseUrl + "/send-email-verification-code"
|
`${this.baseUrl}${AccountApi.sendEmailVerificationCode}`
|
||||||
, data
|
, data
|
||||||
, {
|
, {
|
||||||
authPass: true
|
authPass: true
|
||||||
@@ -46,7 +48,7 @@ export class AccountNetwork extends BaseNetwork {
|
|||||||
|
|
||||||
async verifyCode(data: VerifyCodeRequest) {
|
async verifyCode(data: VerifyCodeRequest) {
|
||||||
return await this.post<VerifyCodeResponse>(
|
return await this.post<VerifyCodeResponse>(
|
||||||
this.baseUrl + "/verify-email-verification-code"
|
`${this.baseUrl}${AccountApi.verifyEmailVerificationCode}`
|
||||||
, data
|
, data
|
||||||
, {
|
, {
|
||||||
authPass: true
|
authPass: true
|
||||||
@@ -56,7 +58,7 @@ export class AccountNetwork extends BaseNetwork {
|
|||||||
|
|
||||||
async signup(data: SignupRequest) {
|
async signup(data: SignupRequest) {
|
||||||
return await this.post<SignupResponse>(
|
return await this.post<SignupResponse>(
|
||||||
this.baseUrl + "/signup"
|
`${this.baseUrl}${AccountApi.signup}`
|
||||||
, data
|
, data
|
||||||
, {
|
, {
|
||||||
authPass: true
|
authPass: true
|
||||||
@@ -66,7 +68,7 @@ export class AccountNetwork extends BaseNetwork {
|
|||||||
|
|
||||||
async login(data: LoginRequest) {
|
async login(data: LoginRequest) {
|
||||||
return await this.post<LoginResponse>(
|
return await this.post<LoginResponse>(
|
||||||
this.baseUrl + "/login"
|
`${this.baseUrl}${AccountApi.login}`
|
||||||
, data
|
, data
|
||||||
, {
|
, {
|
||||||
authPass: true
|
authPass: true
|
||||||
@@ -76,21 +78,21 @@ export class AccountNetwork extends BaseNetwork {
|
|||||||
|
|
||||||
async sendResetPasswordCode(data: SendResetPasswordCodeRequest) {
|
async sendResetPasswordCode(data: SendResetPasswordCodeRequest) {
|
||||||
return await this.post<SendResetPasswordCodeResponse>(
|
return await this.post<SendResetPasswordCodeResponse>(
|
||||||
this.baseUrl + '/send-reset-password-code',
|
`${this.baseUrl}${AccountApi.sendResetPasswordCode}`,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyResetPasswordCode(data: VerifyResetPasswordCodeRequest) {
|
async verifyResetPasswordCode(data: VerifyResetPasswordCodeRequest) {
|
||||||
return await this.post<VerifyResetPasswordCodeResponse>(
|
return await this.post<VerifyResetPasswordCodeResponse>(
|
||||||
this.baseUrl + '/verify-reset-password-code',
|
`${this.baseUrl}${AccountApi.verifyResetPasswordCode}`,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetPassword(data: ResetPasswordRequest) {
|
async resetPassword(data: ResetPasswordRequest) {
|
||||||
return await this.post<ResetPasswordResponse>(
|
return await this.post<ResetPasswordResponse>(
|
||||||
this.baseUrl + '/reset-password',
|
`${this.baseUrl}${AccountApi.resetPassword}`,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { RefreshAccessTokenResponse } from '@/data/response/account/RefreshAccessTokenResponse';
|
import { RefreshAccessTokenResponse } from '@/data/response/account/RefreshAccessTokenResponse';
|
||||||
import type { AuthData } from '@/data/AuthData';
|
import type { AuthData } from '@/data/AuthData';
|
||||||
import { UnauthorizedCode, UnauthorizedMessage } from '@baekyangdan/core-utils';
|
import { HttpApiUrl, UnauthorizedCode, UnauthorizedMessage } from '@baekyangdan/core-utils';
|
||||||
export class BaseNetwork {
|
export class BaseNetwork {
|
||||||
protected instance: AxiosInstance;
|
protected instance: AxiosInstance;
|
||||||
|
|
||||||
@@ -92,13 +92,15 @@ export class BaseNetwork {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleRefreshToken(originalRequest: AxiosRequestConfig) {
|
private async handleRefreshToken(originalRequest: AxiosRequestConfig) {
|
||||||
|
const autoLogin = localStorage.getItem('autoLogin') === 'true';
|
||||||
|
if (autoLogin) {
|
||||||
const authData = useAuthStore.getState().authData;
|
const authData = useAuthStore.getState().authData;
|
||||||
|
|
||||||
if (!authData) {
|
if (!authData) {
|
||||||
useAuthStore.getState().logout();
|
useAuthStore.getState().logout();
|
||||||
return Promise.reject(UnauthorizedMessage.INVALID_TOKEN);
|
return Promise.reject(UnauthorizedMessage.INVALID_TOKEN);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isRefreshing) {
|
if (this.isRefreshing) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -148,6 +150,8 @@ export class BaseNetwork {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async refreshToken() {
|
public async refreshToken() {
|
||||||
|
const autoLogin = localStorage.getItem('autoLogin') === 'true';
|
||||||
|
if (autoLogin) {
|
||||||
const storedAuth = localStorage.getItem('auth-storage');
|
const storedAuth = localStorage.getItem('auth-storage');
|
||||||
|
|
||||||
if (!storedAuth) {
|
if (!storedAuth) {
|
||||||
@@ -161,9 +165,10 @@ export class BaseNetwork {
|
|||||||
localStorage.setItem('autoLogin', 'false');
|
localStorage.setItem('autoLogin', 'false');
|
||||||
throw new Error;
|
throw new Error;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await this.get<RefreshAccessTokenResponse>(
|
const result = await this.get<RefreshAccessTokenResponse>(
|
||||||
'/account/refresh-access-token',
|
`${HttpApiUrl.Account.base}${HttpApiUrl.Account.refreshAccessToken}`,
|
||||||
{
|
{
|
||||||
withCredentials: true
|
withCredentials: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import {
|
|||||||
} from '@/data/request';
|
} from '@/data/request';
|
||||||
import {
|
import {
|
||||||
CreateScheduleResponse,
|
CreateScheduleResponse,
|
||||||
|
ScheduleDetailResponse,
|
||||||
ScheduleListResponse
|
ScheduleListResponse
|
||||||
} from "@/data/response";
|
} from "@/data/response";
|
||||||
|
import { HttpApiUrl } from "@baekyangdan/core-utils";
|
||||||
|
|
||||||
|
const ScheduleApi = HttpApiUrl.Schedule;
|
||||||
export class ScheduleNetwork extends BaseNetwork {
|
export class ScheduleNetwork extends BaseNetwork {
|
||||||
private baseUrl = "/schedule";
|
private baseUrl = ScheduleApi.base;
|
||||||
|
|
||||||
async getList(data: ScheduleListRequest) {
|
async getList(data: ScheduleListRequest) {
|
||||||
return await this.post<ScheduleListResponse>(
|
return await this.post<ScheduleListResponse>(
|
||||||
@@ -21,28 +24,28 @@ export class ScheduleNetwork extends BaseNetwork {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getDetail(id: string) {
|
async getDetail(id: string) {
|
||||||
return await this.get(
|
return await this.get<ScheduleDetailResponse>(
|
||||||
`${this.baseUrl}/${id}`
|
`${this.baseUrl}/${id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: CreateScheduleRequest) {
|
async create(data: CreateScheduleRequest) {
|
||||||
return await this.post<CreateScheduleResponse>(
|
return await this.post<CreateScheduleResponse>(
|
||||||
`${this.baseUrl}/create`,
|
`${this.baseUrl}${ScheduleApi.create}`,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(data: UpdateScheduleRequest) {
|
async update(data: UpdateScheduleRequest) {
|
||||||
return await this.post(
|
return await this.post(
|
||||||
`${this.baseUrl}/update`,
|
`${this.baseUrl}${ScheduleApi.update}`,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async del(data: DeleteScheduleRequest) {
|
async del(data: DeleteScheduleRequest) {
|
||||||
return await this.post(
|
return await this.post(
|
||||||
`${this.baseUrl}/delete`,
|
`${this.baseUrl}${ScheduleApi.delete}`,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ScheduleListData } from "@/data/response";
|
|||||||
import { CustomCalendarCN } from "./CustomCalendarCN";
|
import { CustomCalendarCN } from "./CustomCalendarCN";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Converter } from "@/util/Converter";
|
import { Converter } from "@/util/Converter";
|
||||||
|
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
|
||||||
|
|
||||||
interface CustomCalendarProps {
|
interface CustomCalendarProps {
|
||||||
data?: any;
|
data?: any;
|
||||||
@@ -27,13 +28,16 @@ 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 = ({ data }: CustomCalendarProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
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 [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 [popoverDetailId, setPopoverDetailId] = useState('');
|
||||||
const [month, setMonth] = useState(new Date());
|
const [month, setMonth] = useState(new Date());
|
||||||
const [currentDataMonth, setCurrentDataMonth] = useState(month);
|
|
||||||
const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight });
|
const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight });
|
||||||
const [maxVisibleEvents, setMaxVisibleEvents] = useState(3);
|
const [maxVisibleEvents, setMaxVisibleEvents] = useState(3);
|
||||||
const [overflowTrackIndex, setOverflowTrackIndex] = useState(maxVisibleEvents + 1);
|
const [overflowTrackIndex, setOverflowTrackIndex] = useState(maxVisibleEvents + 1);
|
||||||
@@ -41,6 +45,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
const [barPositions, setBarPositions] = useState<Array<EventBarPosition>>([]);
|
const [barPositions, setBarPositions] = useState<Array<EventBarPosition>>([]);
|
||||||
const scheduleNetwork = new ScheduleNetwork();
|
const scheduleNetwork = new ScheduleNetwork();
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const cellInfoMapRef = useRef<Map<string, { cell: HTMLElement, rect: DOMRect }>>(new Map());
|
||||||
|
|
||||||
const updateWeekCount = () => {
|
const updateWeekCount = () => {
|
||||||
if (containerRef === null) return;
|
if (containerRef === null) return;
|
||||||
@@ -73,11 +78,12 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 화면 상의 달이 바뀌면 req 하는 로직
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
updateWeekCount();
|
updateWeekCount();
|
||||||
|
|
||||||
const reqList = async () => {
|
const reqList = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
const requestedMonth = month;
|
const requestedMonth = month;
|
||||||
|
|
||||||
const monthStart = startOfMonth(month);
|
const monthStart = startOfMonth(month);
|
||||||
@@ -96,12 +102,13 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
if (result.data.success) {
|
if (result.data.success) {
|
||||||
if (result.data.data) {
|
if (result.data.data) {
|
||||||
if (isSameMonth(requestedMonth, month)) {
|
if (isSameMonth(requestedMonth, month)) {
|
||||||
|
// setCurrentDataMonth(month);
|
||||||
setScheduleList(result.data.data!);
|
setScheduleList(result.data.data!);
|
||||||
setCurrentDataMonth(month);
|
|
||||||
}
|
}
|
||||||
setScheduleList(result.data.data);
|
// setScheduleList(result.data.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -118,19 +125,25 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
)
|
)
|
||||||
updateWeekCount();
|
updateWeekCount();
|
||||||
});
|
});
|
||||||
}, [month]);
|
}, [month, refetchTrigger]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
const refetchList = () => {
|
||||||
if (!isSameMonth(month, currentDataMonth)) {
|
setRefetchTrigger(prev => prev + 1);
|
||||||
setBarPositions([]);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 이벤트 bar 그리는 로직
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// if (!isSameMonth(month, currentDataMonth)) {
|
||||||
|
// setBarPositions([]);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
if (!containerRef.current || scheduleList.length === 0) {
|
if (!containerRef.current || scheduleList.length === 0) {
|
||||||
setBarPositions([]);
|
// setBarPositions([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setBarPositions([]);
|
||||||
const containerRect = containerRef.current.getBoundingClientRect();
|
const containerRect = containerRef.current.getBoundingClientRect();
|
||||||
const dayCells = containerRef.current.querySelectorAll('.rdp-day button');
|
const dayCells = containerRef.current.querySelectorAll('.rdp-day button');
|
||||||
|
|
||||||
@@ -138,15 +151,20 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
|
|
||||||
dayCells.forEach((cell) => {
|
dayCells.forEach((cell) => {
|
||||||
const dayButton = cell as HTMLButtonElement;
|
const dayButton = cell as HTMLButtonElement;
|
||||||
const ariaLabel = dayButton.getAttribute('aria-label');
|
let ariaLabel = dayButton.getAttribute('aria-label');
|
||||||
|
|
||||||
if (ariaLabel) {
|
if (ariaLabel) {
|
||||||
try {
|
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());
|
const parsedDate = parse(ariaLabel, DATE_FORMAT_ARIA, new Date());
|
||||||
|
|
||||||
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()
|
||||||
@@ -156,6 +174,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
cellInfoMapRef.current = cellInfoMap;
|
||||||
|
|
||||||
const scheduleListWithTrack: (ScheduleListData & { trackIndex: number })[] = [];
|
const scheduleListWithTrack: (ScheduleListData & { trackIndex: number })[] = [];
|
||||||
const occupiedTrackList = new Map<string, number[]>();
|
const occupiedTrackList = new Map<string, number[]>();
|
||||||
@@ -292,7 +311,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
setBarPositions([...regularPositions, ...overflowPositions]);
|
setBarPositions([...regularPositions, ...overflowPositions]);
|
||||||
}, [scheduleList, month, currentDataMonth, windowSize]);
|
}, [scheduleList, month, windowSize]);
|
||||||
|
|
||||||
const createAndPushPosition = (
|
const createAndPushPosition = (
|
||||||
positions: EventBarPosition[],
|
positions: EventBarPosition[],
|
||||||
@@ -334,6 +353,7 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMonthChange = async (month: Date) => {
|
const handleMonthChange = async (month: Date) => {
|
||||||
|
if (isLoading) return;
|
||||||
setMonth(month);
|
setMonth(month);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,6 +371,8 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
setPopoverOpen(false);
|
setPopoverOpen(false);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSelectedDate(undefined);
|
setSelectedDate(undefined);
|
||||||
|
setPopoverDetailId('');
|
||||||
|
setPopoverMode('list');
|
||||||
}, 150);
|
}, 150);
|
||||||
return;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full h-full relative"
|
className="w-full h-full relative"
|
||||||
@@ -434,9 +501,11 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
{
|
{
|
||||||
barPositions.map(pos => (
|
barPositions.map(pos => (
|
||||||
<div
|
<div
|
||||||
|
onClick={(e: React.MouseEvent<HTMLDivElement>) => handleEventBarClick(e, pos)}
|
||||||
key={pos.segmentId}
|
key={pos.segmentId}
|
||||||
|
id={pos.segmentId}
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex flex-row justify-start items-center absolute`,
|
`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 text-white overflow-hidden"
|
||||||
)}
|
)}
|
||||||
style={{...pos.positionStyle, backgroundColor: pos.style}}
|
style={{...pos.positionStyle, backgroundColor: pos.style}}
|
||||||
@@ -448,9 +517,15 @@ export const CustomCalendar = ({ data }: CustomCalendarProps) => {
|
|||||||
<SchedulePopover
|
<SchedulePopover
|
||||||
date={selectedDate}
|
date={selectedDate}
|
||||||
open={popoverOpen}
|
open={popoverOpen}
|
||||||
|
mode={popoverMode}
|
||||||
|
setMode={setPopoverMode}
|
||||||
|
detailId={popoverDetailId}
|
||||||
|
setDetailId={setPopoverDetailId}
|
||||||
popoverSide={popoverSide}
|
popoverSide={popoverSide}
|
||||||
popoverAlign={popoverAlign}
|
popoverAlign={popoverAlign}
|
||||||
|
onScheduleCreated={refetchList}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const CustomCalendarCN = {
|
|||||||
),
|
),
|
||||||
nav: cn(
|
nav: cn(
|
||||||
defaultCN.nav,
|
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(
|
month: cn(
|
||||||
defaultCN.month,
|
defaultCN.month,
|
||||||
|
|||||||
@@ -3,17 +3,22 @@ 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';
|
||||||
|
|
||||||
interface ScheduleSheetProps {
|
interface ScheduleSheetProps {
|
||||||
date: Date | undefined;
|
date: Date | undefined;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
popoverSide: 'left' | 'right';
|
popoverSide: 'left' | 'right';
|
||||||
popoverAlign: 'start' | 'end';
|
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) => {
|
export const SchedulePopover = ({ date, open, mode, setMode, detailId, setDetailId, popoverSide, popoverAlign, onScheduleCreated }: ScheduleSheetProps) => {
|
||||||
const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list');
|
|
||||||
const [detailId, setDetailId] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@@ -57,6 +62,7 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
|
|||||||
popoverAlign={popoverAlign}
|
popoverAlign={popoverAlign}
|
||||||
popoverSide={popoverSide}
|
popoverSide={popoverSide}
|
||||||
open={open}
|
open={open}
|
||||||
|
refetchList={onScheduleCreated}
|
||||||
/>
|
/>
|
||||||
case 'detail':
|
case 'detail':
|
||||||
return <ScheduleDetailContent
|
return <ScheduleDetailContent
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
date: Date | undefined;
|
date: Date | undefined;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
popoverSide: 'left' | 'right';
|
popoverSide: 'left' | 'right';
|
||||||
popoverAlign: 'start' | 'end';
|
popoverAlign: 'start' | 'end';
|
||||||
setMode: (mode: 'list' | 'create' | 'detail' | 'update') => void;
|
setMode: (mode: SchedulePopoverMode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleCreateContentProps extends BaseProps {
|
export interface ScheduleCreateContentProps extends BaseProps {
|
||||||
|
refetchList: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleListContentProps extends BaseProps {
|
export interface ScheduleListContentProps extends BaseProps {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { TypePickPopover } from '../popover/TypePickPopover';
|
|||||||
import type { ScheduleCreateContentProps } from './ContentProps';
|
import type { ScheduleCreateContentProps } from './ContentProps';
|
||||||
import { Converter } from '@/util/Converter';
|
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 [colorPopoverOpen, setColorPopoverOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { getPaletteByKey } = usePalette();
|
const { getPaletteByKey } = usePalette();
|
||||||
@@ -74,6 +74,7 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
|||||||
|
|
||||||
const reqCreate = async () => {
|
const reqCreate = async () => {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
name,
|
name,
|
||||||
startDate: Converter.dateToUTC9(startDate),
|
startDate: Converter.dateToUTC9(startDate),
|
||||||
@@ -87,28 +88,35 @@ export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign
|
|||||||
style
|
style
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPromise = scheduleNetwork.create(data);
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
toast.promise(
|
|
||||||
createPromise,
|
const createPromise = scheduleNetwork.create(data);
|
||||||
{
|
|
||||||
|
toast.promise(createPromise, {
|
||||||
loading: '일정 생성 중입니다',
|
loading: '일정 생성 중입니다',
|
||||||
success: (res) => {
|
});
|
||||||
setIsLoading(false);
|
|
||||||
if (res.data.success) {
|
try {
|
||||||
setMode('list');
|
const res = await createPromise;
|
||||||
return '일정이 생성되었습니다'
|
|
||||||
} else {
|
if (!res.data.success) {
|
||||||
throw new Error(res.data.error);
|
throw new Error(res.data.error);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
error: (err: Error) => {
|
toast.success('일정이 생성되었습니다');
|
||||||
|
|
||||||
|
// ✅ 기존 동작 그대로
|
||||||
|
setMode('list');
|
||||||
|
refetchList();
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : '에러 발생';
|
||||||
|
|
||||||
|
toast.error(message);
|
||||||
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return err.message || "에러 발생";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const selectColor = (color: ColorPaletteType) => {
|
const selectColor = (color: ColorPaletteType) => {
|
||||||
createScheduleForm.setValue('style', color.style);
|
createScheduleForm.setValue('style', color.style);
|
||||||
|
|||||||
@@ -1,11 +1,117 @@
|
|||||||
|
import { ScheduleNetwork } from '@/network/ScheduleNetwork';
|
||||||
import type { ScheduleDetailContentProps } from './ContentProps';
|
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 (
|
return (
|
||||||
<div
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import type { SchedulePopoverMode } from "@/const/schedule/SchedulePopoverMode";
|
||||||
import { ScheduleTypeLabel } from "@/const/schedule/ScheduleType";
|
import { ScheduleTypeLabel } from "@/const/schedule/ScheduleType";
|
||||||
import { ScheduleListData } from "@/data/response";
|
import { ScheduleListData } from "@/data/response";
|
||||||
import { Converter } from "@/util/Converter";
|
import { Converter } from "@/util/Converter";
|
||||||
|
|
||||||
interface ScheduleListTileProps {
|
interface ScheduleListTileProps {
|
||||||
setMode: (mode: 'list' | 'create' | 'detail' | 'update') => void;
|
setMode: (mode: SchedulePopoverMode) => void;
|
||||||
data: ScheduleListData;
|
data: ScheduleListData;
|
||||||
onClick: (id: string) => void;
|
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">
|
<div className="flex-6 h-full flex flex-row justify-end items-start">
|
||||||
<span
|
<span
|
||||||
className="text-lg font-semibold text-gray-700"
|
className="text-lg font-semibold text-indigo-300"
|
||||||
>
|
>
|
||||||
{data.name}
|
{data.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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)}
|
{formatter(data.startDate)} - {formatter(data.endDate)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user