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

- 시작/종료 시간 설정 화면 및 로직 구현
- 일정 상세 사항 화면 및 로직 구현
- 참여자 추가 화면 구현 중
This commit is contained in:
geonhee-min
2025-12-09 17:09:31 +09:00
parent 47d2eae519
commit a30c2bbb32
18 changed files with 722 additions and 241 deletions

View File

@@ -19,6 +19,7 @@
"hooks": "@/hooks" "hooks": "@/hooks"
}, },
"registries": { "registries": {
"@reui": "https://reui.io/r/{name}.json" "@reui": "https://reui.io/r/{name}.json",
"@diceui": "https://diceui.com/r/{name}.json"
} }
} }

67
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "scheduler", "name": "scheduler",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@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",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
@@ -376,6 +377,50 @@
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@diceui/mention": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@diceui/mention/-/mention-0.8.0.tgz",
"integrity": "sha512-BHTYLG4qJA+9TJ/n5otlIHoJuLz5v13xAayYsRL7lnCnp4ik37uYrLpA+25y+uQmJ5i1W3kv2MfD2ZasvDzDDw==",
"license": "MIT",
"dependencies": {
"@diceui/shared": "0.12.0",
"@floating-ui/react": "^0.27.4"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@diceui/shared": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@diceui/shared/-/shared-0.12.0.tgz",
"integrity": "sha512-aJ+gxHTleFD8b9DgXOb7G0sHymW8nMK4C3KXDMca4+PPl1NsfRG/Lnb2dN8vq44XOb/W7Wjn3gmgakXAOzgrJA==",
"peerDependencies": {
"@floating-ui/react": "^0.27.4",
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.11", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
@@ -968,6 +1013,22 @@
"@floating-ui/utils": "^0.2.10" "@floating-ui/utils": "^0.2.10"
} }
}, },
"node_modules/@floating-ui/react": {
"version": "0.27.16",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
"integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@floating-ui/react-dom": "^2.1.6",
"@floating-ui/utils": "^0.2.10",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@floating-ui/react-dom": { "node_modules/@floating-ui/react-dom": {
"version": "2.1.6", "version": "2.1.6",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
@@ -6375,6 +6436,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/tabbable": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz",
"integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==",
"license": "MIT"
},
"node_modules/tailwind-merge": { "node_modules/tailwind-merge": {
"version": "3.4.0", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",

View File

@@ -11,6 +11,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@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",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",

View File

@@ -0,0 +1,91 @@
import * as MentionPrimitive from "@diceui/mention";
import type * as React from "react";
import { cn } from "@/lib/utils";
function Mention({
className,
...props
}: React.ComponentProps<typeof MentionPrimitive.Root>) {
return (
<MentionPrimitive.Root
data-slot="mention"
className={cn(
"**:data-tag:rounded **:data-tag:bg-blue-200 **:data-tag:py-px **:data-tag:text-blue-950 dark:**:data-tag:bg-blue-800 dark:**:data-tag:text-blue-50",
className,
)}
{...props}
/>
);
}
function MentionLabel({
className,
...props
}: React.ComponentProps<typeof MentionPrimitive.Label>) {
return (
<MentionPrimitive.Label
data-slot="mention-label"
className={cn("px-0.5 py-1.5 font-semibold text-sm", className)}
{...props}
/>
);
}
function MentionInput({
className,
...props
}: React.ComponentProps<typeof MentionPrimitive.Input>) {
return (
<MentionPrimitive.Input
data-slot="mention-input"
className={cn(
"flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
);
}
function MentionContent({
className,
children,
...props
}: React.ComponentProps<typeof MentionPrimitive.Content>) {
return (
<MentionPrimitive.Portal>
<MentionPrimitive.Content
data-slot="mention-content"
className={cn(
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in",
className,
)}
{...props}
>
{children}
</MentionPrimitive.Content>
</MentionPrimitive.Portal>
);
}
function MentionItem({
className,
children,
...props
}: React.ComponentProps<typeof MentionPrimitive.Item>) {
return (
<MentionPrimitive.Item
data-slot="mention-item"
className={cn(
"relative flex w-full cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-50",
className,
)}
{...props}
>
{children}
</MentionPrimitive.Item>
);
}
export { Mention, MentionContent, MentionInput, MentionItem, MentionLabel };

View File

@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea <textarea
data-slot="textarea" data-slot="textarea"
className={cn( className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className className
)} )}
{...props} {...props}

View File

@@ -3,6 +3,7 @@ import * as z from 'zod';
export const CreateScheduleSchema = z.object({ export const CreateScheduleSchema = z.object({
name: z name: z
.string() .string()
.nonempty()
, startDate: z , startDate: z
.date() .date()
, endDate: z , endDate: z
@@ -21,4 +22,6 @@ export const CreateScheduleSchema = z.object({
.string() .string()
, dayList: z , dayList: z
.string() .string()
, participantList: z
.array(z.string())
}); });

View File

@@ -6,7 +6,7 @@ export const LoginSchema = z.object({
.string() .string()
.refine((val) => { .refine((val) => {
if (val.includes('@')) { if (val.includes('@')) {
return Validator.isEmail(val);; return Validator.isEmail(val);
} }
return true; return true;
}, { }, {

View File

@@ -3,7 +3,8 @@ import * as z from 'zod';
export const SignUpSchema = z.object({ export const SignUpSchema = z.object({
accountId: z accountId: z
.string() .string()
.min(5, "아이디는 5 자리 이상이어야 합니다.") .min(5, "아이디는 6-14자여야 합니다.")
.max(14, "아이디는 6-14자여야합니다.")
.refine((val) => { .refine((val) => {
return /^[a-zA-z-_.]*$/.test(val); return /^[a-zA-z-_.]*$/.test(val);
}, { }, {
@@ -14,12 +15,13 @@ export const SignUpSchema = z.object({
.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, "너무 긴 이름은 사용하실 수 없습니다.")
, nickname: z , nickname: z
.string() .string()
.min(1, "닉네임을 입력해주십시오.") .min(1, "닉네임을 입력해주십시오.")

21
src/hooks/use-time.ts Normal file
View File

@@ -0,0 +1,21 @@
export function useTime() {
const getNowString = () => {
const now = new Date();
const hour = now.getHours();
const minute = now.getMinutes();
const ampm = hour < 12 ? '오전' : '오후';
return `${ampm} ${hour > 12 ? hour - 12 : hour}${minute.toString().padStart(2, '0')}`;
}
const timeStringToISOString = (time: string) => {
const timeArray = time.split(' ');
const hour = timeArray[0] === '오전' ? Number(timeArray[1]) : Number(timeArray[1]) + 12;
return `${hour.toString().padStart(2, '0')}:${timeArray[2]}:00`
}
return {
getNowString,
timeStringToISOString
}
}

View File

@@ -56,15 +56,15 @@ export const DatePickPopover = ({ ...props } : DaetPickPopoverProps) => {
return ( return (
<div <div
className="w-full h-full flex flex-row justify-around items-center" className="w-full h-full flex flex-row justify-between gap-3 items-center"
> >
<Popover <Popover
open={startOpen} open={startOpen}
onOpenChange={setStartOpen} onOpenChange={setStartOpen}
> >
<PopoverTrigger> <PopoverTrigger asChild>
<Button <Button
className="border border-indigo-100 bg-white hover:bg-indigo-100 text-black" className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
disabled={disabled} disabled={disabled}
> >
{ {
@@ -91,14 +91,14 @@ export const DatePickPopover = ({ ...props } : DaetPickPopoverProps) => {
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<span> - </span> <div className="flex-2 h-px bg-indigo-300" />
<Popover <Popover
open={endOpen} open={endOpen}
onOpenChange={setEndOpen} onOpenChange={setEndOpen}
> >
<PopoverTrigger> <PopoverTrigger asChild>
<Button <Button
className="border border-indigo-100 bg-white hover:bg-indigo-100 text-black" className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
disabled={disabled} disabled={disabled}
> >
{ {

View File

@@ -0,0 +1,101 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import * as Mention from '@diceui/mention';
import { AtSign, PlusIcon, SearchIcon } from "lucide-react";
import { useRef, useState } from "react";
const dummyUser = [
{
accountId: "dummy1",
name: "더미1"
},
{
accountId: "test2",
name: "테스트2"
},
{
accountId: "dummy3",
name: "테스트3"
},
{
accountId: "test4",
name: "더미4"
}
]
export const ParticipantPopover = () => {
const [open, setOpen] = useState(false);
const [filterString, setFilterString] = useState('');
return (
<div className="w-full h-10 flex flex-row justify-start items-center">
<Popover
open={open}
onOpenChange={setOpen}
>
<PopoverTrigger asChild>
<Button
className={cn(
!open
? "bg-white text-indigo-300 hover:bg-indigo-300 hover:text-white"
: "bg-indigo-300 text-white hover:bg-indigo-300!"
)}
type="button"
>
<PlusIcon
/>
<span>
</span>
</Button>
</PopoverTrigger>
<PopoverContent
className="py-0 px-0 w-60 h-80 flex flex-col justify-start items-start"
>
<div
className="flex flex-row items-center px-3 h-10 w-full border-b border-b-indigo-100"
>
<SearchIcon size={16} className="stroke-indigo-300" />
<Input
placeholder="검색"
value={filterString}
className="shadow-none placeholder-indigo-300! focus-visible:placeholder-indigo-400! flex-1 focus-visible:ring-0 focus-visible:border-0 border-0"
/>
</div>
<ScrollArea
className="w-full h-60 flex flex-col"
>
</ScrollArea>
<div
className="border-t border-t-indigo-300 w-full h-10 flex flex-row cursor-default"
>
<div
className={cn(
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
"bg-white text-indigo-300",
"hover:bg-indigo-300 hover:text-white"
)}
>
</div>
<div
className={cn(
"flex-5 h-full flex flex-row justify-center items-center transition-all duration-150",
"bg-white text-red-400",
"hover:bg-red-400 hover:text-white"
)}
>
</div>
</div>
</PopoverContent>
</Popover>
</div>
)
}

View File

@@ -3,7 +3,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { usePalette } from '@/hooks/use-palette'; import { usePalette } from '@/hooks/use-palette';
import { ColorPalette, type ColorPaletteType } from '@/const/ColorPalette'; import { type ColorPaletteType } from '@/const/ColorPalette';
import { ColorPickPopover } from './ColorPickPopover'; import { ColorPickPopover } from './ColorPickPopover';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
@@ -20,6 +20,8 @@ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { DatePickPopover } from './DatePickPopover'; import { DatePickPopover } from './DatePickPopover';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { TimePickPopover } from './TimePickPopover'; import { TimePickPopover } from './TimePickPopover';
import { useTime } from '@/hooks/use-time';
import { ScheduleCreateContent } from './content/ScheduleCreateContent';
interface ScheduleSheetProps { interface ScheduleSheetProps {
date: Date | undefined; date: Date | undefined;
@@ -29,85 +31,8 @@ interface ScheduleSheetProps {
} }
export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: ScheduleSheetProps) => { export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: ScheduleSheetProps) => {
const {
getPaletteByKey
} = usePalette();
const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list'); const [mode, setMode] = useState<'list' | 'create' | 'detail' | 'update'>('list');
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
const dayLabelList = useRecord(ScheduleDay).keys.map((key) => {
return {
day: Number(key),
label: ScheduleDay[Number(key)]
} as { day: number, label: string };
})
const createScheduleForm = useForm<z.infer<typeof CreateScheduleSchema>>({
resolver: zodResolver(CreateScheduleSchema),
defaultValues: {
name: "",
startDate: date || new Date(),
endDate: date || new Date(),
content: "",
startTime: "",
endTime: "",
type: "once",
status: "yet",
style: getPaletteByKey('Black').style,
dayList: ""
}
});
useEffect(() => {
console.log(date);
if (open && date) {
createScheduleForm.setValue('startDate', date);
createScheduleForm.setValue('endDate', date);
return;
}
setTimeout(() => {
setMode('list');
createScheduleForm.clearErrors();
createScheduleForm.reset();
}, 150);
}, [open]);
const {
name,
startDate,
endDate,
content,
startTime,
endTime,
type,
status,
style,
dayList
} = createScheduleForm.watch();
const selectColor = (color: ColorPaletteType) => {
createScheduleForm.setValue('style', color.style);
setColorPopoverOpen(false);
}
const selectType = (type: ScheduleType) => {
createScheduleForm.setValue('type', type);
}
const selectDayList = (newValues: string[]) => {
const sortedValues = newValues.sort();
const newDayList = sortedValues.join('');
createScheduleForm.setValue('dayList', newDayList);
}
const selectDate = (type: 'startDate' | 'endDate', date: Date | undefined) => {
if (!date) return;
createScheduleForm.setValue(type, date);
}
const selectTime = (type: 'startTime' | 'endTime', time: string) => {
createScheduleForm.setValue(type, time);
}
const ListContent = () => { const ListContent = () => {
return ( return (
@@ -117,143 +42,6 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
) )
} }
const CreateContent = () => {
const OnceContent = () => {
return (
<div
className="w-full h-10"
>
<DatePickPopover
disabled={false}
mode={'range'}
popoverAlign={popoverAlign}
startDate={startDate}
endDate={endDate}
setStartDate={(date: Date | undefined) => selectDate('startDate', date)}
setEndDate={(date: Date | undefined) => selectDate('endDate', date)}
/>
</div>
)
}
const DailyContent = () => {
return null;
}
const DefaultContent = () => {
return (
(
<ToggleGroup
type="multiple"
className="w-full h-10 flex flex-row justify-center items-center"
onValueChange={selectDayList}
>
{
dayLabelList.map((day) => (
<ToggleGroupItem
value={`${day.day}`}
className="border rounded-none not-last:border-r-0"
>
{day.label}
</ToggleGroupItem>
))
}
</ToggleGroup>
)
)
}
const renderContent = () => {
switch (type) {
case 'Once':
return <OnceContent />;
case 'Daily':
return <DailyContent />;
default:
return <DefaultContent />;
}
}
return (
<div className="w-full h-full flex flex-col justify-start items-start gap-4">
<div className="w-full flex flex-row justify-center items-center gap-5">
<Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
<div className="w-6 h-6 flex justify-center items-center">
<PopoverTrigger asChild>
<div
className={cn(
'rounded-full w-5 h-5 border-2 border-gray-300',
)}
style={{
backgroundColor: `${style}`,
}}
/>
</PopoverTrigger>
</div>
<ColorPickPopover
setColor={selectColor}
/>
</Popover>
<Controller
name="name"
control={createScheduleForm.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
{...field}
id="form-create-schedule-name"
placeholder="제목"
className="font-bold border-t-0 border-r-0 border-l-0 p-0 border-b-2 rounded-none shadow-none border-indigo-300 focus-visible:ring-0 focus-visible:border-b-indigo-500"
style={{
fontSize: '20px'
}}
tabIndex={1}
arai-invalid={fieldState.invalid}
/>
<FieldError errors={[fieldState.error]} />
</Field>
)}
/>
</div>
<Popover>
<PopoverTrigger asChild>
<div className="hover:bg-gray-100 cursor-default w-full h-10 border flex justify-center items-center rounded-sm">
{ScheduleTypeLabel[type as keyof typeof ScheduleTypeLabel]}
</div>
</PopoverTrigger>
<TypePickPopover
setType={selectType}
popoverSide={popoverSide}
/>
</Popover>
<div
className="w-full h-10"
>
{renderContent()}
</div>
<div className="w-full h-10">
<TimePickPopover
mode='range'
popoverAlign={popoverAlign}
disabled={false}
startTime=''
setStartTime={(time: string | undefined) => {}}
endTime=''
setEndTime={(time: string | undefined) => {}}
/>
</div>
<div
className="flex flex-row self-end justify-self-end items-center justify-center cursor-default"
onClick={() => setMode('list')}
>
<X />
<span></span>
</div>
</div>
)
}
const DetailContent = () => { const DetailContent = () => {
return ( return (
<div> <div>
@@ -275,7 +63,13 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
case 'list': case 'list':
return <ListContent /> return <ListContent />
case 'create': case 'create':
return <CreateContent /> return <ScheduleCreateContent
setMode={setMode}
date={date}
popoverAlign={popoverAlign}
popoverSide={popoverSide}
open={open}
/>
case 'detail': case 'detail':
return <DetailContent /> return <DetailContent />
case 'update': case 'update':
@@ -288,11 +82,10 @@ export const SchedulePopover = ({ date, open, popoverSide, popoverAlign }: Sched
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]"
align={popoverAlign} side={popoverSide} align={popoverAlign} side={popoverSide}
> >
<div>{date && format(date, "yyyy년 MM월 dd일")}</div>
<ScrollArea <ScrollArea
className={ className={
cn( cn(
"min-h-[125px] h-[calc(100vh/2.5)] p-2.5 w-full flex flex-col", "min-h-[125px] h-[calc(100vh/2.3)] w-full flex flex-col",
) )
} }
> >

View File

@@ -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 { useState } from "react";
interface BaseProps { interface BaseProps {
disabled: boolean; disabled: boolean;
@@ -33,20 +34,141 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
) )
} }
// const { startTime, setStartTime, endTime, setEndTime } = props; const [startOpen, setStartOpen] = useState(false);
const [endOpen, setEndOpen] = useState(false);
const { startTime, setStartTime, endTime, setEndTime } = props;
const onStartOpenChange = (open: boolean) => {
if (open) {
setStartOpen(true);
if (startTime) {
const startTimeArray = startTime.split(' ');
setTimeout(() => {
const startAmPmList = document.querySelectorAll('#startAmPm button');
const targetAmPm = Array.from(startAmPmList).find(button => {
return button.textContent.trim() === startTimeArray[0];
});
const startHourList = document.querySelectorAll('#startHour button');
const targetHour = Array.from(startHourList).find(button => {
return button.textContent.trim() === startTimeArray[1].slice(0, 2);
});
const startMinuteList = document.querySelectorAll('#startMinute button');
const targetMinute = Array.from(startMinuteList).find(button => {
return button.textContent.trim() === startTimeArray[2].slice(0, 2);
});
[targetAmPm, targetHour, targetMinute].forEach(element => {
(element as HTMLButtonElement).click();
});
}, 150);
}
} else {
setStartOpen(false);
}
}
const applyStartTime = () => {
const startOnStateElements = document.querySelectorAll('[id^="start"] [data-state="on"]');
if (startOnStateElements.length < 3) {
return;
}
const {
startAmPm,
startHour,
startMinute
} = {
startAmPm: document.querySelector('#startAmPm [data-state="on"]')?.textContent.trim(),
startHour: document.querySelector('#startHour [data-state="on"]')?.textContent.trim(),
startMinute: document.querySelector('#startMinute [data-state="on"]')?.textContent.trim()
};
const startTime = `${startAmPm} ${startHour}${startMinute}`;
setStartTime(startTime);
}
const cancelStartTime = () => {
setStartOpen(false);
setTimeout(() => {
const startOnElements = document.querySelectorAll('[id^="start"] [data-state="on"]');
startOnElements.forEach(element => {
(element as HTMLButtonElement).click();
});
}, 150);
}
const onEndOpenChange = (open: boolean) => {
if (open) {
setEndOpen(true);
if (endTime) {
const endTimeArray = endTime.split(' ');
setTimeout(() => {
const endAmPmList = document.querySelectorAll('#endAmPm button');
const targetAmPm = Array.from(endAmPmList).find(button => {
return button.textContent.trim() === endTimeArray[0];
});
const endHourList = document.querySelectorAll('#endHour button');
const targetHour = Array.from(endHourList).find(button => {
return button.textContent.trim() === endTimeArray[1].slice(0, 2);
});
const endMinuteList = document.querySelectorAll('#endMinute button');
const targetMinute = Array.from(endMinuteList).find(button => {
return button.textContent.trim() === endTimeArray[2].slice(0, 2);
});
[targetAmPm, targetHour, targetMinute].forEach(element => {
(element as HTMLButtonElement).click();
});
}, 150);
}
} else {
setEndOpen(false);
}
}
const applyEndTime = () => {
const endOnStateElements = document.querySelectorAll('[id^="end"] [data-state="on"]');
if (endOnStateElements.length < 3) {
return;
}
const {
endAmPm,
endHour,
endMinute
} = {
endAmPm: document.querySelector('#endAmPm [data-state="on"]')?.textContent.trim(),
endHour: document.querySelector('#endHour [data-state="on"]')?.textContent.trim(),
endMinute: document.querySelector('#endMinute [data-state="on"]')?.textContent.trim()
};
const endTime = `${endAmPm} ${endHour}${endMinute}`;
setEndTime(endTime);
}
const cancelEndTime = () => {
setEndOpen(false);
setTimeout(() => {
const endOnElements = document.querySelectorAll('[id^="end"] [data-state="on"]');
endOnElements.forEach(element => {
(element as HTMLButtonElement).click();
});
}, 150);
}
return ( return (
<div <div
className="w-full h-full flex flex-row justify-around items-center" className="w-full h-full flex flex-row justify-between gap-3 items-center"
> >
<Popover <Popover
open={startOpen}
onOpenChange={onStartOpenChange}
> >
<PopoverTrigger> <PopoverTrigger asChild>
<Button <Button
className="border border-indigo-100 bg-white hover:bg-indigo-100 text-black" className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
disabled={disabled} disabled={disabled}
> >
{
!startTime
? '시작 시간 설정'
: startTime
}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
@@ -55,6 +177,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
side={popoverAlign === 'start' ? 'bottom' : 'top'} side={popoverAlign === 'start' ? 'bottom' : 'top'}
> >
<ToggleGroup <ToggleGroup
id="startAmPm"
type="single" type="single"
className="w-15 flex flex-col justify-start items-center" className="w-15 flex flex-col justify-start items-center"
> >
@@ -76,6 +199,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
> >
<ToggleGroup <ToggleGroup
type="single" type="single"
id="startHour"
className="w-15 border-r rounded-none border-l flex flex-col justify-start items-center" className="w-15 border-r rounded-none border-l flex flex-col justify-start items-center"
> >
{ {
@@ -83,6 +207,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
<ToggleGroupItem <ToggleGroupItem
className="w-full h-7 rounded-none!" className="w-full h-7 rounded-none!"
value={time.toString().padStart(2, '0')} value={time.toString().padStart(2, '0')}
key={`startHour${time.toString().padStart(2, '0')}`}
> >
{time.toString().padStart(2, '0')} {time.toString().padStart(2, '0')}
</ToggleGroupItem> </ToggleGroupItem>
@@ -95,6 +220,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
> >
<ToggleGroup <ToggleGroup
type="single" type="single"
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 flex flex-col justify-start items-center"
> >
{ {
@@ -102,6 +228,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
<ToggleGroupItem <ToggleGroupItem
value={idx.toString().padStart(2, '0')} value={idx.toString().padStart(2, '0')}
className="w-full h-7 rounded-none!" className="w-full h-7 rounded-none!"
key={`startMinute${idx.toString().padStart(2, '0')}`}
> >
{idx.toString().padStart(2, '0')} {idx.toString().padStart(2, '0')}
</ToggleGroupItem> </ToggleGroupItem>
@@ -112,26 +239,34 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
<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="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
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="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
onClick={cancelStartTime}
> >
</div> </div>
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<span> - </span> <div className="flex-2 h-px bg-indigo-300" />
<Popover <Popover
open={endOpen}
onOpenChange={onEndOpenChange}
> >
<PopoverTrigger> <PopoverTrigger asChild>
<Button <Button
className="border border-indigo-100 bg-white hover:bg-indigo-100 text-black" className="flex-9 h-full border border-indigo-100 bg-white hover:bg-indigo-100 text-black"
disabled={disabled} disabled={disabled}
> >
{
!endTime
? '종료 시간 설정'
: `${endTime}`
}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
@@ -140,6 +275,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
side={popoverAlign === 'start' ? 'bottom' : 'top'} side={popoverAlign === 'start' ? 'bottom' : 'top'}
> >
<ToggleGroup <ToggleGroup
id="endAmPm"
type="single" type="single"
className="w-15 flex flex-col justify-start items-center" className="w-15 flex flex-col justify-start items-center"
> >
@@ -160,6 +296,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
className="w-15 h-full" className="w-15 h-full"
> >
<ToggleGroup <ToggleGroup
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 rounded-none border-l flex flex-col justify-start items-center"
> >
@@ -167,6 +304,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
[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="w-full h-7 rounded-none!"
key={`endHour${time.toString().padStart(2, '0')}`}
value={time.toString().padStart(2, '0')} value={time.toString().padStart(2, '0')}
> >
{time.toString().padStart(2, '0')} {time.toString().padStart(2, '0')}
@@ -179,6 +317,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
className="w-15 h-full" className="w-15 h-full"
> >
<ToggleGroup <ToggleGroup
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 flex flex-col justify-start items-center"
> >
@@ -186,6 +325,7 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
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')}`}
className="w-full h-7 rounded-none!" className="w-full h-7 rounded-none!"
> >
{idx.toString().padStart(2, '0')} {idx.toString().padStart(2, '0')}
@@ -197,11 +337,13 @@ export const TimePickPopover = ({ ...props }: TimePickPopoverProps) => {
<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="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
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="cursor-default text-sm flex justify-center items-center w-full h-7 rounded-none rounded-tr-md! hover:bg-gray-100"
onClick={cancelEndTime}
> >
</div> </div>

View File

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

View File

@@ -0,0 +1,246 @@
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 { Textarea } from '@/components/ui/textarea';
import { ParticipantPopover } from '../ParticipantPopover';
export const ScheduleCreateContent = ({ date, open, setMode, popoverSide, popoverAlign }: ScheduleCreateContentProps) => {
const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
const { getPaletteByKey } = usePalette();
const { getNowString } = useTime();
const dayLabelList = useRecord(ScheduleDay).keys.map((key) => {
return {
day: Number(key),
label: ScheduleDay[Number(key)]
} as { day: number, label: string };
})
const createScheduleForm = useForm<z.infer<typeof CreateScheduleSchema>>({
resolver: zodResolver(CreateScheduleSchema),
defaultValues: {
name: "",
startDate: date || new Date(),
endDate: date || new Date(),
content: "",
startTime: getNowString(),
endTime: getNowString(),
type: "once",
status: "yet",
style: getPaletteByKey('Black').style,
dayList: "",
participantList: []
}
});
const {
name,
startDate,
endDate,
content,
startTime,
endTime,
type,
status,
style,
dayList
} = createScheduleForm.watch();
const selectColor = (color: ColorPaletteType) => {
createScheduleForm.setValue('style', color.style);
setColorPopoverOpen(false);
}
const selectType = (type: ScheduleType) => {
createScheduleForm.setValue('type', type);
}
const selectDayList = (newValues: string[]) => {
const sortedValues = newValues.sort();
const newDayList = sortedValues.join('');
createScheduleForm.setValue('dayList', newDayList);
}
const selectDate = (type: 'startDate' | 'endDate', date: Date | undefined) => {
if (!date) return;
createScheduleForm.setValue(type, date);
}
const selectTime = (type: 'startTime' | 'endTime', time: string) => {
createScheduleForm.setValue(type, time);
}
const selectParticipant = (participantList: string[]) => {
createScheduleForm.setValue('participantList', participantList);
}
const OnceContent = () => {
return (
<div
className="w-full h-10"
>
<DatePickPopover
disabled={false}
mode={'range'}
popoverAlign={popoverAlign}
startDate={startDate}
endDate={endDate}
setStartDate={(date: Date | undefined) => selectDate('startDate', date)}
setEndDate={(date: Date | undefined) => selectDate('endDate', date)}
/>
</div>
)
}
const DailyContent = () => {
return null;
}
const DefaultContent = () => {
return (
(
<ToggleGroup
type="multiple"
className="w-full h-10 flex flex-row justify-center items-center"
onValueChange={selectDayList}
>
{
dayLabelList.map((day) => (
<ToggleGroupItem
value={`${day.day}`}
className="border w-[calc(100%/7)] h-10 rounded-none not-last:border-r-0"
>
{day.label}
</ToggleGroupItem>
))
}
</ToggleGroup>
)
)
}
const renderContent = () => {
switch (type) {
case 'once':
return <OnceContent />;
case 'daily':
return <DailyContent />;
default:
return <DefaultContent />;
}
}
return (
<div className="w-full h-full flex flex-col justify-start items-start gap-4">
<div className="w-full flex flex-row justify-between items-center gap-4">
<div
onClick={() => setMode('list')}
>
<ArrowLeft />
</div>
<Controller
name="name"
control={createScheduleForm.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
{...field}
id="form-create-schedule-name"
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"
style={{
fontSize: '20px'
}}
tabIndex={1}
arai-invalid={fieldState.invalid}
/>
<FieldError errors={[fieldState.error]} />
</Field>
)}
/>
<Popover open={colorPopoverOpen} onOpenChange={setColorPopoverOpen}>
<div className="w-6 h-6 flex justify-center items-center">
<PopoverTrigger asChild>
<div
className={cn(
'rounded-full w-5 h-5 border-2 border-gray-300',
)}
style={{
backgroundColor: `${style}`,
}}
/>
</PopoverTrigger>
</div>
<ColorPickPopover
setColor={selectColor}
/>
</Popover>
</div>
<Popover>
<PopoverTrigger asChild>
<div className="hover:bg-indigo-100 cursor-default w-full h-10 border border-indigo-100 flex justify-center items-center rounded-sm">
{ScheduleTypeLabel[type as keyof typeof ScheduleTypeLabel]}
</div>
</PopoverTrigger>
<TypePickPopover
setType={selectType}
popoverSide={popoverSide}
/>
</Popover>
<div
className="w-full h-10"
>
{renderContent()}
</div>
<div className="w-full h-10">
<TimePickPopover
mode='range'
popoverAlign={popoverAlign}
disabled={false}
startTime={startTime}
setStartTime={(time: string | undefined) => selectTime('startTime', time ?? '')}
endTime={endTime}
setEndTime={(time: string | undefined) => selectTime('endTime', time ?? '')}
/>
</div>
<Controller
name="content"
control={createScheduleForm.control}
render={({ field }) => (
<Textarea
{...field}
rows={2}
placeholder="일정 상세 사항"
className="placeholder-gray-300! focus-visible:placeholder-gray-400! border-indigo-100 focus-visible:border-indigo-300 resize-none focus-visible:ring-0"
style={{
'scrollbarWidth': 'none'
}}
/>
)}
/>
<ParticipantPopover />
</div>
)
}