feat: 콘텐츠 및 게시판 메뉴 qrcode 조회 기능 추가
This commit is contained in:
@@ -39,7 +39,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@faker-js/faker": "^9.5.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
|
||||
@@ -4,6 +4,7 @@ const subDomain =
|
||||
process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "15.3.0",
|
||||
"next-qrcode": "^2.5.1",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
@@ -44,7 +45,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@faker-js/faker": "^9.5.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
|
||||
@@ -127,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 };
|
||||
@@ -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", `/cntns/${content.contsSn}`);
|
||||
form.setValue("menuLnkgSn", content.contsSn);
|
||||
form.setValue("menuLnkgSnNm", content.contsTtl);
|
||||
};
|
||||
@@ -56,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>
|
||||
@@ -95,7 +105,8 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{(menuType === "BOARD" ||
|
||||
{(menuType === "CONTENTS" ||
|
||||
menuType === "BOARD" ||
|
||||
menuType === "INSIDE" ||
|
||||
menuType === "OUTSIDE") && (
|
||||
<TableRow>
|
||||
@@ -111,14 +122,10 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder={
|
||||
menuType === "BOARD"
|
||||
? "게시판 선택 시 자동으로 입력됩니다."
|
||||
: menuType === "INSIDE"
|
||||
? "메뉴에 연결된 페이지의 경로를 입력하십시오."
|
||||
: "'http://' 또는 'https://'를 포함하여 입력하십시오."
|
||||
placeholder={getUrlPathPlaceholder()}
|
||||
disabled={
|
||||
menuType === "CONTENTS" || menuType === "BOARD"
|
||||
}
|
||||
disabled={menuType === "BOARD"}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
@@ -298,31 +305,34 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
<TableRow>
|
||||
<TableHead>QR코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
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">
|
||||
<FormField
|
||||
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>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
<QRCodeDialog url={form.watch("urlPath") ?? ""} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
|
||||
Reference in New Issue
Block a user