"use client"; import React, { useState, useRef, useEffect } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } 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; } export const BarcodeScanModal: React.FC = ({ open, onOpenChange, targetField, barcodeFormat = "all", autoSubmit = false, onScanSuccess, }) => { const [isScanning, setIsScanning] = useState(false); const [scannedCode, setScannedCode] = useState(""); const [error, setError] = useState(""); const [hasPermission, setHasPermission] = useState(null); const webcamRef = useRef(null); const codeReaderRef = useRef(null); const scanIntervalRef = useRef(null); // 바코드 리더 초기화 useEffect(() => { if (open) { codeReaderRef.current = new BrowserMultiFormatReader(); // 자동 권한 요청 제거 - 사용자가 버튼을 클릭해야 권한 요청 } return () => { stopScanning(); if (codeReaderRef.current) { codeReaderRef.current.reset(); } }; }, [open]); // 카메라 권한 요청 const requestCameraPermission = async () => { console.log("🎥 카메라 권한 요청 시작..."); // navigator.mediaDevices 지원 확인 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { console.error("❌ navigator.mediaDevices를 사용할 수 없습니다."); console.log("현재 프로토콜:", window.location.protocol); console.log("현재 호스트:", window.location.host); setHasPermission(false); setError( "이 브라우저는 카메라 접근을 지원하지 않거나, 보안 컨텍스트(HTTPS 또는 localhost)가 아닙니다. " + "현재 프로토콜: " + window.location.protocol ); toast.error("카메라 접근이 불가능합니다. 콘솔을 확인해주세요."); return; } try { console.log("🔄 getUserMedia 호출 중..."); const stream = await navigator.mediaDevices.getUserMedia({ video: true }); console.log("✅ 카메라 권한 허용됨!"); setHasPermission(true); stream.getTracks().forEach((track) => track.stop()); // 권한 확인 후 스트림 종료 toast.success("카메라 권한이 허용되었습니다."); } catch (err: any) { console.error("❌ 카메라 권한 오류:", err); console.error("에러 이름:", err.name); console.error("에러 메시지:", err.message); 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(""); // 주기적으로 스캔 시도 (500ms마다) 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; // 이미지를 HTMLImageElement로 변환 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(); console.log("✅ 바코드 스캔 성공:", barcode); setScannedCode(barcode); stopScanning(); toast.success(`바코드 스캔 완료: ${barcode}`); // 자동 제출 옵션이 켜져있으면 바로 콜백 실행 if (autoSubmit) { onScanSuccess(barcode); } } } catch (err) { // NotFoundException은 정상적인 상황 (바코드가 아직 인식되지 않음) if (!(err instanceof NotFoundException)) { console.error("바코드 스캔 오류:", err); } } }; // 수동 확인 버튼 const handleConfirm = () => { if (scannedCode) { onScanSuccess(scannedCode); } else { toast.error("스캔된 바코드가 없습니다."); } }; return ( 바코드 스캔 카메라로 바코드를 스캔하세요. {targetField && ` (대상 필드: ${targetField})`}
{/* 카메라 권한 요청 대기 중 */} {hasPermission === null && (

카메라 권한이 필요합니다

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

💡 권한 요청 안내:

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

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

{error}

📱 권한 허용 방법:

  1. 브라우저 주소창 왼쪽의 🔒 자물쇠 아이콘을 클릭하세요
  2. "카메라" 항목을 찾아 "허용"으로 변경하세요
  3. 페이지를 새로고침하거나 다시 스캔을 시도하세요
)} {/* 웹캠 뷰 */} {hasPermission && (
{/* 스캔 가이드 오버레이 */} {isScanning && (
스캔 중...
)} {/* 스캔 완료 오버레이 */} {scannedCode && (

스캔 완료!

{scannedCode}

)}
)} {/* 바코드 포맷 정보 */}

지원 포맷

{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 && !autoSubmit && ( )}
); };