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
73 changes: 73 additions & 0 deletions src/components/common/post/detail/PostDetailActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useCallback } from "react";
import { usePostDelete } from "@/hooks/usePostDelete";

import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
} from "@/components/ui";

import type { Post } from "@/types/post.type";
import type { User } from "@/types/user.type";

interface Props {
post: Post;
user: User;
}

export default function PostDetailActions({ post, user }: Props) {
const { handleDelete } = usePostDelete();

const handleDeleteClick = useCallback(() => {
handleDelete(post.id);
}, [post.id]);

return (
<div className="flex justify-end p-4">
{post?.author === user?.id && (
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" className="h-8 w-12">
수정
</Button>
<AlertDialog>
<AlertDialogTrigger>
<Button
variant="outline"
size="icon"
className="h-8 w-12 !bg-red-300"
>
삭제
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
해당 모집 게시글을 삭제하시겠습니까?
</AlertDialogTitle>
<AlertDialogDescription>
삭제하시면 영구적으로 삭제되어 복구할 수 없습니다.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>닫기</AlertDialogCancel>
<AlertDialogAction
className="text-foreground bg-red-300 hover:bg-red-700/40"
onClick={handleDeleteClick}
>
삭제
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)}
</div>
);
}
18 changes: 18 additions & 0 deletions src/components/common/post/detail/PostDetailContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Editor } from "@/components/write";
import type { Post } from "@/types/post.type";

interface Props {
post: Post;
}

export default function PostDetailContent({ post }: Props) {
return (
<div className="mt-12 mb-12 flex w-full justify-center">
<main>
<div className="w-full max-w-3xl">
{post?.content && <Editor props={post?.content} readonly />}
</div>
</main>
</div>
);
}
26 changes: 26 additions & 0 deletions src/components/common/post/detail/PostDetailHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Post } from "@/types/post.type";
import { Separator } from "@/components/ui";
import dayjs from "dayjs";

interface Props {
post: Post;
}

export default function PostDetailHeader({ post }: Props) {
return (
<header className="mt-12 flex w-full flex-col items-center">
<div className="flex h-full w-full flex-col items-center">
<span className="mb-4 text-lg"># {post?.category}</span>
<h1 className="scroll-m-20 text-center text-xl font-extrabold tracking-tight sm:text-2xl md:text-4xl">
{post?.title}
</h1>

<Separator className="bg-foreground my-6 !w-6" />

<span className="text-md md:text-lg">
{dayjs(post?.created_at).format("YYYY. MM. DD")}
</span>
</div>
</header>
);
}
103 changes: 103 additions & 0 deletions src/components/common/post/detail/PostDetailMeta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { Post } from "@/types/post.type";
import { Badge } from "@/components/ui";

interface Props {
post: Post;
}

export default function PostDetailMeta({ post }: Props) {
return (
<section className="mx-auto mt-10 flex w-[90%] max-w-2xl flex-col">
<div className="rounded-xl border-1 p-6">
<div className="grid grid-cols-2 gap-y-5 text-lg">
{/* 모집 인원 */}
<div className="flex items-center gap-4">
<span className="text-sm font-semibold text-gray-500 md:text-lg">
모집 인원
</span>
<span className="text-sm font-bold md:text-lg">
{post?.members}
</span>
</div>

{/* 진행 방식 */}
<div className="flex items-center gap-4">
<span className="text-sm font-semibold text-gray-500 md:text-lg">
진행 방식
</span>
<span className="text-sm font-bold md:text-lg">
{post?.progress_method}
</span>
</div>

{/* 예상 기간 */}
<div className="flex items-center gap-4">
<span className="text-sm font-semibold text-gray-500 md:text-lg">
예상 기간
</span>
<span className="text-sm font-bold md:text-lg">
{post?.duration}
</span>
</div>

{/* 연락 수단 */}
<div className="flex items-center gap-4">
<span className="text-sm font-semibold text-gray-500 md:text-lg">
연락 수단
</span>
<Badge
variant="outline"
className="bg-slate-50 text-xs font-bold text-slate-500 md:text-base"
>
<a
href={post?.contact_url}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{post?.contact}
</a>
</Badge>
</div>
</div>

{/* 모집 분야 */}
<div className="mt-5 flex flex-col gap-6">
<div className="flex flex-wrap items-center gap-4">
<span className="text-sm font-semibold text-gray-500 md:text-lg">
모집 분야
</span>
<div className="flex flex-wrap gap-2">
{post?.position?.map((pos: string) => (
<Badge
key={pos}
variant="outline"
className="bg-slate-50 text-xs font-bold text-slate-500 md:text-base"
>
{pos}
</Badge>
))}
</div>
</div>

{/* 기술 스택 */}
<div className="flex flex-wrap items-center gap-5">
<span className="text-sm font-semibold text-gray-500 md:text-lg">
기술 스택
</span>
<div className="flex flex-wrap gap-2">
{post?.tech_stack?.map((tech: string) => (
<img
key={tech}
src={`/images/icons/tech/${tech.toLowerCase()}.svg`}
alt={tech}
className="size-5 md:size-7"
/>
))}
</div>
</div>
</div>
</div>
</section>
);
}
30 changes: 30 additions & 0 deletions src/hooks/usePostDelete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import supabase from "@/lib/supabase";
import { useCallback } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";

export const usePostDelete = () => {
const navigate = useNavigate();

const handleDelete = useCallback(async (id: number) => {
if (!id) {
toast.error("게시글 ID를 찾을 수 없습니다.");
return;
}
try {
const { error } = await supabase.from("post").delete().eq("id", id);

if (error) {
toast.error(error.message);
return;
}

toast.success("글을 삭제하였습니다.");
navigate("/");
} catch (error) {
console.log(error);
throw error;
}
}, []);
return { handleDelete };
};
Loading