119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
|
|
/**
|
||
|
|
* Zebra 프린터 Web Bluetooth LE 연동
|
||
|
|
* Chrome/Edge (Chromium) 에서만 지원. BLE로 ZPL 전송 (512바이트 청크)
|
||
|
|
* 참고: https://developer.zebra.com/content/printing-webapp-using-webbluetooth
|
||
|
|
*/
|
||
|
|
|
||
|
|
const ZEBRA_BLE_SERVICE_UUID = "38eb4a80-c570-11e3-9507-0002a5d5c51b";
|
||
|
|
const ZEBRA_BLE_CHAR_UUID = "38eb4a82-c570-11e3-9507-0002a5d5c51b";
|
||
|
|
const CHUNK_SIZE = 512;
|
||
|
|
const CHUNK_DELAY_MS = 20;
|
||
|
|
|
||
|
|
export function isWebBluetoothSupported(): boolean {
|
||
|
|
if (typeof window === "undefined") return false;
|
||
|
|
return !!(navigator.bluetooth && navigator.bluetooth.requestDevice);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 지원 브라우저 안내 문구 */
|
||
|
|
export function getUnsupportedMessage(): string {
|
||
|
|
if (!isWebBluetoothSupported()) {
|
||
|
|
return "이 브라우저는 Web Bluetooth를 지원하지 않습니다. Chrome 또는 Edge(Chromium)에서 열어주세요. HTTPS 또는 localhost 필요.";
|
||
|
|
}
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ZebraPrintResult {
|
||
|
|
success: boolean;
|
||
|
|
message: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Zebra 프린터를 BLE로 선택·연결 후 ZPL 데이터 전송
|
||
|
|
* - 사용자에게 블루투스 기기 선택 창이 뜸 (Zebra 프린터 BLE 선택)
|
||
|
|
* - ZPL을 512바이트 단위로 나누어 순차 전송
|
||
|
|
*/
|
||
|
|
export async function printZPLToZebraBLE(zpl: string): Promise<ZebraPrintResult> {
|
||
|
|
if (!isWebBluetoothSupported()) {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
message: "Web Bluetooth를 지원하지 않는 브라우저입니다. Chrome 또는 Edge에서 시도해주세요.",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
let device: BluetoothDevice | null = null;
|
||
|
|
let server: BluetoothRemoteGATTServer | null = null;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 1) 서비스 UUID로만 필터 시 Android에서 Zebra가 광고하지 않으면 목록에 안 나옴.
|
||
|
|
// 2) acceptAllDevices + optionalServices 로 모든 BLE 기기 표시 후, 연결해 Zebra 서비스 사용.
|
||
|
|
const useAcceptAll =
|
||
|
|
typeof navigator !== "undefined" &&
|
||
|
|
/Android/i.test(navigator.userAgent);
|
||
|
|
|
||
|
|
if (useAcceptAll) {
|
||
|
|
device = await navigator.bluetooth.requestDevice({
|
||
|
|
acceptAllDevices: true,
|
||
|
|
optionalServices: [ZEBRA_BLE_SERVICE_UUID],
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
device = await navigator.bluetooth.requestDevice({
|
||
|
|
filters: [{ services: [ZEBRA_BLE_SERVICE_UUID] }],
|
||
|
|
optionalServices: [ZEBRA_BLE_SERVICE_UUID],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!device) {
|
||
|
|
return { success: false, message: "프린터를 선택하지 않았습니다." };
|
||
|
|
}
|
||
|
|
|
||
|
|
server = await device.gatt!.connect();
|
||
|
|
let service: BluetoothRemoteGATTService;
|
||
|
|
try {
|
||
|
|
service = await server.getPrimaryService(ZEBRA_BLE_SERVICE_UUID);
|
||
|
|
} catch {
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
message:
|
||
|
|
"선택한 기기는 Zebra 프린터가 아니거나 BLE 인쇄를 지원하지 않습니다. 'ZD421' 등 Zebra 프린터를 선택해 주세요.",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
const characteristic = await service.getCharacteristic(ZEBRA_BLE_CHAR_UUID);
|
||
|
|
|
||
|
|
const encoder = new TextEncoder();
|
||
|
|
const data = encoder.encode(zpl);
|
||
|
|
const totalChunks = Math.ceil(data.length / CHUNK_SIZE);
|
||
|
|
|
||
|
|
for (let i = 0; i < totalChunks; i++) {
|
||
|
|
const start = i * CHUNK_SIZE;
|
||
|
|
const end = Math.min(start + CHUNK_SIZE, data.length);
|
||
|
|
const chunk = data.slice(start, end);
|
||
|
|
await characteristic.writeValue(chunk);
|
||
|
|
if (i < totalChunks - 1 && CHUNK_DELAY_MS > 0) {
|
||
|
|
await new Promise((r) => setTimeout(r, CHUNK_DELAY_MS));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return { success: true, message: "Zebra 프린터로 전송했습니다." };
|
||
|
|
} catch (err: unknown) {
|
||
|
|
const e = err as Error & { name?: string };
|
||
|
|
if (e.name === "NotFoundError") {
|
||
|
|
return { success: false, message: "Zebra 프린터(BLE)를 찾을 수 없습니다. 프린터 전원과 블루투스 설정을 확인하세요." };
|
||
|
|
}
|
||
|
|
if (e.name === "NotAllowedError") {
|
||
|
|
return { success: false, message: "블루투스 연결이 거부되었습니다." };
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
success: false,
|
||
|
|
message: e.message || "Zebra BLE 출력 중 오류가 발생했습니다.",
|
||
|
|
};
|
||
|
|
} finally {
|
||
|
|
if (server && device?.gatt?.connected) {
|
||
|
|
try {
|
||
|
|
device.gatt.disconnect();
|
||
|
|
} catch {
|
||
|
|
// ignore
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|