Skip to content

Commit ea5b498

Browse files
authored
Merge pull request #98 from part3-4team-Taskify/modalInputApi
[Feat] 할일 생성모달 Api 연결
2 parents 62ac3d4 + 33b0450 commit ea5b498

File tree

9 files changed

+242
-86
lines changed

9 files changed

+242
-86
lines changed

src/api/axiosInstance.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
import axios from "axios";
22

3-
// 👉 디버깅용 로그 출력
43
console.log("🔐 BASE_URL:", process.env.NEXT_PUBLIC_BASE_URL);
54
console.log("🔐 API_TOKEN:", process.env.NEXT_PUBLIC_API_TOKEN);
65

76
const axiosInstance = axios.create({
87
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
98
});
109

11-
// 👉 Authorization 헤더 자동 설정
12-
axiosInstance.defaults.headers.common["Authorization"] =
13-
`Bearer ${process.env.NEXT_PUBLIC_API_TOKEN}`;
14-
15-
// 👉 요청 보낼 때마다 토큰 자동 추가
10+
// ✅ Authorization 헤더 자동 추가
1611
axiosInstance.interceptors.request.use((config) => {
1712
const token = localStorage.getItem("accessToken");
1813
if (token) {

src/api/card.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import axiosInstance from "./axiosInstance";
2+
3+
/** 1. 카드 이미지 업로드 */
4+
export const uploadCardImage = async ({
5+
teamId,
6+
columnId,
7+
imageFile,
8+
}: {
9+
teamId: string;
10+
columnId: number;
11+
imageFile: File;
12+
}): Promise<string> => {
13+
const formData = new FormData();
14+
formData.append("image", imageFile);
15+
16+
const response = await axiosInstance.post(
17+
`/${teamId}/columns/${columnId}/card-image`,
18+
formData,
19+
{
20+
headers: {
21+
"Content-Type": "multipart/form-data",
22+
},
23+
}
24+
);
25+
26+
return response.data.imageUrl;
27+
};
28+
29+
/** 2. 카드 생성 */
30+
export const createCard = async ({
31+
teamId,
32+
assigneeUserId,
33+
dashboardId,
34+
columnId,
35+
title,
36+
description,
37+
dueDate,
38+
tags,
39+
imageUrl,
40+
}: {
41+
teamId: string;
42+
assigneeUserId: number;
43+
dashboardId: number;
44+
columnId: number;
45+
title: string;
46+
description: string;
47+
dueDate: string;
48+
tags: string[];
49+
imageUrl?: string;
50+
}) => {
51+
const response = await axiosInstance.post(`/${teamId}/cards`, {
52+
assigneeUserId,
53+
dashboardId,
54+
columnId,
55+
title,
56+
description,
57+
dueDate,
58+
tags,
59+
imageUrl,
60+
});
61+
62+
return response.data;
63+
};
64+
65+
/** 3. 대시보드 멤버 조회 (담당자용) */
66+
export const getDashboardMembers = async ({
67+
teamId,
68+
dashboardId,
69+
page = 1,
70+
size = 20,
71+
}: {
72+
teamId: string;
73+
dashboardId: number;
74+
page?: number;
75+
size?: number;
76+
}) => {
77+
const res = await axiosInstance.get(`/${teamId}/members`, {
78+
params: {
79+
page,
80+
size,
81+
dashboardId,
82+
},
83+
});
84+
85+
return res.data.members;
86+
};

src/components/columnCard/Column.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// components/column/Column.tsx
2-
import { useState } from "react";
1+
import { useEffect, useState } from "react";
32
import Image from "next/image";
43
import { CardType } from "@/types/task";
54
import Card from "./Card";
@@ -8,6 +7,7 @@ import TodoButton from "@/components/button/TodoButton";
87
import ColumnManageModal from "@/components/columnCard/ColumnManageModal";
98
import ColumnDeleteModal from "@/components/columnCard/ColumnDeleteModal";
109
import { updateColumn, deleteColumn } from "@/api/dashboards";
10+
import { getDashboardMembers } from "@/api/card";
1111

1212
type ColumnProps = {
1313
columnId: number;
@@ -29,6 +29,34 @@ export default function Column({
2929
const [isTodoModalOpen, setIsTodoModalOpen] = useState(false);
3030
const [columnTitle, setColmnTitle] = useState(title);
3131

32+
const [members, setMembers] = useState<
33+
{ id: number; userId: number; nickname: string }[]
34+
>([]);
35+
36+
// ✅ 멤버 불러오기
37+
useEffect(() => {
38+
const fetchMembers = async () => {
39+
try {
40+
const result = await getDashboardMembers({
41+
teamId,
42+
dashboardId,
43+
});
44+
45+
const parsed = result.map((m: any) => ({
46+
id: m.id,
47+
userId: m.userId,
48+
nickname: m.nickname || m.email,
49+
}));
50+
51+
setMembers(parsed);
52+
} catch (error) {
53+
console.error("멤버 불러오기 실패:", error);
54+
}
55+
};
56+
57+
fetchMembers();
58+
}, [teamId, dashboardId]);
59+
3260
const handleEditColumn = async (newTitle: string) => {
3361
if (!newTitle.trim()) {
3462
alert("칼럼 이름을 입력해주세요.");
@@ -51,7 +79,7 @@ export default function Column({
5179
await deleteColumn({ teamId, columnId });
5280
setIsDeleteModalOpen(false);
5381
alert("칼럼이 삭제되었습니다.");
54-
// 👉 부모에서 상태를 관리 중이라면 삭제 후 다시 데이터를 불러오거나, 상태 업데이트 필요!
82+
// :point_right: 부모에서 상태를 관리 중이라면 삭제 후 다시 데이터를 불러오거나, 상태 업데이트 필요!
5583
} catch (error) {
5684
console.error("칼럼 삭제 실패:", error);
5785
alert("칼럼 삭제에 실패했습니다.");
@@ -99,10 +127,12 @@ export default function Column({
99127
{/* Todo 모달 */}
100128
{isTodoModalOpen && (
101129
<TodoModal
102-
isOpen={isTodoModalOpen} // todo todomodal에서 타입정의 추가하기 (isOpen, teamId, dashboardId)
130+
isOpen={isTodoModalOpen}
103131
onClose={() => setIsTodoModalOpen(false)}
104132
teamId={teamId}
105133
dashboardId={dashboardId}
134+
columnId={columnId}
135+
members={members} // ✅ 멤버 넘겨줌
106136
/>
107137
)}
108138

src/components/modalInput/AssigneeSelect.tsx

Lines changed: 19 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,32 @@
11
import { useState } from "react";
2-
import clsx from "clsx";
3-
import Image from "next/image";
42

53
interface AssigneeSelectProps {
64
value: string;
75
onChange: (value: string) => void;
6+
users: string[]; // users는 string[]이어야 합니다.
87
label?: string;
98
required?: boolean;
109
}
1110

12-
const ASSIGNEES = ["배유철", "배동석", "이지은"];
13-
14-
function getInitial(name: string) {
15-
return name.charAt(0).toUpperCase();
16-
}
17-
18-
function getColor(index: number) {
19-
const colors = ["bg-[#A0E6FF]", "bg-[#FFD29D]", "bg-[#C2A1FF]"];
20-
return colors[index % colors.length];
21-
}
22-
2311
export default function AssigneeSelect({
2412
value,
2513
onChange,
14+
users,
2615
label,
2716
required,
2817
}: AssigneeSelectProps) {
2918
const [isOpen, setIsOpen] = useState(false);
3019
const [filter, setFilter] = useState("");
3120

32-
const filtered = ASSIGNEES.filter((name) => name.includes(filter));
21+
const filtered = users.filter((name) =>
22+
name.toLowerCase().includes(filter.toLowerCase() || "")
23+
);
3324

3425
return (
3526
<div className="inline-flex flex-col items-start gap-2.5 w-full max-w-[520px]">
3627
{label && (
3728
<p className="font-18m text-[var(--color-black)]">
38-
{label}{" "}
29+
{label}
3930
{required && <span className="text-[var(--color-purple)]">*</span>}
4031
</p>
4132
)}
@@ -46,22 +37,19 @@ export default function AssigneeSelect({
4637
onClick={() => setIsOpen(!isOpen)}
4738
>
4839
<div className="flex items-center gap-2">
49-
<span
50-
className={clsx(
51-
"w-6 h-6 rounded-full text-xs text-white flex items-center justify-center",
52-
getColor(ASSIGNEES.indexOf(value))
53-
)}
54-
>
55-
{getInitial(value)}
56-
</span>
57-
<span className="font-18r">{value || "이름을 입력해주세요"}</span>
40+
{value ? (
41+
<>
42+
<span className="w-6 h-6 rounded-full text-xs text-white flex items-center justify-center bg-[#A0E6FF]">
43+
{value.charAt(0).toUpperCase()}
44+
</span>
45+
<span className="font-18r">{value}</span>
46+
</>
47+
) : (
48+
<span className="font-18r text-[var(--color-gray2)]">
49+
이름을 입력해주세요
50+
</span>
51+
)}
5852
</div>
59-
<Image
60-
src="/svgs/arrow-down.svg"
61-
width={20}
62-
height={20}
63-
alt="dropdown"
64-
/>
6553
</div>
6654

6755
{isOpen && (
@@ -76,17 +64,7 @@ export default function AssigneeSelect({
7664
}}
7765
className="px-4 py-2 cursor-pointer hover:bg-[var(--color-gray1)] flex items-center justify-between"
7866
>
79-
<div className="flex items-center gap-2">
80-
<span
81-
className={clsx(
82-
"w-6 h-6 rounded-full text-xs text-white flex items-center justify-center",
83-
getColor(idx)
84-
)}
85-
>
86-
{getInitial(name)}
87-
</span>
88-
<span className="text-sm">{name}</span>
89-
</div>
67+
<span className="text-sm">{name}</span>
9068
{value === name && (
9169
<span className="text-[var(--primary)]"></span>
9270
)}

src/components/modalInput/ModalImage.tsx

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,50 @@
11
import Image from "next/image";
22
import { ChangeEvent, useRef, useState } from "react";
3-
43
import AddButton from "./AddButton";
4+
import { uploadCardImage } from "@/api/card";
55

66
interface ModalImageProps {
77
label: string;
8-
onImageSelect: (imageUrl: File) => void;
8+
teamId: string;
9+
columnId: number;
10+
onImageSelect: (imageUrl: string) => void;
911
}
1012

11-
export default function ModalImage({ label, onImageSelect }: ModalImageProps) {
13+
export default function ModalImage({
14+
label,
15+
teamId,
16+
columnId,
17+
onImageSelect,
18+
}: ModalImageProps) {
1219
const [backgroundImage, setBackgroundImage] = useState<string | null>(null);
1320
const fileInputRef = useRef<HTMLInputElement | null>(null);
1421

1522
const handleFileInputClick = () => {
1623
fileInputRef.current?.click();
1724
};
1825

19-
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
20-
const selectedFile = e.target.files?.[0];
26+
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
27+
const file = e.target.files?.[0];
28+
if (!file) return;
29+
30+
// 이미지 미리보기
31+
const reader = new FileReader();
32+
reader.onload = (event) => {
33+
const imageSrc = event.target?.result as string;
34+
setBackgroundImage(imageSrc);
35+
};
36+
reader.readAsDataURL(file);
2137

22-
if (selectedFile) {
23-
const reader = new FileReader();
24-
reader.onload = (event) => {
25-
const imageSrc = event.target?.result as string;
26-
setBackgroundImage(imageSrc);
27-
onImageSelect(selectedFile);
28-
};
29-
reader.readAsDataURL(selectedFile);
38+
try {
39+
const imageUrl = await uploadCardImage({
40+
teamId,
41+
columnId,
42+
imageFile: file, // ✅ File만 넘김
43+
});
44+
onImageSelect(imageUrl); // 부모로 전달
45+
} catch (error) {
46+
console.error("이미지 업로드 실패:", error);
47+
alert("이미지 업로드에 실패했어요.");
3048
}
3149
};
3250

@@ -67,6 +85,7 @@ export default function ModalImage({ label, onImageSelect }: ModalImageProps) {
6785
type="file"
6886
ref={fileInputRef}
6987
className="hidden"
88+
accept="image/*"
7089
onChange={handleFileChange}
7190
/>
7291
</button>

src/components/modalInput/ModalInput.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export default function ModalInput({
3131
}: ModalInputProps) {
3232
const [tagInput, setTagInput] = useState<string>("");
3333
const [tags, setTags] = useState<Tag[]>([]);
34-
3534
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
3635
const [selectedDate, setSelectedDate] = useState(defaultValue);
3736

@@ -78,10 +77,12 @@ export default function ModalInput({
7877
onValueChange(updatedTags.map((tag) => tag.text));
7978
};
8079

80+
// ✅ 마감일 포맷 수정 (YYYY-MM-DD HH:mm)
8181
const handleDateChange = (date: moment.Moment | string) => {
8282
if (moment.isMoment(date)) {
83-
setSelectedDate(date.format("YYYY.MM.DD"));
84-
onValueChange([date.format("YYYY.MM.DD")]);
83+
const formatted = date.format("YYYY-MM-DD HH:mm"); // ← 이 줄이 핵심!
84+
setSelectedDate(formatted);
85+
onValueChange([formatted]);
8586
} else {
8687
setSelectedDate(date);
8788
onValueChange([date]);

0 commit comments

Comments
 (0)