바코드/QR코드 투명 배경 처리 및 QR코드 에러 복구 버그 수정

This commit is contained in:
dohyeons 2025-12-22 11:29:35 +09:00
parent 506a31df02
commit c5cb4336e5
2 changed files with 145 additions and 117 deletions

View File

@ -19,7 +19,16 @@ interface BarcodeRendererProps {
margin: number; margin: number;
} }
function BarcodeRenderer({ value, format, width, height, displayValue, lineColor, background, margin }: BarcodeRendererProps) { function BarcodeRenderer({
value,
format,
width,
height,
displayValue,
lineColor,
background,
margin,
}: BarcodeRendererProps) {
const svgRef = useRef<SVGSVGElement>(null); const svgRef = useRef<SVGSVGElement>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -53,14 +62,16 @@ function BarcodeRenderer({ value, format, width, height, displayValue, lineColor
// JsBarcode는 format을 소문자로 받음 // JsBarcode는 format을 소문자로 받음
const barcodeFormat = format.toLowerCase(); const barcodeFormat = format.toLowerCase();
// transparent는 빈 문자열로 변환 (SVG 배경 없음)
const bgColor = background === "transparent" ? "" : background;
JsBarcode(svgRef.current, trimmedValue, { JsBarcode(svgRef.current, trimmedValue, {
format: barcodeFormat, format: barcodeFormat,
width: 2, width: 2,
height: Math.max(30, height - (displayValue ? 30 : 10)), height: Math.max(30, height - (displayValue ? 30 : 10)),
displayValue: displayValue, displayValue: displayValue,
lineColor: lineColor, lineColor: lineColor,
background: background, background: bgColor,
margin: margin, margin: margin,
fontSize: 12, fontSize: 12,
textMargin: 2, textMargin: 2,
@ -74,10 +85,7 @@ function BarcodeRenderer({ value, format, width, height, displayValue, lineColor
return ( return (
<div className="relative h-full w-full"> <div className="relative h-full w-full">
{/* SVG는 항상 렌더링 (에러 시 숨김) */} {/* SVG는 항상 렌더링 (에러 시 숨김) */}
<svg <svg ref={svgRef} className={`max-h-full max-w-full ${error ? "hidden" : ""}`} />
ref={svgRef}
className={`max-h-full max-w-full ${error ? "hidden" : ""}`}
/>
{/* 에러 메시지 오버레이 */} {/* 에러 메시지 오버레이 */}
{error && ( {error && (
<div className="absolute inset-0 flex flex-col items-center justify-center text-xs text-red-500"> <div className="absolute inset-0 flex flex-col items-center justify-center text-xs text-red-500">
@ -103,40 +111,48 @@ function QRCodeRenderer({ value, size, fgColor, bgColor, level }: QRCodeRenderer
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
if (canvasRef.current && value) { if (!canvasRef.current || !value) return;
QRCode.toCanvas(
canvasRef.current, // 매번 에러 상태 초기화 후 재시도
value, setError(null);
{
width: Math.max(50, size), // qrcode 라이브러리는 hex 색상만 지원, transparent는 흰색으로 대체
margin: 2, const lightColor = bgColor === "transparent" ? "#ffffff" : bgColor;
color: {
dark: fgColor, QRCode.toCanvas(
light: bgColor, canvasRef.current,
}, value,
errorCorrectionLevel: level, {
width: Math.max(50, size),
margin: 2,
color: {
dark: fgColor,
light: lightColor,
}, },
(err) => { errorCorrectionLevel: level,
if (err) { },
setError("QR코드 생성 실패"); (err) => {
} else { if (err) {
setError(null); // 실제 에러 메시지 표시
} setError(err.message || "QR코드 생성 실패");
} }
); },
} );
}, [value, size, fgColor, bgColor, level]); }, [value, size, fgColor, bgColor, level]);
if (error) { return (
return ( <div className="relative h-full w-full">
<div className="flex h-full w-full flex-col items-center justify-center text-xs text-red-500"> {/* Canvas는 항상 렌더링 (에러 시 숨김) */}
<span>{error}</span> <canvas ref={canvasRef} className={`max-h-full max-w-full ${error ? "hidden" : ""}`} />
<span className="mt-1 text-gray-400">{value}</span> {/* 에러 메시지 오버레이 */}
</div> {error && (
); <div className="absolute inset-0 flex flex-col items-center justify-center text-xs text-red-500">
} <span>{error}</span>
<span className="mt-1 text-gray-400">{value}</span>
return <canvas ref={canvasRef} className="max-h-full max-w-full" />; </div>
)}
</div>
);
} }
interface CanvasComponentProps { interface CanvasComponentProps {
@ -560,7 +576,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
backgroundColor: "transparent", backgroundColor: "transparent",
}), }),
...(component.lineStyle === "double" && { ...(component.lineStyle === "double" && {
boxShadow: isHorizontal boxShadow: isHorizontal
? `0 ${dividerLineWidth * 2}px 0 0 ${dividerLineColor}` ? `0 ${dividerLineWidth * 2}px 0 0 ${dividerLineColor}`
: `${dividerLineWidth * 2}px 0 0 0 ${dividerLineColor}`, : `${dividerLineWidth * 2}px 0 0 0 ${dividerLineColor}`,
}), }),
@ -814,7 +830,12 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
}; };
// 쿼리 바인딩된 값 가져오기 // 쿼리 바인딩된 값 가져오기
const getCalcItemValue = (item: { label: string; value: number | string; operator: string; fieldName?: string }): number => { const getCalcItemValue = (item: {
label: string;
value: number | string;
operator: string;
fieldName?: string;
}): number => {
if (item.fieldName && component.queryId) { if (item.fieldName && component.queryId) {
const queryResult = getQueryResult(component.queryId); const queryResult = getQueryResult(component.queryId);
if (queryResult && queryResult.rows && queryResult.rows.length > 0) { if (queryResult && queryResult.rows && queryResult.rows.length > 0) {
@ -829,14 +850,18 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
// 계산 결과 (첫 번째 항목은 기준값, 두 번째부터 연산자 적용) // 계산 결과 (첫 번째 항목은 기준값, 두 번째부터 연산자 적용)
const calculateResult = (): number => { const calculateResult = (): number => {
if (calcItems.length === 0) return 0; if (calcItems.length === 0) return 0;
// 첫 번째 항목은 기준값 // 첫 번째 항목은 기준값
let result = getCalcItemValue(calcItems[0] as { label: string; value: number | string; operator: string; fieldName?: string }); let result = getCalcItemValue(
calcItems[0] as { label: string; value: number | string; operator: string; fieldName?: string },
);
// 두 번째 항목부터 연산자 적용 // 두 번째 항목부터 연산자 적용
for (let i = 1; i < calcItems.length; i++) { for (let i = 1; i < calcItems.length; i++) {
const item = calcItems[i]; const item = calcItems[i];
const val = getCalcItemValue(item as { label: string; value: number | string; operator: string; fieldName?: string }); const val = getCalcItemValue(
item as { label: string; value: number | string; operator: string; fieldName?: string },
);
switch (item.operator) { switch (item.operator) {
case "+": case "+":
result += val; result += val;
@ -861,38 +886,40 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
<div className="flex h-full w-full flex-col overflow-hidden"> <div className="flex h-full w-full flex-col overflow-hidden">
{/* 항목 목록 */} {/* 항목 목록 */}
<div className="flex-1 overflow-auto px-2 py-1"> <div className="flex-1 overflow-auto px-2 py-1">
{calcItems.map((item: { label: string; value: number | string; operator: string; fieldName?: string }, index: number) => { {calcItems.map(
const itemValue = getCalcItemValue(item); (
return ( item: { label: string; value: number | string; operator: string; fieldName?: string },
<div key={index} className="flex items-center justify-between py-1"> index: number,
<span ) => {
className="flex-shrink-0" const itemValue = getCalcItemValue(item);
style={{ return (
width: `${calcLabelWidth}px`, <div key={index} className="flex items-center justify-between py-1">
fontSize: `${calcLabelFontSize}px`, <span
color: calcLabelColor, className="flex-shrink-0"
}} style={{
> width: `${calcLabelWidth}px`,
{item.label} fontSize: `${calcLabelFontSize}px`,
</span> color: calcLabelColor,
<span }}
className="text-right" >
style={{ {item.label}
fontSize: `${calcValueFontSize}px`, </span>
color: calcValueColor, <span
}} className="text-right"
> style={{
{formatNumber(itemValue)} fontSize: `${calcValueFontSize}px`,
</span> color: calcValueColor,
</div> }}
); >
})} {formatNumber(itemValue)}
</span>
</div>
);
},
)}
</div> </div>
{/* 구분선 */} {/* 구분선 */}
<div <div className="mx-1 flex-shrink-0 border-t" style={{ borderColor: component.borderColor || "#374151" }} />
className="mx-1 flex-shrink-0 border-t"
style={{ borderColor: component.borderColor || "#374151" }}
/>
{/* 결과 */} {/* 결과 */}
<div className="flex items-center justify-between px-2 py-2"> <div className="flex items-center justify-between px-2 py-2">
<span <span
@ -923,7 +950,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
const barcodeType = component.barcodeType || "CODE128"; const barcodeType = component.barcodeType || "CODE128";
const showBarcodeText = component.showBarcodeText !== false; const showBarcodeText = component.showBarcodeText !== false;
const barcodeColor = component.barcodeColor || "#000000"; const barcodeColor = component.barcodeColor || "#000000";
const barcodeBackground = component.barcodeBackground || "#ffffff"; const barcodeBackground = component.barcodeBackground || "transparent";
const barcodeMargin = component.barcodeMargin ?? 10; const barcodeMargin = component.barcodeMargin ?? 10;
const qrErrorLevel = component.qrErrorCorrectionLevel || "M"; const qrErrorLevel = component.qrErrorCorrectionLevel || "M";
@ -947,7 +974,7 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
const isQR = barcodeType === "QR"; const isQR = barcodeType === "QR";
return ( return (
<div <div
className="flex h-full w-full items-center justify-center overflow-hidden" className="flex h-full w-full items-center justify-center overflow-hidden"
style={{ backgroundColor: barcodeBackground }} style={{ backgroundColor: barcodeBackground }}
> >
@ -1003,39 +1030,39 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
const isChecked = getCheckboxValue(); const isChecked = getCheckboxValue();
return ( return (
<div <div
className={`flex h-full w-full items-center gap-2 ${ className={`flex h-full w-full items-center gap-2 ${
checkboxLabelPosition === "left" ? "flex-row-reverse justify-end" : "" checkboxLabelPosition === "left" ? "flex-row-reverse justify-end" : ""
}`} }`}
>
{/* 체크박스 */}
<div
className="flex items-center justify-center rounded-sm border-2 transition-colors"
style={{
width: `${checkboxSize}px`,
height: `${checkboxSize}px`,
borderColor: isChecked ? checkboxColor : checkboxBorderColor,
backgroundColor: isChecked ? checkboxColor : "transparent",
}}
> >
{/* 체크박스 */} {isChecked && (
<div <svg
className="flex items-center justify-center rounded-sm border-2 transition-colors" viewBox="0 0 24 24"
style={{ fill="none"
width: `${checkboxSize}px`, stroke="white"
height: `${checkboxSize}px`, strokeWidth="3"
borderColor: isChecked ? checkboxColor : checkboxBorderColor, strokeLinecap="round"
backgroundColor: isChecked ? checkboxColor : "transparent", strokeLinejoin="round"
}} style={{
> width: `${checkboxSize * 0.7}px`,
{isChecked && ( height: `${checkboxSize * 0.7}px`,
<svg }}
viewBox="0 0 24 24" >
fill="none" <polyline points="20 6 9 17 4 12" />
stroke="white" </svg>
strokeWidth="3" )}
strokeLinecap="round" </div>
strokeLinejoin="round" {/* 레이블 */}
style={{
width: `${checkboxSize * 0.7}px`,
height: `${checkboxSize * 0.7}px`,
}}
>
<polyline points="20 6 9 17 4 12" />
</svg>
)}
</div>
{/* 레이블 */}
{/* 레이블 */} {/* 레이블 */}
{checkboxLabel && ( {checkboxLabel && (
<span <span
@ -1098,18 +1125,19 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
{isSelected && !isLocked && ( {isSelected && !isLocked && (
<div <div
className={`resize-handle absolute h-3 w-3 rounded-full bg-blue-500 ${ className={`resize-handle absolute h-3 w-3 rounded-full bg-blue-500 ${
component.type === "divider" component.type === "divider"
? component.orientation === "vertical" ? component.orientation === "vertical"
? "bottom-0 left-1/2 cursor-s-resize" // 세로 구분선: 하단 중앙 ? "bottom-0 left-1/2 cursor-s-resize" // 세로 구분선: 하단 중앙
: "right-0 top-1/2 cursor-e-resize" // 가로 구분선: 우측 중앙 : "top-1/2 right-0 cursor-e-resize" // 가로 구분선: 우측 중앙
: "right-0 bottom-0 cursor-se-resize" // 일반 컴포넌트: 우하단 : "right-0 bottom-0 cursor-se-resize" // 일반 컴포넌트: 우하단
}`} }`}
style={{ style={{
transform: component.type === "divider" transform:
? component.orientation === "vertical" component.type === "divider"
? "translate(-50%, 50%)" // 세로 구분선 ? component.orientation === "vertical"
: "translate(50%, -50%)" // 가로 구분선 ? "translate(-50%, 50%)" // 세로 구분선
: "translate(50%, 50%)" // 일반 컴포넌트 : "translate(50%, -50%)" // 가로 구분선
: "translate(50%, 50%)", // 일반 컴포넌트
}} }}
onMouseDown={handleResizeStart} onMouseDown={handleResizeStart}
/> />

View File

@ -217,7 +217,7 @@ export function ReportDesignerCanvas() {
barcodeFieldName: "", barcodeFieldName: "",
showBarcodeText: true, showBarcodeText: true,
barcodeColor: "#000000", barcodeColor: "#000000",
barcodeBackground: "#ffffff", barcodeBackground: "transparent",
barcodeMargin: 10, barcodeMargin: 10,
qrErrorCorrectionLevel: "M" as const, qrErrorCorrectionLevel: "M" as const,
}), }),