Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs_cs
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/faq";
|
||||
|
||||
export default async function PstBbsFaqPostModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FaqPage } from "@/pages/(pst)/faq";
|
||||
|
||||
export default async function PstBbsFaqPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <FaqPage bbsId={bbsId} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CreatePage } from "@/pages/(pst)/faq";
|
||||
|
||||
export default async function PstBbsFaqRegisterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <CreatePage bbsId={bbsId} />;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/movie";
|
||||
|
||||
export default async function PstBbsMoviePostModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { MoviePost } from "@/pages/(pst)/movie";
|
||||
|
||||
export default async function PstBbsMoviePostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <MoviePost bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { MoviePage } from "@/pages/(pst)/movie";
|
||||
|
||||
export default async function PstBbsMoviePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <MoviePage bbsId={bbsId} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CreatePage } from "@/pages/(pst)/movie";
|
||||
|
||||
export default async function PstBbsMovieRegisterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <CreatePage bbsId={bbsId} />;
|
||||
}
|
||||
@@ -52,7 +52,15 @@ export class AttachmentApiService implements AttachmentService {
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
public async getMovieThumbnailUrl(atflId: string): Promise<string> {
|
||||
public async getStreamVideoUrlByAtflId(atflId: string): Promise<string> {
|
||||
const response = await fetch(
|
||||
`${ENDPOINT.ATTACH_SERVICE.STREAM_VIDEO}/${atflId}`,
|
||||
);
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
public async getMovieThumbnailUrlByAtflId(atflId: string): Promise<string> {
|
||||
const response = await fetch(
|
||||
`${ENDPOINT.ATTACH_SERVICE.MOVIE_THUMBNAIL}/${atflId}`,
|
||||
);
|
||||
@@ -60,6 +68,14 @@ export class AttachmentApiService implements AttachmentService {
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
public async getMovieThumbnailUrlByAtflCd(atflCd: string): Promise<string> {
|
||||
const response = await fetch(
|
||||
`${ENDPOINT.ATTACH_SERVICE.MOVIE_THUMBNAIL}/atflCd/${atflCd}`,
|
||||
);
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
public async downloadAttachmentByAtflCd(
|
||||
atflCd: string,
|
||||
): Promise<AxiosResponse> {
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface AttachmentService {
|
||||
deleteAttachmentByAtflCd: (atflCd: string) => Promise<void>;
|
||||
getImageUrlByAtflId: (atflId: string) => Promise<string>;
|
||||
getImageUrlByAtflCd: (atflCd: string) => Promise<string>;
|
||||
getMovieThumbnailUrl: (atflId: string) => Promise<string>;
|
||||
getStreamVideoUrlByAtflId: (atflId: string) => Promise<string>;
|
||||
getMovieThumbnailUrlByAtflId: (atflId: string) => Promise<string>;
|
||||
getMovieThumbnailUrlByAtflCd: (atflCd: string) => Promise<string>;
|
||||
downloadAttachmentByAtflCd: (atflCd: string) => Promise<AxiosResponse>;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
|
||||
export class PostApiService implements PostService {
|
||||
public async getPostList(
|
||||
bbsId: string,
|
||||
bbsId: number,
|
||||
params: PostParams,
|
||||
): Promise<PostList> {
|
||||
const response = await client.get<PostList>(
|
||||
@@ -24,7 +24,7 @@ export class PostApiService implements PostService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getPostInfo(bbsId: string, pstNo: string): Promise<PostInfo> {
|
||||
public async getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo> {
|
||||
const response = await client.get<PostInfo>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}/${pstNo}`,
|
||||
);
|
||||
@@ -41,8 +41,8 @@ export class PostApiService implements PostService {
|
||||
}
|
||||
|
||||
public async updatePost(
|
||||
bbsId: string,
|
||||
pstNo: string,
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
data: PostUpdateFormData,
|
||||
): Promise<Post> {
|
||||
const validatedData = postUpdateFormSchema.parse(data);
|
||||
@@ -54,8 +54,8 @@ export class PostApiService implements PostService {
|
||||
}
|
||||
|
||||
public async deletePost(
|
||||
bbsId: string,
|
||||
pstNo: string,
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
pstPswd?: string,
|
||||
): Promise<void> {
|
||||
await client.delete(
|
||||
|
||||
@@ -8,13 +8,15 @@ export interface PostParams extends PageableParams {
|
||||
upndFixYn?: string;
|
||||
prdPstgYn?: string;
|
||||
siteId?: string;
|
||||
bbsTypeCd?: string;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
bbsId: number;
|
||||
rowNo: number;
|
||||
bbsId: number;
|
||||
pstNo: number;
|
||||
pstTtl: string;
|
||||
pstCn: string;
|
||||
inqNocs: number;
|
||||
rfrncUrl: string;
|
||||
popupYn: string;
|
||||
@@ -73,13 +75,13 @@ export type PostFormData = z.infer<typeof postFormSchema>;
|
||||
export type PostUpdateFormData = z.infer<typeof postUpdateFormSchema>;
|
||||
|
||||
export interface PostService {
|
||||
getPostList(bbsId: string, params: PostParams): Promise<PostList>;
|
||||
getPostInfo(bbsId: string, pstNo: string): Promise<PostInfo>;
|
||||
getPostList(bbsId: number, params: PostParams): Promise<PostList>;
|
||||
getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo>;
|
||||
createPost(data: PostFormData): Promise<Post>;
|
||||
updatePost(
|
||||
bbsId: string,
|
||||
pstNo: string,
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
data: PostUpdateFormData,
|
||||
): Promise<Post>;
|
||||
deletePost(bbsId: string, pstNo: string, pstPswd?: string): Promise<void>;
|
||||
deletePost(bbsId: number, pstNo: number, pstPswd?: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { FaqPage } from "./ui/faq-page";
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function CreatePage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createPost } = useMutation({
|
||||
mutationFn: (data: PostFormData) => PostApi.createPost(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostFormData>({
|
||||
resolver: zodResolver(postFormSchema),
|
||||
values: {
|
||||
bbsId: parseInt(bbsId),
|
||||
pstTtl: "",
|
||||
wrtrNm: userInfo?.userName ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">FAQ 게시글 등록</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={createPost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,316 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Skeleton,
|
||||
} from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { PostApi, type PostParams } from "@/entities/post";
|
||||
import { useAlertStore, useMenuStore, useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import dayjs from "dayjs";
|
||||
import React from "react";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
function FaqPage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { confirm } = useConfirm();
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state);
|
||||
|
||||
const [postParams, setPostParams] = useState<PostParams>({
|
||||
keywordType: "title",
|
||||
keyword: "",
|
||||
});
|
||||
const [selectedPostNumbers, setSelectedPostNumbers] = useState<number[]>([]);
|
||||
|
||||
const {
|
||||
data: postList,
|
||||
isFetched,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsInfo?.bbsTypeCd],
|
||||
queryFn: () =>
|
||||
PostApi.getPostList(parseInt(bbsId), {
|
||||
...postParams,
|
||||
bbsTypeCd: bbsInfo?.bbsTypeCd,
|
||||
}),
|
||||
enabled: !!bbsInfo?.bbsTypeCd,
|
||||
});
|
||||
|
||||
const { mutate: deletePost } = useMutation({
|
||||
mutationFn: ({ pstNo, pstPswd }: { pstNo: number; pstPswd?: string }) =>
|
||||
PostApi.deletePost(parseInt(bbsId), pstNo, pstPswd),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.");
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
const handleDeletePost = async (pstNo: number) => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
deletePost({ pstNo, pstPswd: userInfo?.userUnqId });
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectedPostDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
await Promise.all(
|
||||
selectedPostNumbers.map((pstNo) =>
|
||||
PostApi.deletePost(parseInt(bbsId), pstNo, userInfo?.userUnqId),
|
||||
),
|
||||
);
|
||||
refetch();
|
||||
setSelectedPostNumbers([]);
|
||||
alert("삭제되었습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (pstNo: number, checked: boolean | string) => {
|
||||
setSelectedPostNumbers(
|
||||
checked === true
|
||||
? [...selectedPostNumbers, pstNo]
|
||||
: selectedPostNumbers.filter((num) => num !== pstNo),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAllCheckboxChange = (checked: boolean | string) => {
|
||||
setSelectedPostNumbers(
|
||||
checked === true ? postList?.content.map((item) => item.pstNo) || [] : [],
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2 text-label">검색</Label>
|
||||
<Select
|
||||
value={postParams.keywordType}
|
||||
onValueChange={(keywordType) => {
|
||||
setPostParams({ ...postParams, keywordType });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="title">제목</SelectItem>
|
||||
<SelectItem value="all">제목+내용</SelectItem>
|
||||
<SelectItem value="writer">작성자</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={postParams.keyword}
|
||||
onChange={(e) =>
|
||||
setPostParams({ ...postParams, keyword: e.target.value })
|
||||
}
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
{pageMenuAuthority?.regAuthrtYn === "Y" && (
|
||||
<Button onClick={() => router.push(`${pathname}/register`)}>
|
||||
등록
|
||||
</Button>
|
||||
)}
|
||||
{pageMenuAuthority?.delAuthrtYn === "Y" && (
|
||||
<Button
|
||||
color="red"
|
||||
onClick={onSelectedPostDelete}
|
||||
disabled={!selectedPostNumbers.length}
|
||||
>
|
||||
선택삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
|
||||
<Checkbox
|
||||
id="all-select"
|
||||
checked={selectedPostNumbers.length === postList?.content.length}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={handleAllCheckboxChange}
|
||||
/>
|
||||
<Label htmlFor="all-select" className="text-2xl">
|
||||
전체선택
|
||||
</Label>
|
||||
</div>
|
||||
{isFetched ? (
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="secondary flex flex-col gap-4"
|
||||
>
|
||||
{postList?.content.map((item) => (
|
||||
<div key={item.pstNo} className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id={item.pstNo.toString()}
|
||||
checked={selectedPostNumbers.includes(item.pstNo)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCheckboxChange(item.pstNo, checked)
|
||||
}
|
||||
className="mx-2 my-6"
|
||||
/>
|
||||
<AccordionItem
|
||||
value={item.pstNo.toString()}
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionTrigger>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-primary">Q</span>
|
||||
<span>{item.pstTtl}</span>
|
||||
</div>
|
||||
<span className="px-4 text-[1.2rem] font-normal text-label">
|
||||
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
A
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
|
||||
{item.pstCn}
|
||||
</pre>
|
||||
<Attachment atflCd={item.atflCd} />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() =>
|
||||
router.push(`${pathname}/${item.pstNo}`)
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() => handleDeletePost(item.pstNo)}
|
||||
>
|
||||
<i className="diveicon di-trash" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
))}
|
||||
</Accordion>
|
||||
) : (
|
||||
<Skeleton className="h-[36rem] w-full" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Attachment({ atflCd }: { atflCd: string }) {
|
||||
const [filename, setFilename] = useState("");
|
||||
|
||||
const { data: downloadResponse, isFetching } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.DOWNLOAD, atflCd],
|
||||
queryFn: () => AttachmentApi.downloadAttachmentByAtflCd(atflCd),
|
||||
enabled: !!atflCd,
|
||||
});
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!downloadResponse) return;
|
||||
try {
|
||||
const blob = new Blob([downloadResponse.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = url;
|
||||
link.download = filename || "download";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!downloadResponse) return;
|
||||
try {
|
||||
let filename = "";
|
||||
const contentDisposition =
|
||||
downloadResponse.headers["content-disposition"];
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(
|
||||
/filename\*=UTF-8''([^;]+)/,
|
||||
);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = decodeURIComponent(filenameMatch[1]);
|
||||
}
|
||||
}
|
||||
setFilename(filename);
|
||||
} catch {
|
||||
setFilename("");
|
||||
}
|
||||
}, [downloadResponse]);
|
||||
|
||||
return isFetching ? (
|
||||
<div className="my-8">
|
||||
<Skeleton className="h-[2.5rem] w-[32rem]" />
|
||||
</div>
|
||||
) : (
|
||||
filename && (
|
||||
<div className="my-8 flex items-center gap-4">
|
||||
<span className="font-bold text-label">첨부파일</span>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
className="flex w-fit items-center gap-2"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<i className="diveicon di-download" />
|
||||
<span>{filename}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { FaqPage };
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import {
|
||||
PostApi,
|
||||
postUpdateFormSchema,
|
||||
type PostUpdateFormData,
|
||||
} from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostUpdateFormData>({
|
||||
resolver: zodResolver(postUpdateFormSchema),
|
||||
values: {
|
||||
...postInfo,
|
||||
pstTtl: postInfo?.pstTtl ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">FAQ 게시글 수정</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={updatePost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -20,12 +20,12 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(bbsId, pstNo),
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(bbsId, pstNo, data),
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { MoviePage } from "./ui/movie-page";
|
||||
export { MoviePost } from "./ui/movie-post";
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function CreatePage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createPost } = useMutation({
|
||||
mutationFn: (data: PostFormData) => PostApi.createPost(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostFormData>({
|
||||
resolver: zodResolver(postFormSchema),
|
||||
values: {
|
||||
bbsId: parseInt(bbsId),
|
||||
pstTtl: "",
|
||||
wrtrNm: userInfo?.userName ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">동영상 게시글 등록</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={createPost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import {
|
||||
PostApi,
|
||||
postUpdateFormSchema,
|
||||
type PostUpdateFormData,
|
||||
} from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostUpdateFormData>({
|
||||
resolver: zodResolver(postUpdateFormSchema),
|
||||
values: {
|
||||
...postInfo,
|
||||
pstTtl: postInfo?.pstTtl ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">동영상 게시글 수정</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={updatePost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { PostPage as MoviePage } from "@/widgets/post";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { Post as MoviePost } from "@/widgets/post";
|
||||
@@ -20,12 +20,12 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(bbsId, pstNo),
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(bbsId, pstNo, data),
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
|
||||
@@ -20,12 +20,12 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(bbsId, pstNo),
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(bbsId, pstNo, data),
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ export const ENDPOINT = {
|
||||
MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail",
|
||||
ATTACHMENTS: "/attach-service/api/v1/atfl/attachments",
|
||||
DOWNLOAD: "/attach-service/api/v1/atflcd/download",
|
||||
STREAM_VIDEO: "/attach-service/api/v1/stream/video",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
LOGIN: "/user-service/login",
|
||||
|
||||
@@ -7,18 +7,18 @@ export const usePrompt = () => {
|
||||
|
||||
const prompt = (message: string) => {
|
||||
setMessage(message);
|
||||
return new Promise<string>((resolve) => {
|
||||
return new Promise<{ resolved: boolean; value?: string }>((resolve) => {
|
||||
setResolve(() => resolve);
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (value: string) => {
|
||||
resolve?.(value);
|
||||
resolve?.({ resolved: true, value });
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
resolve?.("");
|
||||
resolve?.({ resolved: false });
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,15 @@ import { createContext, useState } from "react";
|
||||
|
||||
interface PromptContext {
|
||||
message?: string;
|
||||
resolve?: (value: string) => void;
|
||||
resolve?: ({
|
||||
resolved,
|
||||
value,
|
||||
}: {
|
||||
resolved: boolean;
|
||||
value?: string;
|
||||
}) => void;
|
||||
setMessage: (message: string) => void;
|
||||
setResolve: (resolve: (value: string) => void) => void;
|
||||
setResolve: (resolve: () => void) => void;
|
||||
}
|
||||
|
||||
export const PromptContext = createContext<PromptContext>({
|
||||
@@ -16,7 +22,10 @@ export const PromptContext = createContext<PromptContext>({
|
||||
|
||||
export const PromptProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [message, setMessage] = useState<string>();
|
||||
const [resolve, setResolve] = useState<(value: string) => void>();
|
||||
const [resolve, setResolve] =
|
||||
useState<
|
||||
({ resolved, value }: { resolved: boolean; value?: string }) => void
|
||||
>();
|
||||
|
||||
return (
|
||||
<PromptContext.Provider
|
||||
|
||||
@@ -56,7 +56,7 @@ function GridViewDataTable<T>({
|
||||
key={row.id}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
className={cn(
|
||||
"rounded-md border border-border bg-background p-4 shadow-md",
|
||||
"overflow-hidden rounded-md border border-border bg-background p-4 shadow-md",
|
||||
onRowClick && "cursor-pointer hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import dayjs from "dayjs";
|
||||
import Image from "next/image";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge, Checkbox, Skeleton } from "@common/components";
|
||||
import { Badge, Checkbox, Skeleton, cn } from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import type { Post } from "@/entities/post";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import type { BbsType } from "@/shared/store/menu-store";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface TableMeta<TData> {
|
||||
bbsTypeCd?: BbsType;
|
||||
}
|
||||
}
|
||||
|
||||
export const createColumns = (
|
||||
thumbRefs: React.MutableRefObject<Map<number, string>>,
|
||||
@@ -22,68 +30,120 @@ export const createColumns = (
|
||||
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"
|
||||
cell: ({ row, table }) => {
|
||||
const { atflCd, pstNo, pstTtl, inqNocs, rgtrNm, regDt } = row.original;
|
||||
const bbsTypeCd = table.options.meta?.bbsTypeCd;
|
||||
|
||||
if (bbsTypeCd === "BBSIMAGE") {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<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>
|
||||
)}
|
||||
<span className="text-[1.6rem] font-bold text-label">
|
||||
{pstTtl}
|
||||
</span>
|
||||
</div>
|
||||
<ThumbImage
|
||||
atflCd={atflCd}
|
||||
pstNo={pstNo}
|
||||
thumbRefs={thumbRefs}
|
||||
bbsTypeCd={bbsTypeCd}
|
||||
/>
|
||||
{inqNocs >= 100 && <Badge color="red">HOT</Badge>}
|
||||
{dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && (
|
||||
<Badge color="blue">NEW</Badge>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
{rgtrNm}
|
||||
</span>
|
||||
<span className="text-[1.2rem] text-label">
|
||||
{dayjs(regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-3xl font-bold leading-none text-label">
|
||||
{pstTtl}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
if (bbsTypeCd === "BBSMOVIE") {
|
||||
return (
|
||||
<div className="-m-4">
|
||||
<ThumbImage
|
||||
atflCd={atflCd}
|
||||
pstNo={pstNo}
|
||||
thumbRefs={thumbRefs}
|
||||
bbsTypeCd={bbsTypeCd}
|
||||
classes={{
|
||||
image: "object-cover",
|
||||
}}
|
||||
/>
|
||||
<div className="p-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<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(regDt), "hour") < 24 && (
|
||||
<Badge color="blue">NEW</Badge>
|
||||
)}
|
||||
<span className="text-[1.6rem] font-bold text-label">
|
||||
{pstTtl}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
{rgtrNm}
|
||||
</span>
|
||||
<span className="text-[1.2rem] text-label">
|
||||
{dayjs(regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "atflCd",
|
||||
cell: ({ row }) => {
|
||||
const { atflCd, pstNo } = row.original;
|
||||
return <ThumbImage atflCd={atflCd} pstNo={pstNo} thumbRefs={thumbRefs} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "rgtrNm",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
{row.original.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>>;
|
||||
bbsTypeCd?: string;
|
||||
classes?: {
|
||||
image?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => {
|
||||
const ThumbImage = ({
|
||||
atflCd,
|
||||
pstNo,
|
||||
thumbRefs,
|
||||
bbsTypeCd,
|
||||
classes,
|
||||
}: ThumbImageProps) => {
|
||||
const prevImage = thumbRefs.current.get(pstNo);
|
||||
|
||||
const { data: imageUrl, isLoading } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd],
|
||||
queryFn: () => AttachmentApi.getImageUrlByAtflCd(atflCd),
|
||||
queryFn: () => {
|
||||
if (bbsTypeCd === "BBSIMAGE") {
|
||||
return AttachmentApi.getImageUrlByAtflCd(atflCd);
|
||||
} else if (bbsTypeCd === "BBSMOVIE") {
|
||||
return AttachmentApi.getMovieThumbnailUrlByAtflCd(atflCd);
|
||||
}
|
||||
},
|
||||
select: (url) => {
|
||||
if (url) thumbRefs.current.set(pstNo, url);
|
||||
return url;
|
||||
@@ -93,17 +153,17 @@ const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => {
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="my-6 w-full">
|
||||
<Skeleton className="mx-auto h-[16rem] w-full" />
|
||||
<div className="h-[16rem] w-full">
|
||||
<Skeleton className="h-full w-full" />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (imageUrl)
|
||||
return (
|
||||
<div className="relative my-6 flex h-[16rem] w-full items-center justify-center">
|
||||
<div className="relative flex h-[16rem] w-full items-center justify-center">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
className="object-contain"
|
||||
className={cn("object-contain", classes?.image)}
|
||||
fill
|
||||
alt="썸네일 이미지"
|
||||
/>
|
||||
@@ -111,7 +171,7 @@ const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="my-6 flex h-[16rem] w-full items-center justify-center rounded-sm border border-border bg-background">
|
||||
<div className="flex h-[16rem] w-full items-center justify-center rounded-sm border border-border bg-background">
|
||||
<i className="diveicon di-image text-3xl" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const columns: ColumnDef<Post & { isUpndFixPost?: boolean }>[] = [
|
||||
return (
|
||||
<Badge
|
||||
color="orange"
|
||||
className="flex size-10 items-center justify-center"
|
||||
className="flex size-8 items-center justify-center"
|
||||
>
|
||||
<i className="diveicon di-volume-up-fill" />
|
||||
</Badge>
|
||||
|
||||
@@ -35,7 +35,7 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
{children}
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
@@ -68,7 +68,7 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,8 @@ function PostComment({ bbsId, pstNo }: { bbsId: number; pstNo: number }) {
|
||||
if (userInfo?.userUnqId) {
|
||||
cmntPswd = userInfo?.userUnqId;
|
||||
} else {
|
||||
cmntPswd = await prompt("비밀번호를 입력해주세요.");
|
||||
const { resolved, value } = await prompt("비밀번호를 입력해주세요.");
|
||||
if (resolved) cmntPswd = value ?? "";
|
||||
}
|
||||
return cmntPswd;
|
||||
};
|
||||
|
||||
@@ -24,11 +24,10 @@ import {
|
||||
} from "@common/components";
|
||||
import { PostApi, type Post, type PostParams } from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { BbsType } from "@/shared/store/menu-store";
|
||||
import { useMenuStore, useUserStore } from "@/shared/lib";
|
||||
import { GridViewDataTable } from "@/shared/ui/grid-view-data-table";
|
||||
import { columns } from "../model/post-table-columns";
|
||||
import { createColumns } from "../model/grid-view-table-columns";
|
||||
import { columns as tableColumns } from "../model/post-table-columns";
|
||||
import { createColumns as createGridViewColumns } from "../model/grid-view-table-columns";
|
||||
|
||||
function PostPage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
@@ -36,7 +35,6 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
const { confirm } = useConfirm();
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state);
|
||||
const bbsType = bbsInfo?.bbsTypeCd as BbsType;
|
||||
|
||||
const [postParams, setPostParams] = useState<PostParams>({
|
||||
keywordType: "title",
|
||||
@@ -53,7 +51,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
const { data: upndFixPostList } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, bbsInfo],
|
||||
queryFn: () =>
|
||||
PostApi.getPostList(bbsId, {
|
||||
PostApi.getPostList(parseInt(bbsId), {
|
||||
...postParams,
|
||||
upndFixYn: bbsInfo?.upndFixUseYn,
|
||||
}),
|
||||
@@ -77,7 +75,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
} = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId],
|
||||
queryFn: () =>
|
||||
PostApi.getPostList(bbsId, {
|
||||
PostApi.getPostList(parseInt(bbsId), {
|
||||
...postParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
@@ -85,9 +83,14 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const isGridViewPage =
|
||||
bbsInfo?.bbsTypeCd === "BBSIMAGE" || bbsInfo?.bbsTypeCd === "BBSMOVIE";
|
||||
|
||||
const table = useReactTable({
|
||||
data: [...(upndFixPostList?.content ?? []), ...(postList?.content ?? [])],
|
||||
columns,
|
||||
data: isGridViewPage
|
||||
? (postList?.content ?? [])
|
||||
: [...(upndFixPostList?.content ?? []), ...(postList?.content ?? [])],
|
||||
columns: isGridViewPage ? createGridViewColumns(thumbRefs) : tableColumns,
|
||||
state: { pagination, rowSelection },
|
||||
rowCount: postList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
@@ -95,19 +98,10 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
meta: { bbsTypeCd: bbsInfo?.bbsTypeCd },
|
||||
});
|
||||
|
||||
const gridViewTable = useReactTable({
|
||||
data: postList?.content ?? [],
|
||||
columns: createColumns(thumbRefs),
|
||||
state: { pagination, rowSelection },
|
||||
rowCount: postList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
const DataTableComponent = isGridViewPage ? GridViewDataTable : DataTable;
|
||||
|
||||
const onPostDelete = async () => {
|
||||
const selectedPosts = table
|
||||
@@ -118,7 +112,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
setRowSelection({});
|
||||
await Promise.all(
|
||||
selectedPosts.map((pstNo) =>
|
||||
PostApi.deletePost(bbsId, pstNo.toString(), userInfo?.userUnqId),
|
||||
PostApi.deletePost(parseInt(bbsId), pstNo, userInfo?.userUnqId),
|
||||
),
|
||||
);
|
||||
refetch();
|
||||
@@ -211,19 +205,11 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
{isFetched ? (
|
||||
bbsType === "BBSIMAGE" ? (
|
||||
<GridViewDataTable
|
||||
table={gridViewTable}
|
||||
onRowClick={handleRowClick}
|
||||
viewPagination
|
||||
/>
|
||||
) : (
|
||||
<DataTable
|
||||
table={table}
|
||||
onRowClick={handleRowClick}
|
||||
viewPagination
|
||||
/>
|
||||
)
|
||||
<DataTableComponent
|
||||
table={table}
|
||||
onRowClick={handleRowClick}
|
||||
viewPagination
|
||||
/>
|
||||
) : (
|
||||
<Skeleton className="h-[36rem] w-full" />
|
||||
)}
|
||||
|
||||
@@ -29,14 +29,17 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postData } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(bbsId, pstNo),
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { data: downloadResponse, isFetching } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.DOWNLOAD, postData?.atflCd],
|
||||
queryFn: () =>
|
||||
AttachmentApi.downloadAttachmentByAtflCd(postData?.atflCd as string),
|
||||
enabled: !!postData?.atflCd && bbsInfo?.bbsTypeCd !== "BBSIMAGE",
|
||||
enabled:
|
||||
!!postData?.atflCd &&
|
||||
bbsInfo?.bbsTypeCd !== "BBSIMAGE" &&
|
||||
bbsInfo?.bbsTypeCd !== "BBSMOVIE",
|
||||
});
|
||||
|
||||
const { data: imageUrl, isLoading } = useQuery({
|
||||
@@ -46,8 +49,25 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
enabled: !!postData?.atflCd,
|
||||
});
|
||||
|
||||
const { data: videoAttachment } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, postData?.atflCd],
|
||||
queryFn: () => AttachmentApi.getAttachmentsInfo(postData?.atflCd as string),
|
||||
select: (data) => data[0],
|
||||
enabled: !!postData?.atflCd && bbsInfo?.bbsTypeCd === "BBSMOVIE",
|
||||
});
|
||||
|
||||
const { data: videoUrl } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.STREAM_VIDEO, videoAttachment?.atflId],
|
||||
queryFn: () =>
|
||||
AttachmentApi.getStreamVideoUrlByAtflId(
|
||||
videoAttachment?.atflId as string,
|
||||
),
|
||||
enabled: !!videoAttachment?.atflId,
|
||||
});
|
||||
|
||||
const { mutate: deletePost } = useMutation({
|
||||
mutationFn: (pstPswd: string) => PostApi.deletePost(bbsId, pstNo, pstPswd),
|
||||
mutationFn: (pstPswd: string) =>
|
||||
PostApi.deletePost(parseInt(bbsId), parseInt(pstNo), pstPswd),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
@@ -58,7 +78,8 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
if (userInfo?.userUnqId) {
|
||||
pstPswd = userInfo?.userUnqId;
|
||||
} else {
|
||||
pstPswd = await prompt("비밀번호를 입력해주세요.");
|
||||
const { resolved, value } = await prompt("비밀번호를 입력해주세요.");
|
||||
if (resolved) pstPswd = value ?? "";
|
||||
}
|
||||
deletePost(pstPswd);
|
||||
};
|
||||
@@ -142,6 +163,11 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{bbsInfo?.bbsTypeCd === "BBSMOVIE" && (
|
||||
<div className="relative my-6 flex h-[56.25rem] w-[100rem]">
|
||||
<video src={videoUrl} controls className="object-contain" />
|
||||
</div>
|
||||
)}
|
||||
<pre className="font-noto min-h-[40rem] whitespace-pre-wrap p-8 text-2xl tracking-tighter text-foreground">
|
||||
{postData?.pstCn ?? ""}
|
||||
</pre>
|
||||
|
||||
@@ -50,7 +50,7 @@ function WithPreviewFileUploader<T extends PostFormData | PostUpdateFormData>({
|
||||
imageUrl = await AttachmentApi.getImageUrlByAtflId(atflId);
|
||||
}
|
||||
if (bbsInfo?.bbsTypeCd === "BBSMOVIE") {
|
||||
imageUrl = await AttachmentApi.getMovieThumbnailUrl(atflId);
|
||||
imageUrl = await AttachmentApi.getMovieThumbnailUrlByAtflId(atflId);
|
||||
}
|
||||
setThumbnailUrl(imageUrl);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage } from "@/pages/(post)/faq";
|
||||
|
||||
export default async function PstBbsFaqPostModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FaqPage } from "@/pages/(post)/faq";
|
||||
|
||||
export default async function PstBbsFaqPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <FaqPage bbsId={bbsId} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CreatePage } from "@/pages/(post)/faq";
|
||||
|
||||
export default async function PstBbsFaqRegisterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <CreatePage bbsId={bbsId} />;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { RecentPage } from "@/pages/recent";
|
||||
export default RecentPage;
|
||||
@@ -6,8 +6,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
import { StoreProvider } from "@/shared/providers";
|
||||
import { PromptProvider, StoreProvider } from "@/shared/providers";
|
||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
import { PromptDialog } from "@/shared/ui/prompt-dialog";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { initializeNextClientConfig } from "@common/config";
|
||||
|
||||
@@ -19,17 +20,23 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StoreProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
{children}
|
||||
</DndProvider>
|
||||
</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
<PromptProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>
|
||||
<DndProvider
|
||||
backend={MultiBackend}
|
||||
options={getBackendOptions()}
|
||||
>
|
||||
{children}
|
||||
</DndProvider>
|
||||
</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</PromptProvider>
|
||||
</StoreProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
} from "../model/board-interface";
|
||||
|
||||
export class BoardApiService implements BoardService {
|
||||
public async getBoardList(params: BoardParams): Promise<BoardList> {
|
||||
public async getBoardList(params?: BoardParams): Promise<BoardList> {
|
||||
const response = await client.get<BoardList>(
|
||||
`${ENDPOINT.BOARD_SERVICE.BBS}`,
|
||||
{ params },
|
||||
|
||||
@@ -4,29 +4,43 @@ import {
|
||||
postFormSchema,
|
||||
postUpdateFormSchema,
|
||||
type Post,
|
||||
type MovePost,
|
||||
type DeletePost,
|
||||
type PostInfo,
|
||||
type PostList,
|
||||
type RecentPostList,
|
||||
type PostParams,
|
||||
type PostService,
|
||||
type RecentPostParams,
|
||||
type PostFormData,
|
||||
type PostUpdateFormData,
|
||||
type PostService,
|
||||
} from "../model/post-interface";
|
||||
|
||||
export class PostApiService implements PostService {
|
||||
public async getPostList(
|
||||
bbsId: string,
|
||||
bbsId: number,
|
||||
params: PostParams,
|
||||
): Promise<PostList> {
|
||||
const response = await client.get<PostList>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}`,
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getPostInfo(bbsId: string, pstNo: string): Promise<PostInfo> {
|
||||
public async getRecentPostList(
|
||||
params?: RecentPostParams,
|
||||
): Promise<RecentPostList> {
|
||||
const response = await client.get<RecentPostList>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo> {
|
||||
const response = await client.get<PostInfo>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}/${pstNo}`,
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -34,34 +48,49 @@ export class PostApiService implements PostService {
|
||||
public async createPost(data: PostFormData): Promise<Post> {
|
||||
const validatedData = postFormSchema.parse(data);
|
||||
const response = await client.post<Post>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}`,
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}`,
|
||||
validatedData,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updatePost(
|
||||
bbsId: string,
|
||||
pstNo: string,
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
data: PostUpdateFormData,
|
||||
): Promise<Post> {
|
||||
const validatedData = postUpdateFormSchema.parse(data);
|
||||
const response = await client.put<Post>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}/${pstNo}`,
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`,
|
||||
validatedData,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deletePost(
|
||||
bbsId: string,
|
||||
pstNo: string,
|
||||
pstPswd?: string,
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
delRsn?: string,
|
||||
): Promise<void> {
|
||||
await client.delete(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}/${pstNo}`,
|
||||
{ params: { pstPswd } },
|
||||
);
|
||||
await client.delete(`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`, {
|
||||
params: { delRsn },
|
||||
});
|
||||
}
|
||||
|
||||
public async softDeletePost(data: DeletePost[]): Promise<void> {
|
||||
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/softDeleteSels`, data);
|
||||
}
|
||||
|
||||
public async hardDeletePost(data: DeletePost[]): Promise<void> {
|
||||
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/hardDeleteSels`, data);
|
||||
}
|
||||
|
||||
public async movePost(data: MovePost[]): Promise<void> {
|
||||
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/movePstByBbsId`, data);
|
||||
}
|
||||
|
||||
public async restorePost(data: DeletePost[]): Promise<void> {
|
||||
await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/restoreSels`, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,13 @@ export {
|
||||
postFormSchema,
|
||||
postUpdateFormSchema,
|
||||
type Post,
|
||||
type RecentPost,
|
||||
type MovePost,
|
||||
type DeletePost,
|
||||
type PostList,
|
||||
type PostParams,
|
||||
type PostService,
|
||||
type RecentPostParams,
|
||||
type PostFormData,
|
||||
type PostUpdateFormData,
|
||||
type PostService,
|
||||
} from "./model/post-interface";
|
||||
|
||||
@@ -8,13 +8,24 @@ export interface PostParams extends PageableParams {
|
||||
upndFixYn?: string;
|
||||
prdPstgYn?: string;
|
||||
siteId?: string;
|
||||
bbsTypeCd?: string;
|
||||
}
|
||||
|
||||
export interface RecentPostParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
bbsId?: number | null;
|
||||
delYn?: string;
|
||||
frDateYmd?: string;
|
||||
toDateYmd?: string;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
bbsId: number;
|
||||
rowNo: number;
|
||||
bbsId: number;
|
||||
pstNo: number;
|
||||
pstTtl: string;
|
||||
pstCn: string;
|
||||
inqNocs: number;
|
||||
rfrncUrl: string;
|
||||
popupYn: string;
|
||||
@@ -31,6 +42,11 @@ export interface Post {
|
||||
cmntCnt: number;
|
||||
}
|
||||
|
||||
export interface RecentPost extends Post {
|
||||
bbsNm: string;
|
||||
bbsTypeCd: string;
|
||||
}
|
||||
|
||||
export interface PostInfo extends Post {
|
||||
pstCn: string;
|
||||
rgtr: string;
|
||||
@@ -38,6 +54,18 @@ export interface PostInfo extends Post {
|
||||
mdfr: string;
|
||||
}
|
||||
|
||||
export interface MovePost {
|
||||
fromBbsId: number;
|
||||
toBbsId?: number;
|
||||
pstNo: number;
|
||||
}
|
||||
|
||||
export interface DeletePost {
|
||||
bbsId: number;
|
||||
pstNo: number;
|
||||
delRsn?: string;
|
||||
}
|
||||
|
||||
const postEtcSchema = z.object({
|
||||
bbsPstgTypeCd: z.string(),
|
||||
pstgPrdSeCd: z.string(),
|
||||
@@ -69,17 +97,22 @@ export const postUpdateFormSchema = postFormSchema.omit({
|
||||
});
|
||||
|
||||
export type PostList = PageableList<Post>;
|
||||
export type RecentPostList = PageableList<RecentPost>;
|
||||
export type PostFormData = z.infer<typeof postFormSchema>;
|
||||
export type PostUpdateFormData = z.infer<typeof postUpdateFormSchema>;
|
||||
|
||||
export interface PostService {
|
||||
getPostList(bbsId: string, params: PostParams): Promise<PostList>;
|
||||
getPostInfo(bbsId: string, pstNo: string): Promise<PostInfo>;
|
||||
getPostList(bbsId: number, params: PostParams): Promise<PostList>;
|
||||
getRecentPostList(params: RecentPostParams): Promise<RecentPostList>;
|
||||
getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo>;
|
||||
createPost(data: PostFormData): Promise<Post>;
|
||||
updatePost(
|
||||
bbsId: string,
|
||||
pstNo: string,
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
data: PostUpdateFormData,
|
||||
): Promise<Post>;
|
||||
deletePost(bbsId: string, pstNo: string, pstPswd?: string): Promise<void>;
|
||||
deletePost(bbsId: number, pstNo: number, delRsn?: string): Promise<void>;
|
||||
softDeletePost(data: DeletePost[]): Promise<void>;
|
||||
hardDeletePost(data: DeletePost[]): Promise<void>;
|
||||
movePost(data: MovePost[]): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
{parseInt(rememberPeriodCode[site.siteId] ?? "") ===
|
||||
0 && (
|
||||
<DateRangePicker
|
||||
handleDateChange={(date) =>
|
||||
onValueChange={(date) =>
|
||||
handlePeriodDateChange(site.siteId)(
|
||||
date as DateRange | undefined,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { FaqPage } from "./ui/faq-page";
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function CreatePage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createPost } = useMutation({
|
||||
mutationFn: (data: PostFormData) => PostApi.createPost(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostFormData>({
|
||||
resolver: zodResolver(postFormSchema),
|
||||
values: {
|
||||
bbsId: parseInt(bbsId),
|
||||
pstTtl: "",
|
||||
wrtrNm: userInfo?.userName ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">FAQ 게시글 등록</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={createPost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,317 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Skeleton,
|
||||
} from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { PostApi, type PostParams } from "@/entities/post";
|
||||
import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
function FaqPage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { prompt } = usePrompt();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state);
|
||||
|
||||
const [postParams, setPostParams] = useState<PostParams>({
|
||||
keywordType: "title",
|
||||
keyword: "",
|
||||
});
|
||||
const [selectedPostNumbers, setSelectedPostNumbers] = useState<number[]>([]);
|
||||
|
||||
const {
|
||||
data: postList,
|
||||
isFetched,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsInfo?.bbsTypeCd],
|
||||
queryFn: () =>
|
||||
PostApi.getPostList(parseInt(bbsId), {
|
||||
...postParams,
|
||||
bbsTypeCd: bbsInfo?.bbsTypeCd,
|
||||
}),
|
||||
enabled: !!bbsInfo?.bbsTypeCd,
|
||||
});
|
||||
|
||||
const { mutate: deletePost } = useMutation({
|
||||
mutationFn: ({ pstNo, delRsn }: { pstNo: number; delRsn?: string }) =>
|
||||
PostApi.deletePost(parseInt(bbsId), pstNo, delRsn),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.");
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
const getPromptValue = async (message: string) => {
|
||||
const { resolved, value } = await prompt(message);
|
||||
if (!resolved) return "";
|
||||
return value;
|
||||
};
|
||||
|
||||
const handleDeletePost = async (pstNo: number) => {
|
||||
const delRsn = await getPromptValue("삭제 사유를 입력해주세요.");
|
||||
deletePost({ pstNo, delRsn });
|
||||
};
|
||||
|
||||
const onSelectedPostDelete = async () => {
|
||||
const delRsn = await getPromptValue("삭제 사유를 입력해주세요.");
|
||||
await Promise.all(
|
||||
selectedPostNumbers.map((pstNo) =>
|
||||
PostApi.deletePost(parseInt(bbsId), pstNo, delRsn),
|
||||
),
|
||||
);
|
||||
refetch();
|
||||
setSelectedPostNumbers([]);
|
||||
alert("삭제되었습니다.");
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (pstNo: number, checked: boolean | string) => {
|
||||
setSelectedPostNumbers(
|
||||
checked === true
|
||||
? [...selectedPostNumbers, pstNo]
|
||||
: selectedPostNumbers.filter((num) => num !== pstNo),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAllCheckboxChange = (checked: boolean | string) => {
|
||||
setSelectedPostNumbers(
|
||||
checked === true ? postList?.content.map((item) => item.pstNo) || [] : [],
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2 text-label">검색</Label>
|
||||
<Select
|
||||
value={postParams.keywordType}
|
||||
onValueChange={(keywordType) => {
|
||||
setPostParams({ ...postParams, keywordType });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="title">제목</SelectItem>
|
||||
<SelectItem value="all">제목+내용</SelectItem>
|
||||
<SelectItem value="writer">작성자</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={postParams.keyword}
|
||||
onChange={(e) =>
|
||||
setPostParams({ ...postParams, keyword: e.target.value })
|
||||
}
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
{pageMenuAuthority?.regAuthrtYn === "Y" && (
|
||||
<Button onClick={() => router.push(`${pathname}/register`)}>
|
||||
등록
|
||||
</Button>
|
||||
)}
|
||||
{pageMenuAuthority?.delAuthrtYn === "Y" && (
|
||||
<Button
|
||||
color="red"
|
||||
onClick={onSelectedPostDelete}
|
||||
disabled={!selectedPostNumbers.length}
|
||||
>
|
||||
선택삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
|
||||
<Checkbox
|
||||
id="all-select"
|
||||
checked={selectedPostNumbers.length === postList?.content.length}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={handleAllCheckboxChange}
|
||||
/>
|
||||
<Label htmlFor="all-select" className="text-2xl">
|
||||
전체선택
|
||||
</Label>
|
||||
</div>
|
||||
{isFetched ? (
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="secondary flex flex-col gap-4"
|
||||
>
|
||||
{postList?.content.map((item) => (
|
||||
<div key={item.pstNo} className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id={item.pstNo.toString()}
|
||||
checked={selectedPostNumbers.includes(item.pstNo)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCheckboxChange(item.pstNo, checked)
|
||||
}
|
||||
className="mx-2 my-6"
|
||||
/>
|
||||
<AccordionItem
|
||||
value={item.pstNo.toString()}
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionTrigger>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-primary">Q</span>
|
||||
<span>{item.pstTtl}</span>
|
||||
</div>
|
||||
<span className="px-4 text-[1.2rem] font-normal text-label">
|
||||
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
A
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
|
||||
{item.pstCn}
|
||||
</pre>
|
||||
<Attachment atflCd={item.atflCd} />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() =>
|
||||
router.push(`${pathname}/${item.pstNo}`)
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() => handleDeletePost(item.pstNo)}
|
||||
>
|
||||
<i className="diveicon di-trash" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
))}
|
||||
</Accordion>
|
||||
) : (
|
||||
<Skeleton className="h-[36rem] w-full" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Attachment({ atflCd }: { atflCd: string }) {
|
||||
const [filename, setFilename] = useState("");
|
||||
|
||||
const { data: downloadResponse, isFetching } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.DOWNLOAD, atflCd],
|
||||
queryFn: () => AttachmentApi.downloadAttachmentByAtflCd(atflCd),
|
||||
enabled: !!atflCd,
|
||||
});
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!downloadResponse) return;
|
||||
try {
|
||||
const blob = new Blob([downloadResponse.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = url;
|
||||
link.download = filename || "download";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!downloadResponse) return;
|
||||
try {
|
||||
let filename = "";
|
||||
const contentDisposition =
|
||||
downloadResponse.headers["content-disposition"];
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(
|
||||
/filename\*=UTF-8''([^;]+)/,
|
||||
);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = decodeURIComponent(filenameMatch[1]);
|
||||
}
|
||||
}
|
||||
setFilename(filename);
|
||||
} catch {
|
||||
setFilename("");
|
||||
}
|
||||
}, [downloadResponse]);
|
||||
|
||||
return isFetching ? (
|
||||
<div className="my-8">
|
||||
<Skeleton className="h-[2.5rem] w-[32rem]" />
|
||||
</div>
|
||||
) : (
|
||||
filename && (
|
||||
<div className="my-8 flex items-center gap-4">
|
||||
<span className="font-bold text-label">첨부파일</span>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
className="flex w-fit items-center gap-2"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<i className="diveicon di-download" />
|
||||
<span>{filename}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { FaqPage };
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import {
|
||||
PostApi,
|
||||
postUpdateFormSchema,
|
||||
type PostUpdateFormData,
|
||||
} from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostUpdateFormData>({
|
||||
resolver: zodResolver(postUpdateFormSchema),
|
||||
values: {
|
||||
...postInfo,
|
||||
pstTtl: postInfo?.pstTtl ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">FAQ 게시글 수정</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={updatePost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -19,13 +19,13 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(bbsId, pstNo),
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(bbsId, pstNo, data),
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
|
||||
@@ -19,13 +19,13 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(bbsId, pstNo),
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(bbsId, pstNo, data),
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
|
||||
@@ -19,13 +19,13 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(bbsId, pstNo),
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(bbsId, pstNo, data),
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
|
||||
@@ -19,13 +19,13 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(bbsId, pstNo),
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(bbsId, pstNo, data),
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import dayjs from "dayjs";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { ColumnDef, Row } from "@tanstack/react-table";
|
||||
@@ -61,6 +62,7 @@ const ManageButton = ({ row }: { row: Row<Board> }) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`${pathname}/${bbsTypeCd.toLowerCase()}/${bbsId}`);
|
||||
// router.push(`/pst/${bbsTypeCd.toLowerCase()}/${bbsId}`); // 공통 개발용
|
||||
setBbsInfo(JSON.stringify(row.original));
|
||||
setStoreBbsInfo(row.original);
|
||||
}}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { RecentPage } from "./ui/recent-page";
|
||||
@@ -0,0 +1,117 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button, Checkbox } from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import type { RecentPost } from "@/entities/post";
|
||||
|
||||
export const columns: ColumnDef<RecentPost>[] = [
|
||||
{
|
||||
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"
|
||||
/>
|
||||
),
|
||||
meta: {
|
||||
width: 40,
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "rowNo",
|
||||
header: "NO",
|
||||
meta: {
|
||||
width: 80,
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "bbsNm",
|
||||
header: "게시판",
|
||||
meta: {
|
||||
width: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
cell: ({ row }) => <div className="text-left">{row.original.pstTtl}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "atflCd",
|
||||
header: "첨부파일",
|
||||
cell: ({ row }) => {
|
||||
const atflCd = row.original.atflCd;
|
||||
if (!atflCd) return null;
|
||||
return <AttachmentButton atflCd={atflCd} />;
|
||||
},
|
||||
meta: {
|
||||
width: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "rgtrNm",
|
||||
header: "작성자",
|
||||
meta: {
|
||||
width: 120,
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "등록일",
|
||||
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
||||
meta: {
|
||||
width: 120,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useRef } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import type { Board } from "@/entities/board";
|
||||
import type { MovePost } from "@/entities/post";
|
||||
|
||||
type MovePostDialogProps = {
|
||||
selectedPosts: MovePost[];
|
||||
boardList: Board[];
|
||||
onMovePost: (data: MovePost[]) => void;
|
||||
};
|
||||
|
||||
function MovePostDialog({
|
||||
selectedPosts,
|
||||
boardList,
|
||||
onMovePost,
|
||||
}: MovePostDialogProps) {
|
||||
const selectedPostsRef = useRef<MovePost[]>(selectedPosts);
|
||||
|
||||
const onSelectValueChange = (toBbsId: string) => {
|
||||
selectedPostsRef.current = selectedPosts.map((post) => ({
|
||||
...post,
|
||||
toBbsId: parseInt(toBbsId),
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="emerald" disabled={!selectedPosts.length}>
|
||||
게시물 이동
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<p className="pt-8 text-center text-[1.3rem]">
|
||||
이동할 게시판을 선택해 주세요.
|
||||
</p>
|
||||
<Select onValueChange={onSelectValueChange}>
|
||||
<SelectTrigger className="my-4">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{boardList.map((board) => (
|
||||
<SelectItem key={board.bbsId} value={board.bbsId.toString()}>
|
||||
{board.bbsNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button onClick={() => onMovePost(selectedPostsRef.current)}>
|
||||
이동
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { MovePostDialog };
|
||||
@@ -0,0 +1,342 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
DataTable,
|
||||
} from "@common/components";
|
||||
import {
|
||||
PostApi,
|
||||
type DeletePost,
|
||||
type MovePost,
|
||||
type RecentPost,
|
||||
type RecentPostParams,
|
||||
} from "@/entities/post";
|
||||
import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib";
|
||||
import { BoardApi } from "@/entities/board";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { MovePostDialog } from "./move-post-dialog";
|
||||
|
||||
type DateRange = {
|
||||
from: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
};
|
||||
|
||||
function RecentPage() {
|
||||
const router = useRouter();
|
||||
const { prompt } = usePrompt();
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { pageMenuAuthority } = useMenuStore((state) => state);
|
||||
const [dateRange, setDateRange] = useState<DateRange>();
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [recentPostParams, setRecentPostParams] = useState<RecentPostParams>({
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
bbsId: null,
|
||||
delYn: "N",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
});
|
||||
const [isDeletePostData, setIsDeletePostData] = useState(false);
|
||||
|
||||
const { data: recentPostList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, pagination],
|
||||
queryFn: () =>
|
||||
PostApi.getRecentPostList({
|
||||
...recentPostParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const { data: boardList } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS],
|
||||
queryFn: () => BoardApi.getBoardList(),
|
||||
});
|
||||
|
||||
const { mutate: movePost } = useMutation({
|
||||
mutationFn: (data: MovePost[]) => PostApi.movePost(data),
|
||||
onSuccess: () => {
|
||||
alert("이동이 완료되었습니다.", refetch);
|
||||
},
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: recentPostList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: recentPostList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0;
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDeletePostData(recentPostParams.delYn === "Y");
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: RecentPost) => {
|
||||
router.push(
|
||||
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
|
||||
);
|
||||
};
|
||||
|
||||
const handleRecentPostParamsChange =
|
||||
(key: keyof RecentPostParams) => (value: unknown) => {
|
||||
setRecentPostParams({ ...recentPostParams, [key]: value });
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
let presetDateRange: DateRange | undefined;
|
||||
switch (value) {
|
||||
case "1W":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(7, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
case "1M":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(30, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
case "3M":
|
||||
presetDateRange = {
|
||||
from: dayjs().subtract(90, "day").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
presetDateRange = undefined;
|
||||
}
|
||||
setDateRange(presetDateRange);
|
||||
};
|
||||
|
||||
const onSoftDeletePost = async () => {
|
||||
const { resolved, value: delRsn } =
|
||||
await prompt("삭제 사유를 입력해주세요.");
|
||||
if (!resolved) return;
|
||||
|
||||
const selectedPosts: DeletePost[] = table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => ({
|
||||
bbsId: row.original.bbsId,
|
||||
pstNo: row.original.pstNo,
|
||||
delRsn,
|
||||
}));
|
||||
PostApi.softDeletePost(selectedPosts);
|
||||
alert("삭제가 완료되었습니다.", refetch);
|
||||
setRowSelection({});
|
||||
};
|
||||
|
||||
const onHardDeletePost = async () => {
|
||||
if (
|
||||
await confirm(
|
||||
"게시물을 완전 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.",
|
||||
)
|
||||
) {
|
||||
const selectedPosts: DeletePost[] = table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => ({
|
||||
bbsId: row.original.bbsId,
|
||||
pstNo: row.original.pstNo,
|
||||
}));
|
||||
PostApi.hardDeletePost(selectedPosts);
|
||||
alert("삭제가 완료되었습니다.", refetch);
|
||||
setRowSelection({});
|
||||
}
|
||||
};
|
||||
|
||||
const onRestorePost = async () => {
|
||||
if (await confirm("게시물을 복원하시겠습니까?")) {
|
||||
const selectedPosts: DeletePost[] = table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => ({
|
||||
bbsId: row.original.bbsId,
|
||||
pstNo: row.original.pstNo,
|
||||
}));
|
||||
PostApi.restorePost(selectedPosts);
|
||||
alert("복원이 완료되었습니다.", refetch);
|
||||
setRowSelection({});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2">날짜</Label>
|
||||
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
|
||||
<div className="card card-border flex items-center gap-2">
|
||||
<Select onValueChange={handlePresetDateRange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1W">일주일</SelectItem>
|
||||
<SelectItem value="1M">1개월</SelectItem>
|
||||
<SelectItem value="3M">3개월</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DateRangePicker>
|
||||
|
||||
<Label className="ml-4 mr-2 text-label">게시판</Label>
|
||||
<Select onValueChange={handleRecentPostParamsChange("bbsId")}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{boardList?.content.map((board) => (
|
||||
<SelectItem key={board.bbsId} value={board.bbsId.toString()}>
|
||||
{board.bbsNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Label className="ml-4 mr-2 text-label">상태</Label>
|
||||
<Select onValueChange={handleRecentPostParamsChange("delYn")}>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="N">정상</SelectItem>
|
||||
<SelectItem value="Y">삭제</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Label className="ml-4 mr-2 text-label">검색</Label>
|
||||
<Select
|
||||
value={recentPostParams.keywordType}
|
||||
onValueChange={handleRecentPostParamsChange("keywordType")}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="title">제목</SelectItem>
|
||||
<SelectItem value="all">제목+내용</SelectItem>
|
||||
<SelectItem value="writer">작성자</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={recentPostParams.keyword}
|
||||
onChange={(e) =>
|
||||
handleRecentPostParamsChange("keyword")(e.target.value)
|
||||
}
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
{pageMenuAuthority?.mngAuthrtYn === "Y" &&
|
||||
(isDeletePostData ? (
|
||||
<>
|
||||
<Button
|
||||
color="emerald"
|
||||
onClick={onRestorePost}
|
||||
disabled={!isSelectedRow}
|
||||
>
|
||||
게시물 복원
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={onHardDeletePost}
|
||||
disabled={!isSelectedRow}
|
||||
>
|
||||
완전삭제
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MovePostDialog
|
||||
selectedPosts={table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => ({
|
||||
fromBbsId: row.original.bbsId,
|
||||
pstNo: row.original.pstNo,
|
||||
}))}
|
||||
boardList={boardList?.content ?? []}
|
||||
onMovePost={movePost}
|
||||
/>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={onSoftDeletePost}
|
||||
disabled={!isSelectedRow}
|
||||
>
|
||||
선택삭제
|
||||
</Button>
|
||||
</>
|
||||
))}
|
||||
<Select
|
||||
value={table.getState().pagination.pageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(parseInt(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[12rem]">
|
||||
<SelectValue placeholder="페이지 크기" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={pageSize.toString()}>
|
||||
{pageSize}개씩 보기
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { RecentPage };
|
||||
@@ -42,7 +42,7 @@ export const ENDPOINT = {
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt",
|
||||
POSTS_USER: "/board-service/api/v1/posts/user",
|
||||
POSTS: "/board-service/api/v1/posts",
|
||||
POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,18 +7,18 @@ export const usePrompt = () => {
|
||||
|
||||
const prompt = (message: string) => {
|
||||
setMessage(message);
|
||||
return new Promise<string>((resolve) => {
|
||||
return new Promise<{ resolved: boolean; value?: string }>((resolve) => {
|
||||
setResolve(() => resolve);
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (value: string) => {
|
||||
resolve?.(value);
|
||||
resolve?.({ resolved: true, value });
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
resolve?.("");
|
||||
resolve?.({ resolved: false });
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { StoreProvider } from "./store-provider";
|
||||
export { PromptProvider } from "./prompt-provider";
|
||||
|
||||
@@ -2,9 +2,15 @@ import { createContext, useState } from "react";
|
||||
|
||||
interface PromptContext {
|
||||
message?: string;
|
||||
resolve?: (value: string) => void;
|
||||
resolve?: ({
|
||||
resolved,
|
||||
value,
|
||||
}: {
|
||||
resolved: boolean;
|
||||
value?: string;
|
||||
}) => void;
|
||||
setMessage: (message: string) => void;
|
||||
setResolve: (resolve: (value: string) => void) => void;
|
||||
setResolve: (resolve: () => void) => void;
|
||||
}
|
||||
|
||||
export const PromptContext = createContext<PromptContext>({
|
||||
@@ -16,7 +22,10 @@ export const PromptContext = createContext<PromptContext>({
|
||||
|
||||
export const PromptProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [message, setMessage] = useState<string>();
|
||||
const [resolve, setResolve] = useState<(value: string) => void>();
|
||||
const [resolve, setResolve] =
|
||||
useState<
|
||||
({ resolved, value }: { resolved: boolean; value?: string }) => void
|
||||
>();
|
||||
|
||||
return (
|
||||
<PromptContext.Provider
|
||||
|
||||
@@ -56,7 +56,7 @@ function GridViewDataTable<T>({
|
||||
key={row.id}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
className={cn(
|
||||
"rounded-md border border-border bg-background p-4 shadow-md",
|
||||
"overflow-hidden rounded-md border border-border bg-background p-4 shadow-md",
|
||||
onRowClick && "cursor-pointer hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import { usePrompt } from "@/shared/lib/use-prompt";
|
||||
|
||||
function PromptDialog() {
|
||||
const { message, isOpen, onSubmit, onCancel } = usePrompt();
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onCancel}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="flex flex-col gap-6 pb-4 pt-8">
|
||||
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
|
||||
{message}
|
||||
</p>
|
||||
<Input
|
||||
type="text"
|
||||
value={value}
|
||||
className="!text-[1.3rem]"
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button onClick={() => onSubmit(value)}>확인</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={onCancel} color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { PromptDialog };
|
||||
@@ -1,12 +1,19 @@
|
||||
import dayjs from "dayjs";
|
||||
import Image from "next/image";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge, Checkbox, Skeleton } from "@common/components";
|
||||
import { Badge, Checkbox, Skeleton, cn } from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import type { Post } from "@/entities/post";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import type { BbsType } from "@/shared/store/menu-store";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface TableMeta<TData> {
|
||||
bbsTypeCd?: BbsType;
|
||||
}
|
||||
}
|
||||
|
||||
export const createColumns = (
|
||||
thumbRefs: React.MutableRefObject<Map<number, string>>,
|
||||
@@ -23,73 +30,121 @@ export const createColumns = (
|
||||
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"
|
||||
cell: ({ row, table }) => {
|
||||
const { atflCd, pstNo, pstTtl, pstCn, inqNocs, rgtrNm, regDt } =
|
||||
row.original;
|
||||
const bbsTypeCd = table.options.meta?.bbsTypeCd;
|
||||
|
||||
if (bbsTypeCd === "BBSIMAGE") {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<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>
|
||||
)}
|
||||
<span className="text-[1.6rem] font-bold text-label">
|
||||
{pstTtl}
|
||||
</span>
|
||||
</div>
|
||||
<ThumbImage
|
||||
atflCd={atflCd}
|
||||
pstNo={pstNo}
|
||||
thumbRefs={thumbRefs}
|
||||
bbsTypeCd={bbsTypeCd}
|
||||
/>
|
||||
{inqNocs >= 100 && <Badge color="red">HOT</Badge>}
|
||||
{dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && (
|
||||
<Badge color="blue">NEW</Badge>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
{rgtrNm}
|
||||
</span>
|
||||
<span className="text-[1.2rem] text-label">
|
||||
{dayjs(regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-3xl font-bold leading-none text-label">
|
||||
{pstTtl}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
if (bbsTypeCd === "BBSMOVIE") {
|
||||
return (
|
||||
<div className="-m-4">
|
||||
<ThumbImage
|
||||
atflCd={atflCd}
|
||||
pstNo={pstNo}
|
||||
thumbRefs={thumbRefs}
|
||||
bbsTypeCd={bbsTypeCd}
|
||||
classes={{
|
||||
image: "object-cover",
|
||||
}}
|
||||
/>
|
||||
<div className="p-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<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(regDt), "hour") < 24 && (
|
||||
<Badge color="blue">NEW</Badge>
|
||||
)}
|
||||
<span className="text-[1.6rem] font-bold text-label">
|
||||
{pstTtl}
|
||||
</span>
|
||||
</div>
|
||||
<p className="line-clamp-3 overflow-hidden text-[1.2rem] text-label">
|
||||
{pstCn}
|
||||
</p>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
{rgtrNm}
|
||||
</span>
|
||||
<span className="text-[1.2rem] text-label">
|
||||
{dayjs(regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "atflCd",
|
||||
cell: ({ row }) => {
|
||||
const { atflCd, pstNo } = row.original;
|
||||
return <ThumbImage atflCd={atflCd} pstNo={pstNo} thumbRefs={thumbRefs} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "rgtrNm",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
{row.original.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>>;
|
||||
bbsTypeCd?: string;
|
||||
classes?: {
|
||||
image?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => {
|
||||
const ThumbImage = ({
|
||||
atflCd,
|
||||
pstNo,
|
||||
thumbRefs,
|
||||
bbsTypeCd,
|
||||
classes,
|
||||
}: ThumbImageProps) => {
|
||||
const prevImage = thumbRefs.current.get(pstNo);
|
||||
|
||||
const { bbsInfo } = useMenuStore((state) => state);
|
||||
|
||||
const { data: imageUrl, isLoading } = useQuery({
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd],
|
||||
queryFn: () => {
|
||||
if (bbsInfo?.bbsTypeCd === "BBSIMAGE") {
|
||||
if (bbsTypeCd === "BBSIMAGE") {
|
||||
return AttachmentApi.getImageUrlByAtflCd(atflCd);
|
||||
} else if (bbsInfo?.bbsTypeCd === "BBSMOVIE") {
|
||||
} else if (bbsTypeCd === "BBSMOVIE") {
|
||||
return AttachmentApi.getMovieThumbnailUrlByAtflCd(atflCd);
|
||||
}
|
||||
},
|
||||
@@ -100,19 +155,21 @@ const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => {
|
||||
enabled: !prevImage && !!atflCd,
|
||||
});
|
||||
|
||||
const imageUrl = prevImage ?? data;
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="my-6 w-full">
|
||||
<Skeleton className="mx-auto h-[16rem] w-full" />
|
||||
<div className="h-[16rem] w-full">
|
||||
<Skeleton className="h-full w-full" />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (imageUrl)
|
||||
return (
|
||||
<div className="relative my-6 flex h-[16rem] w-full items-center justify-center">
|
||||
<div className="relative flex h-[16rem] w-full items-center justify-center">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
className="object-contain"
|
||||
className={cn("object-contain", classes?.image)}
|
||||
fill
|
||||
alt="썸네일 이미지"
|
||||
/>
|
||||
@@ -120,7 +177,7 @@ const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="my-6 flex h-[16rem] w-full items-center justify-center rounded-sm border border-border bg-background">
|
||||
<div className="flex h-[16rem] w-full items-center justify-center rounded-sm border border-border bg-background">
|
||||
<i className="diveicon di-image text-3xl" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const columns: ColumnDef<Post & { isUpndFixPost?: boolean }>[] = [
|
||||
return (
|
||||
<Badge
|
||||
color="orange"
|
||||
className="flex size-10 items-center justify-center"
|
||||
className="flex size-8 items-center justify-center"
|
||||
>
|
||||
<i className="diveicon di-volume-up-fill" />
|
||||
</Badge>
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button } from "@common/components";
|
||||
import { useMenuStore, useUserStore } from "@/shared/lib";
|
||||
import { useMenuStore, usePrompt, useUserStore } from "@/shared/lib";
|
||||
|
||||
type PostActionProps = {
|
||||
bbsId: string;
|
||||
pstNo: string;
|
||||
children: React.ReactNode;
|
||||
handleDelete: () => Promise<void>;
|
||||
handleDelete: (delRsn?: string) => Promise<void>;
|
||||
};
|
||||
|
||||
function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { confirm } = useConfirm();
|
||||
const { prompt } = usePrompt();
|
||||
const { bbsInfo } = useMenuStore((state) => state);
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) handleDelete();
|
||||
const { resolved, value } = await prompt("삭제 사유를 입력해주세요.");
|
||||
if (resolved) handleDelete(value);
|
||||
};
|
||||
|
||||
const checkAnsUseAuthrt = () => {
|
||||
@@ -35,7 +35,7 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
{children}
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
@@ -68,7 +68,7 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,8 @@ function PostComment({ bbsId, pstNo }: { bbsId: number; pstNo: number }) {
|
||||
if (userInfo?.userUnqId) {
|
||||
cmntPswd = userInfo?.userUnqId;
|
||||
} else {
|
||||
cmntPswd = await prompt("비밀번호를 입력해주세요.");
|
||||
const { resolved, value } = await prompt("비밀번호를 입력해주세요.");
|
||||
if (resolved) cmntPswd = value ?? "";
|
||||
}
|
||||
return cmntPswd;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
} from "@common/components";
|
||||
import { PostApi, type Post, type PostParams } from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useMenuStore, useUserStore } from "@/shared/lib";
|
||||
import { useMenuStore, usePrompt } from "@/shared/lib";
|
||||
import { GridViewDataTable } from "@/shared/ui/grid-view-data-table";
|
||||
import { columns as tableColumns } from "../model/post-table-columns";
|
||||
import { createColumns as createGridViewColumns } from "../model/grid-view-table-columns";
|
||||
@@ -32,8 +31,7 @@ import { createColumns as createGridViewColumns } from "../model/grid-view-table
|
||||
function PostPage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { confirm } = useConfirm();
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
const { prompt } = usePrompt();
|
||||
const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state);
|
||||
const thumbRefs = useRef<Map<number, string>>(new Map());
|
||||
|
||||
@@ -48,9 +46,9 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: upndFixPostList } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, bbsInfo],
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, bbsInfo],
|
||||
queryFn: () =>
|
||||
PostApi.getPostList(bbsId, {
|
||||
PostApi.getPostList(parseInt(bbsId), {
|
||||
...postParams,
|
||||
upndFixYn: bbsInfo?.upndFixUseYn,
|
||||
}),
|
||||
@@ -69,14 +67,15 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
isFetched,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId],
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsInfo?.bbsTypeCd],
|
||||
queryFn: () =>
|
||||
PostApi.getPostList(bbsId, {
|
||||
PostApi.getPostList(parseInt(bbsId), {
|
||||
...postParams,
|
||||
bbsTypeCd: bbsInfo?.bbsTypeCd,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: false,
|
||||
enabled: !!bbsInfo?.bbsTypeCd,
|
||||
});
|
||||
|
||||
const isGridViewPage =
|
||||
@@ -94,6 +93,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
meta: { bbsTypeCd: bbsInfo?.bbsTypeCd },
|
||||
});
|
||||
|
||||
const DataTableComponent = isGridViewPage ? GridViewDataTable : DataTable;
|
||||
@@ -103,15 +103,16 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => row.original.pstNo);
|
||||
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setRowSelection({});
|
||||
await Promise.all(
|
||||
selectedPosts.map((pstNo) =>
|
||||
PostApi.deletePost(bbsId, pstNo.toString(), userInfo?.userUnqId),
|
||||
),
|
||||
);
|
||||
refetch();
|
||||
}
|
||||
const { resolved, value: delRsn } =
|
||||
await prompt("삭제 사유를 입력해주세요.");
|
||||
if (!resolved) return;
|
||||
setRowSelection({});
|
||||
await Promise.all(
|
||||
selectedPosts.map((pstNo) =>
|
||||
PostApi.deletePost(parseInt(bbsId), pstNo, delRsn),
|
||||
),
|
||||
);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -123,10 +124,6 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
router.push(`${pathname}/${row.pstNo}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
|
||||
@@ -8,28 +8,21 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Button, Skeleton } from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { PostApi } from "@/entities/post";
|
||||
import {
|
||||
useAlertStore,
|
||||
useUserStore,
|
||||
useMenuStore,
|
||||
usePrompt,
|
||||
} from "@/shared/lib";
|
||||
import { useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PostAction } from "./post-action";
|
||||
import { PostComment } from "./post-comment";
|
||||
|
||||
function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const router = useRouter();
|
||||
const { prompt } = usePrompt();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { userInfo } = useUserStore((state) => state);
|
||||
const { bbsInfo } = useMenuStore((state) => state);
|
||||
|
||||
const [filename, setFilename] = useState("");
|
||||
|
||||
const { data: postData } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(bbsId, pstNo),
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { data: downloadResponse, isFetching } = useQuery({
|
||||
@@ -65,23 +58,14 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
enabled: !!videoAttachment?.atflId,
|
||||
});
|
||||
|
||||
const { mutate: deletePost } = useMutation({
|
||||
mutationFn: (pstPswd: string) => PostApi.deletePost(bbsId, pstNo, pstPswd),
|
||||
const { mutateAsync: deletePost } = useMutation({
|
||||
mutationFn: (delRsn?: string) =>
|
||||
PostApi.deletePost(parseInt(bbsId), parseInt(pstNo), delRsn),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = async () => {
|
||||
let pstPswd = "";
|
||||
if (userInfo?.userUnqId) {
|
||||
pstPswd = userInfo?.userUnqId;
|
||||
} else {
|
||||
pstPswd = await prompt("비밀번호를 입력해주세요.");
|
||||
}
|
||||
deletePost(pstPswd);
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!downloadResponse) return;
|
||||
try {
|
||||
@@ -123,7 +107,7 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostAction bbsId={bbsId} pstNo={pstNo} handleDelete={handleDelete}>
|
||||
<PostAction bbsId={bbsId} pstNo={pstNo} handleDelete={deletePost}>
|
||||
<div className="card card-border !p-8">
|
||||
<h3 className="text-[2rem] font-bold text-foreground">
|
||||
{postData?.pstTtl}
|
||||
|
||||
@@ -76,9 +76,7 @@ function PostingPeriodSelector({
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{periodCode === "0" && (
|
||||
<DateRangePicker handleDateChange={onDateChange} />
|
||||
)}
|
||||
{periodCode === "0" && <DateRangePicker onValueChange={onDateChange} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import "./button-group.css";
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { RadioGroup, RadioGroupItem } from "../radio-group";
|
||||
import { ButtonGroup, ButtonGroupItem } from "../button-group";
|
||||
|
||||
const meta: Meta<typeof RadioGroup> = {
|
||||
const meta: Meta<typeof ButtonGroup> = {
|
||||
title: "Elements/ButtonGroup",
|
||||
component: RadioGroup,
|
||||
component: ButtonGroup,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
@@ -19,49 +19,49 @@ ButtonGroup 컴포넌트는 RadioGroup 컴포넌트를 기반으로 버튼 모
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof RadioGroup>;
|
||||
type Story = StoryObj<typeof ButtonGroup>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="comfortable" variant="button">
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroup defaultValue="comfortable" variant="button">
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="comfortable" variant="button" color="secondary">
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroup defaultValue="comfortable" variant="button" color="secondary">
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
export const Rounded: Story = {
|
||||
render: () => (
|
||||
<RadioGroup defaultValue="comfortable" variant="button" shape="rounded">
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroup defaultValue="comfortable" variant="button" shape="rounded">
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
export const SecondaryRounded: Story = {
|
||||
render: () => (
|
||||
<RadioGroup
|
||||
<ButtonGroup
|
||||
defaultValue="comfortable"
|
||||
variant="button"
|
||||
color="secondary"
|
||||
shape="rounded"
|
||||
>
|
||||
<RadioGroupItem value="default">Default</RadioGroupItem>
|
||||
<RadioGroupItem value="comfortable">Comfortable</RadioGroupItem>
|
||||
<RadioGroupItem value="compact">Compact</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
<ButtonGroupItem value="default">Default</ButtonGroupItem>
|
||||
<ButtonGroupItem value="comfortable">Comfortable</ButtonGroupItem>
|
||||
<ButtonGroupItem value="compact">Compact</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import "./button-group.css";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
|
||||
const ButtonGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> & {
|
||||
variant?: "radio" | "button";
|
||||
color?: "primary" | "secondary";
|
||||
shape?: "square" | "rounded";
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ variant = "radio", color = "primary", shape = "square", ...props },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
{...props}
|
||||
ref={ref}
|
||||
data-variant={variant}
|
||||
data-color={color}
|
||||
data-shape={shape}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
ButtonGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||
|
||||
const ButtonGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ ...props }, ref) => {
|
||||
return <RadioGroupPrimitive.Item ref={ref} {...props} />;
|
||||
});
|
||||
ButtonGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||
|
||||
export { ButtonGroup, ButtonGroupItem };
|
||||
@@ -16,6 +16,7 @@ import type { HTMLAttributes } from "react";
|
||||
declare module "@tanstack/react-table" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
width?: number;
|
||||
align?: "left" | "center" | "right";
|
||||
rowSpan?: number;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
import type { DateRange } from "react-day-picker";
|
||||
@@ -11,20 +11,38 @@ import { Calendar } from "../calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
|
||||
type DateRangePickerProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
value?: DateRange;
|
||||
defaultValue?: DateRange;
|
||||
disabled?: boolean;
|
||||
handleDateChange?: (date: DateRange | undefined) => void;
|
||||
onValueChange?: (date: DateRange | undefined) => void;
|
||||
};
|
||||
|
||||
export function DateRangePicker({
|
||||
value,
|
||||
defaultValue,
|
||||
disabled,
|
||||
className,
|
||||
handleDateChange,
|
||||
children,
|
||||
onValueChange,
|
||||
}: DateRangePickerProps) {
|
||||
const [date, setDate] = useState<DateRange>();
|
||||
const [dateState, setDateState] = useState<DateRange | undefined>(
|
||||
defaultValue,
|
||||
);
|
||||
|
||||
const isControlled = value !== undefined;
|
||||
const dateValue = isControlled ? value : dateState;
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled) {
|
||||
setDateState(value);
|
||||
}
|
||||
}, [isControlled, value]);
|
||||
|
||||
const handleSelect = (date: DateRange | undefined) => {
|
||||
setDate(date);
|
||||
handleDateChange?.(date);
|
||||
if (!isControlled) {
|
||||
setDateState(date);
|
||||
}
|
||||
onValueChange?.(date);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -36,19 +54,19 @@ export function DateRangePicker({
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-[3.2rem] min-w-[12rem] justify-start border-input !bg-background px-4 py-2 text-left text-[1.3rem] font-normal text-muted-foreground hover:!bg-accent",
|
||||
!date && "text-muted-foreground",
|
||||
!dateValue && "text-muted-foreground",
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<CalendarIcon className="!size-[1.6rem]" />
|
||||
{date?.from ? (
|
||||
date.to ? (
|
||||
{dateValue?.from ? (
|
||||
dateValue.to ? (
|
||||
<>
|
||||
{format(date.from, "yyyy-MM-dd")} ~{" "}
|
||||
{format(date.to, "yyyy-MM-dd")}
|
||||
{format(dateValue.from, "yyyy-MM-dd")} ~{" "}
|
||||
{format(dateValue.to, "yyyy-MM-dd")}
|
||||
</>
|
||||
) : (
|
||||
format(date.from, "yyyy-MM-dd")
|
||||
format(dateValue.from, "yyyy-MM-dd")
|
||||
)
|
||||
) : (
|
||||
<span>기간 선택</span>
|
||||
@@ -56,14 +74,17 @@ export function DateRangePicker({
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={date?.from}
|
||||
selected={date}
|
||||
onSelect={handleSelect}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
<div>
|
||||
{children}
|
||||
<Calendar
|
||||
initialFocus
|
||||
mode="range"
|
||||
defaultMonth={dateValue?.from}
|
||||
selected={dateValue}
|
||||
onSelect={handleSelect}
|
||||
numberOfMonths={2}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./accordion";
|
||||
export * from "./badge";
|
||||
export * from "./button";
|
||||
export * from "./button-group";
|
||||
export * from "./calendar";
|
||||
export * from "./carousel";
|
||||
export * from "./checkbox";
|
||||
|
||||
Reference in New Issue
Block a user