- 일정 추가 로직 1차 구현 - 일정 목록 화면 구현 중
This commit is contained in:
@@ -1,20 +1,20 @@
|
|||||||
import { Validator } from '@/util/Validator';
|
import { Validator } from '@/util/Validator';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
export const LoginSchema = z.object({
|
export const LoginSchema = z.object({
|
||||||
id: z
|
id: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => {
|
.refine((val) => {
|
||||||
if (val.includes('@')) {
|
if (val.includes('@')) {
|
||||||
return Validator.isEmail(val);
|
return Validator.isEmail(val);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}, {
|
}, {
|
||||||
message: "이메일 형식이 올바르지 않습니다."
|
message: "이메일 형식이 올바르지 않습니다."
|
||||||
})
|
})
|
||||||
, password: z
|
, password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8, "비밀번호는 8-12 자리여야 합니다.")
|
.min(8, "비밀번호는 8-12 자리여야 합니다.")
|
||||||
.max(12, "비밀번호는 8-12 자리여야 합니다.")
|
.max(12, "비밀번호는 8-12 자리여야 합니다.")
|
||||||
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "비밀번호는 영소문자로 시작하여 숫자, 특수문자(!@#$)를 한 개 이상 포함하여야 합니다.")
|
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "비밀번호는 영소문자로 시작하여 숫자, 특수문자(!@#$)를 한 개 이상 포함하여야 합니다.")
|
||||||
});
|
});
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
export const ResetPasswordSchema = z.object({
|
export const ResetPasswordSchema = z.object({
|
||||||
email: z
|
email: z
|
||||||
.email()
|
.email()
|
||||||
, code: z
|
, code: z
|
||||||
.string()
|
.string()
|
||||||
.length(8)
|
.length(8)
|
||||||
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
||||||
, password: z
|
, password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8, "비밀번호는 8-12 자리여야 합니다.")
|
.min(8, "비밀번호는 8-12 자리여야 합니다.")
|
||||||
.max(12, "비밀번호는 8-12 자리여야 합니다.")
|
.max(12, "비밀번호는 8-12 자리여야 합니다.")
|
||||||
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
||||||
, passwordConfirm: z
|
, passwordConfirm: z
|
||||||
.string()
|
.string()
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.passwordConfirm, {
|
.refine((data) => data.password === data.passwordConfirm, {
|
||||||
path: ["passwordConfirm"],
|
path: ["passwordConfirm"],
|
||||||
error: "비밀번호가 일치하지 않습니다."
|
error: "비밀번호가 일치하지 않습니다."
|
||||||
});
|
});
|
||||||
@@ -1,34 +1,34 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
export const SignUpSchema = z.object({
|
export const SignUpSchema = z.object({
|
||||||
accountId: z
|
accountId: z
|
||||||
.string()
|
.string()
|
||||||
.min(5, "아이디는 6-14자여야 합니다.")
|
.min(5, "아이디는 6-14자여야 합니다.")
|
||||||
.max(14, "아이디는 6-14자여야합니다.")
|
.max(14, "아이디는 6-14자여야합니다.")
|
||||||
.refine((val) => {
|
.refine((val) => {
|
||||||
return /^[a-zA-z-_.]*$/.test(val);
|
return /^[a-zA-z-_.]*$/.test(val);
|
||||||
}, {
|
}, {
|
||||||
message: "영문, 숫자, '- _ .' 를 제외한 문자를 사용할 수 없습니다."
|
message: "영문, 숫자, '- _ .' 를 제외한 문자를 사용할 수 없습니다."
|
||||||
})
|
})
|
||||||
, email: z
|
, email: z
|
||||||
.string()
|
.string()
|
||||||
.min(5, "이메일을 입력해주십시오.")
|
.min(5, "이메일을 입력해주십시오.")
|
||||||
, password: z
|
, password: z
|
||||||
.string()
|
.string()
|
||||||
.min(8, "비밀번호는 8-12자여야 합니다.")
|
.min(8, "비밀번호는 8-12자여야 합니다.")
|
||||||
.max(12, "비밀번호는 8-12자여야 합니다.")
|
.max(12, "비밀번호는 8-12자여야 합니다.")
|
||||||
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
.regex(/^(?=.*[0-9])(?=.*[!@#$%^])[a-zA-Z0-9!@#$%^]+$/, "영소문자로 시작하고 숫자와 특수문자(!@#$%^)를 포함해야 합니다.")
|
||||||
, name: z
|
, name: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "이름을 입력해주시십시오.")
|
.min(1, "이름을 입력해주시십시오.")
|
||||||
.max(14, "너무 긴 이름은 사용하실 수 없습니다.")
|
.max(14, "너무 긴 이름은 사용하실 수 없습니다.")
|
||||||
, nickname: z
|
, nickname: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "닉네임을 입력해주십시오.")
|
.min(1, "닉네임을 입력해주십시오.")
|
||||||
, passwordConfirm: z
|
, passwordConfirm: z
|
||||||
.string()
|
.string()
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.passwordConfirm, {
|
.refine((data) => data.password === data.passwordConfirm, {
|
||||||
path: ["passwordConfirm"],
|
path: ["passwordConfirm"],
|
||||||
error: "비밀번호가 일치하지 않습니다."
|
error: "비밀번호가 일치하지 않습니다."
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export { SignUpSchema } from './signup.schema';
|
export { SignUpSchema } from './account/signup.schema';
|
||||||
export { LoginSchema } from './login.schema';
|
export { LoginSchema } from './account/login.schema';
|
||||||
export { ResetPasswordSchema } from './resetPassword.schema';
|
export { ResetPasswordSchema } from './account/resetPassword.schema';
|
||||||
export { EmailVerificationSchema } from './emailVerification.schema';
|
export { EmailVerificationSchema } from './account/emailVerification.schema';
|
||||||
22
src/data/form/schedule/listSchedule.schema.ts
Normal file
22
src/data/form/schedule/listSchedule.schema.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as z from "zod";
|
||||||
|
|
||||||
|
export const ListScheduleSchema = z.object({
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
, startDate: z
|
||||||
|
.date()
|
||||||
|
.optional()
|
||||||
|
, endDate: z
|
||||||
|
.date()
|
||||||
|
.optional()
|
||||||
|
, status: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
, styleList: z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
, typeList: z
|
||||||
|
.array(z.string())
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
@@ -11,7 +11,7 @@ export class CreateScheduleRequest {
|
|||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
style: string;
|
style: string;
|
||||||
participantList: string[];
|
// participantList: string[];
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
name: string,
|
name: string,
|
||||||
@@ -23,7 +23,7 @@ export class CreateScheduleRequest {
|
|||||||
startTime: string,
|
startTime: string,
|
||||||
endTime: string,
|
endTime: string,
|
||||||
style: string,
|
style: string,
|
||||||
participantList: string[]
|
// participantList: string[]
|
||||||
) {
|
) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
@@ -34,6 +34,6 @@ export class CreateScheduleRequest {
|
|||||||
this.startTime = startTime;
|
this.startTime = startTime;
|
||||||
this.endTime = endTime;
|
this.endTime = endTime;
|
||||||
this.style = style;
|
this.style = style;
|
||||||
this.participantList = participantList;
|
// this.participantList = participantList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,10 @@ import type { ScheduleStatus } from "@/const/schedule/ScheduleStatus";
|
|||||||
import type { ScheduleType } from "@/const/schedule/ScheduleType";
|
import type { ScheduleType } from "@/const/schedule/ScheduleType";
|
||||||
|
|
||||||
export class ScheduleListRequest {
|
export class ScheduleListRequest {
|
||||||
filterAccountIdList?: string[];
|
startDate?: Date;
|
||||||
filterStartDate?: Date;
|
endDate?: Date;
|
||||||
filterEndDate?: Date;
|
typeList?: ScheduleType[];
|
||||||
filterDate?: Date;
|
styleList?: string[];
|
||||||
filterTypeList?: ScheduleType[];
|
status?: ScheduleStatus;
|
||||||
filterStyleList?: string[];
|
name?: string;
|
||||||
filterStatusList?: ScheduleStatus[];
|
|
||||||
filterName?: string;
|
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,57 @@
|
|||||||
export function useTime() {
|
export function useTime() {
|
||||||
const getNowString = () => {
|
const getCurrentTimeString = (type: 'standard' | 'continental') => {
|
||||||
const now = new Date();
|
const current = new Date();
|
||||||
const hour = now.getHours();
|
current.setSeconds(0, 0);
|
||||||
const minute = now.getMinutes();
|
const formatter = new Intl.DateTimeFormat('ko-KR', {
|
||||||
const ampm = hour < 12 ? '오전' : '오후';
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: type === 'standard' ? undefined : '2-digit',
|
||||||
|
hour12: type === 'standard',
|
||||||
|
hourCycle: type ==='standard' ? 'h12' : 'h23'
|
||||||
|
});
|
||||||
|
if (type === 'standard') {
|
||||||
|
return formatter.format(current).replace(':', '시 ').replace(/(\d+)\s*$/, '$1분');
|
||||||
|
}
|
||||||
|
return formatter.format(current);
|
||||||
|
};
|
||||||
|
|
||||||
return `${ampm} ${hour > 12 ? hour - 12 : hour}시 ${minute.toString().padStart(2, '0')}분`;
|
const standardTimeToContinentalTime = (standardTime: string) => {
|
||||||
|
const match = standardTime.match(/(오전|오후)\s*(\d+)\s*시\s*(\d+)\s*분/);
|
||||||
|
|
||||||
|
if (!match) return '';
|
||||||
|
|
||||||
|
const [_, ampm, hourString, minuteString] = match;
|
||||||
|
|
||||||
|
let hour = parseInt(hourString, 10);
|
||||||
|
const minute = parseInt(minuteString, 10);
|
||||||
|
|
||||||
|
if (ampm === '오후' && hour !== 12) {
|
||||||
|
hour += 12;
|
||||||
|
} else if (ampm === '오전' && hour === 12) {
|
||||||
|
hour = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:00`
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeStringToISOString = (time: string) => {
|
const continentalTimeToStandardTime = (continentalTime: string) => {
|
||||||
const timeArray = time.split(' ');
|
const [hour, minute, _] = continentalTime.split(':');
|
||||||
const hour = timeArray[0] === '오전' ? Number(timeArray[1]) : Number(timeArray[1]) + 12;
|
const date = new Date();
|
||||||
return `${hour.toString().padStart(2, '0')}:${timeArray[2]}:00`
|
date.setHours(parseInt(hour, 10), parseInt(minute, 10), 0, 0);
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat('ko-KR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true,
|
||||||
|
hourCycle: 'h12'
|
||||||
|
});
|
||||||
|
|
||||||
|
return formatter.format(date).replace(':', '시 ').replace(/(\d+)\s*$/, '$1분');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getNowString,
|
getCurrentTimeString,
|
||||||
timeStringToISOString
|
standardTimeToContinentalTime,
|
||||||
|
continentalTimeToStandardTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import type { CreateScheduleRequest } from "@/data/request/schedule/CreateScheduleRequest";
|
|
||||||
import { BaseNetwork } from "./BaseNetwork"
|
import { BaseNetwork } from "./BaseNetwork"
|
||||||
import type { UpdateScheduleRequest } from "@/data/request/schedule/UpdateScheduleRequest";
|
import {
|
||||||
import type { DeleteScheduleRequest } from "@/data/request/schedule/DeleteScheduleRequest";
|
ScheduleListRequest,
|
||||||
import type { ScheduleListRequest } from "@/data/request/schedule/ScheduleListRequest";
|
CreateScheduleRequest,
|
||||||
|
UpdateScheduleRequest,
|
||||||
|
DeleteScheduleRequest
|
||||||
|
} from '@/data/request';
|
||||||
|
import { CreateScheduleResponse } from "@/data/response";
|
||||||
|
|
||||||
export class ScheduleNetwork extends BaseNetwork {
|
export class ScheduleNetwork extends BaseNetwork {
|
||||||
private baseUrl = "/schedule";
|
private baseUrl = "/schedule";
|
||||||
@@ -21,7 +24,7 @@ export class ScheduleNetwork extends BaseNetwork {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create(data: CreateScheduleRequest) {
|
async create(data: CreateScheduleRequest) {
|
||||||
return await this.post(
|
return await this.post<CreateScheduleResponse>(
|
||||||
`${this.baseUrl}/create`,
|
`${this.baseUrl}/create`,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { SidebarTrigger } from '@/components/ui/sidebar';
|
import { SidebarTrigger } from '@/components/ui/sidebar';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { useState } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { LogOutIcon } from 'lucide-react';
|
import { LogOutIcon } from 'lucide-react';
|
||||||
@@ -24,31 +23,31 @@ export default function Header() {
|
|||||||
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
||||||
<Label>{import.meta.env.BASE_URL}</Label>
|
<Label>{import.meta.env.BASE_URL}</Label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className={`
|
className={`
|
||||||
group flex items-center justify-start
|
group flex items-center justify-start
|
||||||
pr-2 pl-2 border border-red-500 bg-white
|
pr-2 pl-2 border border-red-500 bg-white
|
||||||
transition-all duration-150
|
transition-all duration-150
|
||||||
w-10 hover:w-25 hover:bg-red-500
|
w-10 hover:w-25 hover:bg-red-500
|
||||||
overflow-hidden rounded-md
|
overflow-hidden rounded-md
|
||||||
`}
|
`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClickLogoutButton}
|
onClick={handleClickLogoutButton}
|
||||||
>
|
>
|
||||||
<LogOutIcon
|
<LogOutIcon
|
||||||
className="text-red-500 transition-colors duration-150 group-hover:text-white"
|
className="text-red-500 transition-colors duration-150 group-hover:text-white"
|
||||||
/>
|
/>
|
||||||
<span className="
|
<span className="
|
||||||
text-red-500 group-hover:text-white
|
text-red-500 group-hover:text-white
|
||||||
opacity-0 scale-1
|
opacity-0 scale-1
|
||||||
transition-all duration-150
|
transition-all duration-150
|
||||||
group-hover:opacity-100 group-hover:scale-100
|
group-hover:opacity-100 group-hover:scale-100
|
||||||
">
|
">
|
||||||
로그아웃
|
로그아웃
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { PopoverContent } from "@/components/ui/popover"
|
import { PopoverContent } from "@/components/ui/popover"
|
||||||
import type { ColorPaletteType } from "@/const/ColorPalette"
|
import type { ColorPaletteType } from "@/const/ColorPalette"
|
||||||
import { usePalette } from "@/hooks/use-palette";
|
import { usePalette } from "@/hooks/use-palette";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Triangle } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
interface ColorPickPopoverProps {
|
interface ColorPickPopoverProps {
|
||||||
@@ -28,7 +30,10 @@ export const ColorPickPopover = ({ setColor }: ColorPickPopoverProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="flex flex-col gap-1.5 w-fit"
|
className={cn(
|
||||||
|
"flex flex-col gap-1.5 w-fit relative",
|
||||||
|
seeMore ? "h-40" : "h-26"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{getSlicedList(mainPaletteList, 5).map((list) => (
|
{getSlicedList(mainPaletteList, 5).map((list) => (
|
||||||
<div className="flex flex-row gap-2.5">
|
<div className="flex flex-row gap-2.5">
|
||||||
@@ -41,10 +46,8 @@ export const ColorPickPopover = ({ setColor }: ColorPickPopoverProps) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{
|
{ seeMore && (
|
||||||
!seeMore
|
<>
|
||||||
? <div className="w-full" onClick={() => setSeeMore(true)}>더 보기</div>
|
|
||||||
: <>
|
|
||||||
{getSlicedList(extraPaletteList, 5).map((list) => (
|
{getSlicedList(extraPaletteList, 5).map((list) => (
|
||||||
<div className="flex flex-row gap-2.5">
|
<div className="flex flex-row gap-2.5">
|
||||||
{list.map((palette) => (
|
{list.map((palette) => (
|
||||||
@@ -59,12 +62,29 @@ export const ColorPickPopover = ({ setColor }: ColorPickPopoverProps) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{
|
<div
|
||||||
seeMore
|
className={cn(
|
||||||
? <div className="w-full" onClick={() => setSeeMore(false)}>접기</div>
|
"absolute h-8 bottom-0 left-0 w-full flex flex-row justify-center items-center gap-4 group bg-white hover:bg-indigo-300 transition-all duration-150",
|
||||||
: null
|
"rounded-b-md"
|
||||||
}
|
)}
|
||||||
|
onClick={() => setSeeMore(prev => !prev)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-indigo-300 group-hover:text-white transition-all duration-150"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{ seeMore ? " 접기 " : "더 보기"}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-0 h-0 border-l-8 border-l-transparent border-r-8 border-r-transparent border-b-14 border-b-indigo-300",
|
||||||
|
"group-hover:border-b-white trnasition-all duration-150",
|
||||||
|
seeMore && "rotate-180"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ export const DatePickPopover = ({ ...props } : DaetPickPopoverProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const { startDate, setStartDate, endDate, setEndDate } = props;
|
const { startDate, setStartDate, endDate, setEndDate } = props;
|
||||||
const [startOpen, setStartOpen] = useState(false);
|
const [startOpen, setStartOpen] = useState(false);
|
||||||
const [endOpen, setEndOpen] = useState(false);
|
const [endOpen, setEndOpen] = useState(false);
|
||||||
@@ -64,7 +66,12 @@ export const DatePickPopover = ({ ...props } : DaetPickPopoverProps) => {
|
|||||||
>
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
className={cn(
|
||||||
|
"flex-9 h-full",
|
||||||
|
!startOpen
|
||||||
|
? "bg-white text-indigo-300 hover:bg-indigo-300 hover:text-white"
|
||||||
|
: "bg-indigo-300 text-white hover:bg-indigo-300"
|
||||||
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
@@ -98,7 +105,12 @@ export const DatePickPopover = ({ ...props } : DaetPickPopoverProps) => {
|
|||||||
>
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
className={cn(
|
||||||
|
"flex-9 h-full",
|
||||||
|
!endOpen
|
||||||
|
? "bg-white text-indigo-300 hover:bg-indigo-300 hover:text-white"
|
||||||
|
: "bg-indigo-300 text-white hover:bg-indigo-300"
|
||||||
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
|||||||
11
src/ui/component/popover/schedule/FilterPopover.tsx
Normal file
11
src/ui/component/popover/schedule/FilterPopover.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Popover, PopoverTrigger } from "@/components/ui/popover"
|
||||||
|
|
||||||
|
export const FilterPopover = () => {
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
|
||||||
|
</PopoverTrigger>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ import { useState } from 'react';
|
|||||||
import { PenSquare } from 'lucide-react';
|
import { PenSquare } from 'lucide-react';
|
||||||
|
|
||||||
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
|
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ScheduleNetwork } from '@/network/ScheduleNetwork';
|
||||||
|
import { ScheduleListContent } from './content/ScheduleListContent';
|
||||||
|
|
||||||
interface ScheduleSheetProps {
|
interface ScheduleSheetProps {
|
||||||
date: Date | undefined;
|
date: Date | undefined;
|
||||||
@@ -14,17 +17,8 @@ interface ScheduleSheetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: ScheduleSheetProps) => {
|
export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: ScheduleSheetProps) => {
|
||||||
|
|
||||||
const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list');
|
const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list');
|
||||||
|
|
||||||
const ListContent = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PenSquare onClick={() => setMode('create')}/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DetailContent = () => {
|
const DetailContent = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -44,7 +38,13 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
|
|||||||
const SchedulePopoverContent = () => {
|
const SchedulePopoverContent = () => {
|
||||||
switch(mode) {
|
switch(mode) {
|
||||||
case 'list':
|
case 'list':
|
||||||
return <ListContent />
|
return <ScheduleListContent
|
||||||
|
setMode={setMode}
|
||||||
|
date={date}
|
||||||
|
popoverAlign={popoverAlign}
|
||||||
|
popoverSide={popoverSide}
|
||||||
|
open={open}
|
||||||
|
/>
|
||||||
case 'create':
|
case 'create':
|
||||||
return <ScheduleCreateContent
|
return <ScheduleCreateContent
|
||||||
setMode={setMode}
|
setMode={setMode}
|
||||||
@@ -62,18 +62,10 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="rounded-xl xl:w-[calc(100vw/4)] xl:max-w-[480px] min-w-[384px]"
|
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}
|
align={popoverAlign} side={popoverSide}
|
||||||
>
|
>
|
||||||
<ScrollArea
|
{<SchedulePopoverContent />}
|
||||||
className={
|
|
||||||
cn(
|
|
||||||
"min-h-[125px] h-[calc(100vh/2.3)] w-full flex flex-col",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{<SchedulePopoverContent />}
|
|
||||||
</ScrollArea>
|
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
@@ -83,6 +84,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
};
|
};
|
||||||
const startTime = `${startAmPm} ${startHour}시 ${startMinute}분`;
|
const startTime = `${startAmPm} ${startHour}시 ${startMinute}분`;
|
||||||
setStartTime(startTime);
|
setStartTime(startTime);
|
||||||
|
setStartOpen(false);
|
||||||
}
|
}
|
||||||
const cancelStartTime = () => {
|
const cancelStartTime = () => {
|
||||||
setStartOpen(false);
|
setStartOpen(false);
|
||||||
@@ -128,7 +130,6 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
endAmPm,
|
endAmPm,
|
||||||
endHour,
|
endHour,
|
||||||
@@ -140,6 +141,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
};
|
};
|
||||||
const endTime = `${endAmPm} ${endHour}시 ${endMinute}분`;
|
const endTime = `${endAmPm} ${endHour}시 ${endMinute}분`;
|
||||||
setEndTime(endTime);
|
setEndTime(endTime);
|
||||||
|
setEndOpen(false);
|
||||||
}
|
}
|
||||||
const cancelEndTime = () => {
|
const cancelEndTime = () => {
|
||||||
setEndOpen(false);
|
setEndOpen(false);
|
||||||
@@ -161,7 +163,12 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
>
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
className={cn(
|
||||||
|
"flex-9 h-full",
|
||||||
|
!startOpen
|
||||||
|
? "bg-white text-indigo-300 hover:bg-indigo-300 hover:text-white"
|
||||||
|
: "bg-indigo-300 text-white hover:bg-indigo-300"
|
||||||
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
@@ -183,13 +190,21 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
>
|
>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value={"오전"}
|
value={"오전"}
|
||||||
className="w-full h-7 rounded-none! rounded-tl-md!"
|
className={cn(
|
||||||
|
"w-full h-7 rounded-none! rounded-tl-md!",
|
||||||
|
"text-indigo-300! bg-white! hover:bg-indigo-300! hover:text-white!",
|
||||||
|
"data-[state=on]:bg-indigo-300! data-[state=on]:text-white!"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
오전
|
오전
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value={"오후"}
|
value={"오후"}
|
||||||
className="w-full h-7 rounded-none!"
|
className={cn(
|
||||||
|
"w-full h-7 rounded-none!",
|
||||||
|
"text-indigo-300! bg-white! hover:bg-indigo-300! hover:text-white!",
|
||||||
|
"data-[state=on]:bg-indigo-300! data-[state=on]:text-white!"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
오후
|
오후
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
@@ -200,18 +215,22 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
id="startHour"
|
id="startHour"
|
||||||
className="w-15 border-r rounded-none border-l flex flex-col justify-start items-center"
|
className="w-15 border-r border-r-indigo-300 rounded-none border-l border-l-indigo-300 flex flex-col justify-start items-center"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
[1,2,3,4,5,6,7,8,9,10,11,12].map((time) => (
|
[1,2,3,4,5,6,7,8,9,10,11,12].map((time) => {
|
||||||
<ToggleGroupItem
|
return <ToggleGroupItem
|
||||||
className="w-full h-7 rounded-none!"
|
className={cn(
|
||||||
|
"w-full h-7 rounded-none!",
|
||||||
|
"bg-white! text-indigo-300! hover:bg-indigo-300! hover:text-white!",
|
||||||
|
"data-[state=on]:bg-indigo-300! data-[state=on]:text-white!"
|
||||||
|
)}
|
||||||
value={time.toString().padStart(2, '0')}
|
value={time.toString().padStart(2, '0')}
|
||||||
key={`startHour${time.toString().padStart(2, '0')}`}
|
key={`startHour${time.toString().padStart(2, '0')}`}
|
||||||
>
|
>
|
||||||
{time.toString().padStart(2, '0')}
|
{time.toString().padStart(2, '0')}
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
))
|
})
|
||||||
}
|
}
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@@ -221,13 +240,17 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
id="startMinute"
|
id="startMinute"
|
||||||
className="w-full h-full rounded-none border-r flex flex-col justify-start items-center"
|
className="w-full h-full rounded-none border-r border-r-indigo-300 flex flex-col justify-start items-center"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
Array.from({ length: 60 }).map((_, idx) => (
|
Array.from({ length: 60 }).map((_, idx) => (
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value={idx.toString().padStart(2, '0')}
|
value={idx.toString().padStart(2, '0')}
|
||||||
className="w-full h-7 rounded-none!"
|
className={cn(
|
||||||
|
"w-full h-7 rounded-none!",
|
||||||
|
"bg-white! text-indigo-300! hover:bg-indigo-300! hover:text-white!",
|
||||||
|
"data-[state=on]:bg-indigo-300! data-[state=on]:text-white!"
|
||||||
|
)}
|
||||||
key={`startMinute${idx.toString().padStart(2, '0')}`}
|
key={`startMinute${idx.toString().padStart(2, '0')}`}
|
||||||
>
|
>
|
||||||
{idx.toString().padStart(2, '0')}
|
{idx.toString().padStart(2, '0')}
|
||||||
@@ -238,13 +261,21 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<div className="w-15 h-full flex flex-col">
|
<div className="w-15 h-full flex flex-col">
|
||||||
<div
|
<div
|
||||||
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
className={cn(
|
||||||
|
"cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md!",
|
||||||
|
"text-indigo-300 bg-white tarnsition-all duration-150",
|
||||||
|
"hover:text-white hover:bg-indigo-300"
|
||||||
|
)}
|
||||||
onClick={applyStartTime}
|
onClick={applyStartTime}
|
||||||
>
|
>
|
||||||
적용
|
확인
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
className={cn(
|
||||||
|
"cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none",
|
||||||
|
"text-red-400 bg- transition-all duration-150",
|
||||||
|
"hover:text-white hover:bg-red-400"
|
||||||
|
)}
|
||||||
onClick={cancelStartTime}
|
onClick={cancelStartTime}
|
||||||
>
|
>
|
||||||
취소
|
취소
|
||||||
@@ -259,7 +290,12 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
>
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
|
className={cn(
|
||||||
|
"flex-9 h-full",
|
||||||
|
!endOpen
|
||||||
|
? "bg-white text-indigo-300 hover:bg-indigo-300 hover:text-white"
|
||||||
|
: "bg-indigo-300 text-white hover:bg-indigo-300"
|
||||||
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
@@ -281,13 +317,21 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
>
|
>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value={"오전"}
|
value={"오전"}
|
||||||
className="w-full h-7 rounded-none! rounded-tl-md!"
|
className={cn(
|
||||||
|
"w-full h-7 rounded-none! rounded-tl-md!",
|
||||||
|
"text-indigo-300! bg-white! hover:bg-indigo-300! hover:text-white!",
|
||||||
|
"data-[state=on]:bg-indigo-300! data-[state=on]:text-white!"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
오전
|
오전
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value={"오후"}
|
value={"오후"}
|
||||||
className="w-full h-7 rounded-none!"
|
className={cn(
|
||||||
|
"w-full h-7 rounded-none!",
|
||||||
|
"text-indigo-300! bg-white! hover:bg-indigo-300! hover:text-white!",
|
||||||
|
"data-[state=on]:bg-indigo-300! data-[state=on]:text-white!"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
오후
|
오후
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
@@ -298,12 +342,16 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
id="endHour"
|
id="endHour"
|
||||||
type="single"
|
type="single"
|
||||||
className="w-15 border-r rounded-none border-l flex flex-col justify-start items-center"
|
className="w-15 border-r border-r-indigo-300 rounded-none border-l border-l-indigo-300 flex flex-col justify-start items-center"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
[1,2,3,4,5,6,7,8,9,10,11,12].map((time) => (
|
[1,2,3,4,5,6,7,8,9,10,11,12].map((time) => (
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className="w-full h-7 rounded-none!"
|
className={cn(
|
||||||
|
"w-full h-7 rounded-none!",
|
||||||
|
"bg-white! text-indigo-300! hover:bg-indigo-300! hover:text-white!",
|
||||||
|
"data-[state=on]:bg-indigo-300! data-[state=on]:text-white!"
|
||||||
|
)}
|
||||||
key={`endHour${time.toString().padStart(2, '0')}`}
|
key={`endHour${time.toString().padStart(2, '0')}`}
|
||||||
value={time.toString().padStart(2, '0')}
|
value={time.toString().padStart(2, '0')}
|
||||||
>
|
>
|
||||||
@@ -319,14 +367,18 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
id="endMinute"
|
id="endMinute"
|
||||||
type="single"
|
type="single"
|
||||||
className="w-full h-full rounded-none border-r flex flex-col justify-start items-center"
|
className="w-full h-full rounded-none border-r border-r-indigo-300 flex flex-col justify-start items-center"
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
Array.from({ length: 60 }).map((_, idx) => (
|
Array.from({ length: 60 }).map((_, idx) => (
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
value={idx.toString().padStart(2, '0')}
|
value={idx.toString().padStart(2, '0')}
|
||||||
key={`endMinute${idx.toString().padStart(2, '0')}`}
|
key={`endMinute${idx.toString().padStart(2, '0')}`}
|
||||||
className="w-full h-7 rounded-none!"
|
className={cn(
|
||||||
|
"w-full h-7 rounded-none!",
|
||||||
|
"bg-white! text-indigo-300! hover:bg-indigo-300! hover:text-white!",
|
||||||
|
"data-[state=on]:bg-indigo-300! data-[state=on]:text-white!"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{idx.toString().padStart(2, '0')}
|
{idx.toString().padStart(2, '0')}
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
@@ -336,13 +388,21 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<div className="w-15 h-full flex flex-col">
|
<div className="w-15 h-full flex flex-col">
|
||||||
<div
|
<div
|
||||||
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
className={cn(
|
||||||
|
"cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md!",
|
||||||
|
"text-indigo-300 bg-white transition-all duration-150",
|
||||||
|
"hover:text-white hover:bg-indigo-300"
|
||||||
|
)}
|
||||||
onClick={applyEndTime}
|
onClick={applyEndTime}
|
||||||
>
|
>
|
||||||
확인
|
확인
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
|
className={cn(
|
||||||
|
"cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none",
|
||||||
|
"text-red-400 bg-white transition-all duration-150",
|
||||||
|
"hover:text-white hover:bg-red-400"
|
||||||
|
)}
|
||||||
onClick={cancelEndTime}
|
onClick={cancelEndTime}
|
||||||
>
|
>
|
||||||
취소
|
취소
|
||||||
|
|||||||
@@ -1,31 +1,68 @@
|
|||||||
import { PopoverContent } from "@/components/ui/popover"
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||||
import { ScheduleTypeLabel, type ScheduleType } from "@/const/schedule/ScheduleType";
|
import { ScheduleTypeLabel, type ScheduleType } from "@/const/schedule/ScheduleType";
|
||||||
import { useRecord } from "@/hooks/use-record";
|
import { useRecord } from "@/hooks/use-record";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
interface TypePickPopoverProps {
|
interface TypePickPopoverProps {
|
||||||
popoverSide : 'left' | 'right';
|
popoverSide : 'left' | 'right';
|
||||||
|
type: ScheduleType;
|
||||||
setType: (type: ScheduleType) => void;
|
setType: (type: ScheduleType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TypePickPopover = ({ popoverSide, setType }: TypePickPopoverProps) => {
|
export const TypePickPopover = ({ popoverSide, type, setType }: TypePickPopoverProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const typeLabelList = useRecord(ScheduleTypeLabel).keys.map((key) => {
|
const typeLabelList = useRecord(ScheduleTypeLabel).keys.map((key) => {
|
||||||
return {
|
return {
|
||||||
type: key,
|
type: key,
|
||||||
label: ScheduleTypeLabel[key as keyof typeof ScheduleTypeLabel]
|
label: ScheduleTypeLabel[key as keyof typeof ScheduleTypeLabel]
|
||||||
} as { type: ScheduleType, label: string};
|
} as { type: ScheduleType, label: string};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectType = (type: ScheduleType) => {
|
||||||
|
setType(type);
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<PopoverContent side={popoverSide} align={'start'} className="p-0 w-fit h-fit">
|
<div className="w-full h-10">
|
||||||
<div className="w-20 h-62.5 flex flex-col">
|
<Popover
|
||||||
{typeLabelList.map((type) => (
|
open={open}
|
||||||
<div
|
onOpenChange={setOpen}
|
||||||
className="cursor-default flex-1 h-full flex justify-center items-center hover:bg-gray-100"
|
>
|
||||||
onClick={() => setType(type.type)}
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
"w-full h-10 rounded-md",
|
||||||
|
!open
|
||||||
|
? "bg-white text-indigo-300 hover:bg-indigo-300 hover:text-white"
|
||||||
|
: "bg-indigo-300 text-white hover:bg-indigo-300!"
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
{type.label}
|
{ScheduleTypeLabel[type as keyof typeof ScheduleTypeLabel]}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent side={popoverSide} align={'start'} className="p-0 w-fit h-fit">
|
||||||
|
<div className="w-20 h-62.5 flex flex-col rounded-md">
|
||||||
|
{typeLabelList.map((label, idx) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"cursor-default flex-1 h-full flex justify-center items-center transition-all duration-150",
|
||||||
|
type === label.type
|
||||||
|
? "bg-indigo-300 text-white hover:bg-indigo-300!"
|
||||||
|
: "bg-white text-indigo-300 hover:bg-indigo-300 hover:text-white",
|
||||||
|
(idx === 0 && "rounded-t-md"),
|
||||||
|
(idx === typeLabelList.length - 1 && "rounded-b-md")
|
||||||
|
)}
|
||||||
|
onClick={() => selectType(label.type)}
|
||||||
|
>
|
||||||
|
{label.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</PopoverContent>
|
||||||
</div>
|
</Popover>
|
||||||
</PopoverContent>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import type { ColorPaletteType } from "@/const/ColorPalette";
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleCreateContentProps extends BaseProps {
|
export interface ScheduleCreateContentProps extends BaseProps {
|
||||||
date: Date | undefined;
|
|
||||||
setMode: (mode: 'list' | 'create' | 'detail' | 'update') => void;
|
}
|
||||||
|
|
||||||
|
export interface ScheduleListContentProps extends BaseProps {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,41 +1,44 @@
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { usePalette } from '@/hooks/use-palette';
|
|
||||||
import { type ColorPaletteType } from '@/const/ColorPalette';
|
|
||||||
import { ColorPickPopover } from '../ColorPickPopover';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
|
||||||
import * as z from 'zod';
|
|
||||||
import { CreateScheduleSchema } from '@/data/form/createSchedule.schema';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { Field, FieldError } from '@/components/ui/field';
|
|
||||||
import { ArrowLeft, PenSquare, X, XIcon } from 'lucide-react';
|
|
||||||
import { ScheduleTypeLabel, type ScheduleType } from '@/const/schedule/ScheduleType';
|
|
||||||
import { useRecord } from '@/hooks/use-record';
|
|
||||||
import { TypePickPopover } from '../TypePickPopover';
|
|
||||||
import { ScheduleDay } from '@/const/schedule/ScheduleDay';
|
|
||||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
||||||
import { DatePickPopover } from '../DatePickPopover';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { TimePickPopover } from '../TimePickPopover';
|
|
||||||
import { useTime } from '@/hooks/use-time';
|
|
||||||
import type { ScheduleCreateContentProps } from './ContentProps';
|
|
||||||
import { Button } from '@/components/ui/button';
|
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 { Textarea } from '@/components/ui/textarea';
|
||||||
import { ParticipantPopover } from '../ParticipantPopover';
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||||
|
import { type ColorPaletteType } from '@/const/ColorPalette';
|
||||||
|
import { ScheduleDay } from '@/const/schedule/ScheduleDay';
|
||||||
|
import type { ScheduleStatus } from '@/const/schedule/ScheduleStatus';
|
||||||
|
import { type ScheduleType } from '@/const/schedule/ScheduleType';
|
||||||
|
import { CreateScheduleSchema } from '@/data/form/schedule/createSchedule.schema';
|
||||||
|
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 { useState } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { ColorPickPopover } from '../ColorPickPopover';
|
||||||
|
import { DatePickPopover } from '../DatePickPopover';
|
||||||
|
import { TimePickPopover } from '../TimePickPopover';
|
||||||
|
import { TypePickPopover } from '../TypePickPopover';
|
||||||
|
import type { ScheduleCreateContentProps } from './ContentProps';
|
||||||
|
|
||||||
export const ScheduleCreateContent = ({ date, open, setMode, popoverSide, popoverAlign }: ScheduleCreateContentProps) => {
|
export const ScheduleCreateContent = ({ date, setMode, popoverSide, popoverAlign }: ScheduleCreateContentProps) => {
|
||||||
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
|
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { getPaletteByKey } = usePalette();
|
const { getPaletteByKey } = usePalette();
|
||||||
const { getNowString } = useTime();
|
const { getCurrentTimeString, standardTimeToContinentalTime } = useTime();
|
||||||
const dayLabelList = useRecord(ScheduleDay).keys.map((key) => {
|
const dayLabelList = useRecord(ScheduleDay).keys.map((key) => {
|
||||||
return {
|
return {
|
||||||
day: Number(key),
|
day: Number(key),
|
||||||
label: ScheduleDay[Number(key)]
|
label: ScheduleDay[Number(key)]
|
||||||
} as { day: number, label: string };
|
} as { day: number, label: string };
|
||||||
})
|
});
|
||||||
|
const scheduleNetwork = new ScheduleNetwork();
|
||||||
|
|
||||||
const createScheduleForm = useForm<z.infer<typeof CreateScheduleSchema>>({
|
const createScheduleForm = useForm<z.infer<typeof CreateScheduleSchema>>({
|
||||||
resolver: zodResolver(CreateScheduleSchema),
|
resolver: zodResolver(CreateScheduleSchema),
|
||||||
@@ -44,8 +47,8 @@ export const ScheduleCreateContent = ({ date, open, setMode, popoverSide, popove
|
|||||||
startDate: date || new Date(),
|
startDate: date || new Date(),
|
||||||
endDate: date || new Date(),
|
endDate: date || new Date(),
|
||||||
content: "",
|
content: "",
|
||||||
startTime: getNowString(),
|
startTime: getCurrentTimeString('standard'),
|
||||||
endTime: getNowString(),
|
endTime: getCurrentTimeString('standard'),
|
||||||
type: "once",
|
type: "once",
|
||||||
status: "yet",
|
status: "yet",
|
||||||
style: getPaletteByKey('Black').style,
|
style: getPaletteByKey('Black').style,
|
||||||
@@ -65,10 +68,48 @@ export const ScheduleCreateContent = ({ date, open, setMode, popoverSide, popove
|
|||||||
status,
|
status,
|
||||||
style,
|
style,
|
||||||
dayList,
|
dayList,
|
||||||
participantList
|
// participantList
|
||||||
} = createScheduleForm.watch();
|
} = createScheduleForm.watch();
|
||||||
|
|
||||||
const selectColor = (color: ColorPaletteType) => {
|
const reqCreate = async () => {
|
||||||
|
if (isLoading) return;
|
||||||
|
const data = {
|
||||||
|
name,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
content,
|
||||||
|
startTime: standardTimeToContinentalTime(startTime),
|
||||||
|
endTime: standardTimeToContinentalTime(endTime),
|
||||||
|
type: type as ScheduleType,
|
||||||
|
status: status as ScheduleStatus,
|
||||||
|
dayList,
|
||||||
|
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 selectColor = (color: ColorPaletteType) => {
|
||||||
createScheduleForm.setValue('style', color.style);
|
createScheduleForm.setValue('style', color.style);
|
||||||
setColorPopoverOpen(false);
|
setColorPopoverOpen(false);
|
||||||
}
|
}
|
||||||
@@ -92,9 +133,9 @@ export const ScheduleCreateContent = ({ date, open, setMode, popoverSide, popove
|
|||||||
createScheduleForm.setValue(type, time);
|
createScheduleForm.setValue(type, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectParticipant = (participantList: string[]) => {
|
// const selectParticipant = (participantList: string[]) => {
|
||||||
createScheduleForm.setValue('participantList', participantList);
|
// createScheduleForm.setValue('participantList', participantList);
|
||||||
}
|
// }
|
||||||
|
|
||||||
const OnceContent = () => {
|
const OnceContent = () => {
|
||||||
return (
|
return (
|
||||||
@@ -158,7 +199,7 @@ export const ScheduleCreateContent = ({ date, open, setMode, popoverSide, popove
|
|||||||
<div
|
<div
|
||||||
onClick={() => setMode('list')}
|
onClick={() => setMode('list')}
|
||||||
>
|
>
|
||||||
<ArrowLeft />
|
<ArrowLeft className="stroke-indigo-100 hover:stroke-indigo-300 transition-all duration-150" />
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
@@ -169,7 +210,7 @@ export const ScheduleCreateContent = ({ date, open, setMode, popoverSide, popove
|
|||||||
{...field}
|
{...field}
|
||||||
id="form-create-schedule-name"
|
id="form-create-schedule-name"
|
||||||
placeholder="제목"
|
placeholder="제목"
|
||||||
className="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"
|
className="placeholder-indigo-200! font-bold border-t-0 border-r-0 border-l-0 p-0 border-b-2 rounded-none shadow-none border-indigo-100 focus-visible:ring-0 focus-visible:border-b-indigo-300"
|
||||||
style={{
|
style={{
|
||||||
fontSize: '20px'
|
fontSize: '20px'
|
||||||
}}
|
}}
|
||||||
@@ -198,52 +239,82 @@ export const ScheduleCreateContent = ({ date, open, setMode, popoverSide, popove
|
|||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<Popover>
|
<ScrollArea
|
||||||
<PopoverTrigger asChild>
|
className={
|
||||||
<div className="hover:bg-indigo-100 cursor-default w-full h-10 border border-indigo-100 flex justify-center items-center rounded-sm">
|
cn(
|
||||||
{ScheduleTypeLabel[type as keyof typeof ScheduleTypeLabel]}
|
"min-h-[125px] h-[calc(100vh/2.3-40px)]! w-full",
|
||||||
</div>
|
)
|
||||||
</PopoverTrigger>
|
}
|
||||||
<TypePickPopover
|
>
|
||||||
|
<div
|
||||||
|
className="w-full h-full flex! flex-col! gap-4!"
|
||||||
|
>
|
||||||
|
<TypePickPopover
|
||||||
|
type={type as ScheduleType}
|
||||||
setType={selectType}
|
setType={selectType}
|
||||||
popoverSide={popoverSide}
|
popoverSide={popoverSide}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
<div
|
||||||
<div
|
className="w-full h-10"
|
||||||
className="w-full h-10"
|
>
|
||||||
>
|
{renderContent()}
|
||||||
{renderContent()}
|
</div>
|
||||||
</div>
|
<div className="w-full h-10">
|
||||||
<div className="w-full h-10">
|
<TimePickPopover
|
||||||
<TimePickPopover
|
mode='range'
|
||||||
mode='range'
|
popoverAlign={popoverAlign}
|
||||||
popoverAlign={popoverAlign}
|
disabled={false}
|
||||||
disabled={false}
|
startTime={startTime}
|
||||||
startTime={startTime}
|
setStartTime={(time: string | undefined) => selectTime('startTime', time ?? '')}
|
||||||
setStartTime={(time: string | undefined) => selectTime('startTime', time ?? '')}
|
endTime={endTime}
|
||||||
endTime={endTime}
|
setEndTime={(time: string | undefined) => selectTime('endTime', time ?? '')}
|
||||||
setEndTime={(time: string | undefined) => selectTime('endTime', time ?? '')}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<Controller
|
||||||
<Controller
|
name="content"
|
||||||
name="content"
|
control={createScheduleForm.control}
|
||||||
control={createScheduleForm.control}
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<Textarea
|
||||||
<Textarea
|
{...field}
|
||||||
{...field}
|
rows={2}
|
||||||
rows={2}
|
placeholder="일정 상세 사항"
|
||||||
placeholder="일정 상세 사항"
|
className="placeholder-indigo-200! focus-visible:placeholder-indigo-300! border-indigo-100 focus-visible:border-indigo-300 resize-none focus-visible:ring-0"
|
||||||
className="placeholder-gray-300! focus-visible:placeholder-gray-400! border-indigo-100 focus-visible:border-indigo-300 resize-none focus-visible:ring-0"
|
style={{
|
||||||
style={{
|
'scrollbarWidth': 'none'
|
||||||
'scrollbarWidth': 'none'
|
}}
|
||||||
}}
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
{/* <ParticipantPopover
|
||||||
/>
|
participantList={participantList}
|
||||||
<ParticipantPopover
|
setParticipantList={selectParticipant}
|
||||||
participantList={participantList}
|
/> */}
|
||||||
setParticipantList={selectParticipant}
|
</div>
|
||||||
/>
|
</ScrollArea>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
)}
|
||||||
|
type="button"
|
||||||
|
onClick={reqCreate}
|
||||||
|
>
|
||||||
|
추가
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
"h-full flex-5 rounded-none rounded-br-md flex justify-center items-center",
|
||||||
|
"bg-white text-red-400",
|
||||||
|
"hover:text-white hover:bg-red-400"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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 { 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";
|
||||||
|
|
||||||
|
export const ScheduleListContent = ({ date, setMode, popoverAlign, popoverSide, open }: ScheduleListContentProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [scheduleList, setScheduleList] = useState([]);
|
||||||
|
const [filteredList, setFilteredList] = useState([]);
|
||||||
|
const scheduleNetwork = new ScheduleNetwork();
|
||||||
|
|
||||||
|
const listScheduleForm = useForm<z.infer<typeof ListScheduleSchema>>({
|
||||||
|
resolver: zodResolver(ListScheduleSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: undefined,
|
||||||
|
startDate: undefined,
|
||||||
|
endDate: undefined,
|
||||||
|
status: undefined,
|
||||||
|
typeList: undefined,
|
||||||
|
styleList: undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
status,
|
||||||
|
typeList,
|
||||||
|
styleList
|
||||||
|
} = listScheduleForm.watch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const reqList = async () => {
|
||||||
|
const data = {
|
||||||
|
name,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
status: status as ScheduleStatus | undefined,
|
||||||
|
styleList,
|
||||||
|
typeList: typeList as ScheduleType[] | undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await scheduleNetwork.getList(data);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="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">
|
||||||
|
<PenSquare
|
||||||
|
className="transition-all duration-150 group-hover:stroke-indigo-600 stroke-indigo-400"
|
||||||
|
size={18}
|
||||||
|
onClick={() => setTimeout(() => {setMode('create')}, 150)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-[calc(100%-40px)]">
|
||||||
|
<ScrollArea
|
||||||
|
className="w-full h-full flex flex-col justify-start items-center"
|
||||||
|
>
|
||||||
|
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ const certPath = path.resolve(__dirname, 'certs');
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
// host: '0.0.0.0',
|
||||||
port: 5185,
|
port: 5185,
|
||||||
https: {
|
https: {
|
||||||
key: fs.readFileSync(path.join(certPath, 'localhost+2-key.pem')),
|
key: fs.readFileSync(path.join(certPath, 'localhost+2-key.pem')),
|
||||||
|
|||||||
Reference in New Issue
Block a user