feat: post 폴더 추가(cs)

This commit is contained in:
2025-05-13 17:37:39 +09:00
parent 2352a395fb
commit d1400a6908
65 changed files with 3010 additions and 8 deletions
@@ -0,0 +1,123 @@
import dayjs from "dayjs";
import Image from "next/image";
import type { ColumnDef } from "@tanstack/react-table";
import { Badge, Checkbox, Skeleton } from "@common/components";
import type { Post } from "@/entities/post";
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { AttachmentApi } from "@/entities/attachment";
export const createColumns = (
thumbRefs: React.MutableRefObject<Map<number, string>>,
): ColumnDef<Post>[] => [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) =>
table.toggleAllPageRowsSelected(!!checked)
}
className="h-4 w-4 rounded border-gray-300"
/>
),
cell: ({ row }) => {
const { pstTtl, inqNocs } = row.original;
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Checkbox
checked={row.getIsSelected()}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
className="h-4 w-4 rounded border-gray-300"
/>
{inqNocs >= 100 && <Badge color="red">HOT</Badge>}
{dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && (
<Badge color="blue">NEW</Badge>
)}
</div>
<span className="text-3xl font-bold leading-none text-label">
{pstTtl}
</span>
</div>
);
},
},
{
id: "atflCd",
cell: ({ row }) => {
const atflCd = row.original.atflCd;
return (
<ThumbImage
atflCd={atflCd}
pstNo={row.original.pstNo}
thumbRefs={thumbRefs}
/>
);
},
},
{
id: "rgtrNm",
cell: ({ row }) => {
const { rgtrNm } = row.original;
return (
<span className="text-[1.4rem] font-bold text-label">{rgtrNm}</span>
);
},
},
{
id: "regDt",
cell: ({ row }) => (
<span className="text-[1.2rem] text-label">
{dayjs(row.original.regDt).format("YYYY-MM-DD")}
</span>
),
},
];
type ThumbImageProps = {
atflCd: string;
pstNo: number;
thumbRefs: React.MutableRefObject<Map<number, string>>;
};
const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => {
const prevImage = thumbRefs.current.get(pstNo);
const { data: imageUrl, isLoading } = useQuery({
queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd],
queryFn: () => AttachmentApi.getImageUrlByAtflCd(atflCd),
select: (url) => {
if (url) thumbRefs.current.set(pstNo, url);
return url;
},
enabled: !prevImage && !!atflCd,
});
if (isLoading)
return (
<div className="my-6 w-full">
<Skeleton className="mx-auto h-[16rem] w-[16rem]" />
</div>
);
if (imageUrl)
return (
<div className="relative my-6 flex h-[16rem] w-full items-center justify-center">
<Image
src={imageUrl}
className="object-contain"
fill
alt="썸네일 이미지"
/>
</div>
);
return (
<div className="my-6 flex h-[16rem] w-full items-center justify-center rounded-sm border border-border bg-background">
<i className="diveicon di-image text-3xl" />
</div>
);
};
@@ -0,0 +1,141 @@
import dayjs from "dayjs";
import type { ColumnDef } from "@tanstack/react-table";
import { Badge, Button, Checkbox } from "@common/components";
import { AttachmentApi } from "@/entities/attachment";
import type { Post } from "@/entities/post";
export const columns: ColumnDef<Post & { isUpndFixPost?: boolean }>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) =>
table.toggleAllPageRowsSelected(!!checked)
}
className="h-4 w-4 rounded border-gray-300"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
className="h-4 w-4 rounded border-gray-300"
/>
),
},
{
accessorKey: "rowNo",
header: "NO",
cell: ({ row }) => {
if (row.original.isUpndFixPost) {
return (
<Badge
color="orange"
className="flex size-10 items-center justify-center"
>
<i className="diveicon di-volume-up-fill" />
</Badge>
);
}
return row.original.rowNo;
},
},
{
accessorKey: "pstTtl",
header: "제목",
cell: ({ row }) => {
const { ansPstn, pstTtl, inqNocs, cmntCnt } = row.original;
return (
<div
className="flex min-w-[20vw] items-center gap-2 text-left"
style={{ paddingInlineStart: `${(ansPstn - 1) * 20}px` }}
>
{ansPstn > 0 && (
<div className="flex items-center gap-1">
<div className="-rotate-90">
<i className="diveicon di-arrow-90left-down rotate-90" />
</div>
<Badge color="smokyBlue">답변</Badge>
</div>
)}
{inqNocs >= 100 && <Badge color="red">HOT</Badge>}
{dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && (
<Badge color="blue">NEW</Badge>
)}
{pstTtl}
{cmntCnt > 0 && (
<div className="ml-4 flex items-end gap-2">
<i className="diveicon di-chat-box-left-text text-gray-400" />
<span className="text-gray-400">{cmntCnt}</span>
</div>
)}
</div>
);
},
},
{
accessorKey: "inqNocs",
header: "조회수",
},
{
accessorKey: "atflCd",
header: "첨부파일",
cell: ({ row }) => {
const atflCd = row.original.atflCd;
if (!atflCd) return null;
return <AttachmentButton atflCd={atflCd} />;
},
},
{
accessorKey: "rgtrNm",
header: "작성자",
},
{
accessorKey: "regDt",
header: "등록일",
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
},
];
const AttachmentButton = ({ atflCd }: { atflCd: string }) => {
const handleDownload = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
try {
let filename = "";
const response = await AttachmentApi.downloadAttachmentByAtflCd(atflCd);
const contentDisposition = response.headers["content-disposition"];
if (contentDisposition) {
const filenameMatch = contentDisposition.match(
/filename\*=UTF-8''([^;]+)/,
);
if (filenameMatch) {
filename = decodeURIComponent(filenameMatch[1]);
}
}
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
} catch (error) {
console.error(error);
}
};
return (
<Button size="icon" color="white" onClick={handleDownload}>
<i className="diveicon di-download" />
</Button>
);
};