Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"react-datepicker": "^8.2.1",
"react-dom": "^18",
"react-hook-form": "^7.54.2",
"react-spinners": "^0.16.1",
"tailwind-merge": "^3.0.2",
"tailwind-scrollbar-hide": "^2.0.0",
"zod": "^3.24.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { useEffect, useState, useRef } from "react";
import { useIntersection } from "@/lib/hooks/useIntersection";
import { DashboardColumn, TaskCardList } from "@/lib/types";
import { fetchTaskCardList } from "@/lib/apis/cardsApi";
import EditColumnButton from "./EditColumnButton";
import AddTaskButton from "./AddTaskButton";
import TaskCard from "./TaskCard";
import EditColumnButton from "@/app/(after-login)/dashboard/[dashboardid]/_components/EditColumnButton";
import AddTaskButton from "@/app/(after-login)/dashboard/[dashboardid]/_components/AddTaskButton";
import TaskCard from "@/app/(after-login)/dashboard/[dashboardid]/_components/TaskCard";
import Cookies from "js-cookie";

const PAGE_SIZE = 3;

Expand All @@ -17,7 +18,8 @@ export default function Column({ id, title }: DashboardColumn) {
const [isLoading, setIsLoading] = useState(false);
const [isLast, setIsLast] = useState(false);
const observerRef = useRef<HTMLDivElement | null>(null);
const accessToken = localStorage.getItem("accessToken") ?? "";
// const accessToken = localStorage.getItem("accessToken") ?? "";
const accessToken = Cookies.get("accessToken") ?? "";
Comment on lines 20 to +22

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안쓰는 코드는 삭제하는게 어떨까요?


const handleLoad = async () => {
if (isLoading || isLast) return;
Expand Down
5 changes: 5 additions & 0 deletions src/app/(after-login)/mypage/_components/ProfileSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default function ProfileSection() {
const [profileImageUrl, setProfileImageUrl] = useState<string | null>(null);
const [imageError, setImageError] = useState<string | null>(null);
const [initialData, setInitialData] = useState<UserInfo | null>(null);
const [isImageUploading, setIsImageUploading] = useState(false);
const accessToken = Cookies.get("accessToken") ?? "";
const { openAlert } = useAlertStore();

Expand Down Expand Up @@ -73,6 +74,7 @@ export default function ProfileSection() {
if (!file) return;

setImageError(null);
setIsImageUploading(true);
try {
const res = await uploadProfileImage({ token: accessToken, image: file });
setProfileImageUrl(res.profileImageUrl);
Expand All @@ -86,6 +88,8 @@ export default function ProfileSection() {
} else {
setImageError("이미지 업로드에 실패했습니다.");
}
} finally {
setIsImageUploading(false);
}
};

Expand Down Expand Up @@ -125,6 +129,7 @@ export default function ProfileSection() {
variant="profile"
initialImageUrl={profileImageUrl}
onChange={handleImageUpload}
isLoading={isImageUploading}
/>
{imageError && <p className="text-red text-sm">{imageError}</p>}
</div>
Expand Down
39 changes: 13 additions & 26 deletions src/components/common/input/ImageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ChangeEvent, useState, useEffect } from "react";
import Image from "next/image";
import { twMerge } from "tailwind-merge";
import clsx from "clsx";
import { postImage } from "@/lib/apis/imageApi";
import { HashLoader } from "react-spinners";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spinner 컴포넌트는 지피티로도 간단하게 만들 수 있어서 굳이 라이브러리로 안해도 될 거 같아요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 라이브러리 사용보다 ai로 컴포넌트 만드는게 더 간단하게 할 수 있는 방법이군요! 참고하겠습니다🙏🏻


interface BaseImageInputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
Expand All @@ -12,6 +12,7 @@ interface BaseImageInputProps
initialImageUrl?: string | null;
onImageUrlChange?: (url: string) => void;
token?: string;
isLoading?: boolean;
}

interface TaskImageInputProps extends BaseImageInputProps {
Expand All @@ -29,6 +30,7 @@ const ImageInput = ({
label,
variant,
initialImageUrl,
isLoading = false,
...props
}: ImageInputProps) => {
const [uploadImgUrl, setUploadImgUrl] = useState<string>(
Expand All @@ -40,7 +42,7 @@ const ImageInput = ({
setUploadImgUrl(initialImageUrl || "");
}, [initialImageUrl]);

const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;

Expand All @@ -50,32 +52,9 @@ const ImageInput = ({
return;
}

const localUrl = URL.createObjectURL(file);
setUploadImgUrl(localUrl);
setError(null);

if (props.onChange) {
props.onChange(e);
return;
}

try {
const columnId =
variant === "task"
? (props as TaskImageInputProps).columnId
: undefined;
const imageUrl = await postImage(variant, columnId, file, props.token);
setUploadImgUrl(imageUrl);

if (props.onImageUrlChange) {
props.onImageUrlChange(imageUrl);
}
} catch (error: unknown) {
if (error instanceof Error) {
setError(error.message || "이미지 업로드에 실패했습니다.");
} else {
setError("알 수 없는 오류가 발생했습니다.");
}
}
};

Expand Down Expand Up @@ -105,7 +84,15 @@ const ImageInput = ({
</span>
)}
<label htmlFor={label || "file"} className={imageWrapperStyles}>
{uploadImgUrl ? (
{isLoading ? (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100 bg-opacity-75 rounded-md">
<HashLoader
color="#5534DA"
size={variant === "task" ? 30 : 50}
className="z-20"
/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

중첩 3항연산자를 사용하면 코드 가독성이 내려가요.
보통 삼항연산자는 해당 논리를 다 읽어야 이 코드가 어떻게 돌아가는지 이해가 가능한데 그 논리를 중첩으로 쓰면 더 이해하기 어렵겠죠?
그래서 이런경우에는 ts-pattern같은 라이브러리를 활용해보거나 함수로 분리하는 등 다른 방법을 고려해보는게 좋을 거 같아요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토스 ts-pattern
관련 글 찾아서 읽어봤는데 처음보는 방법이라 흥미롭네요! 견해를 넓혀주셔서 감사합니다😆

</div>
) : uploadImgUrl ? (
<Image
src={uploadImgUrl}
alt="업로드 이미지 미리보기"
Expand Down
32 changes: 29 additions & 3 deletions src/components/modal/create-task/CreateTaskModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import DateInput from "@/components/common/input/DateInput";
import ImageInput from "@/components/common/input/ImageInput";
import Textarea from "@/components/common/textarea/Textarea";
import UserIcon from "@/components/common/user-icon/UserIcon";
import dropdownIcon from "../../../../public/icon/dropdown_icon.svg";
import dropdownIcon from "public/icon/dropdown_icon.svg";
import { fetchDashboardMember } from "@/lib/apis/membersApi";
import { DashboardMember } from "@/lib/types";
import { useDashboardStore } from "@/lib/store/useDashboardStore";
import { useColumnStore } from "@/lib/store/useColumnStore";
import checkItem from "../../../../public/icon/check_icon.svg";
import checkItem from "public/icon/check_icon.svg";
import { createCard } from "@/lib/apis/cardsApi";
import { postImage } from "@/lib/apis/imageApi";
import Cookies from "js-cookie";

export default function CreateDashboardModal() {
const { dashboardId } = useDashboardStore();
Expand All @@ -26,12 +28,13 @@ export default function CreateDashboardModal() {
const [dueDate, setDueDate] = useState<string>("");
const [tags, setTags] = useState<string[]>([]);
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [isImageUploading, setIsImageUploading] = useState(false);
const [items, setItems] = useState<DashboardMember[]>([]);

const [isFormValid, setIsFormValid] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);

const accessToken = localStorage.getItem("accessToken") ?? "";
const accessToken = Cookies.get("accessToken") ?? "";

const fetchMembers = async () => {
if (!dashboardId) return;
Expand Down Expand Up @@ -112,6 +115,26 @@ export default function CreateDashboardModal() {
window.location.reload();
};

const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !selectedColumnId) return;

setIsImageUploading(true);
try {
const imageUrl = await postImage(
"task",
selectedColumnId,
file,
accessToken
);
setImageUrl(imageUrl);
} catch (error) {
console.error("이미지 업로드 에러:", error);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런부분 console이 아니라 Toast로 에러메시지 띄어주면 유저가 더 이해하기 수월하겠죠?

toast.open("이미지 업로드 에러")

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토스트 알림을 사용하면 유저 경험이 확실히 좋아질것 같다고 느꼈습니다.🥲 우선 해당 페이지 일관성을 위해서 콘솔로 에러처리 해뒀는데 다시 리팩토링 해보겠습니다 의견주셔서 너무 감사드려요!👍🏻

} finally {
setIsImageUploading(false);
}
};

if (!selectedColumnId) return;

return (
Expand Down Expand Up @@ -211,7 +234,10 @@ export default function CreateDashboardModal() {
variant="task"
columnId={selectedColumnId}
token={accessToken}
initialImageUrl={imageUrl}
onImageUrlChange={(url) => setImageUrl(url)}
onChange={handleImageUpload}
isLoading={isImageUploading}
/>
</div>
</Modal>
Expand Down
33 changes: 29 additions & 4 deletions src/components/modal/edit-task/EditTaskModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ 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 { postImage } from "@/lib/apis/imageApi";
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 ColumnDropdown from "@/components/modal/edit-task/ColumnDropdown";
import AssigneeDropdown from "@/components/modal/edit-task/AssigneeDropdown";
import { useDashboardStore } from "@/lib/store/useDashboardStore";
import Cookies from "js-cookie";

export default function EditTaskModal() {
const { openModal } = useModalStore();
const { selectedTaskId } = useTaskStore();
const { dashboardId } = useDashboardStore();
const [isLoading, setIsLoading] = useState(false);
const accessToken = localStorage.getItem("accessToken") ?? "";
const accessToken = Cookies.get("accessToken") ?? "";

const [isFormValid, setIsFormValid] = useState(false);
const [selectedColumn, setSelectedColumn] = useState(0);
Expand All @@ -30,6 +32,7 @@ export default function EditTaskModal() {
const [tagValues, setTagValues] = useState<string[]>([]);
const [dueDate, setDueDate] = useState("");
const [cardImg, setItemImg] = useState<string | null>(null);
const [isImageUploading, setIsImageUploading] = useState(false);

const [initialValues, setInitialValues] = useState<{
title: string;
Expand Down Expand Up @@ -104,6 +107,26 @@ export default function EditTaskModal() {
}
};

const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;

setIsImageUploading(true);
try {
const imageUrl = await postImage(
"task",
selectedColumn,
file,
accessToken
);
setItemImg(imageUrl);
} catch (error) {
console.error("이미지 업로드 에러:", error);
} finally {
setIsImageUploading(false);
}
};

useEffect(() => {
handleLoad();
}, []);
Expand Down Expand Up @@ -184,9 +207,11 @@ export default function EditTaskModal() {
label="이미지"
variant="task"
columnId={selectedColumn}
token={accessToken}
initialImageUrl={cardImg}
onImageUrlChange={(url) => setItemImg(url)}
token={accessToken}
onChange={handleImageUpload}
isLoading={isImageUploading}
/>
</div>
</Modal>
Expand Down