This commit is contained in:
2025-07-01 10:51:57 +09:00
90 changed files with 4706 additions and 2209 deletions
+13 -3
View File
@@ -21,20 +21,28 @@ import {
Skeleton,
} from "@common/components";
import { AttachmentApi } from "@/entities/attachment";
import { PostApi, type PostParams } from "@/entities/post";
import {
PostApi,
deleteReasonMessages,
type PostParams,
} from "@/entities/post";
import { AuthorityButton } from "@/entities/menu-authority";
import { ENDPOINT } from "@/shared/config";
import {
useAlertStore,
useMenuStore,
usePrompt,
useSelectPrompt,
useMutationWrapper,
} from "@/shared/lib";
const selectData = Object.entries(deleteReasonMessages).map(
([label, value]) => ({ label, value }),
);
function FaqPage({ bbsId }: { bbsId: string }) {
const router = useRouter();
const pathname = usePathname();
const { prompt } = usePrompt();
const { prompt } = useSelectPrompt(selectData);
const alert = useAlertStore((state) => state.setMessage);
const bbsInfo = useMenuStore((state) => state.bbsInfo);
@@ -75,11 +83,13 @@ function FaqPage({ bbsId }: { bbsId: string }) {
const handleDeletePost = async (pstNo: number) => {
const delRsn = await getPromptValue("삭제 사유를 입력해주세요.");
if (!delRsn) return;
deletePost({ pstNo, delRsn });
};
const onSelectedPostDelete = async () => {
const delRsn = await getPromptValue("삭제 사유를 입력해주세요.");
if (!delRsn) return;
await Promise.all(
selectedPostNumbers.map((pstNo) =>
PostApi.deletePost(parseInt(bbsId), pstNo, delRsn),
@@ -26,7 +26,7 @@ export const columns: ColumnDef<GroupUserListResponseDto>[] = [
enableHiding: false,
},
{
accessorKey: "groupId",
accessorKey: "userId",
header: "사용자ID",
},
{
@@ -57,7 +57,6 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
onRowSelectionChange: setRowSelection,
getRowId: (row) => row.userUnqId,
},
dataTableOptions: {
viewPagination: true,
@@ -65,15 +64,18 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
});
const handleDeleteSelected = async () => {
const selectedRows = Object.keys(rowSelection);
const selectedRows = table.getSelectedRowModel().rows.map((row) => ({
groupId: row.original.groupId,
userUnqId: row.original.userUnqId,
}));
if (selectedRows.length === 0 || !groupId) return;
if (!(await confirm("사용자를 삭제하시겠습니까?"))) return;
try {
const usersToDelete = selectedRows.map((userUnqId) => ({
const usersToDelete = selectedRows.map((row) => ({
mode: "D",
groupId,
userUnqId,
groupId: row.groupId,
userUnqId: row.userUnqId,
}));
await GroupUsersApi.saveGroupUser(usersToDelete);
@@ -90,6 +92,7 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {
useEffect(() => {
setPagination({ ...pagination, pageIndex: 0 });
setRowSelection({});
}, [groupId]);
return (
-1
View File
@@ -1 +0,0 @@
export { LoginlogPage } from "./ui/loginlog-page";
+12 -1
View File
@@ -75,6 +75,9 @@ function MenuPage() {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
});
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
});
},
});
@@ -91,6 +94,9 @@ function MenuPage() {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
});
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
});
},
});
@@ -101,6 +107,9 @@ function MenuPage() {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
});
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
});
},
});
@@ -118,8 +127,10 @@ function MenuPage() {
menuLnkgTypeCd: menuInfo?.menuLnkgTypeCd ?? MENU_TYPE.EMPTY,
iconNm: menuInfo?.iconName,
otptYn: menuInfo?.otptYn ?? "Y",
evlUseYn: menuInfo?.evlUseYn ?? "Y",
evlUseYn: menuInfo?.evlUseYn ?? "N",
qrcd: menuInfo?.qrcd ?? "N",
useYn: menuInfo?.useYn ?? "Y",
newWndwYn: menuInfo?.newWndwYn ?? "N",
},
});
@@ -0,0 +1,81 @@
import React, { useEffect, useRef, useState } from "react";
import { useQRCode } from "next-qrcode";
import {
Button,
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@common/components";
import { useClipboard } from "@frontend-opensource/use-react-hooks";
function QRCodeDialog({ url }: { url: string }) {
const { Canvas } = useQRCode();
const { copied: isCopied, copyText } = useClipboard();
const canvasRef = useRef<HTMLDivElement>(null);
const [originUrl, setOriginUrl] = useState("");
const qrCodeUrl = originUrl + url;
const handleDownload = () => {
const canvas = canvasRef.current?.querySelector("canvas");
if (!canvas) return;
const url = canvas.toDataURL("image/png");
const link = document.createElement("a");
link.href = url;
link.download = "qr-code.png";
link.click();
};
useEffect(() => {
if (typeof window === "undefined") return;
setOriginUrl(window.location.origin);
}, []);
return (
<Dialog>
<DialogTrigger asChild>
<Button disabled={!url}>QR 조회</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>QR 코드 스캔</DialogTitle>
</DialogHeader>
<div className="flex items-center justify-center p-4">
<div ref={canvasRef}>
<Canvas
text={url}
options={{
errorCorrectionLevel: "M",
margin: 1,
width: 200,
}}
/>
</div>
</div>
<div className="flex items-center justify-between rounded-sm bg-accent p-2 text-[1.3rem] text-accent-foreground">
<p className="break-all px-2">{qrCodeUrl}</p>
<Tooltip open={isCopied}>
<TooltipTrigger asChild>
<button
onClick={() => copyText(qrCodeUrl)}
className="size-10 shrink-0 rounded-sm hover:bg-[rgba(0,0,0,0.1)]"
>
<i className="diveicon di-clipboard" />
</button>
</TooltipTrigger>
<TooltipContent>
<span className="text-base">복사완료</span>
</TooltipContent>
</Tooltip>
</div>
<Button onClick={handleDownload}>QR 코드 다운로드</Button>
</DialogContent>
</Dialog>
);
}
export { QRCodeDialog };
+45 -24
View File
@@ -1,5 +1,5 @@
import dayjs from "dayjs";
import { type UseFormReturn } from "react-hook-form";
import type { UseFormReturn } from "react-hook-form";
import {
DatePicker,
IconFinder,
@@ -21,6 +21,7 @@ import type { MenuFormData, MenuType } from "../model/menu-interface";
import { OrganizationSearchDialog } from "./organization-search-dialog";
import { ContentSearchDialog } from "./content-search-dialog";
import { BoardSearchDialog } from "./board-search-dialog";
import { QRCodeDialog } from "./qrcode-dialog";
type TypeMenuFormProps = {
form: UseFormReturn<MenuFormData>;
@@ -33,6 +34,7 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
};
const handleContentSelect = (content: Contents) => {
form.setValue("urlPath", `/cms/cntns/${content.contsSn}`);
form.setValue("menuLnkgSn", content.contsSn);
form.setValue("menuLnkgSnNm", content.contsTtl);
};
@@ -43,6 +45,10 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
};
const handleBoardSelect = (board: BoardPopup) => {
form.setValue(
"urlPath",
`/cms/${board.bbsTypeCd.toLowerCase()}/${board.bbsId}`,
);
form.setValue("menuLnkgSn", board.bbsId);
form.setValue("menuLnkgSnNm", board.bbsNm);
};
@@ -52,6 +58,14 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
form.setValue("menuPicDsgnYmd", dayjs(date).format("YYYYMMDD"));
};
const getUrlPathPlaceholder = () => {
if (menuType === "CONTENTS") return "콘텐츠 선택 시 자동으로 입력됩니다.";
if (menuType === "BOARD") return "게시판 선택 시 자동으로 입력됩니다.";
if (menuType === "INSIDE")
return "메뉴에 연결된 페이지의 경로를 입력하십시오.";
return "'http://' 또는 'https://'를 포함하여 입력하십시오.";
};
return (
<Table variant="secondary" className="-mt-[0.1rem]">
<colgroup>
@@ -91,7 +105,10 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
</TableCell>
</TableRow>
)}
{(menuType === "INSIDE" || menuType === "OUTSIDE") && (
{(menuType === "CONTENTS" ||
menuType === "BOARD" ||
menuType === "INSIDE" ||
menuType === "OUTSIDE") && (
<TableRow>
<TableHead>연결 URL</TableHead>
<TableCell className="border">
@@ -105,10 +122,9 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder={
menuType === "INSIDE"
? "메뉴에 연결된 페이지의 경로를 입력하십시오."
: "'http://' 또는 'https://'를 포함하여 입력하십시오."
placeholder={getUrlPathPlaceholder()}
disabled={
menuType === "CONTENTS" || menuType === "BOARD"
}
/>
</FormControl>
@@ -293,25 +309,30 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
control={form.control}
name="qrcd"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
value={field.value ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="qrcdY" />
<Label htmlFor="qrcdY">사용</Label>
<div className="flex items-center gap-4">
<FormItem>
<FormControl>
<RadioGroup
value={field.value ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="qrcdY" />
<Label htmlFor="qrcdY">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="qrcdN" />
<Label htmlFor="qrcdN">사용안함</Label>
</div>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="qrcdN" />
<Label htmlFor="qrcdN">사용안함</Label>
</div>
</div>
</RadioGroup>
</FormControl>
</FormItem>
</RadioGroup>
</FormControl>
</FormItem>
{field.value === "Y" && (
<QRCodeDialog url={form.watch("urlPath") ?? ""} />
)}
</div>
)}
/>
</TableCell>
+7 -2
View File
@@ -23,13 +23,14 @@ import {
} from "@common/components";
import {
PostApi,
deleteReasonMessages,
type DeletePost,
type RecentPost,
type RecentPostParams,
} from "@/entities/post";
import { BoardApi } from "@/entities/board";
import { AuthorityButton } from "@/entities/menu-authority";
import { useAlertStore, usePrompt, useDataTable } from "@/shared/lib";
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { columns } from "../model/table-columns";
import { MovePostDialog } from "./move-post-dialog";
@@ -39,9 +40,13 @@ type DateRange = {
to?: Date | undefined;
};
const selectData = Object.entries(deleteReasonMessages).map(
([label, value]) => ({ label, value }),
);
function RecentPage() {
const router = useRouter();
const { prompt } = usePrompt();
const { prompt } = useSelectPrompt(selectData);
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const [dateRange, setDateRange] = useState<DateRange>();
+9 -6
View File
@@ -18,12 +18,8 @@ function ModifyPage({ siteId }: { siteId: string }) {
queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId],
queryFn: () => SiteApi.siteDetail(siteId),
select: (data) => {
const keys = Object.keys(siteFormSchema.shape);
const filteredData = Object.fromEntries(
Object.entries(data).filter(([key]) => keys.includes(key)),
);
return {
...filteredData,
...data,
faxNo: data.faxNumber,
bscSiteYn: data.basicSiteYn,
};
@@ -50,7 +46,14 @@ function ModifyPage({ siteId }: { siteId: string }) {
const form = useForm<SiteFormData>({
resolver: zodResolver(siteFormSchema),
values: siteDetail as SiteFormData,
values: {
...siteDetail,
siteId: siteDetail?.siteId ?? "",
siteNm: siteDetail?.siteNm ?? "",
siteKndCd: siteDetail?.siteKndCd ?? "",
bscSiteYn: siteDetail?.bscSiteYn ?? "",
useYn: siteDetail?.useYn ?? "",
},
});
return (
+1
View File
@@ -82,6 +82,7 @@ function SiteForm({ form, siteId, ...props }: SiteFormProps) {
<CodeSelect
{...field}
upCd="SITEKNDCD"
value={field.value ?? ""}
onValueChange={field.onChange}
/>
</FormControl>