From 68fda83a31361c3cf1332804daa42713607cccd9 Mon Sep 17 00:00:00 2001 From: Bori-github Date: Mon, 9 Mar 2026 22:31:02 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(react-zpl):=20Circle=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - graphicCircle(^GC) 명령 추가 - diameter 3~4095, thickness 1~4095 검증 - 단위 테스트 추가 --- apps/react-zpl/src/commands/graphicCircle.ts | 15 ++++ apps/react-zpl/src/commands/index.ts | 1 + apps/react-zpl/src/components/Circle.tsx | 48 ++++++++++++ apps/react-zpl/src/components/index.ts | 1 + tests/unit/Circle.test.tsx | 77 ++++++++++++++++++++ 5 files changed, 142 insertions(+) create mode 100644 apps/react-zpl/src/commands/graphicCircle.ts create mode 100644 apps/react-zpl/src/components/Circle.tsx create mode 100644 tests/unit/Circle.test.tsx diff --git a/apps/react-zpl/src/commands/graphicCircle.ts b/apps/react-zpl/src/commands/graphicCircle.ts new file mode 100644 index 0000000..7ea12fb --- /dev/null +++ b/apps/react-zpl/src/commands/graphicCircle.ts @@ -0,0 +1,15 @@ +import { defineCommand } from './base'; + +import { COLOR } from '../constants'; +import { ObjectValues } from '../types'; + +interface GraphicCircleParams { + diameter: number; + thickness?: number; + lineColor?: ObjectValues; +} + +export const graphicCircle = defineCommand( + ({ diameter, thickness = 1, lineColor = COLOR.BLACK }) => + `^GC${diameter},${thickness},${lineColor}^FS` +); diff --git a/apps/react-zpl/src/commands/index.ts b/apps/react-zpl/src/commands/index.ts index f90179a..c05a6cb 100644 --- a/apps/react-zpl/src/commands/index.ts +++ b/apps/react-zpl/src/commands/index.ts @@ -13,3 +13,4 @@ export * from './changeDefaultFont'; export * from './changeInternationalEncoding'; export * from './graphicBox'; export * from './graphicDiagonal'; +export * from './graphicCircle'; diff --git a/apps/react-zpl/src/components/Circle.tsx b/apps/react-zpl/src/components/Circle.tsx new file mode 100644 index 0000000..8d7a42d --- /dev/null +++ b/apps/react-zpl/src/components/Circle.tsx @@ -0,0 +1,48 @@ +import { fieldOrigin, graphicCircle, newLine } from '../commands'; +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 { + diameter: number; + fieldOriginX?: number; + fieldOriginY?: number; + thickness?: number; + lineColor?: ObjectValues; +} + +export const Circle: ZplElement = () => ; + +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()); +}; diff --git a/apps/react-zpl/src/components/index.ts b/apps/react-zpl/src/components/index.ts index f49ba38..1e8f20d 100644 --- a/apps/react-zpl/src/components/index.ts +++ b/apps/react-zpl/src/components/index.ts @@ -2,3 +2,4 @@ export * from './ZplLabel'; export * from './DiagonalLine'; export * from './Line'; export * from './Text'; +export * from './Circle'; diff --git a/tests/unit/Circle.test.tsx b/tests/unit/Circle.test.tsx new file mode 100644 index 0000000..2bde39e --- /dev/null +++ b/tests/unit/Circle.test.tsx @@ -0,0 +1,77 @@ +import { createElement } from 'react'; +import { describe, it, expect } from 'vitest'; + +import { Circle, type ZplElementContext } from '@zpl-kit/react-zpl'; + +const defaultContext: ZplElementContext = { + labelOrientation: 'N', + defaultFontName: 'J', + defaultFontWidth: 30, + defaultFontHeight: 30, +}; + +describe('Circle', () => { + describe('print', () => { + it('기본값으로 ^FO0,0\\&^GC{diameter},1,B^FS 생성', () => { + const el = createElement(Circle, { diameter: 50 }); + expect(Circle.print(el, defaultContext)).toBe('^FO0,0\\&^GC50,1,B^FS'); + }); + + it('fieldOrigin, thickness, lineColor 지정 시 올바른 ZPL 생성', () => { + const el = createElement(Circle, { + diameter: 100, + fieldOriginX: 10, + fieldOriginY: 20, + thickness: 5, + lineColor: 'B', + }); + expect(Circle.print(el, defaultContext)).toBe('^FO10,20\\&^GC100,5,B^FS'); + }); + + it('흰색 원(lineColor=W) 생성', () => { + const el = createElement(Circle, { diameter: 60, lineColor: 'W' }); + expect(Circle.print(el, defaultContext)).toContain('^GC60,1,W^FS'); + }); + + it('채워진 원 (thickness >= diameter) 허용', () => { + const el = createElement(Circle, { diameter: 50, thickness: 50 }); + expect(Circle.print(el, defaultContext)).toBe('^FO0,0\\&^GC50,50,B^FS'); + }); + }); + + describe('print - 검증', () => { + it('diameter < 3 시 에러 throw', () => { + const el = createElement(Circle, { diameter: 2 }); + expect(() => Circle.print(el, defaultContext)).toThrow( + 'diameter는 3~4095 사이여야 합니다' + ); + }); + + it('diameter > 4095 시 에러 throw', () => { + const el = createElement(Circle, { diameter: 4096 }); + expect(() => Circle.print(el, defaultContext)).toThrow( + 'diameter는 3~4095 사이여야 합니다' + ); + }); + + it('thickness < 1 시 에러 throw', () => { + const el = createElement(Circle, { diameter: 50, thickness: 0 }); + expect(() => Circle.print(el, defaultContext)).toThrow( + 'thickness는 1~4095 사이여야 합니다' + ); + }); + + it('thickness > 4095 시 에러 throw', () => { + const el = createElement(Circle, { diameter: 50, thickness: 4096 }); + expect(() => Circle.print(el, defaultContext)).toThrow( + 'thickness는 1~4095 사이여야 합니다' + ); + }); + }); + + describe('displayName', () => { + it('Circle으로 설정됨', () => { + expect(Circle.displayName).toBe('Circle'); + }); + }); +}); From 2761f95134f0f6ebb5521302e1eb96f44b457a1d Mon Sep 17 00:00:00 2001 From: Bori-github Date: Mon, 9 Mar 2026 22:35:42 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(demos):=20Circle=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=8D=B0=EB=AA=A8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - web, electron 데모에 Circle 예제 추가 --- demos/electron/src/renderer/src/App.tsx | 3 ++- demos/web/src/App.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/demos/electron/src/renderer/src/App.tsx b/demos/electron/src/renderer/src/App.tsx index 5acbe27..c556033 100644 --- a/demos/electron/src/renderer/src/App.tsx +++ b/demos/electron/src/renderer/src/App.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' -import { DiagonalLine, Line, Text, ZplLabel } from '@zpl-kit/react-zpl' +import { Circle, DiagonalLine, Line, Text, ZplLabel } from '@zpl-kit/react-zpl' const TestLabel = ({ text }: { text: string }) => { return ( @@ -41,6 +41,7 @@ const TestLabel = ({ text }: { text: string }) => { fieldOriginY={20} thickness={2} /> + ) } diff --git a/demos/web/src/App.tsx b/demos/web/src/App.tsx index e8970fb..50cd336 100644 --- a/demos/web/src/App.tsx +++ b/demos/web/src/App.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { DiagonalLine, Line, Text, ZplLabel } from '@zpl-kit/react-zpl'; +import { Circle, DiagonalLine, Line, Text, ZplLabel } from '@zpl-kit/react-zpl'; import './App.css'; @@ -55,6 +55,12 @@ const TestLabel = ({ text }: { text: string }) => { fieldOriginY={20} thickness={2} /> + ); }; From 312248903fe836d5d71c4362a72153d42fc5f00e Mon Sep 17 00:00:00 2001 From: Bori-github Date: Mon, 9 Mar 2026 22:42:08 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(react-zpl):=20Ellipse=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - graphicEllipse(^GE) 명령 추가 - width, height 3~4095, thickness 1~4095 검증 - 단위 테스트 추가 --- apps/react-zpl/src/commands/graphicEllipse.ts | 16 ++++ apps/react-zpl/src/commands/index.ts | 1 + apps/react-zpl/src/components/Ellipse.tsx | 50 ++++++++++++ apps/react-zpl/src/components/index.ts | 1 + tests/unit/Ellipse.test.tsx | 80 +++++++++++++++++++ 5 files changed, 148 insertions(+) create mode 100644 apps/react-zpl/src/commands/graphicEllipse.ts create mode 100644 apps/react-zpl/src/components/Ellipse.tsx create mode 100644 tests/unit/Ellipse.test.tsx diff --git a/apps/react-zpl/src/commands/graphicEllipse.ts b/apps/react-zpl/src/commands/graphicEllipse.ts new file mode 100644 index 0000000..12a6dba --- /dev/null +++ b/apps/react-zpl/src/commands/graphicEllipse.ts @@ -0,0 +1,16 @@ +import { defineCommand } from './base'; + +import { COLOR } from '../constants'; +import { ObjectValues } from '../types'; + +interface GraphicEllipseParams { + width: number; + height: number; + thickness?: number; + lineColor?: ObjectValues; +} + +export const graphicEllipse = defineCommand( + ({ width, height, thickness = 1, lineColor = COLOR.BLACK }) => + `^GE${width},${height},${thickness},${lineColor}^FS` +); diff --git a/apps/react-zpl/src/commands/index.ts b/apps/react-zpl/src/commands/index.ts index c05a6cb..127e51f 100644 --- a/apps/react-zpl/src/commands/index.ts +++ b/apps/react-zpl/src/commands/index.ts @@ -14,3 +14,4 @@ export * from './changeInternationalEncoding'; export * from './graphicBox'; export * from './graphicDiagonal'; export * from './graphicCircle'; +export * from './graphicEllipse'; diff --git a/apps/react-zpl/src/components/Ellipse.tsx b/apps/react-zpl/src/components/Ellipse.tsx new file mode 100644 index 0000000..01f2faa --- /dev/null +++ b/apps/react-zpl/src/components/Ellipse.tsx @@ -0,0 +1,50 @@ +import { fieldOrigin, graphicEllipse, newLine } from '../commands'; +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 { + width: number; + height: number; + fieldOriginX?: number; + fieldOriginY?: number; + thickness?: number; + lineColor?: ObjectValues; +} + +export const Ellipse: ZplElement = () => ; + +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()); +}; diff --git a/apps/react-zpl/src/components/index.ts b/apps/react-zpl/src/components/index.ts index 1e8f20d..fde5df3 100644 --- a/apps/react-zpl/src/components/index.ts +++ b/apps/react-zpl/src/components/index.ts @@ -3,3 +3,4 @@ export * from './DiagonalLine'; export * from './Line'; export * from './Text'; export * from './Circle'; +export * from './Ellipse'; diff --git a/tests/unit/Ellipse.test.tsx b/tests/unit/Ellipse.test.tsx new file mode 100644 index 0000000..5b57b34 --- /dev/null +++ b/tests/unit/Ellipse.test.tsx @@ -0,0 +1,80 @@ +import { createElement } from 'react'; +import { describe, it, expect } from 'vitest'; + +import { Ellipse, type ZplElementContext } from '@zpl-kit/react-zpl'; + +const defaultContext: ZplElementContext = { + labelOrientation: 'N', + defaultFontName: 'J', + defaultFontWidth: 30, + defaultFontHeight: 30, +}; + +describe('Ellipse', () => { + describe('print', () => { + it('기본값으로 ^FO0,0\\&^GE{width},{height},1,B^FS 생성', () => { + const el = createElement(Ellipse, { width: 80, height: 40 }); + expect(Ellipse.print(el, defaultContext)).toBe('^FO0,0\\&^GE80,40,1,B^FS'); + }); + + it('fieldOrigin, thickness, lineColor 지정 시 올바른 ZPL 생성', () => { + const el = createElement(Ellipse, { + width: 100, + height: 60, + fieldOriginX: 10, + fieldOriginY: 20, + thickness: 3, + lineColor: 'B', + }); + expect(Ellipse.print(el, defaultContext)).toBe('^FO10,20\\&^GE100,60,3,B^FS'); + }); + + it('흰색 타원(lineColor=W) 생성', () => { + const el = createElement(Ellipse, { width: 80, height: 40, lineColor: 'W' }); + expect(Ellipse.print(el, defaultContext)).toContain('^GE80,40,1,W^FS'); + }); + }); + + describe('print - 검증', () => { + it('width < 3 시 에러 throw', () => { + const el = createElement(Ellipse, { width: 2, height: 40 }); + expect(() => Ellipse.print(el, defaultContext)).toThrow( + 'width와 height는 3~4095 사이여야 합니다' + ); + }); + + it('height < 3 시 에러 throw', () => { + const el = createElement(Ellipse, { width: 80, height: 2 }); + expect(() => Ellipse.print(el, defaultContext)).toThrow( + 'width와 height는 3~4095 사이여야 합니다' + ); + }); + + it('width > 4095 시 에러 throw', () => { + const el = createElement(Ellipse, { width: 4096, height: 40 }); + expect(() => Ellipse.print(el, defaultContext)).toThrow( + 'width와 height는 3~4095 사이여야 합니다' + ); + }); + + it('thickness < 1 시 에러 throw', () => { + const el = createElement(Ellipse, { width: 80, height: 40, thickness: 0 }); + expect(() => Ellipse.print(el, defaultContext)).toThrow( + 'thickness는 1~4095 사이여야 합니다' + ); + }); + + it('thickness > 4095 시 에러 throw', () => { + const el = createElement(Ellipse, { width: 80, height: 40, thickness: 4096 }); + expect(() => Ellipse.print(el, defaultContext)).toThrow( + 'thickness는 1~4095 사이여야 합니다' + ); + }); + }); + + describe('displayName', () => { + it('Ellipse으로 설정됨', () => { + expect(Ellipse.displayName).toBe('Ellipse'); + }); + }); +}); From d11e5e68b97bdc63aaca0846ab4dc6e0ee5a1bf4 Mon Sep 17 00:00:00 2001 From: Bori-github Date: Mon, 9 Mar 2026 22:43:16 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat(demos):=20Ellipse=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=8D=B0=EB=AA=A8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - web, electron 데모에 Ellipse 예제 추가 --- demos/electron/src/renderer/src/App.tsx | 3 ++- demos/web/src/App.tsx | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/demos/electron/src/renderer/src/App.tsx b/demos/electron/src/renderer/src/App.tsx index c556033..afbac4b 100644 --- a/demos/electron/src/renderer/src/App.tsx +++ b/demos/electron/src/renderer/src/App.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' -import { Circle, DiagonalLine, Line, Text, ZplLabel } from '@zpl-kit/react-zpl' +import { Circle, DiagonalLine, Ellipse, Line, Text, ZplLabel } from '@zpl-kit/react-zpl' const TestLabel = ({ text }: { text: string }) => { return ( @@ -42,6 +42,7 @@ const TestLabel = ({ text }: { text: string }) => { thickness={2} /> + ) } diff --git a/demos/web/src/App.tsx b/demos/web/src/App.tsx index 50cd336..bff0169 100644 --- a/demos/web/src/App.tsx +++ b/demos/web/src/App.tsx @@ -1,6 +1,13 @@ import { useState } from 'react'; -import { Circle, DiagonalLine, Line, Text, ZplLabel } from '@zpl-kit/react-zpl'; +import { + Circle, + DiagonalLine, + Ellipse, + Line, + Text, + ZplLabel, +} from '@zpl-kit/react-zpl'; import './App.css'; @@ -61,6 +68,13 @@ const TestLabel = ({ text }: { text: string }) => { fieldOriginY={20} thickness={1} /> + ); };