"use client"; import React, { useState, useRef, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { Camera, CameraOff, CheckCircle2, AlertCircle, Scan } from "lucide-react"; import Webcam from "react-webcam"; import { BrowserMultiFormatReader, NotFoundException } from "@zxing/library"; export interface BarcodeScanModalProps { open: boolean; onOpenChange: (open: boolean) => void; targetField?: string; barcodeFormat?: "all" | "1d" | "2d"; autoSubmit?: boolean; onScanSuccess: (barcode: string) => void; userId?: string; } export const BarcodeScanModal: React.FC = ({ open, onOpenChange, targetField, barcodeFormat = "all", autoSubmit = false, onScanSuccess, userId = "guest", }) => { const [isScanning, setIsScanning] = useState(false); const [scannedCode, setScannedCode] = useState(""); const [manualInput, setManualInput] = useState(""); const [error, setError] = useState(""); const [hasPermission, setHasPermission] = useState(null); const webcamRef = useRef(null); const codeReaderRef = useRef(null); const scanIntervalRef = useRef(null); const manualInputRef = useRef(null); // 바코드 리더 초기화 + 모달 열릴 때 상태 리셋 useEffect(() => { if (open) { setScannedCode(""); setManualInput(""); setError(""); setIsScanning(false); setHasPermission(null); codeReaderRef.current = new BrowserMultiFormatReader(); } return () => { stopScanning(); if (codeReaderRef.current) { codeReaderRef.current.reset(); } }; }, [open]); // 카메라 권한 요청 const requestCameraPermission = async () => { // navigator.mediaDevices 지원 확인 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setHasPermission(false); setError( "이 브라우저는 카메라 접근을 지원하지 않거나, 보안 컨텍스트(HTTPS 또는 localhost)가 아닙니다. " + "현재 프로토콜: " + window.location.protocol ); toast.error("카메라 접근이 불가능합니다."); return; } try { // 후면 카메라 먼저 시도, 실패하면 전면 카메라 fallback let stream: MediaStream; try { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }); } catch { stream = await navigator.mediaDevices.getUserMedia({ video: true }); } setHasPermission(true); stream.getTracks().forEach((track) => track.stop()); } catch (err: any) { setHasPermission(false); if (err.name === "NotAllowedError") { setError("카메라 접근이 거부되었습니다. 브라우저 설정에서 카메라 권한을 허용해주세요."); toast.error("카메라 권한이 거부되었습니다."); } else if (err.name === "NotFoundError") { setError("카메라를 찾을 수 없습니다. 카메라가 연결되어 있는지 확인해주세요."); toast.error("카메라를 찾을 수 없습니다."); } else if (err.name === "NotReadableError") { setError("카메라가 이미 다른 애플리케이션에서 사용 중입니다."); toast.error("카메라가 사용 중입니다."); } else if (err.name === "NotSupportedError") { setError("보안 컨텍스트(HTTPS 또는 localhost)가 아니어서 카메라를 사용할 수 없습니다."); toast.error("HTTPS 환경이 필요합니다."); } else { setError(`카메라 접근 오류: ${err.name} - ${err.message}`); toast.error("카메라 접근 중 오류가 발생했습니다."); } } }; // 스캔 시작 const startScanning = () => { setIsScanning(true); setError(""); setScannedCode(""); scanIntervalRef.current = setInterval(() => { scanBarcode(); }, 500); }; // 스캔 중지 const stopScanning = () => { setIsScanning(false); if (scanIntervalRef.current) { clearInterval(scanIntervalRef.current); scanIntervalRef.current = null; } }; // 바코드 스캔 const scanBarcode = async () => { if (!webcamRef.current || !codeReaderRef.current) return; try { const imageSrc = webcamRef.current.getScreenshot(); if (!imageSrc) return; const img = new Image(); img.src = imageSrc; await new Promise((resolve) => { img.onload = resolve; }); const result = await codeReaderRef.current.decodeFromImageElement(img); if (result) { const barcode = result.getText(); setScannedCode(barcode); stopScanning(); toast.success(`바코드 스캔 완료: ${barcode}`); if (autoSubmit) { onScanSuccess(barcode); } } } catch (err) { if (!(err instanceof NotFoundException)) { // NotFoundException은 정상 (바코드 미인식) } } }; // 수동 확인 버튼 (스캔 결과 또는 직접 입력) const handleConfirm = () => { const code = scannedCode || manualInput.trim(); if (code) { onScanSuccess(code); // 호출 측에서 검색 필드를 덮어쓰기 onOpenChange(false); } else { toast.error("바코드를 스캔하거나 직접 입력해주세요."); } }; return ( 바코드 스캔 카메라로 바코드를 스캔합니다. {targetField && ` (대상 필드: ${targetField})`}
{/* 카메라 권한 요청 대기 중 */} {hasPermission === null && (

카메라 권한이 필요합니다

바코드를 스캔하려면 카메라 접근 권한을 허용해주세요.

권한 요청 안내:

  • 아래 버튼을 클릭하면 브라우저에서 권한 요청 팝업이 표시됩니다
  • 팝업에서 "허용" 버튼을 클릭해주세요
  • 권한은 언제든지 브라우저 설정에서 변경할 수 있습니다
)} {/* 카메라 권한 거부됨 */} {hasPermission === false && (

카메라 접근 권한이 필요합니다

{error}

권한 허용 방법:

  1. 브라우저 주소창 왼쪽의 자물쇠 아이콘을 클릭하세요
  2. "카메라" 항목을 찾아 "허용"으로 변경하세요
  3. 페이지를 새로고침하거나 다시 스캔을 시도하세요
)} {/* 웹캠 뷰 */} {hasPermission && (
{ // environment 카메라 실패 시 자동 fallback (Webcam 내부 처리) }} className="h-full w-full object-cover" /> {/* 스캔 가이드 오버레이 */} {isScanning && (
스캔 중...
)} {/* 스캔 완료 오버레이 */} {scannedCode && (

스캔 완료!

{scannedCode}

)}
)} {/* 수동 입력 (카메라 사용 불가 시 또는 외장 스캐너 사용 시) */}

직접 입력 또는 외장 스캐너

setManualInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && manualInput.trim()) { e.preventDefault(); onScanSuccess(manualInput.trim()); onOpenChange(false); } }} placeholder="바코드/QR 번호 입력 후 Enter" className="flex-1 h-11 rounded-lg border border-border px-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary" autoFocus={hasPermission === false} />
{/* 바코드 포맷 정보 */}

지원 포맷

{barcodeFormat === "all" && "1D/2D 바코드 모두 지원 (Code 128, QR Code 등)"} {barcodeFormat === "1d" && "1D 바코드 (Code 128, Code 39, EAN-13, UPC-A)"} {barcodeFormat === "2d" && "2D 바코드 (QR Code, Data Matrix)"}

{/* 에러 메시지 */} {error && (

{error}

)}
{!isScanning && !scannedCode && hasPermission && ( )} {isScanning && ( )} {scannedCode && ( )} {scannedCode && !autoSubmit && ( )}
); };