Skip to content

Commit

Permalink
프로필 삭제 기능, 프로필 내 앨범 조회 기능 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
ipcgrdn committed Dec 23, 2024
1 parent 1d9780c commit 6c21352
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 59 deletions.
31 changes: 31 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 @@ -11,6 +11,7 @@
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.0",
Expand Down
4 changes: 2 additions & 2 deletions src/app/album/[albumId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default function AlbumPage() {
<div>
{albumProfile && (
<div
onClick={() => router.push(`/profile/${albumProfile.name}`)}
onClick={() => router.push(`/user/${albumProfile.uuid}`)}
className="flex gap-x-2 items-center"
>
<Avatar className="w-6 h-6 lg:w-10 lg:h-10">
Expand Down Expand Up @@ -172,7 +172,7 @@ export default function AlbumPage() {
</TableCell>
<TableCell className="w-full truncate">{song.title}</TableCell>
<TableCell></TableCell>
<TableCell className="text-right">{song.title}</TableCell>
<TableCell className="text-right">{song.duration}</TableCell>
<TableCell>
<IconDotsVertical className="size-6" />
</TableCell>
Expand Down
33 changes: 27 additions & 6 deletions src/app/user/[userId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
Expand All @@ -31,10 +34,12 @@ import { Profile, getProfile } from "@/services/profileService";
import useProfileModal from "@/hooks/modal/use-profile-modal";
import { useUser } from "@/provider/userProvider";
import useProfileEditModal from "@/hooks/modal/use-profileEdit-modal";
import useConfirmModal from "@/hooks/modal/use-confirm-modal";

export default function UserPage() {
const streamingBar = useStreamingBar();
const profileModal = useProfileModal();
const confirmModal = useConfirmModal();
const profileEditModal = useProfileEditModal();
const isMobile = useMediaQuery({ maxWidth: 768 });

Expand All @@ -58,6 +63,10 @@ export default function UserPage() {
fetchProfile();
}, [user]);

const handleConfirm = (uuid: string) => {
confirmModal.onOpen(uuid);
};

const tabs = [
{ id: "track", label: "트랙", icon: IconMusic, onClick: () => {} },
{ id: "album", label: "앨범", icon: IconDisc, onClick: () => {} },
Expand Down Expand Up @@ -94,12 +103,24 @@ export default function UserPage() {
<div className="md:text-3xl font-bold text-2xl">
{profileData?.name || "U"}
</div>
<div
onClick={profileEditModal.onOpen}
className="cursor-pointer"
>
<IconDotsVertical className="size-6 hover:opacity-75" />
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconDotsVertical className="size-6 hover:opacity-75" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>프로필 설정</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={profileEditModal.onOpen}>
프로필 편집
</DropdownMenuItem>
<DropdownMenuItem
className="text-red-500"
onClick={() => handleConfirm(profileData?.uuid || "")}
>
프로필 삭제
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex gap-x-4">
<button className="bg-white text-black hover:bg-black/10 dark:hover:bg-white/75 shadow-lg w-auto font-medium text-sm p-2 rounded-lg">
Expand Down
118 changes: 118 additions & 0 deletions src/components/modal/confirm-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"use client";

import useConfirmModal from "@/hooks/modal/use-confirm-modal";
import { useRouter } from "next/navigation";
import { CustomModal } from "./custom-modal";
import ModalTitle from "./modal-title";
import { IconAlertSquareRounded } from "@tabler/icons-react";
import { Checkbox } from "../ui/checkbox";
import { useState } from "react";
import { deleteProfile } from "@/services/profileService";
import { toast } from "sonner";
import axios from "axios";

const ConfirmModal = () => {
const [loading, setLoading] = useState(false);
const [checked, setChecked] = useState(false);

const router = useRouter();
const confirmModal = useConfirmModal();

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

const handleDelete = async () => {
if (!confirmModal.uuid) {
toast.error("프로필을 찾을 수 없습니다.");
return;
}

try {
setLoading(true);

const uuid = confirmModal.uuid;
const response = await deleteProfile(uuid);

if (!response) {
throw new Error(
`이미지 수정에 실패했습니다. 상태 코드: ${response.status}`
);
}

toast.success("프로필이 삭제되었습니다.");
confirmModal.onClose();
router.refresh();
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("서버 응답:", error.response);
toast.error(
error.response?.data?.message ||
error.response?.data?.detail ||
"프로필 수정 중 오류가 발생했습니다."
);
} else {
console.error("에러 상세:", error);
toast.error("프로필 수정 과정에서 오류가 발생했습니다.");
}
} finally {
setLoading(false);
}
};

return (
<CustomModal
title={
<ModalTitle
icon={<IconAlertSquareRounded className="size-10 p-1" />}
title="경고"
/>
}
description="이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?"
isOpen={confirmModal.isOpen}
onChange={onChange}
className="p-4 flex flex-col items-center justify-center h-[50%]"
>
<div className="flex flex-col items-center justify-center w-full h-full">
<div className="flex w-full items-center space-x-2">
<Checkbox
id="check"
checked={checked}
onCheckedChange={() => setChecked}
/>
<label
htmlFor="check"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
주의사항을 확인하였으며, 삭제에 동의합니다.
</label>
</div>

<div className="flex items-center justify-around w-full pt-10">
<button
className="p-[3px] relative"
onClick={confirmModal.onClose}
disabled={loading}
>
<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"
onClick={handleDelete}
disabled={!checked || loading}
>
<div className="px-8 py-2 bg-[#FF3F8F] rounded-xl relative group transition duration-200 text-white hover:bg-opacity-75 text-sm disabled:cursor-not-allowed disabled:opacity-70">
삭제
</div>
</button>
</div>
</div>
</CustomModal>
);
};

export default ConfirmModal;
29 changes: 29 additions & 0 deletions src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client"

import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { cn } from "@/lib/utils"
import { CheckIcon } from "@radix-ui/react-icons"

const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-neutral-200 shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 dark:border-neutral-800 dark:focus-visible:ring-neutral-300 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=checked]:text-neutral-900",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName

export { Checkbox }
81 changes: 30 additions & 51 deletions src/features/user/user-album.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,46 @@
'use client'

import { useEffect, useState } from "react";
import { usePathname, useRouter } from "next/navigation";

import { Album, getAlbumByProfileUUID } from "@/services/albumService";
import SquareContainer from "@/components/container/square-container";
import { useRouter } from "next/navigation";

const UserAlbum = () => {
const [albums, setAlbums] = useState<Album[]>([]);

const router = useRouter();
const pathname = usePathname();
const profileUUID = String(pathname.split("/").pop());

useEffect(() => {
const getAlbums = async () => {
try {
const data = await getAlbumByProfileUUID(profileUUID);
setAlbums(data);
} catch (error) {
console.error("앨범 로딩 실패:", error);
}
};

const dummy = [
{
id: 1,
src: "/images/music1.png",
name: "ROCK-STAR",
description: "2024 · IPCGRDN",
onClickName: () => router.push("/album/123"),
},
{
id: 2,
src: "/images/music1.png",
name: "ROCK-STAR",
description: "2024 · IPCGRDN",
onClickName: () => router.push("/album/123"),
},
{
id: 3,
src: "/images/music1.png",
name: "ROCK-STAR",
description: "2024 · IPCGRDN",
onClickName: () => router.push("/album/123"),
},
{
id: 4,
src: "/images/music1.png",
name: "BREAK",
description: "2018 · PALM",
onClickName: () => router.push("/album/123"),
},
{
id: 5,
src: "/images/music1.png",
name: "Thirsty",
description: "1988 · RARO",
onClickName: () => router.push("/album/123"),
},
{
id: 6,
src: "/images/music1.png",
name: "산책",
description: "EP · BDD",
onClickName: () => router.push("/album/123"),
},
];
getAlbums();
}, [profileUUID]);

if (!albums.length) {
return null;
}

return (
<div className="h-full w-full">
<div className="w-full gap-x-2 grid grid-cols-2 xl:grid-cols-4">
{dummy.map((item) => (
{albums.map((album) => (
<SquareContainer
key={item.id}
src={item.src}
name={item.name}
description={item.description}
key={album.uuid}
src={album.artImage}
name={album.title}
description={album.description}
design="rounded-xl"
onClickName={item.onClickName}
onClickName={() => router.push(`/album/${album.uuid}`)}
/>
))}
</div>
Expand Down
Loading

0 comments on commit 6c21352

Please sign in to comment.