Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions apps/react-zpl/src/commands/barcodeQR.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { defineCommand } from './base';
import { ORIENTATION } from '../constants';
import type { ObjectValues } from '../types';

/** ZPL `^BQ`의 `d`(에러 정정). `^FD` QR 데이터 첫 문자와 반드시 일치해야 함 */
export type QrErrorCorrection = 'H' | 'Q' | 'M' | 'L';

export interface BarcodeQRParams {
orientation: ObjectValues<typeof ORIENTATION>;
model: 1 | 2;
magnification: number;
errorCorrection: QrErrorCorrection;
maskValue: number;
}

const MIN_MAGNIFICATION = 1;
const MAX_MAGNIFICATION = 100;

/**
* ZPL `^BQ` — QR 코드 필드 명령. `^FD`/`^FS`는 포함하지 않으며 {@link fieldData}와 함께 사용해야 함.
*
* @see https://docs.zebra.com/us/en/printers/software/zpl-pg/c-zpl-zpl-commands/r-zpl-bq.html
*/
export const barcodeQR = defineCommand<BarcodeQRParams>((params) => {
const { orientation, model, magnification, errorCorrection, maskValue } =
params;

if (magnification < MIN_MAGNIFICATION || magnification > MAX_MAGNIFICATION) {
throw new Error(
`barcodeQR: magnification은 ${MIN_MAGNIFICATION}~${MAX_MAGNIFICATION}이어야 합니다. (magnification=${magnification})`
);
}
if (!Number.isInteger(maskValue) || maskValue < 0 || maskValue > 7) {
throw new Error(
`barcodeQR: maskValue는 0~7 정수여야 합니다. (maskValue=${maskValue})`
);
}
if (model !== 1 && model !== 2) {
throw new Error(`barcodeQR: model은 1 또는 2여야 합니다. (model=${model})`);
}

return `^BQ${orientation},${model},${magnification},${errorCorrection},${maskValue}`;
});
1 change: 1 addition & 0 deletions apps/react-zpl/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from './graphicBox';
export * from './graphicDiagonal';
export * from './graphicCircle';
export * from './graphicEllipse';
export * from './barcodeQR';
68 changes: 68 additions & 0 deletions apps/react-zpl/src/components/QrCode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { PropsWithChildren } from 'react';

import {
barcodeQR,
fieldData,
fieldOrigin,
newLine,
type QrErrorCorrection,
} from '../commands';
import { ORIENTATION } from '../constants';
import type { ObjectValues, ZplElement, ZplElementContext } from '../types';

export interface QrCodeProps extends PropsWithChildren {
fieldOriginX?: number;
fieldOriginY?: number;
fieldOrientation?: ObjectValues<typeof ORIENTATION>;
model?: 1 | 2;
magnification?: number;
/** `^BQ`의 `d`와 `^FD` 스위치 첫 글자가 동일해야 함 */
errorCorrectionLevel?: QrErrorCorrection;
maskValue?: number;
}

export const QrCode: ZplElement<QrCodeProps> = ({ children }) => (
<span>{children}</span>
);

QrCode.displayName = 'QrCode';

QrCode.print = (element, context: ZplElementContext) => {
const {
children,
fieldOriginX = 0,
fieldOriginY = 0,
fieldOrientation,
model = 2,
magnification = 5,
errorCorrectionLevel = 'Q',
maskValue = 7,
} = element.props;

if (typeof children !== 'string') {
throw new Error('QrCode 컴포넌트는 children에 문자열만 허용합니다.');
}
if (children.length === 0) {
throw new Error('QrCode: children은 빈 문자열일 수 없습니다.');
}

const orientation: ObjectValues<typeof ORIENTATION> =
fieldOrientation ?? context.labelOrientation;

const output: string[] = [];

output.push(fieldOrigin({ offsetX: fieldOriginX, offsetY: fieldOriginY }));
output.push(
barcodeQR({
orientation,
model,
magnification,
errorCorrection: errorCorrectionLevel,
maskValue,
})
);
/** 자동 데이터 입력 모드 `A` — Zebra QR `^FD` 규칙에 따라 페이로드는 `"{ECC}A,{payload}"` 형식 */
output.push(fieldData(`${errorCorrectionLevel}A,${children}`));

return output.join(newLine());
};
1 change: 1 addition & 0 deletions apps/react-zpl/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './Line';
export * from './Text';
export * from './Circle';
export * from './Ellipse';
export * from './QrCode';
4 changes: 2 additions & 2 deletions apps/react-zpl/src/utils/print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export const printChild = (
// TODO: print element with context
if (isZplElement(element.type)) {
children.push(element.type.print(element, context));
} else if (node.props.children) {
children.push(printChild(node.props.children, context));
} else if (element.props.children) {
children.push(printChild(element.props.children, context));
}
}

Expand Down
2 changes: 1 addition & 1 deletion demos/electron/src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' https://api.labelary.com"
/>
</head>

Expand Down
85 changes: 85 additions & 0 deletions demos/electron/src/renderer/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.app-header {
background: rgba(255, 255, 255, 0.95);
padding: 2rem;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.app-header h1 {
font-size: 2.5rem;
color: #333;
margin-bottom: 0.5rem;
font-weight: 700;
}

.app-header p {
color: #666;
font-size: 1.1rem;
}

.app-main {
flex: 1;
padding: 2rem;
display: flex;
justify-content: center;
align-items: flex-start;
}

.demo-content {
background: white;
border-radius: 12px;
padding: 2.5rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
max-width: 900px;
width: 100%;
}

.demo-content h2 {
color: #333;
margin-bottom: 1rem;
font-size: 1.8rem;
font-weight: 600;
}

.demo-content p {
color: #666;
line-height: 1.8;
margin-bottom: 1rem;
font-size: 1rem;
}

.privacy-notice {
font-size: 0.9rem;
color: #856404;
background: #fff3cd;
padding: 0.5rem 0.75rem;
border-radius: 6px;
margin-bottom: 1rem;
}

.error-message {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem 1rem;
margin-bottom: 1rem;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
color: #b91c1c;
font-size: 0.95rem;
line-height: 1.5;
}

.error-message::before {
content: '⚠';
flex-shrink: 0;
font-size: 1.1rem;
}
Loading
Loading