diff --git a/public/images/arrow-dropdown.svg b/public/images/arrow-dropdown.svg new file mode 100644 index 0000000..ac2d29c --- /dev/null +++ b/public/images/arrow-dropdown.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/close.svg b/public/images/close.svg new file mode 100644 index 0000000..8b9bb24 --- /dev/null +++ b/public/images/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/drop-more.svg b/public/images/drop-more.svg new file mode 100644 index 0000000..7c5cf22 --- /dev/null +++ b/public/images/drop-more.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/dashboard/[id]/page.tsx b/src/app/dashboard/[id]/page.tsx index 4aa117a..0e79228 100644 --- a/src/app/dashboard/[id]/page.tsx +++ b/src/app/dashboard/[id]/page.tsx @@ -2,11 +2,12 @@ import Image from 'next/image' import { useParams } from 'next/navigation' -import { useRef } from 'react' +import { useEffect, useRef } from 'react' import { useCardMutation } from '@/app/features/dashboard_Id/api/useCardMutation' import useColumns from '@/app/features/dashboard_Id/api/useColumns' import Column from '@/app/features/dashboard_Id/Column/Column' +import { useColumnsStore } from '@/app/features/dashboard_Id/store/useColumnsStore' import { useDragStore } from '@/app/features/dashboard_Id/store/useDragStore' import { Card } from '@/app/features/dashboard_Id/type/Card.type' @@ -17,12 +18,23 @@ export default function DashboardID() { // const { data: columns, isLoading, error } = useColumns(id) const { draggingCard, setDraggingCard } = useDragStore() + const { setColumns } = useColumnsStore() const cardMutation = useCardMutation() const touchPos = useRef({ x: 0, y: 0 }) const prevColumn = useRef(null) const longPressTimer = useRef(null) const isLongPressActive = useRef(false) + useEffect(() => { + if (columns) { + const transformed = columns.map((column) => ({ + columnId: column.id, + columnTitle: column.title, + })) + setColumns(transformed) + } + }, [columns, setColumns]) + const handleTouchStart = (e: React.TouchEvent) => { // 1. 터치 대상 찾기 const target = e.target as HTMLElement @@ -127,9 +139,9 @@ export default function DashboardID() { if (error) return

error...{error.message}

return ( <> -
+
setDraggingCard({ cardData: card })} onContextMenu={(e: React.MouseEvent) => e.preventDefault()} className="BG-white Border-section relative rounded-6 border-solid px-20 py-16" + onClick={() => setOpenCard(true)} > {imageUrl && ( {/* 태그 */} - + {tags.length !== 0 && } {/* 마감일 & 담당자 */}
{/* :마감일 */} - {dueDate && ( -
- 마감일 +
+ 마감일 + {dueDate && (
- {dueDate} + {dueDate.split(' ')[0]}
-
- )} - + )} +
{/* :담당자 */} {assignee && (
)}
+ + {/* 카드 모달 */} + {openCard && ( + + setOpenCard(false)} + card={card} + column={column} + /> + + )}
) } diff --git a/src/app/features/dashboard_Id/Card/ColumnTitle.tsx b/src/app/features/dashboard_Id/Card/ColumnTitle.tsx new file mode 100644 index 0000000..9311ac5 --- /dev/null +++ b/src/app/features/dashboard_Id/Card/ColumnTitle.tsx @@ -0,0 +1,10 @@ +export default function ColumnTitle({ title }: { title: string }) { + return ( +
+
+
+ {title} +
+
+ ) +} diff --git a/src/app/features/dashboard_Id/Card/MyAssignee.tsx b/src/app/features/dashboard_Id/Card/MyAssignee.tsx new file mode 100644 index 0000000..8a69e36 --- /dev/null +++ b/src/app/features/dashboard_Id/Card/MyAssignee.tsx @@ -0,0 +1,18 @@ +import { Avatar } from '@/app/shared/components/common/Avatar' + +import { Assignee } from '../type/Card.type' + +export default function MyAssignee({ assignee }: { assignee: Assignee }) { + return ( +
+ + + {assignee.nickname} + +
+ ) +} diff --git a/src/app/features/dashboard_Id/Card/Tags.tsx b/src/app/features/dashboard_Id/Card/Tags.tsx index 6832b12..0e6b613 100644 --- a/src/app/features/dashboard_Id/Card/Tags.tsx +++ b/src/app/features/dashboard_Id/Card/Tags.tsx @@ -1,23 +1,39 @@ +import { useTheme } from 'next-themes' + import { getColor } from '@/app/shared/lib/getColor' export default function Tags({ tags }: { tags: string[] }) { //태그 컬러 - 랜덤 배정 //카드 생성 시 - 동일 태그 입력 불가하도록 const bgColors = ['#F9EEE3', '#E7F7DB', '#F7DBF0', '#DBE6F7'] + const bgColorsDark = ['#774212', '#366712', '#711E5C', '#0F3167'] const textColors = ['#D58D49', '#86D549', '#D549B6', '#4981D5'] + const { resolvedTheme } = useTheme() + const isDark = resolvedTheme === 'dark' + return (
{tags.map((tag) => { - const colorIndex = getColor(tag, bgColors.length) + // const colorIndex = getColor(tag, bgColors.length) + // getColors함수 사용하면 NaN값이 떠서 작동을 안함.. 원래 문제 없었는데 이유를 모르겠음. + const hash = tag + .split('') + .reduce((acc, char) => acc + char.charCodeAt(0), 0) + const colorIndex = hash % 4 + + const backgroundColor = isDark + ? bgColorsDark[colorIndex] + : bgColors[colorIndex] + const textColor = isDark ? bgColors[colorIndex] : textColors[colorIndex] return ( {tag} diff --git a/src/app/features/dashboard_Id/Card/TagsCanDelete.tsx b/src/app/features/dashboard_Id/Card/TagsCanDelete.tsx new file mode 100644 index 0000000..e22dd8b --- /dev/null +++ b/src/app/features/dashboard_Id/Card/TagsCanDelete.tsx @@ -0,0 +1,49 @@ +import { useTheme } from 'next-themes' + +import { getColor } from '@/app/shared/lib/getColor' + +export default function TagsCanDelete({ + tags, + setTags, +}: { + tags: string[] + setTags: React.Dispatch> +}) { + //태그 컬러 - 랜덤 배정 + //카드 생성 시 - 동일 태그 입력 불가하도록 + const bgColors = ['#F9EEE3', '#E7F7DB', '#F7DBF0', '#DBE6F7'] + const textColors = ['#D58D49', '#86D549', '#D549B6', '#4981D5'] + + const { resolvedTheme } = useTheme() + const isDark = resolvedTheme === 'dark' + + return ( +
+ {tags.map((tag) => { + const hash = tag + .split('') + .reduce((acc, char) => acc + char.charCodeAt(0), 0) + const colorIndex = hash % 4 + + const backgroundColor = isDark + ? textColors[colorIndex] + : bgColors[colorIndex] + const textColor = isDark ? bgColors[colorIndex] : textColors[colorIndex] + + return ( + setTags(tags.filter((t) => t !== tag))} + > + {tag} + + ) + })} +
+ ) +} diff --git a/src/app/features/dashboard_Id/Card/cardFormModals/AssigneeList.tsx b/src/app/features/dashboard_Id/Card/cardFormModals/AssigneeList.tsx index 1e5a4a5..d0f3f5a 100644 --- a/src/app/features/dashboard_Id/Card/cardFormModals/AssigneeList.tsx +++ b/src/app/features/dashboard_Id/Card/cardFormModals/AssigneeList.tsx @@ -1,15 +1,14 @@ import { ControllerRenderProps } from 'react-hook-form' +import { Avatar } from '@/app/shared/components/common/Avatar' import { cn } from '@/app/shared/lib/cn' import getDashboardMembers from '../../lib/getDashboardMembers' +import { Assignee } from '../../type/Card.type' import { CardFormData } from '../../type/CardFormData.type' import { Member } from '../../type/Member.type' +import MyAssignee from '../MyAssignee' -export interface Assignee { - userId: number - nickname: string -} interface AssigneeListProps { members: Member[] | undefined setAssignee: (assignee: Assignee) => void @@ -36,13 +35,13 @@ export default function AssigneeList({ 'BG-Input-hovered w-full cursor-pointer px-16 py-11 pt-14 placeholder-gray-400 caret-transparent', index !== 0 && 'border-t', )} - key={assignee.userId} + key={assignee.id} onClick={() => { setAssignee(assignee) // 담당자 업데이트 - controlField.onChange(assignee.userId) // 리액트 훅에는 .userId 값 연결 + controlField.onChange(assignee.id) // 리액트 훅에는 .userId 값 연결 }} > - {assignee.nickname} +
))}
diff --git a/src/app/features/dashboard_Id/Card/cardFormModals/ColumnList.tsx b/src/app/features/dashboard_Id/Card/cardFormModals/ColumnList.tsx new file mode 100644 index 0000000..4f9c1ce --- /dev/null +++ b/src/app/features/dashboard_Id/Card/cardFormModals/ColumnList.tsx @@ -0,0 +1,44 @@ +import { ControllerRenderProps } from 'react-hook-form' + +import { cn } from '@/app/shared/lib/cn' + +import { SimpleColumn, useColumnsStore } from '../../store/useColumnsStore' +import { CardFormData } from '../../type/CardFormData.type' +import ColumnTitle from '../ColumnTitle' + +interface ColumnListProps { + setColumn: (selectedColumn: SimpleColumn) => void + controlField: ControllerRenderProps +} + +// ✅ ColumnList 컴포넌트: 컬럼 목록을 보여주는 드롭다운 리스트 +// 1. 컬럼 목록을 클릭하면: +// - setColumn(column) 실행 → 선택된 담당자 객체를 부모 컴포넌트 하에 관리 (ex. UI에서 닉네임 표시용) +// - controlField.onChange(column.columnId) 실행 → react-hook-form에 columnId 값을 전달 (form 제출에는 Id 데이터만 전달함) + +export default function ColumnList({ + setColumn, + controlField, +}: ColumnListProps) { + const { ColumnsInDashboard } = useColumnsStore() // 컬럼 목록 데이터는 store에서 불러옴 + + return ( +
+ {ColumnsInDashboard.map((column, index) => ( +
{ + setColumn(column) // 담당자 업데이트 + controlField.onChange(column.columnId) // controlField: 폼의 'columnId' 필드와 연결되어 있음. .columnId 값으로 업데이트 + }} + > + +
+ ))} +
+ ) +} diff --git a/src/app/features/dashboard_Id/Card/cardFormModals/CreateCardForm.tsx b/src/app/features/dashboard_Id/Card/cardFormModals/CreateCardForm.tsx index f2815af..2ab4290 100644 --- a/src/app/features/dashboard_Id/Card/cardFormModals/CreateCardForm.tsx +++ b/src/app/features/dashboard_Id/Card/cardFormModals/CreateCardForm.tsx @@ -7,12 +7,16 @@ import { useEffect, useState } from 'react' import DatePicker from 'react-datepicker' import { Controller, useForm } from 'react-hook-form' +import { cn } from '@/app/shared/lib/cn' + import useMembers from '../../api/useMembers' import { usePostCard } from '../../api/usePostCard' import { useUploadCardImage } from '../../api/useUploadCardImage' +import { Assignee } from '../../type/Card.type' import type { CardFormData } from '../../type/CardFormData.type' -import Tags from '../Tags' -import AssigneeList, { Assignee } from './AssigneeList' +import TagsCanDelete from '../TagsCanDelete' +// import AssigneeList, { Assignee } from './AssigneeList' +import AssigneeList from './AssigneeList' import DateInput from './input/DateInput' import Input from './input/Input' @@ -111,11 +115,21 @@ export default function CreateCardForm({ onClick={() => setIsOpen((prev) => !prev)} value={selectedAssignee?.nickname ?? ''} readOnly - className="Input-readOnly" + className="Input-readOnly w-520" id="assigneeUserId" type="text" placeholder="담당자를 선택해 주세요" /> + 화살표 {isOpen && ( - +
)} diff --git a/src/app/features/dashboard_Id/Card/cardFormModals/CreateCardModal.tsx b/src/app/features/dashboard_Id/Card/cardFormModals/CreateCardModal.tsx index 156c0b8..2d8c868 100644 --- a/src/app/features/dashboard_Id/Card/cardFormModals/CreateCardModal.tsx +++ b/src/app/features/dashboard_Id/Card/cardFormModals/CreateCardModal.tsx @@ -2,7 +2,6 @@ import { createPortal } from 'react-dom' interface ModalProps { children: React.ReactNode - onClose: () => void } export default function CreateCardModal({ children }: ModalProps) { const modalRoot = document.getElementById('modal-root') diff --git a/src/app/features/dashboard_Id/Card/cardFormModals/ModifyCardForm.tsx b/src/app/features/dashboard_Id/Card/cardFormModals/ModifyCardForm.tsx new file mode 100644 index 0000000..61ca09d --- /dev/null +++ b/src/app/features/dashboard_Id/Card/cardFormModals/ModifyCardForm.tsx @@ -0,0 +1,366 @@ +import 'react-datepicker/dist/react-datepicker.css' + +import { format } from 'date-fns' +import Image from 'next/image' +import { useParams } from 'next/navigation' +import { useEffect, useState } from 'react' +import DatePicker from 'react-datepicker' +import { Controller, useForm } from 'react-hook-form' + +import { cn } from '@/app/shared/lib/cn' + +import useMembers from '../../api/useMembers' +import { usePutCardMutation } from '../../api/usePutCardMutation' +import { useUploadCardImage } from '../../api/useUploadCardImage' +import { SimpleColumn } from '../../store/useColumnsStore' +import { Assignee, Card } from '../../type/Card.type' +import type { + CardFormData, + CardModifyFormData, +} from '../../type/CardFormData.type' +import ColumnTitle from '../ColumnTitle' +import MyAssignee from '../MyAssignee' +import TagsCanDelete from '../TagsCanDelete' +import AssigneeList from './AssigneeList' +import ColumnList from './ColumnList' +import DateInput from './input/DateInput' +import Input from './input/Input' + +export default function ModifyCardForm({ + onClose, + currentColumn, + card, +}: { + onClose: () => void + currentColumn: SimpleColumn + card: Card +}) { + const [preview, setPreview] = useState(card.imageUrl) // 이미지 URl 임시 저장 + const [tags, setTags] = useState(card.tags) // 태그 목록 임시 저장 + const [tagInput, setTagInput] = useState('') // 작성중인 태그 + const { mutate: uploadImage, isPending: isUploading } = useUploadCardImage() + + // 대시보드 멤버(담당자 선택) + const params = useParams() + const dashboardId = Number(params.id) + const { data } = useMembers(dashboardId) + const [isOpen, setIsOpen] = useState(false) // 담당자 드롭다운 + const [selectedAssignee, setSelectedAssignee] = useState( + card.assignee, + ) // 선택한 담당자 + const { columnId } = card + + // 컬럼 목록 + const [isOpenColumn, setIsOpenColumn] = useState(false) + const [selectedColumn, setSelectedColumn] = useState(currentColumn) + + //useForm + const { + register, + control, + handleSubmit, + setValue, + formState: { errors, isValid, isSubmitting, isDirty }, + } = useForm({ + defaultValues: { + assigneeUserId: card.assignee.id, + dashboardId: card.dashboardId, + columnId: card.columnId, + title: card.title, + description: card.description, + dueDate: card.dueDate, + tags: card.tags, + imageUrl: card.imageUrl, + }, + mode: 'onChange', // isValid와 isDirty가 입력 즉시 반영되도록 + }) + + // React Hook Form 과 tags 값 연결 + useEffect(() => { + setValue('tags', tags) + console.log(tags) + }, [tags, tags.length, setValue]) + + // 상태(컬럼) 선택 시 / assignee 선택 시 드롭다운 닫기 + useEffect(() => { + if (selectedAssignee) { + setIsOpen(false) + } + if (selectedColumn) { + setIsOpenColumn(false) + } + }, [selectedAssignee, selectedColumn]) + + // 이미지 파일 처리 + async function handleFileChange(e: React.ChangeEvent) { + const file = e.target.files?.[0] + if (!file) return + + uploadImage( + { columnId, file }, + { + onSuccess: ({ imageUrl }) => { + setValue('imageUrl', imageUrl) + setPreview(imageUrl) + }, + }, + ) + } + + // 폼 제출 핸들러 함수 + const { mutate: modifyCard, isPending } = usePutCardMutation() + function onSubmit(data: CardModifyFormData) { + const payload: CardModifyFormData = { + ...data, + columnId: columnId, + } + + if (!data.dueDate) delete payload.dueDate + if (!data.imageUrl || !preview) delete payload.imageUrl // delete로 아예 필드의 해당 key를 지워야, 서버가 "없음"으로 인식함.. + + console.log('submitted', payload) + modifyCard({ cardId: card.id, payload: payload }) + onClose() + } + + // ✅ JSX + return ( +
+

할 일 수정

+ +
+ {/* 컬럼 선택 */} + ( + +
+ setIsOpenColumn((prev) => !prev)} + value={selectedColumn?.columnTitle ?? ''} + readOnly + className="Input-readOnly w-217" + id="columnId" + type="text" + placeholder={currentColumn.columnTitle} + /> + {/* 인풋에 보이는 선택된 컬럼 & 오른쪽 화살표 */} +
+ +
+ 화살표 + {/* 컬럼 선택지 */} + {isOpenColumn && ( + + )} +
+ + )} + /> + + {/* 담당자 입력 */} + ( + +
+ setIsOpen((prev) => !prev)} + // value={selectedAssignee?.nickname ?? ''} + readOnly + className="Input-readOnly w-217" + id="assigneeUserId" + type="text" + /> +
+ +
+ 화살표 + {isOpen && ( + + )} +
+ + )} + /> +
+ + {/* 제목 입력 */} + + + + + {/* 설명 입력 */} + +