diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a2eadf..686b150 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,6 @@ "editor.formatOnSave": true, "eslint.format.enable": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" } } diff --git a/middleware.ts b/middleware.ts index cf149c2..358761c 100644 --- a/middleware.ts +++ b/middleware.ts @@ -38,15 +38,15 @@ export function middleware(request: NextRequest): NextResponse { return NextResponse.next(); } -export const config = { - matcher: [ - MYDASHBOARD_PATH, - `${MYDASHBOARD_PATH}/:path*`, - '/mypage', - '/mypage/:path*', - DASHBOARD_PATH, - `${DASHBOARD_PATH}/:path*`, - LOGIN_PATH, - '/signup', - ], -}; +// export const config = { +// matcher: [ +// MYDASHBOARD_PATH, +// `${MYDASHBOARD_PATH}/:path*`, +// '/mypage', +// '/mypage/:path*', +// DASHBOARD_PATH, +// `${DASHBOARD_PATH}/:path*`, +// LOGIN_PATH, +// '/signup', +// ], +// }; diff --git a/src/components/dashboard/add-column-button.tsx b/src/components/dashboard/add-column-button.tsx index 0c0124d..8040429 100644 --- a/src/components/dashboard/add-column-button.tsx +++ b/src/components/dashboard/add-column-button.tsx @@ -1,5 +1,5 @@ import Image from 'next/image'; -import type { AddColumnButtonProps } from './type'; +import type { AddColumnButtonProps } from '@/components/dashboard/type'; export default function AddColumnButton({ onClick }: AddColumnButtonProps) { return ( diff --git a/src/components/dashboard/add-task-button.tsx b/src/components/dashboard/add-task-button.tsx index 0444db6..8e62fe1 100644 --- a/src/components/dashboard/add-task-button.tsx +++ b/src/components/dashboard/add-task-button.tsx @@ -1,5 +1,5 @@ import Image from 'next/image'; -import type { AddTaskButtonProps } from './type'; +import type { AddTaskButtonProps } from '@/components/dashboard/type'; export default function AddTaskButton({ onClick }: AddTaskButtonProps) { return ( diff --git a/src/components/dashboard/column-header.tsx b/src/components/dashboard/column-header.tsx index a40504e..30fba0e 100644 --- a/src/components/dashboard/column-header.tsx +++ b/src/components/dashboard/column-header.tsx @@ -1,5 +1,5 @@ import Image from 'next/image'; -import type { ColumnHeaderProps } from './type'; +import type { ColumnHeaderProps } from '@/components/dashboard/type'; export default function ColumnHeader({ column, diff --git a/src/components/dashboard/column-layout.tsx b/src/components/dashboard/column-layout.tsx index a544056..6626c29 100644 --- a/src/components/dashboard/column-layout.tsx +++ b/src/components/dashboard/column-layout.tsx @@ -1,6 +1,6 @@ -import AddColumnButton from './add-column-button'; -import DashboardColumn from './dashboard-column'; -import type { ColumnType, TaskType } from './type'; +import AddColumnButton from '@/components/dashboard/add-column-button'; +import DashboardColumn from '@/components/dashboard/dashboard-column'; +import type { ColumnType, TaskType } from '@/components/dashboard/type'; interface ColumnLayoutProps { columns: ColumnType[]; diff --git a/src/components/dashboard/column-task-card.tsx b/src/components/dashboard/column-task-card.tsx index 1c2b53b..89bcf2b 100644 --- a/src/components/dashboard/column-task-card.tsx +++ b/src/components/dashboard/column-task-card.tsx @@ -1,11 +1,13 @@ import Image from 'next/image'; +import type { TaskCardProps } from '@/components/dashboard/type'; import ChipProfile from '@/components/ui/chip/chip-profile'; import ChipTag from '@/components/ui/chip/chip-tag'; import { getProfileColor } from '@/utils/profile-color'; -import type { TaskCardProps } from './type'; -const formatDueDate = (dueDate: string) => { - if (!dueDate) {return '';} +const formatDueDate = (dueDate: string | undefined) => { + if (!dueDate) { + return ''; + } const date = new Date(dueDate); const year = date.getFullYear(); @@ -14,7 +16,7 @@ const formatDueDate = (dueDate: string) => { const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); - return `${year}-${month}-${day} ${hours}:${minutes}`; + return `${String(year)}-${month}-${day} ${hours}:${minutes}`; }; export default function ColumnTaskCard({ task, onEditTask }: TaskCardProps) { @@ -25,7 +27,7 @@ export default function ColumnTaskCard({ task, onEditTask }: TaskCardProps) { }; return ( -
@@ -45,20 +47,24 @@ export default function ColumnTaskCard({ task, onEditTask }: TaskCardProps) { {/* 본문 */}
-

+

{task.title}

{/* 태그들 */}
- {task.tags.map((tag) => - { return } - )} + {task.tags.map((tag) => { + return ( + + ); + })}
{/* 날짜와 담당자 */} @@ -75,7 +81,7 @@ export default function ColumnTaskCard({ task, onEditTask }: TaskCardProps) {
@@ -83,6 +89,6 @@ export default function ColumnTaskCard({ task, onEditTask }: TaskCardProps) {
-
+ ); } diff --git a/src/components/dashboard/dashboard-column.tsx b/src/components/dashboard/dashboard-column.tsx index 95488bf..bf6fb47 100644 --- a/src/components/dashboard/dashboard-column.tsx +++ b/src/components/dashboard/dashboard-column.tsx @@ -1,7 +1,7 @@ -import AddTaskButton from './add-task-button'; -import ColumnHeader from './column-header'; -import ColumnTaskCard from './column-task-card'; -import type { ColumnType, TaskType } from './type'; +import AddTaskButton from '@/components/dashboard/add-task-button'; +import ColumnHeader from '@/components/dashboard/column-header'; +import ColumnTaskCard from '@/components/dashboard/column-task-card'; +import type { ColumnType, TaskType } from '@/components/dashboard/type'; interface ColumnProps { column: ColumnType; @@ -32,13 +32,15 @@ export default function DashboardColumn({ {/* 할일 보드 - 스크롤 가능한 영역 */}
- {column.tasks.map((task) => - { return } - )} + {column.tasks.map((task) => { + return ( + + ); + })}
diff --git a/src/components/dashboard/create-column-form.tsx b/src/components/dashboard/modal/create-column-form.tsx similarity index 87% rename from src/components/dashboard/create-column-form.tsx rename to src/components/dashboard/modal/create-column-form.tsx index af9aded..833ab2b 100644 --- a/src/components/dashboard/create-column-form.tsx +++ b/src/components/dashboard/modal/create-column-form.tsx @@ -1,4 +1,5 @@ -import type { CreateColumnFormData } from './type'; +import type { ReactNode } from 'react'; +import type { CreateColumnFormData } from '@/components/dashboard/type'; interface CreateColumnFormProps { formData: CreateColumnFormData; @@ -10,7 +11,7 @@ export default function CreateColumnForm({ formData, setFormData, hasError = false, -}: CreateColumnFormProps): JSX.Element { +}: CreateColumnFormProps): ReactNode { return ( <> {/* 이름 */} diff --git a/src/components/dashboard/create-column-modal.tsx b/src/components/dashboard/modal/create-column-modal.tsx similarity index 68% rename from src/components/dashboard/create-column-modal.tsx rename to src/components/dashboard/modal/create-column-modal.tsx index ce8e6f8..c9d74bf 100644 --- a/src/components/dashboard/create-column-modal.tsx +++ b/src/components/dashboard/modal/create-column-modal.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; +import CreateColumnForm from '@/components/dashboard/modal/create-column-form'; +import type { CreateColumnFormData } from '@/components/dashboard/type'; +import ButtonModal from '@/components/ui/modal/modal-button'; import { useModalKeyHandler } from '@/hooks/useModal'; -import BaseModal from '../ui/base-modal'; -import CreateColumnForm from './create-column-form'; -import type { CreateColumnFormData } from './type'; interface CreateColumnModalProps { isOpen: boolean; @@ -37,30 +37,33 @@ export default function CreateColumnModal({ const isMaxColumnsReached = existingColumns.length >= maxColumns; const handleSubmit = () => { - if (!isDuplicate && !isMaxColumnsReached) { - onSubmit(formData); - handleClose(); + if (isDuplicate) { + return; } + if (isMaxColumnsReached) { + return; + } + onSubmit(formData); + handleClose(); }; const isSubmitDisabled = !formData.name.trim() || isDuplicate || isMaxColumnsReached; + const errorMessage = isMaxColumnsReached + ? `최대 ${String(maxColumns)}개까지만 생성할 수 있습니다.` + : isDuplicate + ? '중복된 컬럼 이름입니다.' + : undefined; return ( - @@ -69,6 +72,6 @@ export default function CreateColumnModal({ setFormData={setFormData} hasError={isDuplicate} /> - + ); } diff --git a/src/components/dashboard/create-task-form.tsx b/src/components/dashboard/modal/create-task-form.tsx similarity index 67% rename from src/components/dashboard/create-task-form.tsx rename to src/components/dashboard/modal/create-task-form.tsx index 9d283ba..fc8f1dc 100644 --- a/src/components/dashboard/create-task-form.tsx +++ b/src/components/dashboard/modal/create-task-form.tsx @@ -1,13 +1,15 @@ import Image from 'next/image'; -import type React from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useState } from 'react'; +import { + type CreateTaskFormData, + getRandomTagColor, +} from '@/components/dashboard/type'; import ChipProfile from '@/components/ui/chip/chip-profile'; import ChipTag from '@/components/ui/chip/chip-tag'; +import Dropdown from '@/components/ui/dropdown'; import { mockProfileColors } from '@/lib/dashboard-mock-data'; import type { UserType } from '@/lib/users/type'; import { getProfileColor } from '@/utils/profile-color'; -import type { CreateTaskFormData } from './type'; -import { getRandomTagColor } from './type'; interface CreateTaskFormProps { formData: CreateTaskFormData; @@ -26,7 +28,6 @@ export default function CreateTaskForm({ }: CreateTaskFormProps): React.ReactElement { const [currentTag, setCurrentTag] = useState(''); const [isAssigneeDropdownOpen, setIsAssigneeDropdownOpen] = useState(false); - const assigneeDropdownRef = useRef(null); const assigneeOptions = [ { @@ -45,49 +46,45 @@ export default function CreateTaskForm({ profileColor: mockProfileColors[2], }, ]; - + const chipProfileLabel = ( + assigneeOptions.find((opt) => opt.value === formData.assignee)?.label || '' + ).slice(0, 1); + const chipProfileColor = getProfileColor( + assigneeOptions.find((opt) => opt.value === formData.assignee) + ?.profileColor || '#10b981' + ); const handleAssigneeSelect = (assignee: string) => { setFormData((prev) => ({ ...prev, assignee })); setIsAssigneeDropdownOpen(false); }; - // 드롭다운 외부 클릭 시 닫기 - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - assigneeDropdownRef.current && - !assigneeDropdownRef.current.contains(event.target as Node) - ) { - setIsAssigneeDropdownOpen(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - const handleTagKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && currentTag.trim()) { - e.preventDefault(); - setFormData((prev) => ({ + if (e.key !== 'Enter') { + return; + } + if (!currentTag.trim()) { + return; + } + e.preventDefault(); + setFormData((prev) => { + return { ...prev, tags: [ ...prev.tags, { label: currentTag.trim(), color: getRandomTagColor() }, ], - })); - setCurrentTag(''); - } + }; + }); + setCurrentTag(''); }; const removeTag = (indexToRemove: number) => { - setFormData((prev) => ({ - ...prev, - tags: prev.tags.filter((_, index) => index !== indexToRemove), - })); + setFormData((prev) => { + return { + ...prev, + tags: prev.tags.filter((_, index) => index !== indexToRemove), + }; + }); }; const handleImageUpload = (e: React.ChangeEvent) => { @@ -116,95 +113,74 @@ export default function CreateTaskForm({ <> {/* 담당자 */}
- -
- - - {/* 드롭다운 옵션들 */} - {isAssigneeDropdownOpen && ( -
- {assigneeOptions.map((option, index) => { - return ( - - ); - })} -
- )} -
+ )} +
+
+ + {option.label} +
+ + ); + })} + + {/* 제목 */} @@ -283,6 +259,7 @@ export default function CreateTaskForm({ const input = document.querySelector( '#dueDate' ) as HTMLInputElement; + input.showPicker(); input.focus(); }} @@ -309,10 +286,10 @@ export default function CreateTaskForm({
- + ); } diff --git a/src/components/dashboard/edit-task-form.tsx b/src/components/dashboard/modal/edit-task-form.tsx similarity index 56% rename from src/components/dashboard/edit-task-form.tsx rename to src/components/dashboard/modal/edit-task-form.tsx index f0eb548..c82d8d1 100644 --- a/src/components/dashboard/edit-task-form.tsx +++ b/src/components/dashboard/modal/edit-task-form.tsx @@ -1,13 +1,16 @@ import Image from 'next/image'; -import { useEffect, useRef, useState } from 'react'; +import { useState } from 'react'; +import { + type EditTaskFormData, + getRandomTagColor, +} from '@/components/dashboard/type'; import ChipProfile from '@/components/ui/chip/chip-profile'; import ChipState from '@/components/ui/chip/chip-state'; import ChipTag from '@/components/ui/chip/chip-tag'; +import Dropdown from '@/components/ui/dropdown'; import { mockProfileColors } from '@/lib/dashboard-mock-data'; import type { UserType } from '@/lib/users/type'; import { getProfileColor } from '@/utils/profile-color'; -import { type EditTaskFormData , getRandomTagColor } from './type'; - interface EditTaskFormProps { formData: EditTaskFormData; @@ -27,13 +30,13 @@ export default function EditTaskForm({ const [currentTag, setCurrentTag] = useState(''); const [isStatusDropdownOpen, setIsStatusDropdownOpen] = useState(false); const [isAssigneeDropdownOpen, setIsAssigneeDropdownOpen] = useState(false); - const statusDropdownRef = useRef(null); - const assigneeDropdownRef = useRef(null); - const statusOptions = columns.map((column) => { return { - value: column.title, - label: column.title, - } }); + const statusOptions = columns.map((column) => { + return { + value: column.title, + label: column.title, + }; + }); const assigneeOptions = [ { @@ -63,71 +66,57 @@ export default function EditTaskForm({ setIsAssigneeDropdownOpen(false); }; - // 드롭다운 외부 클릭 시 닫기 - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - statusDropdownRef.current && - !statusDropdownRef.current.contains(event.target as Node) - ) { - setIsStatusDropdownOpen(false); - } - if ( - assigneeDropdownRef.current && - !assigneeDropdownRef.current.contains(event.target as Node) - ) { - setIsAssigneeDropdownOpen(false); - } - }; - - if (isStatusDropdownOpen || isAssigneeDropdownOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isStatusDropdownOpen, isAssigneeDropdownOpen]); - const handleTagKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && currentTag.trim()) { - e.preventDefault(); - setFormData((prev) => { return { + if (e.key !== 'Enter') { + return; + } + if (!currentTag.trim()) { + return; + } + e.preventDefault(); + setFormData((prev) => { + return { ...prev, tags: [ ...prev.tags, { label: currentTag.trim(), color: getRandomTagColor() }, ], - } }); - setCurrentTag(''); - } + }; + }); + setCurrentTag(''); }; const removeTag = (indexToRemove: number) => { - setFormData((prev) => { return { - ...prev, - tags: prev.tags.filter((_, index) => index !== indexToRemove), - } }); + setFormData((prev) => { + return { + ...prev, + tags: prev.tags.filter((_, index) => index !== indexToRemove), + }; + }); }; const handleImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { - setFormData((prev) => { return { - ...prev, - imageFile: file, - existingImageUrl: undefined, - } }); + setFormData((prev) => { + return { + ...prev, + imageFile: file, + existingImageUrl: undefined, + }; + }); } }; const removeImage = () => { - setFormData((prev) => { return { - ...prev, - imageFile: null, - existingImageUrl: undefined, - } }); + setFormData((prev) => { + return { + ...prev, + imageFile: null, + existingImageUrl: undefined, + }; + }); }; return ( @@ -135,146 +124,135 @@ export default function EditTaskForm({
{/* 상태 */}
- -
- - - {/* 드롭다운 옵션들 */} - {isStatusDropdownOpen && ( -
- {statusOptions.map((option, index) => - { return
+ + + {statusOptions.map((option) => { + return ( + { handleStatusSelect(option.value); }} + onClick={() => { + handleStatusSelect(option.value); + }} > -
- {formData.status === option.value && ( - 선택됨 - )} +
+
+ {formData.status === option.value && ( + 선택됨 + )} +
+
- - } - )} -
- )} -
+ + ); + })} + +
- {/* 담당자 */}
-
- - - {/* 드롭다운 옵션들 */} - {isAssigneeDropdownOpen && ( -
- {assigneeOptions.map((option, index) => - { return } - )} -
- )} -
+ + ); + })} + +
@@ -317,10 +295,12 @@ export default function EditTaskForm({ className='focus:border-violet w-full resize-none rounded-lg border border-gray-300 p-4 focus:outline-none' value={formData.description} onChange={(e) => { - setFormData((prev) => { return { - ...prev, - description: e.target.value, - } }); + setFormData((prev) => { + return { + ...prev, + description: e.target.value, + }; + }); }} /> @@ -342,18 +322,18 @@ export default function EditTaskForm({ setFormData((prev) => ({ ...prev, dueDate: e.target.value })); }} onClick={(e) => { - (e.currentTarget as HTMLInputElement).showPicker?.(); + (e.currentTarget as HTMLInputElement).showPicker(); }} /> -
{ const input = document.querySelector( '#dueDate' ) as HTMLInputElement; - input?.showPicker?.(); - input?.focus(); + input.showPicker(); + input.focus(); }} > -
+ @@ -373,20 +353,25 @@ export default function EditTaskForm({
{/* 기존 태그들 */} - {formData.tags.map((tag, index) => - { return
- - -
} - )} + + +
+ ); + })} {/* 새 태그 입력 */}
- {formData.imageFile ? ( + {formData.imageFile && ( 할일 이미지 - ) : formData.existingImageUrl ? ( + )} + {!formData.imageFile && formData.existingImageUrl && ( 할일 이미지 - ) : null} + )}