{"id":51015228,"url":"https://github.com/infgrp/yanghyeon-pass","last_synced_at":"2026-06-21T09:30:20.154Z","repository":{"id":363737278,"uuid":"1263400776","full_name":"infgrp/yanghyeon-pass","owner":"infgrp","description":"학교 외출·조퇴증 디지털화 — 비용 제로(Supabase+Vercel), QR 실시간 위조검증, 웹푸시 알림, 담임반 권한분리","archived":false,"fork":false,"pushed_at":"2026-06-10T06:13:24.000Z","size":364,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-10T07:21:45.169Z","etag":null,"topics":["pwa","react","rls","school","supabase","typescript","vercel","vite","web-push"],"latest_commit_sha":null,"homepage":"https://yanghyeon-pass.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/infgrp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-08T23:26:03.000Z","updated_at":"2026-06-10T06:13:28.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/infgrp/yanghyeon-pass","commit_stats":null,"previous_names":["infgrp/yanghyeon-pass"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/infgrp/yanghyeon-pass","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infgrp%2Fyanghyeon-pass","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infgrp%2Fyanghyeon-pass/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infgrp%2Fyanghyeon-pass/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infgrp%2Fyanghyeon-pass/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/infgrp","download_url":"https://codeload.github.com/infgrp/yanghyeon-pass/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/infgrp%2Fyanghyeon-pass/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34605335,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-21T02:00:05.568Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["pwa","react","rls","school","supabase","typescript","vercel","vite","web-push"],"created_at":"2026-06-21T09:30:19.187Z","updated_at":"2026-06-21T09:30:20.146Z","avatar_url":"https://github.com/infgrp.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 양현고등학교 스마트 외출·조퇴 시스템\n\n\u003e 종이 외출·조퇴 서류를 디지털화한 경량 웹앱. **비용 제로(Free Tier)** · **데이터 트래픽 최소화** · **위조 방어**에 초점을 맞춘 1인 개발·운영용 시스템.\n\n**라이브:** https://yanghyeon-pass.vercel.app\n\n\u003e 🏫 **다른 학교에서 운영하려면?** → [SETUP.md](SETUP.md) (Fork 후 처음부터 띄우는 단계별 가이드)\n\n| | |\n| :--- | :--- |\n| **프론트엔드** | React + Vite + TypeScript (SPA · PWA) |\n| **백엔드** | Supabase (PostgreSQL + Auth + RLS + Edge Functions) |\n| **호스팅** | Vercel (GitHub push → 자동 배포) · Supabase 모두 무료 티어 |\n| **인증** | 이메일 + 비밀번호 (학교 계정) |\n\n---\n\n## 핵심 설계 — 데이터 절약 (비용 제로)\n\n매번 이미지/PDF를 내려받는 방식은 학생 데이터 요금과 무료 서버 대역폭을 낭비합니다. 본 시스템은 **'텍스트 중심 통신 + 로컬 렌더링'** 전략을 취합니다.\n\n| 전략 | 구현 |\n| :--- | :--- |\n| **Zero-Image UI** | 외출증 외형(엠블럼·테두리·배치)을 코드에 내장([PassCertificate.tsx](src/components/PassCertificate.tsx)). 서버는 텍스트 JSON만 전송 |\n| **경량 JSON** | 분류는 정수 코드(`type`, `status`), 필요한 컬럼만 SELECT, 조회당 1KB 미만 |\n| **로컬 캐싱** | 승인된 외출증을 AES-GCM 암호화하여 localStorage 저장([cache.ts](src/lib/cache.ts)) |\n| **오프라인 검증** | 교문에서 서버 재요청 없이 캐시로 렌더링 (음영 지역 대응) |\n\n---\n\n## 주요 기능\n\n| 역할 | 기능 |\n| :--- | :--- |\n| **학생** | 외출·조퇴 신청/조회, 외출증(홀로그램·실시간시계·QR), 오프라인 표시 |\n| **교사** | 가입 코드로만 등록 + 담임반 입력 → **담임반 학생만** 승인/반려, 신청 시 **웹 푸시 알림** |\n| **관리자** | 교사 가입코드 관리, 사용자 검색·비번초기화·이메일변경·담임반지정·계정삭제 |\n| **교문(공개)** | QR 스캔 → `/verify/:id` 에서 **실시간 진위 확인** |\n\n### 🛡️ 위조 방어\n생성형 AI·이미지 편집으로 \"승인된 외출증\"을 위조할 수 있으므로, **진본임을 실시간으로 증명**하는 데 초점을 둡니다.\n- **QR 실시간 검증**: 외출증 QR → 공개 검증 함수([verify-pass](supabase/functions/verify-pass))가 서버의 **그 순간 상태**(승인/반려/사용완료)를 응답. 위조 이미지의 QR 은 토큰(`verify_token`) 불일치로 검증 실패.\n- **라이브니스 단서**: 실시간 시계(초가 흐름)·홀로그램 애니메이션 → 정지 캡처/AI 이미지와 즉시 구별.\n\n### 🔔 담임 즉시 알림 (웹 푸시, 무료)\n- 교사가 '알림 켜기' → 기기 구독([push.ts](src/lib/push.ts)). 학생 신청 시 [notify-pass](supabase/functions/notify-pass)가 **담임(학번 앞3자리=담임반 매칭)** 기기로 푸시.\n- FCM/APNs(무료)를 경유하므로 SMS 와 달리 **건당 비용 0원**.\n- 📱 iOS 는 사파리에서 '홈 화면에 추가'(PWA 설치) 후 동작. 안드로이드는 브라우저에서 바로 동작.\n\n### 🔐 보안 모델 (RLS 중심)\n- **역할**: `student`(기본) / `teacher`(가입코드로만) / `admin`. 가입 시 클라이언트가 보낸 role 은 **신뢰하지 않고**, 서버 트리거가 코드를 대조해 판정.\n- **담임반 격리**: 교사는 RLS로 **자기 담임반 학생의 신청만** 조회·승인.\n- **권한 상승 차단**: 학생이 본인 행의 `role`/`student_id`/`homeroom` 을 직접 바꾸지 못하게 트리거로 가드.\n- **service_role 키**는 Edge Function 서버에만 존재(프론트엔드 노출 없음).\n\n---\n\n## 아키텍처\n\n```\n              ┌──────────────────────────────┐\n  학생/교사 ──▶│  React SPA (Vercel, PWA)      │\n              └───────────┬──────────────────┘\n                          │ anon key (RLS 적용)\n              ┌───────────▼──────────────────┐\n              │  Supabase                     │\n              │   • PostgreSQL + RLS          │\n              │   • Auth (이메일/비번)         │\n              │   • Edge Functions (Deno)     │\n              │      - admin-users  (관리자)   │\n              │      - verify-pass  (공개 QR)  │\n              │      - notify-pass  (웹푸시)   │\n              └───────────────────────────────┘\n  교문 경비 ──▶ /verify/:id (로그인 불필요, 공개)\n```\n\n## 프로젝트 구조\n\n```\nsrc/\n  lib/         supabase 클라이언트, 타입, 상수, AES-GCM 캐시, 웹푸시 유틸\n  context/     AuthContext (세션·프로필)\n  components/  PassCertificate(외출증), ProtectedRoute\n  pages/       Login · StudentHome · ApplyPass · PassDetail\n               TeacherHome · AdminHome · VerifyPass(공개)\npublic/        manifest.webmanifest · sw.js(서비스워커) · icons/\nsupabase/\n  schema.sql           DB 스키마 + RLS + 트리거 (idempotent)\n  functions/\n    admin-users/       관리자 사용자 관리 (service_role)\n    verify-pass/       공개 QR 진위 검증 (--no-verify-jwt)\n    notify-pass/       신청 시 담임 웹푸시 발송\nscripts/       REST 기반 E2E 검증 스크립트 (Python)\n```\n\n---\n\n## 설치 및 실행 (로컬)\n\n```bash\nnpm install\ncp .env.example .env    # 값 채우기 (아래 환경변수 참고)\nnpm run dev             # 개발 서버 (http://localhost:5173)\nnpm run build           # dist/ 정적 빌드\n```\n\n### 환경변수 (`.env`)\n| 키 | 설명 |\n| :--- | :--- |\n| `VITE_SUPABASE_URL` | Supabase 프로젝트 URL |\n| `VITE_SUPABASE_ANON_KEY` | Supabase anon public key (공개용) |\n| `VITE_VAPID_PUBLIC_KEY` | 웹푸시 VAPID 공개키 (`npx web-push generate-vapid-keys`) |\n| `VITE_PUBLIC_BASE_URL` | (선택) QR 검증 링크 기준 도메인. 미설정 시 실서비스 도메인 기본값 |\n\n---\n\n## 백엔드 설정 (Supabase)\n\n1. [supabase.com](https://supabase.com) 에서 프로젝트 생성 (무료)\n2. **SQL Editor** 에서 [`supabase/schema.sql`](supabase/schema.sql) 전체 실행 (테이블·RLS·트리거 일괄 생성, 재실행 안전)\n3. **Settings → API** 의 `Project URL`·`anon public key` 를 `.env` / Vercel 에 입력\n4. (테스트 편의) **Authentication → Email** 의 \"Confirm email\" 비활성화\n5. **Edge Functions 배포** (Supabase CLI):\n   ```bash\n   npx supabase login\n   npx supabase functions deploy admin-users  --project-ref \u003cref\u003e\n   npx supabase functions deploy verify-pass   --no-verify-jwt --project-ref \u003cref\u003e\n   # 웹푸시: VAPID 비공개키를 시크릿으로 등록 후 배포\n   npx supabase secrets set VAPID_PRIVATE_KEY=\u003cprivate\u003e --project-ref \u003cref\u003e\n   npx supabase functions deploy notify-pass   --project-ref \u003cref\u003e\n   ```\n6. **최초 관리자 지정** (가입 후 SQL Editor 에서):\n   ```sql\n   update public.users set role = 'admin'\n   where id = (select id from auth.users where email = '\u003c관리자이메일\u003e');\n   ```\n7. **교사 가입 코드 발급** — 관리자 화면에서, 또는 SQL:\n   ```sql\n   insert into public.teacher_codes (code, label) values ('CODE-2026', '2026 교직원');\n   ```\n\n\u003e 교사는 회원가입 시 이 코드 + 담임 학년/반을 입력해야 교사 권한을 받습니다.\n\n## 배포 (Vercel)\n\nGitHub 저장소를 Vercel 에 Import 하면 `master` push 마다 자동 배포됩니다.\n- Framework **Vite** 자동 감지, Output `dist`, SPA rewrite 는 [vercel.json](vercel.json)\n- **Environment Variables** 에 위 `VITE_*` 4개 등록 후 **Redeploy** (env 는 빌드 시 주입됨)\n- 커밋 작성자 이메일은 **GitHub 계정에 연결된 이메일**이어야 함 (아니면 Vercel 이 Blocked 처리)\n\n---\n\n## 데이터 모델\n\n- **users** — `id`(auth 연동 UUID), `student_id`(\"30101\"=3학년1반1번), `name`, `role`, `homeroom`(\"301\"=교사 담임반), `parent_phone`\n- **passes** — `type`(1:조퇴, 2:외출), `status`(0:대기, 1:승인, 2:반려, 3:사용완료), 시간·사유·담당교사·`verify_token`\n- **teacher_codes** — 교사 가입 코드(`active`, `expires_at`)\n- **push_subscriptions** — 교사 기기 웹푸시 구독\n\n## 사용 흐름\n\n1. **학생** 로그인 → 외출·조퇴 신청 → (담임에게 즉시 푸시)\n2. **교사** 로그인 → 담임반 대기 목록에서 승인/반려, 하교 시 사용완료\n3. **학생** 승인된 외출증 제시 → 교문에서 **QR 스캔으로 실시간 진위 확인**\n\n## E2E 검증\n\n`scripts/` 의 Python 스크립트가 REST API 로 핵심 시나리오(권한 분리·담임반 격리·QR 검증·관리자 기능)를 검증합니다. 실행 전 환경변수로 자격증명을 주입합니다.\n\n```bash\n# 예: 담임반 격리 검증\nSUPABASE_URL=... SUPABASE_ANON_KEY=... ADMIN_EMAIL=... ADMIN_PASSWORD=... \\\n  python scripts/e2e_homeroom.py\n```\n\n---\n\n## 라이선스 / 운영 메모\n\n학교 내부 운영용 프로젝트입니다. 운영 전 권장:\n- 관리자 비밀번호를 강력한 값으로 변경\n- 이메일 인증 재활성화 + Supabase Auth 의 Site URL 을 실서비스 도메인으로 설정\n- 테스트 계정 정리\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finfgrp%2Fyanghyeon-pass","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Finfgrp%2Fyanghyeon-pass","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finfgrp%2Fyanghyeon-pass/lists"}