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

View File

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