feat: post 폴더 추가(cs)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user