From d1fe2ddb47e52f8d881bbc9b33f29fdd8c487e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:41:07 +0900 Subject: [PATCH] feat/owner-home-qr --- .../app/(tabs)/main/_components/CafeIntro.tsx | 67 +++++-- .../(tabs)/main/_components/QRStampModal.tsx | 182 ++++++++++++++++++ .../src/icons/generated/spriteSymbols.ts | 2 +- .../src/icons/source/CloseButton.svg | 2 +- 4 files changed, 230 insertions(+), 23 deletions(-) create mode 100644 apps/owner/src/app/(tabs)/main/_components/QRStampModal.tsx diff --git a/apps/owner/src/app/(tabs)/main/_components/CafeIntro.tsx b/apps/owner/src/app/(tabs)/main/_components/CafeIntro.tsx index f84fcf2..40e10fc 100644 --- a/apps/owner/src/app/(tabs)/main/_components/CafeIntro.tsx +++ b/apps/owner/src/app/(tabs)/main/_components/CafeIntro.tsx @@ -1,6 +1,8 @@ "use client"; +import { useState } from "react"; import { Icon } from "@compasser/design-system"; +import QRStampModal from "./QRStampModal"; interface CafeIntroProps { cafeName: string; @@ -27,37 +29,60 @@ const formatCafeName = (name: string) => { return { firstLine: name.slice(0, 10).trim(), - secondLine: name.slice(10).trim(), + secondLine: name.slice(10).trim(), }; }; export default function CafeIntro({ cafeName }: CafeIntroProps) { const { firstLine, secondLine } = formatCafeName(cafeName); + const [isQrModalOpen, setIsQrModalOpen] = useState(false); + + const handleOpenQrModal = () => { + setIsQrModalOpen(true); + }; + + const handleCloseQrModal = () => { + setIsQrModalOpen(false); + }; + + const handleSubmitStamp = () => { + console.log("적립하기"); + }; return ( -
-
-

어서오세요!

- -
-

- {firstLine} - {secondLine ? `\n${secondLine}` : ""} -

+ <> +
+
+

어서오세요!

+ +
+

+ {firstLine} + {secondLine ? `\n${secondLine}` : ""} +

+
+ +

입니다.

-

입니다.

-
+ +
- - + ); } \ No newline at end of file diff --git a/apps/owner/src/app/(tabs)/main/_components/QRStampModal.tsx b/apps/owner/src/app/(tabs)/main/_components/QRStampModal.tsx new file mode 100644 index 0000000..cddf485 --- /dev/null +++ b/apps/owner/src/app/(tabs)/main/_components/QRStampModal.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { Icon } from "@compasser/design-system"; + +interface QRStampModalProps { + open: boolean; + onClose: () => void; + onSubmit?: () => void; +} + +interface CornerGuideProps { + className?: string; + position: "top-left" | "top-right" | "bottom-left" | "bottom-right"; +} + +const SCAN_BOX_SIZE = 260; + +export default function QRStampModal({ + open, + onClose, + onSubmit, +}: QRStampModalProps) { + const videoRef = useRef(null); + const streamRef = useRef(null); + const [cameraError, setCameraError] = useState(""); + + useEffect(() => { + if (!open) return; + + const startCamera = async () => { + try { + setCameraError(""); + + const stream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: { ideal: "environment" }, + }, + audio: false, + }); + + streamRef.current = stream; + + if (videoRef.current) { + videoRef.current.srcObject = stream; + await videoRef.current.play(); + } + } catch { + setCameraError("카메라에 접근할 수 없습니다."); + } + }; + + startCamera(); + + return () => { + if (streamRef.current) { + streamRef.current.getTracks().forEach((track) => track.stop()); + streamRef.current = null; + } + }; + }, [open]); + + useEffect(() => { + if (!open && streamRef.current) { + streamRef.current.getTracks().forEach((track) => track.stop()); + streamRef.current = null; + } + }, [open]); + + if (!open) return null; + + return ( +
+
+
+ {!cameraError ? ( +
+
+
+ ); +} + +function ScanFrame({ boxSize }: { boxSize: number }) { + return ( +
+
+ + + + + +
+ ); +} + +function CornerGuide({ className = "", position }: CornerGuideProps) { + const baseClassName = "absolute h-[8rem] w-[8rem] border-primary-variant"; + + const positionClassMap = { + "top-left": "rounded-tl-[1rem] border-l-[0.738rem] border-t-[0.738rem]", + "top-right": "rounded-tr-[1rem] border-r-[0.738rem] border-t-[0.738rem]", + "bottom-left": "rounded-bl-[1rem] border-b-[0.738rem] border-l-[0.738rem]", + "bottom-right": "rounded-br-[1rem] border-b-[0.738rem] border-r-[0.738rem]", + }; + + return ( +
+ ); +} \ No newline at end of file diff --git a/packages/design-system/src/icons/generated/spriteSymbols.ts b/packages/design-system/src/icons/generated/spriteSymbols.ts index 3e227a7..571eaaa 100644 --- a/packages/design-system/src/icons/generated/spriteSymbols.ts +++ b/packages/design-system/src/icons/generated/spriteSymbols.ts @@ -1,2 +1,2 @@ // 이 파일은 자동 생성 파일입니다. (직접 수정 금지) -export const spriteSymbols = ""; +export const spriteSymbols = ""; diff --git a/packages/design-system/src/icons/source/CloseButton.svg b/packages/design-system/src/icons/source/CloseButton.svg index bd10268..714be17 100644 --- a/packages/design-system/src/icons/source/CloseButton.svg +++ b/packages/design-system/src/icons/source/CloseButton.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file