바코드/QR코드 투명 배경 처리 및 QR코드 에러 복구 버그 수정
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -217,7 +217,7 @@ export function ReportDesignerCanvas() {
|
||||
barcodeFieldName: "",
|
||||
showBarcodeText: true,
|
||||
barcodeColor: "#000000",
|
||||
barcodeBackground: "#ffffff",
|
||||
barcodeBackground: "transparent",
|
||||
barcodeMargin: 10,
|
||||
qrErrorCorrectionLevel: "M" as const,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user