Skip to content

Commit c35b553

Browse files
authored
Merge pull request #120 from hyeonjiroh/feat/#112/mydashboard-page
feat: #119/할 일 수정 모달
2 parents aa6fd98 + 1fc9502 commit c35b553

File tree

9 files changed

+447
-32
lines changed

9 files changed

+447
-32
lines changed

src/components/common/input/DateInput.tsx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
"use client";
22

3-
import { forwardRef, useState } from "react";
4-
import { formatDate } from "@/lib/utils/dateUtils";
3+
import { forwardRef } from "react";
54
import { ko } from "date-fns/locale";
65
import DatePicker, { registerLocale } from "react-datepicker";
7-
import "react-datepicker/dist/react-datepicker.css";
86
import Image from "next/image";
7+
import "react-datepicker/dist/react-datepicker.css";
98
import calendarIcon from "../../../../public/icon/calendar_icon.svg";
109

1110
registerLocale("ko", ko);
1211

13-
interface DateInputProps {
12+
interface DateInputTriggerProps {
1413
value: string;
1514
onClick?: () => void;
1615
}
1716

18-
const DateInputTrigger = forwardRef<HTMLDivElement, DateInputProps>(
17+
interface DateInputProps {
18+
value: string;
19+
onChange: (date: string) => void;
20+
}
21+
22+
const DateInputTrigger = forwardRef<HTMLDivElement, DateInputTriggerProps>(
1923
({ value, onClick }, ref) => (
2024
<div
2125
ref={ref}
@@ -33,26 +37,33 @@ const DateInputTrigger = forwardRef<HTMLDivElement, DateInputProps>(
3337
);
3438
DateInputTrigger.displayName = "DateInputTrigger";
3539

36-
const DateInput = () => {
37-
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
38-
39-
const formattedDate = selectedDate
40-
? formatDate(selectedDate.toISOString(), true)
41-
: "";
40+
const DateInput = ({ value, onChange }: DateInputProps) => {
41+
const handleDateChange = (date: Date | null) => {
42+
if (date) {
43+
const localDate = new Date(date.getTime() + 9 * 60 * 60 * 1000);
44+
const formattedDate = localDate
45+
.toISOString()
46+
.slice(0, 16)
47+
.replace("T", " ");
48+
onChange(formattedDate);
49+
} else {
50+
onChange("");
51+
}
52+
};
4253
return (
4354
<div className="flex flex-col">
4455
<label className="font-medium text-gray-800 text-lg mb-[8px] tablet:text-2lg pc:text-2lg">
4556
마감일
4657
</label>
4758
<DatePicker
48-
selected={selectedDate}
49-
onChange={(date) => setSelectedDate(date)}
59+
selected={value ? new Date(value) : null}
60+
onChange={handleDateChange}
5061
dateFormat="Pp"
5162
locale="ko"
5263
showTimeSelect
5364
timeFormat="HH:mm"
5465
timeIntervals={5}
55-
customInput={<DateInputTrigger value={formattedDate} />}
66+
customInput={<DateInputTrigger value={value} />}
5667
/>
5768
</div>
5869
);

src/components/common/input/ImageInput.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface BaseImageInputProps
1010
label?: string;
1111
variant: "task" | "profile";
1212
initialImageUrl?: string | null;
13+
onImageUrlChange?: (url: string) => void;
1314
}
1415

1516
interface TaskImageInputProps extends BaseImageInputProps {
@@ -64,6 +65,10 @@ const ImageInput = ({
6465
: undefined;
6566
const imageUrl = await postImage(variant, columnId, file);
6667
setUploadImgUrl(imageUrl);
68+
69+
if (props.onImageUrlChange) {
70+
props.onImageUrlChange(imageUrl);
71+
}
6772
} catch (error: unknown) {
6873
if (error instanceof Error) {
6974
setError(error.message || "이미지 업로드에 실패했습니다.");

src/components/common/input/TagInput.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33
import { useState } from "react";
44
import TagList from "../tag/TagList";
55

6-
export default function TagInput({
7-
label,
8-
tags,
9-
setTags,
10-
}: {
6+
interface TagInputProps {
117
label: string;
128
tags: string[];
139
setTags: (tags: string[]) => void;
14-
}) {
10+
}
11+
12+
export default function TagInput({ label, tags, setTags }: TagInputProps) {
1513
const [inputValue, setInputValue] = useState("");
1614

1715
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {

src/components/common/modal/Modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export default function Modal({
4545
<div className="flex justify-center items-center fixed top-0 left-0 w-full h-full p-6 bg-black/70 z-50">
4646
<div
4747
className={clsx(
48-
"flex flex-col max-h-[92vh] px-4 rounded border-none bg-white",
48+
"flex flex-col max-h-[80vh] px-4 rounded border-none bg-white",
4949
isPage
5050
? "gap-2 max-w-[327px] py-4 tablet:px-8 tablet:gap-6 tablet:max-w-[1200px] tablet:py-6"
5151
: "gap-8 max-w-[327px] py-6 tablet:max-w-[1200px] tablet:p-8"

src/components/modal/create-task/CreateTaskModal.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Image from "next/image";
33
import Modal from "@/components/common/modal/Modal";
44
import TagInput from "@/components/common/input/TagInput";
55
import Input from "@/components/common/input/Input";
6-
import DateInput from "@/components/common/input/DateInput";
76
import ImageInput from "@/components/common/input/ImageInput";
87
import Textarea from "@/components/common/textarea/Textarea";
98
import dropdownIcon from "../../../../public/icon/dropdown_icon.svg";
@@ -74,7 +73,6 @@ export default function CreateDashboardModal() {
7473
labelClassName="font-medium text-lg tablet:text-2lg"
7574
textareaClassName="font-normal placeholder:text-gray-500 rounded-md text-md h-[84px] px-4 py-[13px] tablet:rounded-lg tablet:h-[126px] tablet:py-[15px] tablet:text-lg"
7675
/>
77-
<DateInput />
7876
<TagInput label="태그" tags={tags} setTags={setTags} />
7977
<ImageInput label="이미지" variant="task" columnId={46355} />
8078
</div>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useState, useEffect } from "react";
2+
import { fetchDashboardMember } from "@/lib/apis/membersApi";
3+
import Image from "next/image";
4+
import dropdownIcon from "../../../../public/icon/dropdown_icon.svg";
5+
import checkItem from "../../../../public/icon/check_icon.svg";
6+
import UserIcon from "@/components/common/user-icon/UserIcon";
7+
8+
interface AssigneeDropdownProps {
9+
token: string;
10+
dashboardId: string;
11+
memberId: number;
12+
onChange: (value: number) => void;
13+
}
14+
15+
export default function AssigneeDropdown({
16+
token,
17+
dashboardId,
18+
memberId,
19+
onChange,
20+
}: AssigneeDropdownProps) {
21+
const [members, setMembers] = useState<
22+
{ userId: number; nickname: string; profileImageUrl: string | null }[]
23+
>([]);
24+
const [isOpen, setIsOpen] = useState(false);
25+
26+
useEffect(() => {
27+
const getData = async () => {
28+
const res = await fetchDashboardMember({
29+
token,
30+
page: 1,
31+
size: 20,
32+
id: dashboardId,
33+
});
34+
setMembers(res.members);
35+
};
36+
37+
getData();
38+
}, []);
39+
40+
const selected = members.find((member) => member.userId === memberId);
41+
42+
return (
43+
<div className="relative flex flex-col w-full tablet:min-w-[217px]">
44+
<label className="block mb-2.5 text-lg font-medium text-gray-800 tablet:mb-2 tablet:text-2lg">
45+
담당자
46+
</label>
47+
<button
48+
type="button"
49+
onClick={() => setIsOpen((prev) => !prev)}
50+
className="h-12 px-4 py-3 font-normal text-md text-gray-500 border border-gray-400 rounded-md tablet:py-[11px] tablet:text-lg"
51+
>
52+
<div className="flex items-center justify-between h-full w-full">
53+
{selected ? (
54+
<div className="flex items-center gap-[6px]">
55+
<UserIcon
56+
name={selected.nickname}
57+
img={selected.profileImageUrl}
58+
size="sm"
59+
/>
60+
<div className="font-normal text-lg text-gray-800">
61+
{selected.nickname}
62+
</div>
63+
</div>
64+
) : (
65+
<span className="text-gray-500">담당자 선택</span>
66+
)}
67+
<Image src={dropdownIcon} width={8} height={8} alt="arrow" />
68+
</div>
69+
</button>
70+
71+
{isOpen && (
72+
<ul className="absolute top-[80px] z-10 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-md max-h-[200px] overflow-y-auto">
73+
{members.map((member) => (
74+
<li
75+
key={member.userId}
76+
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
77+
onClick={() => {
78+
onChange(member.userId);
79+
setIsOpen(false);
80+
}}
81+
>
82+
<div className="flex gap-3 items-center">
83+
<div className="invert brightness-75 relative w-4 h-3">
84+
{member.userId === memberId && (
85+
<Image src={checkItem} fill alt="" />
86+
)}
87+
</div>
88+
<div className="flex items-center gap-[6px]">
89+
<UserIcon
90+
name={member.nickname}
91+
img={member.profileImageUrl}
92+
size="sm"
93+
/>
94+
<div className="font-normal text-lg text-gray-800">
95+
{member.nickname}
96+
</div>
97+
</div>
98+
</div>
99+
</li>
100+
))}
101+
</ul>
102+
)}
103+
</div>
104+
);
105+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useState, useEffect } from "react";
2+
import { fetchColumnList } from "@/lib/apis/columnsApi";
3+
import Image from "next/image";
4+
import dropdownIcon from "../../../../public/icon/dropdown_icon.svg";
5+
import checkItem from "../../../../public/icon/check_icon.svg";
6+
7+
interface ColumnDropdownProps {
8+
token: string;
9+
dashboardId: string;
10+
columnId: number;
11+
onChange: (value: number) => void;
12+
}
13+
14+
export default function ColumnDropdown({
15+
token,
16+
dashboardId,
17+
columnId,
18+
onChange,
19+
}: ColumnDropdownProps) {
20+
const [columns, setColumns] = useState<{ id: number; title: string }[]>([]);
21+
const [isOpen, setIsOpen] = useState(false);
22+
23+
useEffect(() => {
24+
const getData = async () => {
25+
const res = await fetchColumnList({
26+
token,
27+
id: dashboardId,
28+
});
29+
setColumns(res.data);
30+
};
31+
32+
getData();
33+
}, []);
34+
35+
const selected = columns.find((col) => col.id === columnId);
36+
37+
return (
38+
<div className="relative flex flex-col w-full tablet:min-w-[217px]">
39+
<label className="block mb-2.5 text-lg font-medium text-gray-800 tablet:mb-2 tablet:text-2lg">
40+
상태
41+
</label>
42+
<button
43+
type="button"
44+
onClick={() => setIsOpen((prev) => !prev)}
45+
className="h-12 px-4 py-3 font-normal text-md text-gray-500 border border-gray-400 rounded-md tablet:py-[11px] tablet:text-lg"
46+
>
47+
<div className="flex items-center justify-between h-full w-full">
48+
{selected ? (
49+
<div className="flex shrink-0 items-center gap-[6px] px-2 py-1 rounded-2xl bg-violet-8">
50+
<div className="w-[6px] h-[6px] rounded-full bg-violet"></div>
51+
<div className="font-normal text-xs leading-[18px] text-violet">
52+
{selected.title}
53+
</div>
54+
</div>
55+
) : (
56+
<span className="text-gray-500">상태 선택</span>
57+
)}
58+
<Image src={dropdownIcon} width={8} height={8} alt="arrow" />
59+
</div>
60+
</button>
61+
62+
{isOpen && (
63+
<ul className="absolute top-[80px] z-10 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-md max-h-[200px] overflow-y-auto">
64+
{columns.map((col) => (
65+
<li
66+
key={col.id}
67+
className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
68+
onClick={() => {
69+
onChange(col.id);
70+
setIsOpen(false);
71+
}}
72+
>
73+
<div className="flex gap-3 items-center">
74+
<div className="invert brightness-75 relative w-4 h-3">
75+
{col.id === columnId && <Image src={checkItem} fill alt="" />}
76+
</div>
77+
<div className="flex shrink-0 items-center gap-[6px] px-2 py-1 rounded-2xl bg-violet-8 w-fit">
78+
<div className="w-[6px] h-[6px] rounded-full bg-violet"></div>
79+
<div className="font-normal text-xs leading-[18px] text-violet">
80+
{col.title}
81+
</div>
82+
</div>
83+
</div>
84+
</li>
85+
))}
86+
</ul>
87+
)}
88+
</div>
89+
);
90+
}

0 commit comments

Comments
 (0)