feat: 배너 관리 페이지 작업
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
import { BannerPage } from "@/pages/banner";
|
||||
export default BannerPage;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/banner";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ serial: string }>;
|
||||
}) {
|
||||
const { serial } = await params;
|
||||
return <VModifyPage serial={serial} />;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { CreatePage } from "@/pages/banner";
|
||||
export default CreatePage;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
BannerFormData,
|
||||
BannerInfo,
|
||||
BannerList,
|
||||
BannerParams,
|
||||
BannerService,
|
||||
} from "../model/banner-interface";
|
||||
|
||||
export class BannerApiService implements BannerService {
|
||||
public async getBannerList(params: BannerParams): Promise<BannerList> {
|
||||
const response = await client.get<BannerList>(
|
||||
`${ENDPOINT.CMS_SERVICE.BANNERS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getBannerInfo(bnrSn: string): Promise<BannerInfo> {
|
||||
const response = await client.get<BannerInfo>(
|
||||
`${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createBanner(data: BannerFormData): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
ENDPOINT.CMS_SERVICE.BANNERS,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateBanner(
|
||||
bnrSn: string,
|
||||
data: BannerFormData,
|
||||
): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteBanner(bnrSn: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const BannerApi = new BannerApiService();
|
||||
@@ -0,0 +1,3 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { BannerPage } from "./ui/banner-page";
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Upload, type HttpResponse } from "tus-js-client";
|
||||
import { getCookie, setCookie } from "@/shared/lib";
|
||||
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
||||
|
||||
type UploadOptions = {
|
||||
handleUploadStart?: (upload: Upload) => void;
|
||||
handleProgress?: (percentage: string) => void;
|
||||
handleSuccess?: (upload: Upload) => void;
|
||||
handleResponse?: (res: HttpResponse) => void;
|
||||
};
|
||||
|
||||
export const handleUpload = (files: FileList, options?: UploadOptions) => {
|
||||
let metadata = {};
|
||||
|
||||
const file = files.item(0);
|
||||
if (!file) return;
|
||||
metadata = {
|
||||
filename: file.name,
|
||||
filetype: file.type,
|
||||
};
|
||||
|
||||
const atflCd = getCookie("atfl-cd-key");
|
||||
if (atflCd) {
|
||||
metadata = {
|
||||
...metadata,
|
||||
atflCd,
|
||||
};
|
||||
}
|
||||
|
||||
const upload = new Upload(file, {
|
||||
endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`,
|
||||
chunkSize: 10 * 1024 * 1024,
|
||||
headers: {
|
||||
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
|
||||
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
|
||||
},
|
||||
metadata,
|
||||
onError(error) {
|
||||
console.error(`Failed because: ${error}`);
|
||||
},
|
||||
onProgress(bytesUploaded, bytesTotal) {
|
||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
||||
options?.handleProgress?.(percentage);
|
||||
},
|
||||
onAfterResponse(req, res) {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
setCookie("atfl-cd-key", JSON.parse(fileInfo)?.atflCd ?? "");
|
||||
options?.handleResponse?.(res);
|
||||
},
|
||||
onSuccess() {
|
||||
options?.handleSuccess?.(upload);
|
||||
},
|
||||
});
|
||||
|
||||
upload.start();
|
||||
options?.handleUploadStart?.(upload);
|
||||
|
||||
console.log("upload start >>>>>", upload);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export const BANNER_TYPE = {
|
||||
BNRMAIN: "BNRMAIN",
|
||||
BNRFTER: "BNRFTER",
|
||||
} as const;
|
||||
|
||||
export type BannerType = (typeof BANNER_TYPE)[keyof typeof BANNER_TYPE];
|
||||
|
||||
export interface BannerParams extends PageableParams {
|
||||
bnrTypeCd?: BannerType;
|
||||
bnrTtl?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface Banner {
|
||||
bnrSn: number;
|
||||
bnrTypeCd: BannerType;
|
||||
bnrTypeNm: string;
|
||||
bnrTtl: string;
|
||||
lnkgUrlAddr: string;
|
||||
useYn: string;
|
||||
mdfcnDt: string;
|
||||
mdfr: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
export interface BannerInfo extends Omit<Banner, "mdfr"> {
|
||||
bnrExpln: string;
|
||||
newWndwYn: string;
|
||||
atflCd: string;
|
||||
regDt: string;
|
||||
rgTrNm: string;
|
||||
}
|
||||
|
||||
export const bannerFormSchema = z.object({
|
||||
bnrTypeCd: z.string(),
|
||||
bnrTtl: z.string(),
|
||||
bnrExpln: z.string().nullish(),
|
||||
lnkgUrlAddr: z.string().nullish(),
|
||||
newWndwYn: z.string(),
|
||||
atflCd: z.string().nullish(),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
export type BannerFormData = z.infer<typeof bannerFormSchema>;
|
||||
export type BannerList = PageableList<Banner>;
|
||||
|
||||
export interface BannerService {
|
||||
getBannerList: (params: BannerParams) => Promise<BannerList>;
|
||||
getBannerInfo: (bnrSn: string) => Promise<BannerInfo>;
|
||||
createBanner: (data: BannerFormData) => Promise<void>;
|
||||
updateBanner: (bnrSn: string, data: BannerFormData) => Promise<void>;
|
||||
deleteBanner: (bnrSn: string) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import { type Banner } from "./banner-interface";
|
||||
|
||||
export const columns: ColumnDef<Banner>[] = [
|
||||
{
|
||||
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"
|
||||
/>
|
||||
),
|
||||
},
|
||||
// TODO: 배너 이미지 셀 추가
|
||||
{
|
||||
accessorKey: "bnrTtl",
|
||||
header: "배너명",
|
||||
},
|
||||
{
|
||||
accessorKey: "bnrTypeNm",
|
||||
header: "배너구분",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => {
|
||||
const date = row.getValue("mdfcnDt") as string;
|
||||
return date ? dayjs(date).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Post } from "@/entities/posting";
|
||||
|
||||
export const columns: ColumnDef<Post>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "siteNm",
|
||||
header: "사이트",
|
||||
},
|
||||
{
|
||||
accessorKey: "pstgPrdSeNm",
|
||||
header: "게시기간",
|
||||
cell: ({ row }) => {
|
||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||
const pstgBgngDt = row.original.pstgBgngDt;
|
||||
const pstgEndDt = row.original.pstgEndDt;
|
||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,337 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type { HttpResponse, Upload } from "tus-js-client";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
FileUploader,
|
||||
Input,
|
||||
Label,
|
||||
Progress,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import type { BannerFormData } from "../model/banner-interface";
|
||||
import { handleUpload } from "../lib/utils";
|
||||
|
||||
type BannerFormProps = {
|
||||
form: UseFormReturn<BannerFormData>;
|
||||
bnrSn?: string;
|
||||
handleSave: (data: BannerFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function BannerForm({
|
||||
form,
|
||||
bnrSn,
|
||||
handleSave,
|
||||
handleDelete,
|
||||
}: BannerFormProps) {
|
||||
const isModifyMode = !!bnrSn;
|
||||
const atflCd = form.watch("atflCd");
|
||||
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const [attachmentFileId, setAttachmentFileId] = useState("");
|
||||
const [upload, setUpload] = useState<Upload | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const { data: atflId } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd],
|
||||
queryFn: () => AttachmentApi.getAttachments(atflCd!),
|
||||
select: (data) => data?.[data.length - 1]?.atflId,
|
||||
enabled: !!atflCd,
|
||||
});
|
||||
|
||||
const onSubmit = async (data: BannerFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
if (await confirm(confirmMessage)) {
|
||||
await handleSave(data);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
await handleDelete?.();
|
||||
}
|
||||
};
|
||||
|
||||
const handleProgress = (percentage: string) => {
|
||||
setProgress(Number(percentage));
|
||||
};
|
||||
|
||||
const handleUploadResponse = async (res: HttpResponse) => {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
|
||||
if (atflCd) form.setValue("atflCd", atflCd);
|
||||
if (atflId) setAttachmentFileId(atflId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (atflId) setAttachmentFileId(atflId);
|
||||
}, [atflId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>배너명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bnrTtl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="배너명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>배너구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bnrTypeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="BNRTYPE"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>배너 이미지</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflCd"
|
||||
render={() => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<FileUploader
|
||||
id="atflCd"
|
||||
className="!h-auto !max-w-none flex-col !items-start"
|
||||
accept=".jpeg,.jpg,.png"
|
||||
onUpload={(e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
handleUpload(files, {
|
||||
handleUploadStart: setUpload,
|
||||
handleProgress,
|
||||
handleSuccess: setUpload,
|
||||
handleResponse: handleUploadResponse,
|
||||
});
|
||||
}}
|
||||
customPreview={
|
||||
<>
|
||||
<Label htmlFor="atflCd">
|
||||
<div className="relative h-[27rem] w-[48rem] overflow-hidden rounded-sm border border-border bg-accent">
|
||||
<p className="absolute bottom-3 left-0 p-2 text-base leading-none">
|
||||
{(upload?.file as File)?.name}
|
||||
</p>
|
||||
<div className="relative h-full w-full">
|
||||
{attachmentFileId && (
|
||||
<Image
|
||||
src={`${ENDPOINT.ATTACH_SERVICE.IMAGES}/${attachmentFileId}`}
|
||||
alt="업로드된 이미지"
|
||||
fill
|
||||
objectFit="contain"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Progress
|
||||
value={progress}
|
||||
className="absolute bottom-0 left-0"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<div className="mb-2 text-lg font-medium text-label">
|
||||
<ul className="ml-10 list-disc">
|
||||
<li>
|
||||
이미지 파일(jpg, jpeg, png, gif)만
|
||||
업로드 가능합니다.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>링크 URL</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lnkgUrlAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="배너에 연결할 링크를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>링크 열기</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newWndwYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="newWndwYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="newWndwN" />
|
||||
<Label htmlFor="newWndwN">현재창</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="newWndwY" />
|
||||
<Label htmlFor="newWndwY">새창</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>배너 설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bnrExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem] !leading-6"
|
||||
placeholder="배너에 대한 설명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">미사용</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { BannerForm };
|
||||
@@ -0,0 +1,136 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { columns } from "../model/banner-table-columns";
|
||||
import { type Banner, type BannerParams } from "../model/banner-interface";
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
function BannerPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
const [bannerQuery, setBannerQuery] = useState<BannerParams>({ bnrTtl: "" });
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: bannerList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS],
|
||||
queryFn: () =>
|
||||
BannerApi.getBannerList({
|
||||
...bannerQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: bannerList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: bannerList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Banner) => {
|
||||
router.push(`/banner/register/${row.bnrSn}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const postingTargets = table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => ({
|
||||
pstgTrgtNm: row.original.bnrTtl,
|
||||
pstgTrgtNo: row.original.bnrSn,
|
||||
}));
|
||||
|
||||
if (!postingTargets.length) {
|
||||
setPostingTargets([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setPostingTargets(postingTargets);
|
||||
}, [rowSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
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 htmlFor="" className="mr-2 text-label">
|
||||
배너명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={bannerQuery.bnrTtl}
|
||||
onChange={(e) =>
|
||||
setBannerQuery({ ...bannerQuery, bnrTtl: e.target.value })
|
||||
}
|
||||
/>
|
||||
<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">
|
||||
<PostingDialog
|
||||
postingType={POSTING_TYPE.BANNER}
|
||||
postingTargets={postingTargets}
|
||||
/>
|
||||
<Button onClick={() => router.push("/banner/register")}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[30.4rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { BannerPage };
|
||||
@@ -0,0 +1,46 @@
|
||||
"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 { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
bannerFormSchema,
|
||||
type BannerFormData,
|
||||
} from "../model/banner-interface";
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { BannerForm } from "./banner-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createBanner } = useMutation({
|
||||
mutationFn: (data: BannerFormData) => BannerApi.createBanner(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<BannerFormData>({
|
||||
resolver: zodResolver(bannerFormSchema),
|
||||
defaultValues: {
|
||||
newWndwYn: "N",
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(form.formState.errors);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">배너 등록</h3>
|
||||
</div>
|
||||
<BannerForm form={form} handleSave={createBanner} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable } from "@common/components/ui";
|
||||
import { PostingApi, type WithModePost } from "@/entities/posting";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
bannerFormSchema,
|
||||
type BannerFormData,
|
||||
} from "../model/banner-interface";
|
||||
import { columns } from "../model/posting-table-columns";
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { BannerForm } from "./banner-form";
|
||||
|
||||
function ModifyPage({ serial }: { serial: string }) {
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: bannerDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, serial],
|
||||
queryFn: () => BannerApi.getBannerInfo(serial),
|
||||
});
|
||||
|
||||
const { data: postHistory } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, serial],
|
||||
queryFn: () => PostingApi.getPostingHistory({ pstgTrgtNo: Number(serial) }),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateBanner } = useMutation({
|
||||
mutationFn: (data: BannerFormData) => BannerApi.updateBanner(serial, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteBanner } = useMutation({
|
||||
mutationFn: () => BannerApi.deleteBanner(serial),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: requestPosting } = useMutation({
|
||||
mutationFn: (data: WithModePost[]) => PostingApi.requestPosting(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, serial],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<BannerFormData>({
|
||||
resolver: zodResolver(bannerFormSchema),
|
||||
values: bannerDetail,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: postHistory?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleDeletePostingHistory = async () => {
|
||||
const data = table.getFilteredSelectedRowModel().rows.map((row) => ({
|
||||
...row.original,
|
||||
mode: "D",
|
||||
}));
|
||||
|
||||
setOnConfirm(true);
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
await requestPosting(data);
|
||||
alert("삭제되었습니다.");
|
||||
}
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-6 flex flex-col">
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">배너 수정</h3>
|
||||
</div>
|
||||
<BannerForm
|
||||
form={form}
|
||||
bnrSn={serial}
|
||||
handleSave={updateBanner}
|
||||
handleDelete={deleteBanner}
|
||||
/>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
사이트 게시 이력
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
disabled={!table.getFilteredSelectedRowModel().rows.length}
|
||||
onClick={handleDeletePostingHistory}
|
||||
>
|
||||
이력삭제
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -57,7 +57,6 @@ export const columns: ColumnDef<Image>[] = [
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
cell: ({ row }) => (row.getValue("useYn") === "Y" ? "사용" : "미사용"),
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
|
||||
@@ -50,6 +50,7 @@ function ImagePage() {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: imageList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
@@ -86,6 +87,10 @@ function ImagePage() {
|
||||
refetch();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
@@ -120,7 +125,7 @@ function ImagePage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[29.2rem] overflow-auto !p-0">
|
||||
<div className="card !m-[1.2rem] h-[30.4rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ export const ENDPOINT = {
|
||||
CONTENTS: "/cms-service/api/v1/contents",
|
||||
USER_ORGANIZATION: "/cms-service/api/v1/user-ognz",
|
||||
IMAGES: "/cms-service/api/v1/images",
|
||||
BANNERS: "/cms-service/api/v1/bnrs",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
|
||||
Reference in New Issue
Block a user