Skip to content

Commit

Permalink
Merge pull request #21 from depromeet/SCRUM-26-사용자로서-할-일을-등록할-수-있는-서비…
Browse files Browse the repository at this point in the history
…스가-필요합니다

SCRUM-26 사용자로서 할 일을 등록할 수 있는 서비스가 필요합니다.
  • Loading branch information
prgmr99 authored Feb 26, 2025
2 parents dc39592 + a6a51ae commit daf48f2
Show file tree
Hide file tree
Showing 20 changed files with 1,129 additions and 62 deletions.
434 changes: 382 additions & 52 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,22 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-slot": "^1.1.2",
"@tanstack/react-query": "^5.65.1",
"@use-funnel/browser": "^0.0.11",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.474.0",
"next": "15.1.5",
"react": "^19.0.0",
"react-day-picker": "^8.10.1",
"react-dom": "^19.0.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zustand": "^5.0.3"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions public/icons/ArrowLeft.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/icons/CalendarLeft.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/icons/CalendarRight.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/icons/x-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client';

import { useState } from 'react';
import { ChevronDown } from 'lucide-react';
import { format } from 'date-fns';
import { ko } from 'date-fns/locale';
import DatePicker from '../../../../components/datePicker/DatePicker';
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '../../../../components/ui/drawer';
import { Button } from '@/components/ui/button';

interface DateSelectedComponentProps {
selectedDate: Date | undefined;
handleDateChange: (date: Date) => void;
}

const DateSelectedComponent = ({
selectedDate,
handleDateChange,
}: DateSelectedComponentProps) => {
const [isOpen, setIsOpen] = useState(false);
const [temporaryDate, setTemporaryDate] = useState<Date | undefined>(
selectedDate,
);

const handleToggle = () => {
setIsOpen((prev) => !prev);
};

const handleTemporaryDate = (date: Date) => {
setTemporaryDate(date);
};

return (
<Drawer open={isOpen} onDrag={() => setIsOpen(false)}>
<DrawerTrigger>
<div className="relative mt-2 w-full">
<button
className="relative flex w-full flex-col items-start border-b border-gray-300 pb-2"
onClick={handleToggle}
>
<span
className={`absolute left-0 text-gray-500 transition-all duration-200 ${
selectedDate !== undefined
? 'text-neutral b3 top-[-8px]'
: 't3 top-1'
}`}
>
마감일 선택
</span>
<div className="flex w-full items-center justify-between pt-4">
<span className="t3 text-base font-semibold">
{selectedDate
? format(selectedDate, 'M월 d일 (E)', { locale: ko })
: ''}
</span>
<ChevronDown
className={`h-4 w-4 text-gray-500 transition-transform duration-200 ${
isOpen ? 'rotate-180' : ''
}`}
/>
</div>
</button>
<DrawerContent className="w-auto border-0 bg-component-gray-secondary px-5 pb-[33px] pt-2">
<DrawerHeader className="px-0 pb-10 pt-6">
<DrawerTitle className="t3 text-left">
마감일을 선택해주세요
</DrawerTitle>
</DrawerHeader>
<DatePicker
selectedDate={temporaryDate}
handleDateChange={handleTemporaryDate}
/>
<DrawerFooter className="px-0">
<DrawerClose>
<Button
variant="primary"
className="mt-4 flex w-full items-center justify-center"
onClick={() => {
if (temporaryDate) {
handleDateChange(temporaryDate);
setIsOpen(false);
}
}}
>
확인
</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</div>
</DrawerTrigger>
</Drawer>
);
};

export default DateSelectedComponent;
74 changes: 74 additions & 0 deletions src/app/create/_components/taskInput/TaskInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import ClearableInput from '@/components/clearableInput/ClearableInput';
import DateSelectedComponent from '@/app/create/_components/DateSelectedComponent/DateSelectedComponent';
import { useEffect, useRef, useState } from 'react';
import { Button } from '@/components/ui/button';

const WAITING_TIME = 300;
const MAX_TASK_LENGTH = 15;

interface TaskInputProps {
onClick: (task: string) => void;
}

const TaskInput = ({ onClick }: TaskInputProps) => {
const [task, setTask] = useState<string>('');
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);

const inputRef = useRef<HTMLInputElement>(null);

const isInvalid = task.length > MAX_TASK_LENGTH || task.length === 0;

const handleTaskChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setTask(event.target.value);
};

const handleDateChange = (date: Date) => {
setSelectedDate(date);
};

useEffect(() => {
if (inputRef.current) {
setTimeout(() => {
inputRef.current?.focus();
}, WAITING_TIME);
}
}, []);

return (
<div className="w-full">
<div className="pb-10 pt-4">
<span className="t2">어떤 일의 마감이 급하신가요?</span>
</div>
<div className="flex flex-col gap-6">
<div>
<ClearableInput
ref={inputRef}
value={task}
onChange={handleTaskChange}
/>
{task.length > MAX_TASK_LENGTH && (
<p className="mt-2 text-sm text-red-500">
최대 16자 이내로 입력할 수 있어요.
</p>
)}
</div>

<DateSelectedComponent
selectedDate={selectedDate}
handleDateChange={handleDateChange}
/>
</div>

<Button
variant="primary"
className="mt-6"
onClick={() => onClick(task)}
disabled={isInvalid}
>
다음
</Button>
</div>
);
};

export default TaskInput;
54 changes: 54 additions & 0 deletions src/app/create/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
export type TaskInputType = {
task?: string;
deadlineDate?: Date;
deadlineTime?: string;
smallAction?: string;
estimatedHour?: string;
estimatedMinute?: string;
taskType?: string;
moodType?: string;
};

export type SmallActionInputType = {
task: string;
deadlineDate: Date;
deadlineTime: string;
smallAction?: string;
estimatedHour?: string;
estimatedMinute?: string;
taskType?: string;
moodType?: string;
};

export type EstimatedTimeInputType = {
task: string;
deadlineDate: Date;
deadlineTime: string;
smallAction: string;
estimatedHour?: string;
estimatedMinute?: string;
taskType?: string;
moodType?: string;
};

export type BufferTimeType = {
task: string;
deadlineDate: Date;
deadlineTime: string;
smallAction: string;
estimatedHour: string;
estimatedMinute: string;
taskType?: string;
moodType?: string;
};

export type TaskTypeInputType = {
task: string;
deadlineDate: Date;
deadlineTime: string;
smallAction: string;
estimatedHour: string;
estimatedMinute: string;
taskType: string;
moodType: string;
};
80 changes: 80 additions & 0 deletions src/app/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use client';

import { createFunnelSteps, useFunnel } from '@use-funnel/browser';
import {
BufferTimeType,
EstimatedTimeInputType,
SmallActionInputType,
TaskInputType,
TaskTypeInputType,
} from './context';
import useMount from '@/hooks/useMount';
import TaskInput from './_components/taskInput/TaskInput';
import BackHeader from '@/components/backHeader/BackHeader';

type FormState = {
task?: string;
deadlineDate?: Date;
deadlineTime?: string;
smallAction?: string;
estimatedHour?: string;
estimatedMinute?: string;
taskType?: string;
moodType?: string;
};

const steps = createFunnelSteps<FormState>()
.extends('taskForm')
.extends('smallActionInput', {
requiredKeys: ['task', 'deadlineDate', 'deadlineTime'],
})
.extends('estimatedTimeInput', { requiredKeys: 'smallAction' })
.extends('bufferTime', { requiredKeys: ['estimatedHour', 'estimatedMinute'] })
.extends('taskTypeInput', { requiredKeys: ['taskType', 'moodType'] })
.build();

const TaskCreate = () => {
const funnel = useFunnel<{
taskForm: TaskInputType;
smallActionInput: SmallActionInputType;
estimatedTimeInput: EstimatedTimeInputType;
bufferTime: BufferTimeType;
taskTypeInput: TaskTypeInputType;
}>({
id: 'task-create-main',
steps: steps,
initial: {
step: 'taskForm',
context: {},
},
});

const { isMounted } = useMount();

if (!isMounted) return null;

return (
<div className="background-primary flex min-h-screen w-full flex-col items-center justify-start overflow-y-auto px-5">
<BackHeader onClick={() => funnel.history.back()} />
<funnel.Render
taskForm={({ history }) => (
<TaskInput
onClick={(task: string) =>
history.push('smallActionInput', {
task: task,
deadlineDate: new Date(),
deadlineTime: '',
})
}
/>
)}
smallActionInput={() => <div>작은행동 입력</div>}
estimatedTimeInput={() => <div>예상시간 입력</div>}
bufferTime={() => <div>버퍼시간 입력</div>}
taskTypeInput={() => <div>할 일 종류 입력</div>}
/>
</div>
);
};

export default TaskCreate;
18 changes: 17 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ body {

/* 버튼 스타일 */
.button-primary {
@apply bg-main-300 rounded-md px-4 py-2 text-white;
@apply rounded-md bg-main-300 px-4 py-2 text-white;
}

.button-secondary {
Expand Down Expand Up @@ -204,6 +204,10 @@ body {
@apply text-text-primary;
}

.text-red {
@apply text-text-red;
}

.text-secondary {
@apply text-text-secondary;
}
Expand All @@ -220,6 +224,18 @@ body {
@apply text-text-strong;
}

.text-alternative {
@apply text-text-alternative;
}

.text-neutral {
@apply text-text-neutral;
}

.text-normal {
@apply text-text-normal;
}

.bg-point-gradient {
background: var(--point-gradient);
}
Expand Down
Loading

0 comments on commit daf48f2

Please sign in to comment.