지인에게 링크를 공유해서 헤어스타일 투표와 의견을 받는 모바일 전용 단일 페이지.
투표·코멘트는 Firestore에 저장만 되고 투표 화면에는 표시되지 않는다.
결과는 npm run export로 CSV를 받아서 보고, 출처·스타일별 득표 수만 /result 페이지로 공개한다.
계획과 설계 배경은 PLAN.md 참고.
Firebase 프로젝트: hair-d2632 (배포 주소 https://hair-d2632.web.app)
- 웹 앱 설정값은
.env.production에 커밋되어 있다. 브라우저에 공개되는 값이라 커밋해도 안전하고, 데이터 보호는firestore.rules가 담당한다. - Firebase 콘솔에서 Firestore 데이터베이스가 생성되어 있어야 한다 (위치:
asia-northeast3서울 권장). - 로컬에서 배포하려면
npm install후npx firebase login.
public/images/에 이미지 추가 (세로 3:4, 가로 720px 이하 WebP 권장)- 이미지는 1년 캐시되므로 교체할 때는 파일명을 바꾼다 (
style-01-v2.webp)
- 이미지는 1년 캐시되므로 교체할 때는 파일명을 바꾼다 (
src/styles.ts의STYLES수정- id를 추가/변경했다면
firestore.rules의validStyle목록도 똑같이 수정 (npm run test:rules가 둘이 다르면 실패한다) - 공유 미리보기 이미지는
public/og-2.jpg(1200×630). 바꿀 때는 파일명도 바꿔야 메신저 캐시를 피할 수 있다
npm run dev # 개발 서버 (.env 없으면 demo 프로젝트 → 에뮬레이터와 함께 사용)
npm run test:rules # 에뮬레이터로 보안 규칙 테스트 (Java 필요)에뮬레이터에 연결해서 화면을 확인하려면:
npx firebase emulators:start --only firestore --project demo-hair
VITE_USE_EMULATOR=true npm run dev.github/workflows/deploy.yml
- PR: 빌드 + 보안 규칙 테스트만 실행
main에 푸시(머지): 테스트 통과 시 Hosting과 Firestore 규칙을hair-d2632에 배포- Actions 탭에서 수동 실행(Run workflow)도 가능 (
main에서만 배포됨)
- Google Cloud 콘솔 > IAM 및 관리자 > 서비스 계정에서 서비스 계정 만들기
- 이름:
github-deploy - 역할: Firebase 관리자 (
Firebase Admin)
- 이름:
- 만든 계정 > 키 탭 > 키 추가 > 새 키 만들기 > JSON → 파일이 다운로드됨
- GitHub 저장소 > Settings > Secrets and variables > Actions > New repository secret
- Name:
FIREBASE_SERVICE_ACCOUNT - Secret: 다운로드한 JSON 파일 내용 전체
- Name:
- 등록 후 다운로드한 JSON 파일은 삭제 (키는 GitHub에만 보관)
npx firebase login
npm run deploy- Firebase 콘솔 > Hosting > 커스텀 도메인 추가에
choose-hair-of.mongwan.dev입력 - 콘솔이 보여주는 레코드를 Netlify DNS(
mongwan.dev영역)에 그대로 추가 (보통 아래 두 개)타입 이름 값 CNAME choose-hair-ofhair-d2632.web.app(콘솔에 표시된 값)TXT choose-hair-of콘솔이 요구하는 경우에만 - 같은 이름에 이미 다른 A/AAAA/CNAME 레코드가 있으면 삭제
- 콘솔이
_acme-challenge.choose-hair-ofTXT를 추가로 요구하면 그것도 추가
- 콘솔 상태가 연결됨이 되고 SSL 인증서가 발급될 때까지 대기 (수 분~최대 24시간)
.dev는 HTTPS 전용이라 인증서가 나오기 전에는 접속되지 않는다
- 인증서가 나오면
VITE_SITE_URL을 새 도메인으로 바꿔 배포 — 자동화되어 있음.github/workflows/domain-check.yml이 15분마다https://choose-hair-of.mongwan.dev를 확인- 인증서가 유효하고 페이지가 응답하면
.env.production수정 → 배포 실행 → 알림 이슈 생성 → 워크플로 스스로 비활성화 - 결과: 미리보기 이미지(og:image)가 새 도메인으로 바뀌고, 기존
*.web.app주소로 들어온 방문자는 새 도메인으로 이동 (?from=유지) - 진행 상황은 Actions 탭의 "Custom domain check" 로그에서 확인. 바로 확인하려면 Run workflow로 수동 실행
- 도메인을 바꾸면 워크플로의
DOMAIN값을 수정하고 Actions 탭에서 다시 Enable
로컬에서 직접 확인하려면 (macOS 기준, 1분마다 확인 후 알림):
until curl -sSfo /dev/null https://choose-hair-of.mongwan.dev/; do date; sleep 60; done; \
osascript -e 'display notification "인증서 발급 완료" with title "choose-hair-of.mongwan.dev"'?from= 뒤에 출처를 붙여서 공유한다. 영문 소문자, 숫자, -, _ 30자까지.
https://<프로젝트>.web.app/?from=twitter
https://<프로젝트>.web.app/?from=kakao
https://<프로젝트>.web.app/?from=family
- 처음 들어온 출처는 브라우저에 저장되어 재방문 시에도 유지된다. 없으면
direct. - 방문 후 주소창에서
?from=이 지워지므로, 방문자가 링크를 다시 퍼뜨려도 출처가 섞이지 않는다.
gcloud auth application-default login # 최초 1회 (hair-d2632 권한이 있는 계정)
npm run export| 파일 | 내용 | git |
|---|---|---|
exports/votes.csv |
투표 전체 (styleId, styleName, source, updatedAt) |
제외 (나만 봄) |
exports/comments.csv |
의견 전체 (name, body, source, createdAt) |
제외 (나만 봄) |
public/result/stats.csv |
출처 × 스타일 득표 수만 | 커밋해서 /result로 공개 |
- 터미널에 스타일별 득표 수도 출력한다
- Admin SDK로 읽으므로 보안 규칙(읽기 차단)과 관계없이 전부 읽힌다
- 엑셀에서 한글이 깨지지 않도록 BOM을 붙이고, 시간은 한국 시간으로 저장한다
- Node 22.18 이상 필요 (
.ts를 바로 실행)
https://choose-hair-of.mongwan.dev/result/
public/result/stats.csv만 읽어서 총 투표 수, 1위, 스타일별·출처별 득표 막대, 출처 × 스타일 표를 보여준다.
출처 칩·막대·표의 행을 누르면 해당 출처만 표시한다. 이름·의견·시각은 들어가지 않는다.
갱신하려면 npm run export → public/result/stats.csv 커밋 → main에 머지 (배포되면 반영).
로컬에서는 npm run dev 후 http://localhost:5173/result/
- 한 사람에게만 준
?from=링크가 있으면 그 출처의 표는 곧 그 사람의 표가 되니, 그런 출처가 생기면 공개 전에 해당 줄을 지운다
| 컬렉션 | 필드 |
|---|---|
votes/{브라우저ID} |
styleId, source, updatedAt — 브라우저당 1개, 다시 투표하면 덮어씀 |
comments/{자동ID} |
name, body, source, createdAt |
쿼리 빌더에서 styleId == "style-02" 같은 필터와 COUNT 집계로도 볼 수 있다.
보안 규칙상 웹(투표 페이지 포함)에서는 아무도 원본 데이터를 읽을 수 없다.
로그인이 없으므로 시크릿 창이나 다른 브라우저로 중복 투표가 가능하다 (지인 대상이라 허용).