diff --git a/src/apis/tag.ts b/src/apis/tag.ts index c42f521c..084e9b7d 100644 --- a/src/apis/tag.ts +++ b/src/apis/tag.ts @@ -1,10 +1,16 @@ -import http from "@/apis/core"; import { + GetPopularTagsResponse, GetTagsResponse, GetTopTagsFromUploadedResponse, GetTopTagsFromLikedResponse, PostTagResponse, } from "@/types/tag.dto"; +import http from "./core"; + +export const getPopularTags = () => + http.get({ + url: `/v1/tag/popular`, + }); export const getSearchTag = (tag: string) => http.get({ diff --git a/src/apis/zzal.ts b/src/apis/zzal.ts index d0cf7c73..805908d9 100644 --- a/src/apis/zzal.ts +++ b/src/apis/zzal.ts @@ -1,8 +1,27 @@ -import { GetMyLikedZzalsResponse, GetZzalDetailsResponse, GetZzalResponse } from "@/types/zzal.dto"; +import { + PostUploadZzalRequest, + GetMyLikedZzalsResponse, + GetZzalDetailsResponse, + GetZzalResponse, +} from "@/types/zzal.dto"; import http from "./core"; - import { PAGINATION_LIMIT } from "@/constants/api"; +export const postUploadZzal = ({ file, tagIdList, title }: PostUploadZzalRequest) => { + const formData = new FormData(); + formData.append("file", file); + formData.append("tagIdList", tagIdList.join(",")); + formData.append("title", title); + + return http.post({ + url: `/v1/image`, + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); +}; + export const deleteMyZzal = (imageId: number) => { return http.delete({ url: `/v1/image/${imageId}` }); }; diff --git a/src/components/UploadZzal/ImageUpload.tsx b/src/components/UploadZzal/ImageUpload.tsx index caf7e256..4036acbc 100644 --- a/src/components/UploadZzal/ImageUpload.tsx +++ b/src/components/UploadZzal/ImageUpload.tsx @@ -1,29 +1,29 @@ import { Fragment, useRef, useState, DragEvent, ChangeEvent } from "react"; import { Upload } from "lucide-react"; import { cn } from "@/utils/tailwind"; -import ZzalCard from "@/components/common/ZzalCard"; + interface Props { - onChange: (file: File | null) => void; + changeFile: (file: File | null) => void; + file: File | null; } -const ImageUpload = ({ onChange }: Props) => { - const [previewUrl, setPreviewUrl] = useState(null); +const ImageUpload = ({ changeFile, file }: Props) => { const [dragging, setDragging] = useState(false); const inputRef = useRef(null); const handleClickDeleteButton = () => { - onChange(null); - setPreviewUrl(null); + changeFile(null); }; const handleFileChange = (event: ChangeEvent) => { const { files } = event.target; - if (files) { - const [changedFile] = files; - if (changedFile) { - onChange(changedFile); - setPreviewUrl(URL.createObjectURL(changedFile)); - } + + if (!files) return; + + const [changedFile] = files; + + if (changedFile) { + changeFile(changedFile); } }; @@ -37,6 +37,7 @@ const ImageUpload = ({ onChange }: Props) => { event.preventDefault(); const { items } = event.dataTransfer; + if (items?.length > 0) { setDragging(true); } @@ -56,23 +57,23 @@ const ImageUpload = ({ onChange }: Props) => { event.preventDefault(); event.stopPropagation(); - const { dataTransfer } = event; - if (dataTransfer) { - const [changedFile] = dataTransfer.files; - if (changedFile) { - onChange(changedFile); - setPreviewUrl(URL.createObjectURL(changedFile)); - } + const { files } = event.dataTransfer; + + const [changedFile] = files; + + if (changedFile) { + changeFile(changedFile); } + setDragging(false); }; return (
{ onDragLeave={handleDragLeave} onDragOver={handleDragOver} > - {previewUrl && ( - + {file && ( + + 업로드 사진 + + )} - {!previewUrl && ( + {!file && (
@@ -105,7 +113,6 @@ const ImageUpload = ({ onChange }: Props) => { /> )} - {previewUrl && }
); }; diff --git a/src/components/UploadZzal/UploadGuide.tsx b/src/components/UploadZzal/UploadGuide.tsx index 1ed8d070..af8d8e43 100644 --- a/src/components/UploadZzal/UploadGuide.tsx +++ b/src/components/UploadZzal/UploadGuide.tsx @@ -2,7 +2,7 @@ import { Info } from "lucide-react"; const UploadGuide = () => { return ( -
+
업로드를 위해 필수로 사진을 선택하고 1개 이상의 태그를 등록해주세요! diff --git a/src/components/common/RecommendTag.tsx b/src/components/common/RecommendTag.tsx index f55bef02..4eaa0e1b 100644 --- a/src/components/common/RecommendTag.tsx +++ b/src/components/common/RecommendTag.tsx @@ -1,21 +1,23 @@ import { cn } from "@/utils/tailwind"; +import { Tag } from "@/types/tag"; import TagBadge from "./TagBadge"; interface Props { + title?: string; + recommendTags: Tag[]; className?: string; } -const RecommendTag = ({ className }: Props) => { +const RecommendTag = ({ title, recommendTags, className }: Props) => { // TODO: [2024.02.14] api 요청 예정 - const recommendTags = ["분노", "스트레스", "박명수", "직장인", "잠좀자자"]; return (
-
내가 가장 많이 사용한 태그
+
{title}
    - {recommendTags.map((recommendTag, index) => ( -
  • - + {recommendTags.map(({ tagId, tagName }) => ( +
  • +
  • ))}
diff --git a/src/components/common/ZzalCard.tsx b/src/components/common/ZzalCard.tsx index 32855020..cc2bd981 100644 --- a/src/components/common/ZzalCard.tsx +++ b/src/components/common/ZzalCard.tsx @@ -46,7 +46,7 @@ const ZzalCard = ({ hasAnimation ? "transition duration-300 ease-in-out hover:brightness-75" : "none", )} > - {alt} + {src && {alt}}
diff --git a/src/hooks/api/tag/useGetPopularTags.ts b/src/hooks/api/tag/useGetPopularTags.ts new file mode 100644 index 00000000..2e786c9d --- /dev/null +++ b/src/hooks/api/tag/useGetPopularTags.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import { getPopularTags } from "@/apis/tag"; + +const useGetPopularTags = () => { + const { data, ...rest } = useQuery({ + queryKey: ["popularTags"], + queryFn: () => getPopularTags(), + }); + + return { + popularTags: data || [], + ...rest, + }; +}; + +export default useGetPopularTags; diff --git a/src/hooks/api/zzal/usePostUploadZzal.ts b/src/hooks/api/zzal/usePostUploadZzal.ts new file mode 100644 index 00000000..bfcf1305 --- /dev/null +++ b/src/hooks/api/zzal/usePostUploadZzal.ts @@ -0,0 +1,18 @@ +import { useMutation } from "@tanstack/react-query"; +import { postUploadZzal } from "@/apis/zzal"; + +interface Props { + file: File; + tagIdList: Array; + title: string; +} + +const usePostUploadZzal = () => { + const { mutate, ...rest } = useMutation({ + mutationFn: ({ file, tagIdList, title }: Props) => postUploadZzal({ file, tagIdList, title }), + }); + + return { uploadZzal: mutate, ...rest }; +}; + +export default usePostUploadZzal; diff --git a/src/index.css b/src/index.css index 2ffbceb0..ff6aacf1 100644 --- a/src/index.css +++ b/src/index.css @@ -34,3 +34,7 @@ clip: rect(0 0 0 0); clip-path: polygon(0 0, 0 0, 0 0); } + +:root { + --toastify-color-success: #246fff; +} diff --git a/src/routes/upload-zzal/index.tsx b/src/routes/upload-zzal/index.tsx index 6ced409b..4be36a52 100644 --- a/src/routes/upload-zzal/index.tsx +++ b/src/routes/upload-zzal/index.tsx @@ -1,21 +1,105 @@ -import { useState } from "react"; -import { createFileRoute } from "@tanstack/react-router"; +import { ChangeEvent, useState } from "react"; +import { toast } from "react-toastify"; +import { Link, createFileRoute } from "@tanstack/react-router"; +import { useAtom } from "jotai"; +import UploadGuide from "@/components/UploadZzal/UploadGuide"; import ImageUpload from "@/components/UploadZzal/ImageUpload"; +import RecommendTag from "@/components/common/RecommendTag"; import TagSearchForm from "@/components/common/SearchTag/TagSearchForm"; +import usePostUploadZzal from "@/hooks/api/zzal/usePostUploadZzal"; +import useGetPopularTags from "@/hooks/api/tag/useGetPopularTags"; +import { $selectedTags } from "@/store/tag"; const UploadZzal = () => { - const [, setFile] = useState(null); - const handleChangeUpload = (changedFile: File | null) => setFile(changedFile); + const { popularTags } = useGetPopularTags(); + const { uploadZzal } = usePostUploadZzal(); + const [file, setFile] = useState(null); + const [imageTitle, setImageTitle] = useState(""); + const [selectedTags, setSelectedTags] = useAtom($selectedTags); + + const changeFile = (file: File | null) => { + setFile(file); + }; + + const handleClickUploadButton = () => { + if (!file) { + toast.error("사진을 등록해주세요!"); + return; + } + + if (!imageTitle) { + toast.error("제목을 입력해주세요!"); + return; + } + + if (!selectedTags.length) { + toast.error("1개 이상의 태그를 등록해주세요!"); + return; + } + + handleUploadZzal(file); + }; + + const handleUploadZzal = (file: File) => { + uploadZzal( + { + file: file, + // TODO: [2024.02.27] 선택한 태그의 Id를 전달하는 코드 구현 후, 실제 selectedTags Id 넘겨주기 + tagIdList: [2, 3, 4], + title: imageTitle, + }, + { + onSuccess: () => { + toast.success( +
+ 성공적으로 업로드가 되었습니다. + + + +
, + ), + changeFile(null), + setImageTitle(""), + setSelectedTags([]); + }, + onError: () => { + toast.error("사진 업로드에 실패했습니다."); + }, + }, + ); + }; + + const handleChangeImageTitle = (event: ChangeEvent) => { + setImageTitle(event.target.value); + }; return ( -
-
- -
- +
+
짤 업로드
+ +
+
+ +
+
+ 짤 제목 +
+ +
+ + diff --git a/src/types/tag.dto.ts b/src/types/tag.dto.ts index 3d5451ad..55b10a10 100644 --- a/src/types/tag.dto.ts +++ b/src/types/tag.dto.ts @@ -2,6 +2,8 @@ import { Tag } from "./tag"; export type GetTagsResponse = Tag[]; +export type GetPopularTagsResponse = Tag[]; + export type GetTopTagsFromUploadedResponse = Tag[]; export type GetTopTagsFromLikedResponse = Tag[]; diff --git a/src/types/zzal.dto.ts b/src/types/zzal.dto.ts index 63c31c37..678faff9 100644 --- a/src/types/zzal.dto.ts +++ b/src/types/zzal.dto.ts @@ -1,5 +1,11 @@ import { Tag } from "./tag"; +export interface PostUploadZzalRequest { + file: File; + tagIdList: Array; + title: string; +} + export interface GetMyLikedZzalsResponse { imageId: number; path: string;