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
4 changes: 4 additions & 0 deletions .github/workflows/test-react-zpl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
paths:
- 'apps/react-zpl/**'
- 'apps/zpl-core/**'
- 'tests/**'
- '.github/workflows/test-react-zpl.yml'

Expand All @@ -25,6 +26,9 @@ jobs:
- name: Install dependencies
run: pnpm install

- name: Build zpl-core
run: pnpm --filter @zpl-kit/zpl-core build

- name: Build react-zpl
run: pnpm --filter @zpl-kit/react-zpl build

Expand Down
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# zpl-kit

React로 ZPL 라벨을 작성합니다. React 컴포넌트로 ZPL 라벨을 선언적으로 작성하면 `ZplLabel.print()`가 컴포넌트 트리를 ZPL 문자열로 변환합니다.
ZPL 라벨을 생성하는 모노레포입니다. React 컴포넌트로 선언적으로 작성하거나, Node.js 환경에서 순수 함수로 직접 ZPL 문자열을 생성할 수 있습니다.

## 패키지

| 패키지 | 설명 | 환경 |
| --- | --- | --- |
| `@zpl-kit/react-zpl` | React 컴포넌트로 ZPL 생성 | React (브라우저 / SSR) |
| `@zpl-kit/zpl-core` | 순수 함수로 ZPL 생성 | Node.js / 브라우저 |

## @zpl-kit/react-zpl

React 컴포넌트로 ZPL 라벨을 선언적으로 작성하면 `ZplLabel.print()`가 컴포넌트 트리를 ZPL 문자열로 변환합니다.

```tsx
import { ZplLabel, Text, Line } from '@zpl-kit/react-zpl';
Expand Down Expand Up @@ -36,7 +47,26 @@ const zpl = ZplLabel.print(
// ^XZ
```

## 컴포넌트
## @zpl-kit/zpl-core

React 없이 순수 함수로 ZPL 문자열을 생성합니다. Node.js 서버, CLI 도구 등 React가 없는 환경에서 사용할 수 있습니다.

```ts
import { renderLabel, renderText, renderLine, createLabelContext } from '@zpl-kit/zpl-core';

const context = createLabelContext({ width: 800, height: 400 });

const zpl = renderLabel({
type: 'label',
props: { width: 800, height: 400 },
children: [
{ type: 'text', props: { text: 'Hello, ZPL!', fieldOriginX: 50, fieldOriginY: 50 } },
{ type: 'line', props: { direction: 'horizontal', length: 700, fieldOriginX: 50, fieldOriginY: 120, thickness: 3 } },
],
});
```

## react-zpl 컴포넌트

### `<ZplLabel>`

Expand Down Expand Up @@ -147,7 +177,8 @@ const zpl = ZplLabel.print(
```
zpl-kit/
├── apps/
│ ├── react-zpl/ # 코어 라이브러리 (@zpl-kit/react-zpl)
│ ├── react-zpl/ # React 어댑터 (@zpl-kit/react-zpl)
│ ├── zpl-core/ # 순수 함수 코어 (@zpl-kit/zpl-core)
│ └── zpl-viewer/ # ZPL 뷰어 웹
├── demos/
Expand Down Expand Up @@ -190,7 +221,6 @@ pnpm docs:preview # 문서 빌드 미리보기

- **ZPL 출력 줄바꿈**: 현재는 명령 사이에 줄바꿈(`\n`)을 넣습니다. 추후 제거하거나 옵션으로 제어할 예정입니다.
- **React 19**: 모노레포 전반(코어·뷰어·데모·문서)을 React 19 기준으로 맞출 계획입니다.
- **Node 환경**: 브라우저뿐 아니라 Node에서도 `ZplLabel.print()` 등으로 동일하게 ZPL 문자열을 얻을 수 있도록 지원을 추가할 계획입니다.

## ZPL 미리보기

Expand Down
1 change: 1 addition & 0 deletions apps/react-zpl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@zpl-kit/zpl-core": "workspace:*",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
Expand Down
40 changes: 3 additions & 37 deletions apps/react-zpl/src/components/Circle.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,14 @@
import { fieldOrigin, graphicCircle, newLine } from '../commands';
import { ObjectValues } from '../types';
import { COLOR } from '../constants';
import { ObjectValues, ZplElement } from '../types';

const MIN_DIAMETER = 3;
const MAX_DIAMETER = 4095;
const MIN_THICKNESS = 1;
const MAX_THICKNESS = 4095;

interface CircleProps {
export interface CircleProps {
diameter: number;
fieldOriginX?: number;
fieldOriginY?: number;
thickness?: number;
lineColor?: ObjectValues<typeof COLOR>;
}

export const Circle: ZplElement<CircleProps> = () => <span />;
export const Circle = (_props: CircleProps) => <span />;

Circle.displayName = 'Circle';

Circle.print = (element, _context) => {
const {
diameter,
fieldOriginX = 0,
fieldOriginY = 0,
thickness = MIN_THICKNESS,
lineColor = COLOR.BLACK,
} = element.props;

if (diameter < MIN_DIAMETER || diameter > MAX_DIAMETER) {
throw new Error(
`Circle: diameter는 ${MIN_DIAMETER}~${MAX_DIAMETER} 사이여야 합니다. (diameter=${diameter})`
);
}
if (thickness < MIN_THICKNESS || thickness > MAX_THICKNESS) {
throw new Error(
`Circle: thickness는 ${MIN_THICKNESS}~${MAX_THICKNESS} 사이여야 합니다. (thickness=${thickness})`
);
}

const output: string[] = [];

output.push(fieldOrigin({ offsetX: fieldOriginX, offsetY: fieldOriginY }));
output.push(graphicCircle({ diameter, thickness, lineColor }));

return output.flat().join(newLine());
};
49 changes: 3 additions & 46 deletions apps/react-zpl/src/components/DiagonalLine.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { fieldOrigin, graphicDiagonal, newLine } from '../commands';
import { ObjectValues } from '../types';
import { COLOR, DIAGONAL_ORIENTATION } from '../constants';
import { ObjectValues, ZplElement } from '../types';

const MIN_THICKNESS = 1;
const DIAGONAL_MIN = 3;

interface DiagonalLineProps {
export interface DiagonalLineProps {
width: number;
height: number;
orientation?: ObjectValues<typeof DIAGONAL_ORIENTATION>;
Expand All @@ -15,45 +11,6 @@ interface DiagonalLineProps {
lineColor?: ObjectValues<typeof COLOR>;
}

export const DiagonalLine: ZplElement<DiagonalLineProps> = () => <span />;
export const DiagonalLine = (_props: DiagonalLineProps) => <span />;

DiagonalLine.displayName = 'DiagonalLine';

DiagonalLine.print = (element, _context) => {
const {
width,
height,
orientation = DIAGONAL_ORIENTATION.RIGHT_DOWN,
fieldOriginX = 0,
fieldOriginY = 0,
thickness = MIN_THICKNESS,
lineColor = COLOR.BLACK,
} = element.props;

if (width < DIAGONAL_MIN || height < DIAGONAL_MIN) {
throw new Error(
`DiagonalLine: width와 height는 ${DIAGONAL_MIN} 이상이어야 합니다. (width=${width}, height=${height})`
);
}

if (thickness < MIN_THICKNESS) {
throw new Error(
`DiagonalLine: thickness는 ${MIN_THICKNESS} 이상이어야 합니다. (thickness=${thickness})`
);
}

const graphic = graphicDiagonal({
width,
height,
thickness,
lineColor,
orientation,
});

const output: string[] = [];

output.push(fieldOrigin({ offsetX: fieldOriginX, offsetY: fieldOriginY }));
output.push(graphic);

return output.flat().join(newLine());
};
41 changes: 3 additions & 38 deletions apps/react-zpl/src/components/Ellipse.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { fieldOrigin, graphicEllipse, newLine } from '../commands';
import { ObjectValues } from '../types';
import { COLOR } from '../constants';
import { ObjectValues, ZplElement } from '../types';

const MIN_SIZE = 3;
const MAX_SIZE = 4095;
const MIN_THICKNESS = 1;
const MAX_THICKNESS = 4095;

interface EllipseProps {
export interface EllipseProps {
width: number;
height: number;
fieldOriginX?: number;
Expand All @@ -16,35 +10,6 @@ interface EllipseProps {
lineColor?: ObjectValues<typeof COLOR>;
}

export const Ellipse: ZplElement<EllipseProps> = () => <span />;
export const Ellipse = (_props: EllipseProps) => <span />;

Ellipse.displayName = 'Ellipse';

Ellipse.print = (element, _context) => {
const {
width,
height,
fieldOriginX = 0,
fieldOriginY = 0,
thickness = MIN_THICKNESS,
lineColor = COLOR.BLACK,
} = element.props;

if (width < MIN_SIZE || width > MAX_SIZE || height < MIN_SIZE || height > MAX_SIZE) {
throw new Error(
`Ellipse: width와 height는 ${MIN_SIZE}~${MAX_SIZE} 사이여야 합니다. (width=${width}, height=${height})`
);
}
if (thickness < MIN_THICKNESS || thickness > MAX_THICKNESS) {
throw new Error(
`Ellipse: thickness는 ${MIN_THICKNESS}~${MAX_THICKNESS} 사이여야 합니다. (thickness=${thickness})`
);
}

const output: string[] = [];

output.push(fieldOrigin({ offsetX: fieldOriginX, offsetY: fieldOriginY }));
output.push(graphicEllipse({ width, height, thickness, lineColor }));

return output.flat().join(newLine());
};
54 changes: 3 additions & 51 deletions apps/react-zpl/src/components/Line.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { fieldOrigin, graphicBox, newLine } from '../commands';
import { ObjectValues } from '../types';
import { COLOR } from '../constants';
import { ObjectValues, ZplElement } from '../types';

const MIN_LENGTH = 1;
const MIN_THICKNESS = 1;

interface LineProps {
export interface LineProps {
direction: 'horizontal' | 'vertical';
length: number;
fieldOriginX?: number;
Expand All @@ -14,50 +10,6 @@ interface LineProps {
lineColor?: ObjectValues<typeof COLOR>;
}

export const Line: ZplElement<LineProps> = () => <span />;
export const Line = (_props: LineProps) => <span />;

Line.displayName = 'Line';

Line.print = (element, _context) => {
const {
direction,
length,
fieldOriginX = 0,
fieldOriginY = 0,
thickness = MIN_THICKNESS,
lineColor = COLOR.BLACK,
} = element.props;

if (length < MIN_LENGTH) {
throw new Error(
`Line: length는 ${MIN_LENGTH} 이상이어야 합니다. (length=${length})`
);
}
if (thickness < MIN_THICKNESS) {
throw new Error(
`Line: thickness는 ${MIN_THICKNESS} 이상이어야 합니다. (thickness=${thickness})`
);
}

const graphic =
direction === 'horizontal'
? graphicBox({
width: length,
height: thickness,
borderThickness: thickness,
lineColor,
})
: graphicBox({
width: thickness,
height: length,
borderThickness: thickness,
lineColor,
});

const output: string[] = [];

output.push(fieldOrigin({ offsetX: fieldOriginX, offsetY: fieldOriginY }));
output.push(graphic);

return output.flat().join(newLine());
};
53 changes: 3 additions & 50 deletions apps/react-zpl/src/components/QrCode.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,21 @@
import { PropsWithChildren } from 'react';

import {
barcodeQR,
fieldData,
fieldOrigin,
newLine,
type QrErrorCorrection,
} from '../commands';
import { type QrErrorCorrection } from '@zpl-kit/zpl-core';
import { ObjectValues } from '../types';
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 }) => (
export const QrCode = ({ children }: QrCodeProps) => (
<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());
};
Loading
Loading