어린이날 행사장에서 바로 쓸 수 있는 웹 포토부스입니다. 브라우저 카메라로 사진을 찍고, 프레임을 고른 뒤, 낙서와 지우개로 꾸민 최종 이미지를 PNG로 저장합니다. 완성된 이미지는 QR 코드로 같은 네트워크의 다른 기기에서도 열 수 있습니다.
- 1컷, 4컷 레이아웃 선택
- 어린이날 분위기의 SVG 프레임 선택
- 브라우저 카메라 촬영 및 카운트다운
- 직접 그리기와 손 인식 그리기 모드
- 펜 색상 선택, 부분 지우개, 전체 지우기
- 최종 PNG 다운로드
- 백엔드 업로드 후 QR 다운로드 URL 생성
- 로컬 네트워크 IP 또는 외부 터널 URL 기반 공유
- Frontend: React 19, TypeScript, Vite
- Backend: Express 5, TypeScript
- QR:
qrcode.react - 동시 실행:
concurrently - 손 인식: MediaPipe Tasks Vision CDN
photo-booth/
├── backend/
│ └── src/
│ ├── server.ts
│ └── utils/
├── frontend/
│ ├── public/
│ │ ├── assets/fonts/
│ │ └── frames/
│ └── src/
│ ├── constants/
│ ├── pages/
│ ├── utils/
│ └── App.tsx
├── uploads/
├── package.json
└── README.md
루트, 프론트엔드, 백엔드 의존성을 한 번에 설치합니다.
npm run install:all프론트엔드와 백엔드를 동시에 실행합니다.
npm run dev기본 주소는 다음과 같습니다.
- Frontend:
http://localhost:5173 - Backend:
http://localhost:4000 - Health check:
http://localhost:4000/health
각 서버만 따로 실행할 수도 있습니다.
npm run dev:frontend
npm run dev:backendnpm run build프론트엔드는 frontend/dist, 백엔드는 backend/dist에 빌드 결과가 생성됩니다.
- 시작 화면에서 촬영을 시작합니다.
1컷 레이아웃또는4컷 레이아웃을 선택합니다.- 원하는 프레임을 선택합니다.
- 카메라 화면에서 사진을 촬영합니다.
- 낙서 화면에서 펜, 색상, 지우개를 사용해 사진을 꾸밉니다.
- 최종 미리보기에서 PNG를 다운로드하거나 QR 코드로 다른 기기에서 엽니다.
- 프론트엔드가 Canvas로 사진, 낙서, 프레임, 하단 문구를 합성합니다.
- 합성된 PNG data URL을
POST /api/photos로 백엔드에 전송합니다. - 백엔드는 이미지를
uploads/폴더에 저장합니다. - 백엔드는
/download/:filename형식의 다운로드 URL을 반환합니다. - 프론트엔드는 해당 URL을 QR 코드로 표시합니다.
업로드 파일명은 childrens-day-photo-날짜.png 형식으로 생성됩니다.
휴대폰이나 iPad에서 QR 이미지를 열려면 백엔드를 실행 중인 컴퓨터와 같은 Wi-Fi에 연결되어 있어야 합니다.
- 컴퓨터와 다른 기기를 같은 Wi-Fi에 연결합니다.
http://localhost:4000/health에서localIp값을 확인합니다.- 다른 기기에서
http://로컬IP:5173으로 접속합니다.
예시:
http://192.168.0.24:5173
주의할 점:
- 다른 기기에서
localhost는 컴퓨터가 아니라 그 기기 자신을 뜻합니다. - QR URL이 열리지 않으면
localIp, Wi-Fi, 방화벽, 백엔드 실행 상태를 확인합니다. - 모바일 브라우저는 보안 정책 때문에 로컬 IP 접속 시 카메라 권한이 제한될 수 있습니다. 촬영은 앱을 실행한 기기에서 진행하고, QR은 공유용으로 쓰는 흐름이 가장 안정적입니다.
백엔드 포트를 바꿉니다. 기본값은 4000입니다.
PORT=5000 npm run dev:backend백엔드가 자동 감지한 로컬 IP가 맞지 않을 때 직접 지정합니다.
HOST_IP=192.168.0.24 npm run dev:backend루트에서 동시에 실행할 때도 사용할 수 있습니다.
HOST_IP=192.168.0.24 npm run devCloudflare Tunnel 같은 외부 공개 URL을 QR 다운로드 주소로 사용합니다. 같은 Wi-Fi가 아닌 기기에서도 QR을 열어야 할 때 사용합니다.
PUBLIC_BASE_URL=https://example.trycloudflare.com npm run dev:backend설정하면 QR URL은 다음처럼 생성됩니다.
https://example.trycloudflare.com/download/파일명.png
설정하지 않으면 다음처럼 로컬 네트워크 주소를 사용합니다.
http://로컬IP:4000/download/파일명.png
같은 Wi-Fi가 아닌 휴대폰에서 QR 다운로드를 열어야 한다면 백엔드 포트에 터널을 연결합니다.
- 백엔드를 실행합니다.
npm run dev:backend- 다른 터미널에서 터널을 엽니다.
cloudflared tunnel --url http://localhost:4000- 발급된
https://...trycloudflare.com주소를PUBLIC_BASE_URL로 넣어 백엔드를 다시 실행합니다.
PUBLIC_BASE_URL=https://example.trycloudflare.com npm run dev:backendfrontend/public/frames에 PNG 또는 SVG 파일을 추가합니다.frontend/src/constants/frames.ts의FRAMES배열에 항목을 추가합니다.
{
id: 'new-frame',
name: '새 프레임',
description: '프레임 설명',
src: '/frames/new-frame.svg'
}프레임은 최종 합성에서 사진과 낙서 위에 덮입니다. 사진 영역을 가리지 않도록 가장자리와 하단 장식 중심으로 만드는 것이 좋습니다.
레이아웃은 frontend/src/constants/layouts.ts에서 관리합니다.
기본 레이아웃:
single: 1400 x 2000, 1컷quad: 1600 x 2400, 4컷
새 레이아웃을 추가하려면 LAYOUTS 배열에 id, name, ratioLabel, width, height, shotCount를 추가합니다.
- 브라우저 주소창의 카메라 권한을 허용합니다.
- 이미 거부한 경우 사이트 설정에서 권한을 초기화한 뒤 새로고침합니다.
- 다른 앱이 카메라를 사용 중이면 종료합니다.
- iPad Safari에서는 설정 앱에서 Safari 카메라 권한을 확인합니다.
- 인터넷 연결을 확인합니다. 손 인식 모드는 MediaPipe 모델과 WASM 파일을 CDN에서 불러옵니다.
- 브라우저 카메라 권한을 허용합니다.
- 조명이 너무 어둡거나 손이 화면에서 벗어나면 인식이 불안정할 수 있습니다.
- 휴대폰과 컴퓨터가 같은 Wi-Fi에 있는지 확인합니다.
- 백엔드가 실행 중인지
http://localhost:4000/health로 확인합니다. - QR URL의 IP가 현재 컴퓨터 IP와 다르면
HOST_IP를 지정합니다. - 방화벽에서 백엔드 포트, 기본
4000, 접근을 허용합니다. - 같은 Wi-Fi가 아니라면
PUBLIC_BASE_URL과 터널을 사용합니다.
- 프론트엔드는 Vite가 자동으로 다음 포트를 사용할 수 있습니다.
- 백엔드는
PORT환경변수로 포트를 변경합니다.
PORT=5000 npm run dev:backend백엔드 상태, 로컬 IP, 포트, 공개 URL 설정을 반환합니다.
PNG data URL을 받아 uploads/에 저장하고 다운로드 URL을 반환합니다.
요청 예시:
{
"imageData": "data:image/png;base64,..."
}응답 예시:
{
"filename": "childrens-day-photo-2026-05-02T01-23-45-000Z.png",
"url": "http://192.168.0.24:4000/download/childrens-day-photo-2026-05-02T01-23-45-000Z.png"
}저장된 PNG 파일을 브라우저에서 열 수 있게 반환합니다.