Skip to content

Repository files navigation

LifeFivePhoto (분경5컷)

아이패드 웹에서 사진 8장을 촬영하고 그중 5장을 골라 다섯 컷 프레임으로 합성한 뒤, 노트북에 저장하는 로컬 도구.

분당경영고등학교 학과 홍보 포토부스. 화면과 인화물에 쓰는 이름은 분경5컷이다.

상세 기획은 기획서.md 참고.

아이패드를 세로로 세우든 가로로 눕히든 화면이 잘리지 않는다.

예전에는 세로만 전제로 만들어서, 가로로 돌리면 화면 아래가 잘려 버튼을 누를 수 없었다. 지금은 두 방향 모두 들어간다. 어떻게 고쳤는지는 가로 대응 참고.

아이폰 가로만 예외다. 높이가 325~391px 뿐이라 이 크기의 화면을 담을 수 없어 스크롤된다.

구조

[아이패드 · 클라이언트]              [노트북 · 서버 + 핫스팟 호스트]
  촬영 8장 → 5장 선택 → 프레임 선택
  canvas 합성 → 전화번호 입력
        │
        │ GET /          클라이언트 파일 응답
        │◀───────────────
        │ POST /upload   완성 이미지 + 전화번호
        │───────────────▶
                              output/{날짜}/{전화번호}.png 로 저장

노트북이 직접 모바일 핫스팟을 켜고, 아이패드는 그 핫스팟에 접속한다. 장소 Wi-Fi나 인터넷에 의존하지 않는다.

담당 범위
서버 server/, 핫스팟·인증서·방화벽 등 노트북 환경
클라이언트 client-src/ 촬영·선택·프레임·합성 UI

client/client-src/ 를 빌드한 결과물이다. 행사 노트북에서 빌드 없이 바로 서빙하려고 커밋해 둔다. 클라이언트를 고쳤다면 아래를 거쳐야 반영된다.

cd client-src
pnpm install
pnpm build
# 빌드 결과를 client/ 로 옮긴다 (없어진 파일까지 정리되도록 미러링)
robocopy out ..\client /MIR

실행

다른 노트북에서 처음 받아 쓰기

저장소를 받으면 대부분 그대로 돌아간다. client/ 에 빌드 결과물을 커밋해 두기 때문에 클라이언트는 빌드 없이 바로 서빙된다 — 운영만 할 거면 pnpm install 은 필요 없다.

다만 저장소에서 일부러 뺀 것이 있어서 노트북마다 다시 해야 하는 일이 있다.

git clone https://github.com/picky232/LifeFivePhoto.git
cd LifeFivePhoto
부스-시작.bat

부스-시작.bat (scripts/start-booth.ps1) 이 아래를 스스로 챙긴다.

확인 없으면
Node.js 알려주고 멈춘다 — 직접 설치해야 한다
server/node_modules npm install 을 돌린다
server/certs node generate-cert.js 로 새로 만든다
3000번 포트 쓰고 있는 프로그램을 알려주고 멈춘다
모바일 핫스팟 켠다

사람이 해야 하는 것은 두 가지다.

  1. Node.js 설치 — 배치 파일은 있는지 확인만 한다. nodejs.org 의 LTS 를 받는다.
  2. 아이패드에 CA 인증서 다시 설치 — 아래 설명대로.

인증서는 왜 노트북마다 새로 해야 하나

server/certs/ 에는 CA 개인키가 들어 있다. 이게 유출되면 그 CA 를 신뢰하는 기기에 대해 아무 사이트나 사칭할 수 있어서, 저장소에 넣지 않는다. 그래서 노트북마다 generate-cert.js그 노트북만의 CA 를 새로 만든다.

아이패드가 옛 노트북의 CA 를 신뢰하고 있어도 소용없다 — 다른 CA 이기 때문이다. 노트북을 바꿨으면 새 rootCA.pem 을 아이패드에 다시 설치해야 한다.

이걸 빼먹으면 이렇게 된다:

HTTPS 가 신뢰되지 않음  →  Safari 가 getUserMedia 를 막음  →  카메라가 안 열림

인증서를 안 하면 부스가 아예 동작하지 않는다. 절차는 4. 접속 기기에 CA 설치 에 있고, 종이로 뽑으려면 node docs/guides/make-guides.js 를 쓴다.

아이패드가 여러 대여서 다시 설치하기가 번거롭다면, 쓰던 노트북의 server/certs/ca/ 폴더를 새 노트북의 같은 자리로 복사한 뒤 node generate-cert.js 를 돌리는 방법도 있다. 스크립트가 CA 가 이미 있으면 그것을 그대로 쓰기 때문에(generate-cert.js:29) 아이패드는 손댈 필요가 없다. 다만 CA 개인키를 USB 에 담아 옮기는 것이므로, 옮긴 뒤에는 USB 에서 지운다.

그 밖에 알아둘 것

  • 노트북에 모바일 핫스팟이 되는 Wi-Fi 어댑터가 있어야 한다.
  • 첫 실행 때 윈도우 방화벽 허용 창이 뜬다. 개인 네트워크를 체크하고 허용해야 아이패드가 붙는다.
  • output/ 은 따라오지 않는다 (손님 사진과 전화번호라 제외). 빈 채로 시작하는 것이 정상이다.
  • 프레임이나 화면을 고칠 거면 그때는 cd client-src && pnpm install 이 필요하다.
  • 행사 전에 실제로 쓸 노트북에서 촬영까지 한 번 돌려본다. 인증서 신뢰가 빠졌는지는 카메라를 열어봐야 드러난다.

배치 파일 (행사 당일)

준비가 끝난 뒤에는 배치 파일 두 개로 운영한다.

파일 하는 일
부스-시작.bat 핫스팟 켜기 + 서버 실행
부스-종료.bat 서버 멈추기 + 핫스팟 끄기

부스-시작.bat 은 핫스팟을 켜고 Wi-Fi 이름, 비밀번호, 접속 주소를 보여준 뒤 서버를 띄운다. 실행 전에 아래를 먼저 확인하고 빠진 것이 있으면 알아서 채운다.

  • Node.js 설치 여부
  • server/node_modules — 없으면 npm install
  • 인증서 — 없으면 node generate-cert.js
  • 3000번 포트 — 이미 쓰고 있으면 어떤 프로그램인지 알려주고 멈춘다

서버는 그 창에서 계속 돌아간다. 창을 닫거나 Ctrl+C 를 눌러도 멈추지만, 창을 최소화해 잃어버렸을 때는 부스-종료.bat 을 쓴다. 3000번 포트를 쥐고 있는 프로세스만 골라 멈추므로 관계없는 프로그램은 건드리지 않는다. 이미 꺼져 있어도 그냥 넘어간다.

아래 절차는 처음 준비할 때나 문제가 생겼을 때 참고한다.

종이 안내문

행사장에서 쓸 A4 안내문 두 장을 뽑을 수 있다.

node docs/guides/make-guides.js

바탕화면 output\분경5컷_가이드\ 에 PDF 와 미리보기 PNG 가 나온다.

안내문 누구에게
아이패드 인증서 설치 접속 기기를 준비하는 사람
노트북 서버 켜고 끄기 행사 당일 운영자

각각 두 쪽이다. 1쪽이 절차, 2쪽이 문제 해결이라 1쪽만 붙여두고 2쪽은 손 닿는 곳에 두면 된다. 내용은 아래 절차를 옮긴 것이라, 절차가 바뀌면 docs/guides/make-guides.js 도 같이 고쳐야 한다.

0. 노트북 핫스팟 켜기

접속 기기가 붙을 네트워크를 노트북이 직접 만든다.

설정 > 네트워크 및 인터넷 > 모바일 핫스팟 > 켜기

같은 화면에서 네트워크 이름과 비밀번호를 확인할 수 있다. 핫스팟을 켜면 노트북에 192.168.137.1이 부여된다. 이 값은 Windows가 고정으로 지정하므로 장소가 바뀌어도 동일하다.

PowerShell로 조작하려면:

[Windows.Networking.NetworkOperators.NetworkOperatorTetheringManager,Windows.Networking.NetworkOperators,ContentType=WindowsRuntime] | Out-Null
[Windows.Networking.Connectivity.NetworkInformation,Windows.Networking.Connectivity,ContentType=WindowsRuntime] | Out-Null
$profile = [Windows.Networking.Connectivity.NetworkInformation]::GetInternetConnectionProfile()
$tm = [Windows.Networking.NetworkOperators.NetworkOperatorTetheringManager]::CreateFromConnectionProfile($profile)

$tm.TetheringOperationalState       # 현재 상태
$tm.StartTetheringAsync()           # 켜기
$tm.StopTetheringAsync()            # 끄기

GetInternetConnectionProfile()은 노트북에 인터넷 연결이 있어야 값을 반환한다. 핫스팟 자체는 인터넷 없이도 동작하지만, 이 API로 조작하려면 상단 연결이 하나 필요하다.

부여된 주소 확인:

Get-NetIPAddress -AddressFamily IPv4 | Where-Object IPAddress -like '192.168.137.*'

1. 의존성 설치

cd server
npm install

2. 인증서 생성

Safari의 카메라 API(getUserMedia)는 신뢰된 HTTPS에서만 동작하므로 로컬 인증서가 필요하다.

node generate-cert.js

server/certs/ 아래에 CA와 서버 인증서가 생성된다. 이 폴더는 저장소에 포함되지 않으므로 각자 한 번씩 실행해야 한다. CA가 이미 있으면 기존 CA를 재사용한다.

노트북마다 다른 CA 가 만들어진다. 노트북을 바꿨다면 아이패드에도 새 CA 를 다시 설치해야 한다 — 왜 그런지.

3. 서버 실행

npm start

기동 시 접속 가능한 주소가 출력된다. 노트북 핫스팟을 켠 상태라면 https://192.168.137.1:3000이 포함된다.

인증서가 없으면 HTTP로 기동하고 경고를 출력한다. 이 경우 카메라 API는 동작하지 않는다.

4. 접속 기기에 CA 설치 (최초 1회)

server/certs/ca/rootCA.pem을 접속할 기기로 전달한 뒤:

1. 파일을 열면 "프로파일이 다운로드됨" 알림이 뜬다
2. 설정 > 일반 > VPN 및 기기 관리 > 해당 프로파일 > 설치
3. 설정 > 일반 > 정보 > 인증서 신뢰 설정 > LifeFivePhoto Local CA 켜기

3번을 빼먹으면 프로파일을 설치해도 Safari가 인증서를 신뢰하지 않는다.

5. 방화벽 (Windows, 최초 1회)

3000/TCP 인바운드 허용 규칙이 필요하다. 서버 첫 실행 시 뜨는 방화벽 대화상자에서 "개인 네트워크"를 체크하고 허용하면 된다.

API

베이스 주소: https://192.168.137.1:3000

GET /

client/ 폴더의 정적 파일을 서빙한다.

POST /upload

완성 이미지와 전화번호를 업로드한다.

요청multipart/form-data

필드 타입 설명
photo File (Blob) image/png, canvas.toBlob() 결과
phone 문자열 전화번호. 숫자만 추출해 9~11자리만 허용하며 하이픈은 자동 제거된다

응답

// 200
{ "success": true, "filename": "2026-08-13/01012345678.png" }

// 400 — 전화번호 또는 이미지 누락, 형식 오류, 용량 초과(20MB)
{ "success": false, "error": "전화번호가 없습니다." }

// 500 — 저장 실패
{ "success": false, "error": "파일 저장에 실패했습니다." }

같은 날 같은 번호로 다시 업로드하면 덮어쓰지 않고 01012345678_2.png 형태로 저장한다.

GET /health

{ "status": "ok" }

테스트

두 가지가 있다. 둘 다 별도 포트와 임시 저장 경로를 쓰므로, 운영 중인 서버가 3000번에서 돌고 있어도 함께 실행할 수 있고 실제 저장 폴더도 건드리지 않는다.

API 명세 대조

cd server
npm test

명세의 각 항목을 검사한다(16개). 서버 기동부터 정리까지 자동으로 하며, 실패가 하나라도 있으면 종료 코드 1을 반환한다. 인증서가 없으면 실행되지 않으므로 먼저 node generate-cert.js를 실행한다.

부스 흐름 통합 테스트

cd server
npm run test:e2e

브라우저를 띄워 촬영부터 저장까지 실제로 한 바퀴 돌린다. 8장 촬영이 5초 간격이라 1분 안쪽이다.

설치된 Chrome 을 그대로 쓴다(puppeteer-core). 카메라는 Chrome 의 가짜 장치를 써서 권한 창이 뜨지 않는다. 서버는 HTTP 로 띄우고 localhost 로 접속하는데, Chrome 이 http://localhost 를 보안 컨텍스트로 취급하므로 인증서 없이도 getUserMedia 가 열린다.

Chrome 이 기본 경로에 없으면 CHROME_PATH 환경변수로 지정한다.

이 테스트는 클라이언트와 서버가 맞물리는지를 확인할 뿐, iOS Safari 의 인증서 처리까지 검증하지는 않는다.

노트북에서 직접 확인하기

접속 기기 없이 서버만 확인할 때 쓴다.

$ca = "server/certs/ca/rootCA.pem"

curl.exe -s --ssl-revoke-best-effort --cacert $ca https://192.168.137.1:3000/health

curl.exe -s --ssl-revoke-best-effort --cacert $ca -X POST https://192.168.137.1:3000/upload `
  -F "phone=01012345678" -F "photo=@테스트.png;type=image/png"

--ssl-revoke-best-effort가 필요한 이유: Windows의 curl은 schannel 백엔드를 쓰는데, 인증서의 폐기 목록(CRL) 배포 지점을 찾으려다 실패해 CERT_TRUST_REVOCATION_STATUS_UNKNOWN 오류를 낸다. 자체 서명 CA에는 CRL 배포 지점이 없으므로 정상이며, 인증서 자체의 문제가 아니다. iOS Safari는 사용자가 설치한 루트 인증서에 이 검사를 하지 않으므로 실제 사용에는 영향이 없다.

테스트로 만든 파일은 output/ 아래에 남으므로 확인 후 정리한다.

프레임

인화물은 사진을 먼저 깔고 그 위에 프레임 그림을 덮어 만든다. 프레임은 사진이 빠진 뼈대이고, 사진 자리는 투명하게 뚫려 있다. 테두리·장식·학과 이름표·로고가 모두 프레임 그림 안에 들어 있다.

순서가 중요하다. 프레임에는 사진 자리 밖으로 걸치는 장식이 있을 수 있고, 그런 장식은 사진 위에 얹혀야 한다. 뒤에 깔면 사진에 가려 안 보인다.

규격

항목
캔버스 1200 × 1800 px, 300dpi, sRGB
형식 PNG-24 (알파 채널 포함)
실치수 101.6 × 152.4 mm (4 × 6 인치)
인화 용지 100 × 148 mm — 캔버스보다 작아 가장자리가 잘린다

사진 칸 다섯 자리. 좌표는 client-src/src/lib/frame.tsSLOTS 가 기준이다.

x y 크기
1 105 155 450 × 430
2 105 685 450 × 430
3 105 1215 450 × 430
4 645 155 450 × 430
5 645 685 450 × 430
로고 645 1215 450 × 430

위아래 7.0mm(83px)·좌우 4.0mm(47px)는 흰색으로 비운다. 용지(100×148mm)가 캔버스보다 작아 가장자리가 잘리는데, 세로가 더 많이 잘려(4.4mm 대 1.6mm) 위아래를 넓게 잡았다.

구멍이 이 네모보다 작은 것은 괜찮다. 프레임이 사진 가장자리를 덮는 것이다. 반대로 크면 안 된다. 그 틈으로 프레임 뒤 바탕이 비친다.

학과 이름표가 프레임 그림 안에 있으므로, 학과 명단이 바뀌면 departments.ts 와 프레임 PNG 를 같이 고쳐야 한다. 한쪽만 고치면 화면에서 안내한 학과와 종이에 찍힌 학과가 달라진다.

칸 자리는 프레임마다 같아야 한다

프레임이 여러 개여도 사진 칸의 위치와 크기는 모두 같다. 디자인만 다르다.

칸 비율이 프레임마다 다르면 촬영 단계가 꼬인다. 사진은 찍는 순간 컷 비율로 잘라 저장하는데, 프레임은 그 뒤에 고르기 때문이다. 정사각형으로 잘라 둔 사진을 가로로 긴 칸에 넣으면 위아래가 더 잘려 머리가 날아간다. 자리를 고정해두면 이 문제가 아예 생기지 않는다.

프레임이 새로 오면

디자이너가 준 파일을 폴더에 덮어쓰고 한 줄만 실행하면 된다.

node scripts/sync-frames.js            # 바뀐 것만 반영
node scripts/sync-frames.js --watch    # 폴더를 지켜보다 알아서

JPG 면 흰 칸을 뚫고, 칸 자리가 맞는지 재고, 가장자리 흰 테를 재서 frames.tstrim 을 맞추고, 빌드해서 client/ 까지 옮긴다. 손으로 하던 대여섯 단계를 한 번에 한다. 바뀐 파일이 없으면 아무것도 하지 않는다.

칸 자리가 안 맞으면 넣지 않고 멈춘다. 그대로 넣으면 사진 가장자리에 바탕이 비치는데, 인쇄하고 나서야 알게 되기 때문이다.

가져올 폴더와 파일 이름은 scripts/sync-frames.js 위쪽 MAP 에 적혀 있다. --from 으로 다른 폴더를 줄 수도 있다.

맞는지 재보기

프레임 그림이 칸 자리와 맞는지는 붙여보지 않고 잴 수 있다.

node scripts/check-frame.js client-src/public/frames/classic.png
node scripts/check-frame.js --template 새프레임-바탕.png

칸 좌표는 frame.ts 를 그대로 불러와서 쓴다. 위 표는 읽는 사람을 위한 것이고 판단은 항상 코드 쪽 값으로 한다.

잡는 것은 크기 불일치 · 구멍이 칸보다 큼 · 안 뚫림 · PNG 아님이다. 사진 위로 걸치는 장식은 잘못이 아니라 통과시키되, 네 변을 각각 몇 px 덮는지 알려준다. --template 은 칸만 뚫린 반투명 바탕을 만든다.

외주에 넘길 것

디자이너에게 줄 규격서와 예시는 docs/frame-spec/ 에서 뽑는다.

node docs/frame-spec/make-guide.js       규격서 한 장
node docs/frame-spec/make-examples.js    예시 프레임 두 종과 미리보기
node docs/frame-spec/make-blank.js       장식 없는 흰 바탕 틀

기본으로 바탕화면 output\분경5컷_프레임_틀\ 에 나온다. 함께 넘길 문서는 docs/frame-spec/의뢰서.md 다.

그쪽 규격은 지금 앱 값과 다르다. 외주용으로 사진 칸을 키우고 여백을 넓혀 새로 잡은 값이라, 실물을 받아 좌표가 확정되면 frame.tsgeom.js 를 같은 값으로 맞춘다. 자세한 것은 docs/frame-spec/README.md 참고.

프레임 추가

client-src/public/frames/ 에 PNG 를 넣고 client-src/src/lib/frames.ts 에 한 줄 적는다.

{ id: "spring", name: "봄", note: "한 줄 설명", image: "/frames/spring.png" }

지금 들어 있는 두 개는 실물이 오기 전까지 흐름을 확인하려고 만든 임시 그림이다. 실물이 오면 같은 자리에 파일만 바꿔 끼운다.

저장 위치

바탕화면의 output 폴더에 저장한다. 행사 중에 운영자가 사진을 찾아 인쇄해야 하므로 프로젝트 폴더 안쪽보다 바탕화면이 빠르다.

바탕화면/output/
└── 2026-08-13/          업로드 시점 기준 날짜 폴더, 자동 생성
    └── 01012345678.png  전화번호가 파일명

경로는 홈 디렉터리를 기준으로 찾는다. OneDrive 백업으로 바탕화면이 옮겨간 경우도 함께 살펴본다. 서버 기동 시 실제 저장 위치가 출력된다.

다른 곳에 저장하려면 OUTPUT_DIR 환경변수로 덮어쓴다.

OUTPUT_DIR=D:\photos npm start

전화번호와 얼굴 사진이 담기므로 저장 폴더는 공개 저장소에 올리지 않는다.

가로 대응

처음에는 세로(820×1180)만 전제로 만들었다. 가로는 높이가 820px 뿐이라 세로용 크기를 그대로 쓰면 화면 아래가 잘린다. 그 문제와, 고치는 과정에서 함께 드러난 것들을 정리해둔다.

1. 가로에서 화면 아래가 잘렸다

시작 화면의 "화면을 눌러 시작하기", 안내 화면의 "촬영 시작", 번호 입력 화면의 "확인" 이 화면 밖으로 밀려 누를 수 없었다.

글자와 여백이 전부 rem 이므로 뿌리 글자 크기 하나로 레이아웃 전체가 같은 비율로 움직인다. 화면 아홉 개를 각각 손대지 않아도 된다.

html {
  font-size: clamp(8px, min(100svh / 73.75, 100svw / 51.25), 16px);
}

@media (orientation: landscape) {
  html { font-size: clamp(8px, min(100svh / 54.7, 100svw / 51.25), 15px); }
}

73.75 는 1180÷16, 51.25 는 820÷16, 54.7 은 820÷15 다. 높이와 너비 기준을 반드시 함께 둔다. 한쪽만 두면 그 조건을 벗어나는 기기에서 내용이 쏟아진다.

vh 가 아니라 svh 로 잰다. iOS Safari 의 100vh 는 도구막대가 숨은 상태를 기준으로 하므로, 그대로 쓰면 내용이 막대 뒤로 들어가 잘린 것처럼 보인다.

2. 보이는 것과 찍히는 것이 달랐다 (세로에도 해당)

촬영 함수는 카메라 영상을 컷 비율(450×430)로 가운데만 잘라낸다. 그런데 미리보기 상자는 w-full 때문에 가로로 늘어난 직사각형이었다. 화면에서 보이던 좌우가 저장할 때 잘려 나갔다. 가로에서 특히 심했다.

미리보기가 컷 비율을 지키도록 높이로 크기를 정하고 너비는 비율이 따라오게 했다. 이제 보이는 그대로 찍힌다.

카메라와 사진 격자는 높이를 숫자로 정하지 않고 남는 세로를 가져간다. 방향마다 값을 맞춰두면 기기가 바뀔 때 다시 맞춰야 한다.

3. 한글이 단어 중간에서 끊겼다 (세로에도 해당)

"번호를 눌 / 러주세요", "인 / 화에도", "다 / 섯 학과입니다" 처럼 갈라졌다. 한글은 기본 규칙으로 글자 사이 아무 데서나 줄이 바뀐다.

word-break: keep-all;      /* 띄어쓰기에서만 끊는다 */
overflow-wrap: break-word; /* 단, 한 줄에 못 넣을 때는 낱말 안에서도 끊는다 */

둘을 함께 써야 한다. keep-all 만 두면 "인공지능개발과" 처럼 띄어쓰기가 없는 낱말이 아예 안 끊겨 칸 밖으로 삐져나간다.

4. 가로에서 글자와 사진을 키웠다

화면마다 넘치기 직전까지 재본 결과, 고르는 화면 하나가 11.1px 에서 막혀 전체를 끌어내리고 있었다. 사진 여덟 장을 4열 2줄로 깔면 격자 높이가 화면 폭에 묶여 560px 을 넘기 때문이다.

격자 폭을 남는 높이에 맞춰 제한해 병목을 풀었다. 배치는 4열 2줄 그대로이고 높이가 넉넉하면 화면 폭을 다 쓴다. 그 결과 가로에서 15px 까지 올릴 수 있게 되어 빈 자리가 줄었다.

촬영 화면은 아래 필름 칸을 42%로 좁혀 그만큼을 카메라에 넘긴다.

숫자판은 3열을 유지했다. 6열로 눕히면 한 줄에 더 들어가지만 "전체 지우기" 가 9 와 0 사이에 끼어 0 을 누르려다 잘못 누르기 쉽다.

5. 픽셀로 박혀 크기 조절을 안 따라오던 것들

숫자판, 큰 버튼, 인쇄 미리보기가 픽셀 값이라 화면이 줄어도 그대로였다. rem 으로 바꾸되 max(44px, ...) 로 하한을 걸었다. 화면이 줄어도 손가락으로 누를 수 있는 크기는 지킨다.

기기별 확인

Safari 도구막대가 보이는 상태를 가정한 높이로 열네 종을 쟀다. 전체화면(홈 화면에 추가)이면 더 여유가 있다.

기기 크기 결과
iPad Pro 13 세로 / 가로 1024×1279 / 1366×937 모든 화면 들어감
iPad Air 세로 / 가로 820×1093 / 1180×717 모든 화면 들어감
iPad Air 전체화면 820×1180 / 1180×820 모든 화면 들어감
iPad mini 세로 / 가로 744×1046 / 1133×657 모든 화면 들어감
iPhone 세로 (SE / 15 / 16 Max) 375×553 ~ 440×838 모든 화면 들어감
iPhone 가로 (SE / 15 / 16 Max) 667×325 ~ 956×391 시작·안내·번호 입력이 넘쳐 스크롤됨

휴대폰 가로는 높이가 325~391px 뿐이라 이 설계가 들어갈 수 없다. 글자를 최소 크기(8px)까지 줄여도 모자란다. 태블릿을 전제로 만든 화면이라 원래 대상이 아니며, 잘리지 않고 스크롤된다. main 도 같은 하한을 쓰므로 동작이 다르지 않다.

아직 하지 않은 것

배치 자체는 세로와 같은 세로 흐름이다. 가로에서 잘리지 않고 크기도 맞지만, 좌우가 남는다. 가로를 제대로 쓰려면 화면마다 좌우로 나누는 배치가 따로 필요하다. 예를 들어 시작 화면이라면 제목을 왼쪽에, 학과 다섯을 오른쪽에 세로로 두는 식이다.

부스를 어느 방향으로 세울지 정해지면 그때 판단하면 된다.

검증 상태

서버 (npm test, 16개 항목)

항목 결과
핫스팟 IP 192.168.137.1 부여 확인
HTTPS 기동 인증서가 rootCA로 검증됨
GET / 200, text/html
없는 정적 파일 404
없는 화면 404, 클라이언트 404 페이지로 응답
GET /health 200, {"status":"ok"}
POST /upload 정상 200, 응답 경로에 파일 저장 확인 (바이트 단위 일치)
전화번호 하이픈 자동 제거 후 숫자만 파일명에 사용
POST /upload 전화번호·이미지 누락 400
전화번호 형식 위반 400 (비숫자, 자릿수 미달)
경로 조작 시도 400, 상위 경로에 파일 미생성 확인
용량 초과 (20MB) 400
같은 번호 재업로드 _2 접미사로 별도 저장

부스 흐름 (npm run test:e2e, 10단계)

브라우저에서 촬영 8장, 5장 선택, 프레임 선택, 합성(1200×1800), 번호 입력, 업로드, 저장까지 통과.

실기기 (iOS)

CA 설치 후 iPhone Safari 에서 촬영부터 업로드까지 동작을 확인했다. 인증서가 신뢰되지 않으면 getUserMedia 가 막혀 촬영 자체가 되지 않으므로, 업로드된 파일이 남았다는 것은 인증서 설치가 유효했다는 뜻이다.

미검증

  • 아이패드 화면 크기에서의 레이아웃 (지금까지 확인은 iPhone 과 노트북 브라우저 기준)
  • 장소 이동 후 핫스팟 재구성 시 동작
  • 여러 명이 연달아 사용할 때의 안정성

저장소에 포함하지 않는 것

대상 이유 새 노트북에서
server/certs/ CA·서버 개인키. 유출 시 인증서 사칭 및 통신 복호화 가능 generate-cert.js 가 새로 만든다. 아이패드에 CA 재설치 필요
output/ 전화번호와 얼굴 사진 빈 채로 시작한다
node_modules/ 용량이 크고 npm install 로 복원된다 배치 파일이 알아서 설치한다
client-src/.next, client-src/out 빌드 부산물 필요 없다. client/ 를 커밋해 둔다
.frame-sync.json 프레임을 어디까지 반영했는지 적은 로컬 기록 없으면 다음 sync-frames.js 가 전부 다시 반영한다

핫스팟 SSID와 비밀번호도 저장소에 기록하지 않는다.

받아서 쓰는 절차는 다른 노트북에서 처음 받아 쓰기 를 본다.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages