diff --git a/src/components/common/input/DateInput.tsx b/src/components/common/input/DateInput.tsx index 858af6f..83a336a 100644 --- a/src/components/common/input/DateInput.tsx +++ b/src/components/common/input/DateInput.tsx @@ -1,21 +1,25 @@ "use client"; -import { forwardRef, useState } from "react"; -import { formatDate } from "@/lib/utils/dateUtils"; +import { forwardRef } from "react"; import { ko } from "date-fns/locale"; import DatePicker, { registerLocale } from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; import Image from "next/image"; +import "react-datepicker/dist/react-datepicker.css"; import calendarIcon from "../../../../public/icon/calendar_icon.svg"; registerLocale("ko", ko); -interface DateInputProps { +interface DateInputTriggerProps { value: string; onClick?: () => void; } -const DateInputTrigger = forwardRef( +interface DateInputProps { + value: string; + onChange: (date: string) => void; +} + +const DateInputTrigger = forwardRef( ({ value, onClick }, ref) => (
( ); DateInputTrigger.displayName = "DateInputTrigger"; -const DateInput = () => { - const [selectedDate, setSelectedDate] = useState(null); - - const formattedDate = selectedDate - ? formatDate(selectedDate.toISOString(), true) - : ""; +const DateInput = ({ value, onChange }: DateInputProps) => { + const handleDateChange = (date: Date | null) => { + if (date) { + const localDate = new Date(date.getTime() + 9 * 60 * 60 * 1000); + const formattedDate = localDate + .toISOString() + .slice(0, 16) + .replace("T", " "); + onChange(formattedDate); + } else { + onChange(""); + } + }; return (
setSelectedDate(date)} + selected={value ? new Date(value) : null} + onChange={handleDateChange} dateFormat="Pp" locale="ko" showTimeSelect timeFormat="HH:mm" timeIntervals={5} - customInput={} + customInput={} />
); diff --git a/src/components/common/input/ImageInput.tsx b/src/components/common/input/ImageInput.tsx index 740e612..f08c1f0 100644 --- a/src/components/common/input/ImageInput.tsx +++ b/src/components/common/input/ImageInput.tsx @@ -10,6 +10,7 @@ interface BaseImageInputProps label?: string; variant: "task" | "profile"; initialImageUrl?: string | null; + onImageUrlChange?: (url: string) => void; } interface TaskImageInputProps extends BaseImageInputProps { @@ -64,6 +65,10 @@ const ImageInput = ({ : undefined; const imageUrl = await postImage(variant, columnId, file); setUploadImgUrl(imageUrl); + + if (props.onImageUrlChange) { + props.onImageUrlChange(imageUrl); + } } catch (error: unknown) { if (error instanceof Error) { setError(error.message || "이미지 업로드에 실패했습니다."); diff --git a/src/components/common/input/TagInput.tsx b/src/components/common/input/TagInput.tsx index 60d591c..fb2d334 100644 --- a/src/components/common/input/TagInput.tsx +++ b/src/components/common/input/TagInput.tsx @@ -3,15 +3,13 @@ import { useState } from "react"; import TagList from "../tag/TagList"; -export default function TagInput({ - label, - tags, - setTags, -}: { +interface TagInputProps { label: string; tags: string[]; setTags: (tags: string[]) => void; -}) { +} + +export default function TagInput({ label, tags, setTags }: TagInputProps) { const [inputValue, setInputValue] = useState(""); const handleKeyDown = (e: React.KeyboardEvent) => { diff --git a/src/components/common/modal/Modal.tsx b/src/components/common/modal/Modal.tsx index 51c0435..65898eb 100644 --- a/src/components/common/modal/Modal.tsx +++ b/src/components/common/modal/Modal.tsx @@ -45,7 +45,7 @@ export default function Modal({
-
diff --git a/src/components/modal/edit-task/AssigneeDropdown.tsx b/src/components/modal/edit-task/AssigneeDropdown.tsx new file mode 100644 index 0000000..7e62b3f --- /dev/null +++ b/src/components/modal/edit-task/AssigneeDropdown.tsx @@ -0,0 +1,105 @@ +import { useState, useEffect } from "react"; +import { fetchDashboardMember } from "@/lib/apis/membersApi"; +import Image from "next/image"; +import dropdownIcon from "../../../../public/icon/dropdown_icon.svg"; +import checkItem from "../../../../public/icon/check_icon.svg"; +import UserIcon from "@/components/common/user-icon/UserIcon"; + +interface AssigneeDropdownProps { + token: string; + dashboardId: string; + memberId: number; + onChange: (value: number) => void; +} + +export default function AssigneeDropdown({ + token, + dashboardId, + memberId, + onChange, +}: AssigneeDropdownProps) { + const [members, setMembers] = useState< + { userId: number; nickname: string; profileImageUrl: string | null }[] + >([]); + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + const getData = async () => { + const res = await fetchDashboardMember({ + token, + page: 1, + size: 20, + id: dashboardId, + }); + setMembers(res.members); + }; + + getData(); + }, []); + + const selected = members.find((member) => member.userId === memberId); + + return ( +
+ + + + {isOpen && ( +
    + {members.map((member) => ( +
  • { + onChange(member.userId); + setIsOpen(false); + }} + > +
    +
    + {member.userId === memberId && ( + + )} +
    +
    + +
    + {member.nickname} +
    +
    +
    +
  • + ))} +
+ )} +
+ ); +} diff --git a/src/components/modal/edit-task/ColumnDropdown.tsx b/src/components/modal/edit-task/ColumnDropdown.tsx new file mode 100644 index 0000000..e72021a --- /dev/null +++ b/src/components/modal/edit-task/ColumnDropdown.tsx @@ -0,0 +1,90 @@ +import { useState, useEffect } from "react"; +import { fetchColumnList } from "@/lib/apis/columnsApi"; +import Image from "next/image"; +import dropdownIcon from "../../../../public/icon/dropdown_icon.svg"; +import checkItem from "../../../../public/icon/check_icon.svg"; + +interface ColumnDropdownProps { + token: string; + dashboardId: string; + columnId: number; + onChange: (value: number) => void; +} + +export default function ColumnDropdown({ + token, + dashboardId, + columnId, + onChange, +}: ColumnDropdownProps) { + const [columns, setColumns] = useState<{ id: number; title: string }[]>([]); + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + const getData = async () => { + const res = await fetchColumnList({ + token, + id: dashboardId, + }); + setColumns(res.data); + }; + + getData(); + }, []); + + const selected = columns.find((col) => col.id === columnId); + + return ( +
+ + + + {isOpen && ( +
    + {columns.map((col) => ( +
  • { + onChange(col.id); + setIsOpen(false); + }} + > +
    +
    + {col.id === columnId && } +
    +
    +
    +
    + {col.title} +
    +
    +
    +
  • + ))} +
+ )} +
+ ); +} diff --git a/src/components/modal/edit-task/EditTaskModal.tsx b/src/components/modal/edit-task/EditTaskModal.tsx index 2e24e48..131f68f 100644 --- a/src/components/modal/edit-task/EditTaskModal.tsx +++ b/src/components/modal/edit-task/EditTaskModal.tsx @@ -1,26 +1,192 @@ -import { useState } from "react"; +import { useState, useEffect, ChangeEvent } from "react"; +import { useTaskStore } from "@/lib/store/useTaskStore"; +import { useModalStore } from "@/lib/store/useModalStore"; +import { fetchTaskCardDetail, putCard } from "@/lib/apis/cardsApi"; import Modal from "@/components/common/modal/Modal"; import Input from "@/components/common/input/Input"; +import DateInput from "@/components/common/input/DateInput"; +import Textarea from "@/components/common/textarea/Textarea"; +import TagInput from "@/components/common/input/TagInput"; +import ImageInput from "@/components/common/input/ImageInput"; +import ColumnDropdown from "./ColumnDropdown"; +import AssigneeDropdown from "./AssigneeDropdown"; +import { useDashboardStore } from "@/lib/store/useDashboardStore"; export default function EditTaskModal() { - // 해당 폼이 유효성 검사 후 제출 가능해질 때 해당 state 값이 true가 되도록 하기 + const { openModal } = useModalStore(); + const { selectedTaskId } = useTaskStore(); + const { dashboardId } = useDashboardStore(); + const [isLoading, setIsLoading] = useState(false); + const accessToken = localStorage.getItem("accessToken") ?? ""; + const [isFormValid, setIsFormValid] = useState(false); + const [selectedColumn, setSelectedColumn] = useState(0); + const [selectedAssignee, setSelectedAssignee] = useState(0); + const [formData, setFormData] = useState({ + title: "", + description: "", + tag: "", + }); + const [tagValues, setTagValues] = useState([]); + const [dueDate, setDueDate] = useState(""); + const [cardImg, setItemImg] = useState(null); + + const [initialValues, setInitialValues] = useState<{ + title: string; + description: string; + tags: string[]; + dueDate: string; + imageUrl: string | null; + columnId: number; + assigneeUserId: number; + } | null>(null); + + const handleRegister = async () => { + if (!selectedTaskId) return; - // 활성화된 모달 버튼 클릭 시 실행할 함수 - const buttonClick = () => { - alert("Hi"); // 이 부분 바꿔주시면 됩니다 - setIsFormValid(false); // 이 코드는 배포할 때 문제가 있어서 임시로 넣어놓은 코드라 삭제하시면 됩니다 + await putCard({ + token: accessToken, + cardId: selectedTaskId, + columnId: selectedColumn, + assigneeUserId: selectedAssignee, + title: formData.title, + description: formData.description, + dueDate: dueDate, + tags: tagValues, + imageUrl: cardImg, + }); + + openModal("taskDetail"); }; + const handleChange = ( + event: ChangeEvent + ) => { + const { id, value } = event.target; + setFormData((prev) => ({ + ...prev, + [id]: value, + })); + }; + + const handleLoad = async () => { + if (!selectedTaskId || isLoading) return; + setIsLoading(true); + + try { + const data = await fetchTaskCardDetail({ + token: accessToken, + id: selectedTaskId, + }); + + setSelectedColumn(data.columnId); + setSelectedAssignee(data.assignee.id); + setFormData({ + title: data.title, + description: data.description, + tag: "", + }); + setTagValues(data.tags); + setDueDate(data.dueDate); + setItemImg(data.imageUrl); + + setInitialValues({ + title: data.title, + description: data.description, + tags: data.tags, + dueDate: data.dueDate, + imageUrl: data.imageUrl, + columnId: data.columnId, + assigneeUserId: data.assignee.id, + }); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + handleLoad(); + }, []); + + useEffect(() => { + if (!initialValues) return; + + const hasChanged = + formData.title !== initialValues.title || + formData.description !== initialValues.description || + dueDate !== initialValues.dueDate || + cardImg !== initialValues.imageUrl || + selectedColumn !== initialValues.columnId || + selectedAssignee !== initialValues.assigneeUserId || + tagValues.join(",") !== initialValues.tags.join(","); + + const isNotEmpty = + formData.title.trim() !== "" && + formData.description.trim() !== "" && + dueDate.trim() !== ""; + + setIsFormValid(isNotEmpty && hasChanged); + }, [ + formData, + tagValues, + dueDate, + cardImg, + selectedColumn, + selectedAssignee, + initialValues, + ]); + + if (!dashboardId) return; + return ( -
- +
+
+ + +
+ +