Skip to content

Commit

Permalink
잠깐만
Browse files Browse the repository at this point in the history
  • Loading branch information
ipcgrdn committed Dec 22, 2024
1 parent 93b238b commit f0dd8f2
Show file tree
Hide file tree
Showing 8 changed files with 397 additions and 37 deletions.
95 changes: 59 additions & 36 deletions src/app/album/[albumId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,52 @@ import {
} from "@/components/ui/table";

import Image from "next/image";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { useMediaQuery } from "react-responsive";

import useStreamingBar from "@/hooks/modal/use-streaming-bar";
import { cn } from "@/lib/utils";
import { useEffect, useState } from "react";
import { Album, Profile, Track, getAlbumById } from "@/services/albumService";
import { toast } from "sonner";
import useInformationModal from "@/hooks/modal/use-information-modal";
import useAlbumEditModal from "@/hooks/modal/use-albumEdit-modal";

export default function AlbumPage() {
const [albumData, setAlbumData] = useState<Album>();
const [albumTrack, setAlbumTrack] = useState<Track[]>([]);
const [albumProfile, setAlbumProfile] = useState<Profile>();

const router = useRouter();
const streamingBar = useStreamingBar();
const informationModal = useInformationModal();
const albumEditModal = useAlbumEditModal();

const isMobile = useMediaQuery({ maxWidth: 768 });
const pathname = usePathname();
const uuid = String(pathname.split("/").pop());

useEffect(() => {
const getAlbum = async () => {
try {
const data = await getAlbumById(uuid);
setAlbumData(data.albumResponseDto);
setAlbumTrack(data.trackResponseDtos);
setAlbumProfile(data.profileResponseDto);
} catch (error) {
console.error("앨범 로딩 실패:", error);
}
};

const dummy = [
{
id: 1,
title: "피곤해",
duration: "3:02",
},
{
id: 2,
title: "피곤해",
duration: "3:02",
},
{
id: 3,
title: "피곤해",
duration: "3:02",
},
{
id: 4,
title: "피곤해",
duration: "3:02",
},
];
getAlbum();
}, []);

const handleShareClick = () => {
navigator.clipboard
.writeText(window.location.href)
.then(() => toast.success("링크가 복사되었습니다!"))
.catch(() => toast.error("복사 실패!"));
};

return (
<main
Expand All @@ -64,8 +77,8 @@ export default function AlbumPage() {
<div className="flex w-full flex-row md:h-[250px] gap-x-8 pr-2">
<div className="h-full w-full flex flex-col items-center justify-center group">
<Image
src="/images/music1.png"
alt="albumCover"
src={albumData ? albumData?.artImage : ""}
alt="album"
width={250}
height={250}
className="rounded-xl group-hover:opacity-75"
Expand All @@ -79,27 +92,35 @@ export default function AlbumPage() {
</div>
<div className="flex flex-col w-full h-full items-start justify-between py-2 gap-y-4">
<div
onClick={() => router.push("/user/123")}
onClick={() => router.push(`/profile/${albumProfile?.name}`)}
className="flex gap-x-2 items-center"
>
<Avatar className="w-6 h-6 lg:w-10 lg:h-10">
<AvatarImage src="/images/music1.png" alt="profile" />
<AvatarImage
src={`${albumProfile?.profileImage}`}
alt="profile"
/>
<AvatarFallback>U</AvatarFallback>
</Avatar>
<span className="text-sm hover:underline truncate">RARO YANG</span>
<span className="text-sm hover:underline truncate">
{albumProfile?.name}
</span>
</div>
<div className="tracking-wide text-3xl md:text-4xl lg:text-5xl font-extrabold truncate">
THIRSTY
{albumData?.title}
</div>
<div className="flex w-full items-center justify-between">
<div className="flex gap-x-2 items-center justify-center mr-2 lg:mr-0">
<IconHeart className="size-6" />
<span className="text-base">13.1k</span>
</div>
<div className="flex gap-x-3">
<IconShare className="size-6" />
<IconInfoCircle className="size-6" />
<IconDotsVertical className="size-6" />
<IconShare onClick={handleShareClick} className="size-6 cursor-pointer" />
<IconInfoCircle
onClick={() => informationModal.onOpen(albumData)}
className="size-6 cursor-pointer"
/>
<IconDotsVertical onClick={albumEditModal.onOpen} className="size-6 cursor-pointer" />
</div>
</div>
</div>
Expand All @@ -120,13 +141,15 @@ export default function AlbumPage() {
</TableRow>
</TableHeader>
<TableBody>
{dummy.map((song) => (
{albumTrack.map((song) => (
<TableRow
key={song.id}
key={song.uuid}
className="group hover:bg-[#7E47631F] dark:hover:bg-white/10 h-12"
>
<TableCell className="w-[50px] pr-4">
<div className="flex group-hover:hidden pr-4">{song.id}</div>
<div className="flex group-hover:hidden pr-4">
{song.uuid}
</div>
<div
onClick={() => streamingBar.onOpen()}
className="hidden group-hover:flex text-[#FF239C]"
Expand All @@ -136,7 +159,7 @@ export default function AlbumPage() {
</TableCell>
<TableCell className="w-full truncate">{song.title}</TableCell>
<TableCell></TableCell>
<TableCell className="text-right">{song.duration}</TableCell>
<TableCell className="text-right">{song.title}</TableCell>
<TableCell>
<IconDotsVertical className="size-6" />
</TableCell>
Expand Down
2 changes: 1 addition & 1 deletion src/components/container/square-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const SquareContainer = ({

return (
<div className="flex flex-col h-60 w-52 bg-transparent items-center justify-center hover:bg-[#7E47631F] dark:hover:bg-gradient-to-b dark:from-[#D8C2DC4D] dark:to-[#2D1E274D] rounded-lg transition hover:cursor-pointer group">
<div className="relative flex items-center justify-center h-40 w-40">
<div className="relative flex items-center justify-center h-40 w-40 overflow-hidden">
<Image
src={src}
alt="profile"
Expand Down
232 changes: 232 additions & 0 deletions src/components/modal/album/albumEdit-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
"use client";

import { z } from "zod";
import axios, { AxiosResponse } from "axios";
import { toast } from "sonner";
import { useState } from "react";
import { FieldValues, SubmitHandler, useForm } from "react-hook-form";

import { IconUserCircle } from "@tabler/icons-react";
import { zodResolver } from "@hookform/resolvers/zod";

import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { CustomModal } from "@/components/modal/custom-modal";
import { api } from "@/lib/axios";
import { usePathname, useRouter } from "next/navigation";
import useAlbumEditModal from "@/hooks/modal/use-albumEdit-modal";
import ModalTitle from "../modal-title";

const AlbumEditModal = () => {
const [file, setFile] = useState<File | null>(null);
const [isLoading, setIsloading] = useState(false);
const albumEditModal = useAlbumEditModal();
const router = useRouter();

const pathname = usePathname();
const uuid = String(pathname.split("/").pop());

const onChange = (open: boolean) => {
if (!open) {
reset();
albumEditModal.onClose();
}
};

const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files && files.length > 0) {
const file = files[0];
if (!file.type.startsWith("image/")) {
toast.error("이미지 파일만 업로드 가능합니다.");
return;
}

setFile(file);
}
};

const uploadToS3 = async (file: File) => {
const formData = new FormData();
formData.append("file", file);

const response = await api.post(`/upload/images`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});

if (response.status === 201) {
return response.data;
} else {
throw new Error("이미지 업로드에 실패했습니다.");
}
};

const FormSchema = z.object({
title: z
.string()
.min(1, "1자 이상 입력하세요")
.max(20, "20자 이하로 입력하세요"),
description: z
.string()
.max(100, "소개는 100자 이내로 작성하세요")
.nullable()
.transform((val) => val ?? ""),
albumArt: z.string().nullable().optional(),
});

const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(FormSchema),
defaultValues: {
title: "",
description: "",
albumArt: null,
},
});

const onSubmit: SubmitHandler<FieldValues> = async (values) => {
if (!uuid) {
toast.error("앨범 정보를 찾을 수 없습니다.");
return;
}

setIsloading(true);

try {
const albumArtUrl = file ? (await uploadToS3(file)).url : "";

const response = await api.patch(`/album/${uuid}`, {
title: values.title,
description: values.description,
albumArt: albumArtUrl,
});

if (isSuccessResponse(response)) {
handleSuccess();
} else {
throw new Error("앨범 수정에 실패했습니다.");
}
} catch (error) {
handleError(error);
} finally {
setIsloading(false);
}
};

const handleSuccess = () => {
toast.success("앨범이 수정되었습니다.");
reset();
albumEditModal.onClose();
router.refresh();
};

const handleError = (error: unknown) => {
if (axios.isAxiosError(error)) {
console.error("서버 응답:", error.response);
toast.error(
error.response?.data?.message ||
error.response?.data?.detail ||
"앨범 수정 중 오류가 발생했습니다."
);
} else {
console.error("에러 상세:", error);
toast.error("앨범 수정 과정에서 오류가 발생했습니다.");
}
};

const isSuccessResponse = (response: AxiosResponse) => {
return response.status >= 200 && response.status < 300;
};

return (
<CustomModal
title={
<ModalTitle
icon={<IconUserCircle className="size-10 p-1" />}
title="앨범 편집"
/>
}
description="앨범을 소개해주세요"
isOpen={albumEditModal.isOpen}
onChange={onChange}
className="p-4 flex flex-col items-center justify-center"
>
<form
className="flex flex-col h-full w-full items-center justify-center"
onSubmit={handleSubmit(onSubmit)}
>
<div className="flex flex-col gap-y-4 items-center justify-center h-full w-full rounded-md overflow-y-auto">
<Input
id="albumArt"
type="file"
{...register("albumArt", { required: false })}
disabled={isLoading}
className="hidden"
onChange={handleFileUpload}
/>
<label
htmlFor="albumArt"
className="p-[1px] w-full flex bg-gradient-to-br from-[#FF00B1] to-[#875BFF] h-auto cursor-pointer rounded-lg"
>
<div className="w-full bg-neutral-200 dark:bg-neutral-800 rounded-lg transition duration-200 text-black dark:text-white hover:bg-opacity-75 text-sm items-center flex justify-between p-4">
<span className="text-gray-400 truncate max-w-[70%]">
{file ? file.name : "🖼️ 앨범 커버"}
</span>
<span className="bg-black text-white dark:bg-white dark:text-black px-4 py-1 rounded-lg text-sm">
커버 변경
</span>
</div>
</label>
<Input
id="title"
disabled={isLoading}
{...register("title", { required: true })}
placeholder="🚨 앨범 제목"
className="w-full h-14"
/>
<p className={errors.title ? "text-red-500 text-xs" : "hidden"}>
{errors.title ? String(errors.title.message) : null}
</p>
<Textarea
id="description"
disabled={isLoading}
{...register("description", { required: false })}
placeholder="📝 소개"
className="w-full h-full resize-none"
/>
<p className={errors.description ? "text-red-500 text-xs" : "hidden"}>
{errors.description ? String(errors.description.message) : null}
</p>
</div>
<div className="flex items-center justify-around w-full pt-10">
<button
className="p-[3px] relative"
onClick={() => albumEditModal.onClose()}
disabled={isLoading}
>
<div className="px-8 py-2 bg-white rounded-xl relative group text-black hover:bg-neutral-100 text-sm dark:bg-black/95 dark:text-white dark:hover:bg-neutral-800">
취소
</div>
</button>
<button
className="p-[3px] relative"
type="submit"
disabled={isLoading}
>
<div className="px-8 py-2 bg-[#FF3F8F] rounded-xl relative group transition duration-200 text-white hover:bg-opacity-75 text-sm">
확인
</div>
</button>
</div>
</form>
</CustomModal>
);
};

export default AlbumEditModal;
Loading

0 comments on commit f0dd8f2

Please sign in to comment.