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/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 f90179a..127e51f 100644 --- a/apps/react-zpl/src/commands/index.ts +++ b/apps/react-zpl/src/commands/index.ts @@ -13,3 +13,5 @@ export * from './changeDefaultFont'; export * from './changeInternationalEncoding'; export * from './graphicBox'; export * from './graphicDiagonal'; +export * from './graphicCircle'; +export * from './graphicEllipse'; 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/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 f49ba38..fde5df3 100644 --- a/apps/react-zpl/src/components/index.ts +++ b/apps/react-zpl/src/components/index.ts @@ -2,3 +2,5 @@ export * from './ZplLabel'; export * from './DiagonalLine'; export * from './Line'; export * from './Text'; +export * from './Circle'; +export * from './Ellipse'; diff --git a/demos/electron/src/renderer/src/App.tsx b/demos/electron/src/renderer/src/App.tsx index 5acbe27..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 { 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 ( @@ -41,6 +41,8 @@ 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..bff0169 100644 --- a/demos/web/src/App.tsx +++ b/demos/web/src/App.tsx @@ -1,6 +1,13 @@ import { useState } from 'react'; -import { DiagonalLine, Line, Text, ZplLabel } from '@zpl-kit/react-zpl'; +import { + Circle, + DiagonalLine, + Ellipse, + Line, + Text, + ZplLabel, +} from '@zpl-kit/react-zpl'; import './App.css'; @@ -55,6 +62,19 @@ const TestLabel = ({ text }: { text: string }) => { fieldOriginY={20} thickness={2} /> + + ); }; 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'); + }); + }); +}); 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'); + }); + }); +});