바코드/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,6 +62,8 @@ 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,
@ -60,7 +71,7 @@ function BarcodeRenderer({ value, format, width, height, displayValue, lineColor
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,7 +111,14 @@ 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;
// 매번 에러 상태 초기화 후 재시도
setError(null);
// qrcode 라이브러리는 hex 색상만 지원, transparent는 흰색으로 대체
const lightColor = bgColor === "transparent" ? "#ffffff" : bgColor;
QRCode.toCanvas( QRCode.toCanvas(
canvasRef.current, canvasRef.current,
value, value,
@ -112,31 +127,32 @@ function QRCodeRenderer({ value, size, fgColor, bgColor, level }: QRCodeRenderer
margin: 2, margin: 2,
color: { color: {
dark: fgColor, dark: fgColor,
light: bgColor, light: lightColor,
}, },
errorCorrectionLevel: level, errorCorrectionLevel: level,
}, },
(err) => { (err) => {
if (err) { if (err) {
setError("QR코드 생성 실패"); // 실제 에러 메시지 표시
} else { setError(err.message || "QR코드 생성 실패");
setError(null);
}
} }
},
); );
}
}, [value, size, fgColor, bgColor, level]); }, [value, size, fgColor, bgColor, level]);
if (error) {
return ( return (
<div className="flex h-full w-full flex-col items-center justify-center text-xs text-red-500"> <div className="relative h-full w-full">
{/* Canvas는 항상 렌더링 (에러 시 숨김) */}
<canvas ref={canvasRef} className={`max-h-full max-w-full ${error ? "hidden" : ""}`} />
{/* 에러 메시지 오버레이 */}
{error && (
<div className="absolute inset-0 flex flex-col items-center justify-center text-xs text-red-500">
<span>{error}</span> <span>{error}</span>
<span className="mt-1 text-gray-400">{value}</span> <span className="mt-1 text-gray-400">{value}</span>
</div> </div>
)}
</div>
); );
}
return <canvas ref={canvasRef} className="max-h-full max-w-full" />;
} }
interface CanvasComponentProps { interface CanvasComponentProps {
@ -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) {
@ -831,12 +852,16 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
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,7 +886,11 @@ 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(
(
item: { label: string; value: number | string; operator: string; fieldName?: string },
index: number,
) => {
const itemValue = getCalcItemValue(item); const itemValue = getCalcItemValue(item);
return ( return (
<div key={index} className="flex items-center justify-between py-1"> <div key={index} className="flex items-center justify-between py-1">
@ -886,13 +915,11 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
</span> </span>
</div> </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";
@ -1101,15 +1128,16 @@ export function CanvasComponent({ component }: CanvasComponentProps) {
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.type === "divider"
? component.orientation === "vertical" ? component.orientation === "vertical"
? "translate(-50%, 50%)" // 세로 구분선 ? "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,
}), }),