{"id":48184230,"url":"https://github.com/doublesilver/subway-board","last_synced_at":"2026-04-04T17:47:19.676Z","repository":{"id":331052707,"uuid":"1125046414","full_name":"doublesilver/subway-board","owner":"doublesilver","description":"출근길 지하철 실시간 익명 채팅 서비스 '가기싫어' | React 19 + Express 5 + Socket.IO + PostgreSQL + Apps in Toss","archived":false,"fork":false,"pushed_at":"2026-02-27T11:30:34.000Z","size":22458,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-27T12:58:20.522Z","etag":null,"topics":["anonymous","apps-in-toss","chat-application","commute","express","fullstack","korean","postgresql","railway","react","realtime","socket-io","subway","typescript","vercel"],"latest_commit_sha":null,"homepage":"https://gagisiro.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/doublesilver.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-12-30T04:03:25.000Z","updated_at":"2026-02-27T11:30:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/doublesilver/subway-board","commit_stats":null,"previous_names":["doublesilver/subway-board"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/doublesilver/subway-board","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doublesilver%2Fsubway-board","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doublesilver%2Fsubway-board/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doublesilver%2Fsubway-board/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doublesilver%2Fsubway-board/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/doublesilver","download_url":"https://codeload.github.com/doublesilver/subway-board/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doublesilver%2Fsubway-board/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31407653,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["anonymous","apps-in-toss","chat-application","commute","express","fullstack","korean","postgresql","railway","react","realtime","socket-io","subway","typescript","vercel"],"created_at":"2026-04-04T17:47:19.139Z","updated_at":"2026-04-04T17:47:19.582Z","avatar_url":"https://github.com/doublesilver.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 가기싫어 - 출근길 익명 채팅방\n\n\u003e **\"오늘 아침, 당신의 출근길은 어땠나요?\"**\n\u003e\n\u003e 같은 호선, 같은 방향으로 향하는 사람들을 연결하는 **디지털 대나무 숲**.\n\n\u003cdiv align=\"center\"\u003e\n\n[![Deploy Status](https://img.shields.io/badge/deploy-live-brightgreen?style=for-the-badge)](https://gagisiro.com)\n[![CI/CD](https://img.shields.io/badge/CI%2FCD-GitHub_Actions-2088FF?style=for-the-badge\u0026logo=github-actions\u0026logoColor=white)](https://github.com/doublesilver/subway-board/actions)\n[![TypeScript](https://img.shields.io/badge/TypeScript-100%25-3178C6?style=for-the-badge\u0026logo=typescript\u0026logoColor=white)](https://www.typescriptlang.org/)\n[![React](https://img.shields.io/badge/React-19-61DAFB?style=for-the-badge\u0026logo=react\u0026logoColor=black)](https://react.dev/)\n[![Express](https://img.shields.io/badge/Express-5-000000?style=for-the-badge\u0026logo=express\u0026logoColor=white)](https://expressjs.com/)\n[![Tests](https://img.shields.io/badge/Tests-1847_unit%2Fintegration_passed-brightgreen?style=for-the-badge)](https://github.com)\n[![Coverage](https://img.shields.io/badge/Coverage-80%25%2B-success?style=for-the-badge)](https://github.com)\n[![Apps in Toss](https://img.shields.io/badge/Apps_in_Toss-ready-blue?style=for-the-badge)](https://apps-in-toss.toss.im/)\n\n**Live Demo**: [https://gagisiro.com](https://gagisiro.com)\n\n\u003c/div\u003e\n\n---\n\n## 스크린샷\n\n\u003cdiv align=\"center\"\u003e\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\" width=\"33%\"\u003e\n\u003cimg src=\"assets/designs/stitch/stitch/home__line_selection_grid/screen.png\" width=\"240\" alt=\"홈 - 호선 선택\" /\u003e\n\u003cbr /\u003e\u003cb\u003e홈 화면\u003c/b\u003e\u003cbr /\u003e\n\u003csub\u003e호선별 실시간 혼잡도 + 접속자 수\u003c/sub\u003e\n\u003c/td\u003e\n\u003ctd align=\"center\" width=\"33%\"\u003e\n\u003cimg src=\"assets/designs/stitch/stitch/chat_room__line_2_%EC%8B%A4%EC%8B%9C%EA%B0%84_%EB%8C%80%ED%99%94/screen.png\" width=\"240\" alt=\"채팅방 - 2호선\" /\u003e\n\u003cbr /\u003e\u003cb\u003e실시간 채팅\u003c/b\u003e\u003cbr /\u003e\n\u003csub\u003e답장 · 타이핑 표시 · 스와이프 UX\u003c/sub\u003e\n\u003c/td\u003e\n\u003ctd align=\"center\" width=\"33%\"\u003e\n\u003cimg src=\"docs/screenshots/operating-hours-closed.png\" width=\"240\" alt=\"운영시간 외 대기실 - 잡지식\" /\u003e\n\u003cbr /\u003e\u003cb\u003e대기실 (잡지식)\u003c/b\u003e\u003cbr /\u003e\n\u003csub\u003e운영시간 외 카운트다운 + 지하철 잡지식\u003c/sub\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd align=\"center\" width=\"33%\"\u003e\n\u003cimg src=\"docs/screenshots/operating-hours-quiz.png\" width=\"240\" alt=\"운영시간 외 대기실 - 퀴즈\" /\u003e\n\u003cbr /\u003e\u003cb\u003e대기실 (퀴즈)\u003c/b\u003e\u003cbr /\u003e\n\u003csub\u003e4지선다 지하철 퀴즈 + 정답 피드백\u003c/sub\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\u003c/div\u003e\n\n\u003e **운영 시간**: 평일 오전 7시 ~ 9시 (KST)에만 채팅 가능. 그 외 시간에는 서울 지하철 잡지식과 퀴즈를 즐길 수 있어요.\n\n---\n\n## 프로젝트 개요\n\n| 항목 | 내용 |\n|------|------|\n| **프로젝트명** | 가기싫어 (출근길 익명 채팅방) |\n| **개발 기간** | 2025.12 ~ 현재 (12주+) |\n| **개발 인원** | 1인 풀스택 (기획 / 디자인 / 개발 / 배포 / 운영) |\n| **서비스 URL** | [gagisiro.com](https://gagisiro.com) |\n| **운영 시간** | 평일 07:00 ~ 09:00 (KST) — 출근 시간대 한정 운영 |\n| **현재 버전** | v4.6 — 운영시간 전체 차단 + 잡지식/퀴즈 대기실, Request ID tracing, Prometheus metrics |\n\n---\n\n## 주요 기능\n\n| 기능 | 설명 |\n|------|------|\n| **호선별 실시간 채팅** | 1~9호선 독립 채널, Socket.IO 양방향 통신, 실시간 접속자 카운팅 |\n| **완전한 익명성** | 회원가입 불필요, UUID 세션 관리, 매일 자정 메시지 자동 삭제 |\n| **답장(Reply)** | 특정 메시지에 답장, 원본 미리보기, 스와이프 UX |\n| **AI 콘텐츠 필터링** | 1차 로컬 Regex + 2차 OpenAI Moderation API, Fail-Open 전략 |\n| **관리자 대시보드** | DAU/WAU/MAU 시각화, 호선별/시간대별 분석, 커스텀 SQL 쿼리 |\n| **모바일 최적화** | 모바일 퍼스트 반응형, iOS/Android 키보드 대응, 다크모드 |\n| **게시글 검색** | pg_trgm 기반 한국어 트라이그램 검색 |\n| **신고 시스템** | 게시글 신고 + 관리자 리뷰 워크플로우 |\n| **운영시간 차단 + 대기실** | 서비스 전체 운영시간 외 차단, 잡지식/퀴즈 탭으로 대기 중 재미 요소 제공 |\n| **실시간 혼잡도** | 서울 열린데이터 API 연동 (1~8호선) |\n| **푸시 알림** | Web Push (VAPID) 구독 + DB 영속화 |\n| **Observability** | Request ID tracing, Prometheus `/metrics` 엔드포인트 |\n| **앱인토스 미니앱** | 토스 앱 내 미니앱 배포, 인앱 광고 3종 (배너/전면/보상형), WebView UX 적응 |\n| **법적 페이지** | 개인정보처리방침, 이용약관 |\n\n---\n\n## 기술 스택\n\n```mermaid\nflowchart LR\n    subgraph Frontend[\"Frontend (Vercel + Apps in Toss)\"]\n        React[React 19 + TSX]\n        Vite[Vite 6]\n        Router[React Router 7]\n        SIO_C[Socket.IO Client]\n        AiT[Apps in Toss SDK]\n    end\n\n    subgraph Backend[\"Backend (Railway)\"]\n        Node[Node.js 22 LTS]\n        Express[Express 5]\n        SIO_S[Socket.IO Server]\n        Helmet[Helmet 8]\n    end\n\n    subgraph Database[\"Database (Railway)\"]\n        PG[(PostgreSQL 15)]\n        Redis[(Redis Cache)]\n    end\n\n    React --\u003e SIO_C\n    SIO_C \u003c--\u003e|WebSocket| SIO_S\n    React --\u003e|HTTP| Express\n    Express --\u003e PG\n    Express --\u003e Redis\n    Express -.-\u003e|Content Filter| OpenAI[OpenAI Moderation]\n```\n\n### Frontend\n| 기술 | 버전 | 용도 |\n|------|------|------|\n| React | 19.x | UI 컴포넌트 (100% TSX) |\n| Vite | 6.x | 빌드 도구, HMR |\n| React Router | 7.x | SPA 라우팅 |\n| Socket.IO Client | 4.8 | 실시간 통신 |\n| @apps-in-toss/web-framework | 1.13 | 토스 미니앱 SDK (광고, 빌드, 배포) |\n| Vitest | 3.1 | 단위 테스트 (844 tests) |\n| Playwright | 1.50 | E2E 테스트 (9 specs) |\n| Axios | 1.x | HTTP 클라이언트 |\n\n### Backend\n| 기술 | 버전 | 용도 |\n|------|------|------|\n| Node.js | 22 LTS | 런타임 |\n| Express | 5.0 | API 서버 (async/await 네이티브) |\n| Socket.IO | 4.8 | WebSocket 서버 |\n| PostgreSQL | 15 | 메인 DB |\n| Redis | 5.10 | 캐싱 (Fail-safe, optional) |\n| Winston | 3.x | 구조화 로깅 |\n| bcrypt | 5.x | 관리자 비밀번호 해싱 |\n| Sentry | 10.35 | 에러 모니터링 (breadcrumbs, WebSocket context) |\n| Jest | 30.x | 테스트 (63 suites, 1,003 tests) |\n\n### Infrastructure\n| 서비스 | 용도 |\n|--------|------|\n| Railway | 백엔드 API + PostgreSQL + Redis 호스팅 |\n| Vercel | 프론트엔드 SPA 호스팅 |\n| Apps in Toss | 토스 앱 내 미니앱 배포 (.ait 패키징) |\n| GitHub Actions | CI/CD 파이프라인 |\n\n---\n\n## 시스템 아키텍처\n\n```mermaid\nflowchart TB\n    subgraph Client[\"Client Browser\"]\n        ReactApp[\"React SPA\"]\n        WS_Client[\"WebSocket Client\"]\n    end\n\n    subgraph Vercel[\"Vercel (Frontend)\"]\n        SPA[\"gagisiro.com\u003cbr/\u003eStatic SPA Hosting\"]\n    end\n\n    subgraph Railway[\"Railway (Backend)\"]\n        ExpressAPI[\"Express API :5001\"]\n        WS_Server[\"Socket.IO Server\"]\n        PostgreSQL[(\"PostgreSQL 15\")]\n        RedisCache[(\"Redis Cache\")]\n    end\n\n    subgraph External[\"External\"]\n        OpenAI[\"OpenAI Moderation API\"]\n    end\n\n    ReactApp --\u003e|HTTPS| SPA\n    SPA --\u003e|API Calls| ExpressAPI\n    WS_Client \u003c--\u003e|WebSocket| ExpressAPI\n    ExpressAPI --\u003e PostgreSQL\n    ExpressAPI --\u003e RedisCache\n    ExpressAPI -.-\u003e|Content Filter| OpenAI\n```\n\n### 실시간 채팅 흐름\n```mermaid\nsequenceDiagram\n    participant U as 사용자\n    participant C as React Client\n    participant S as Socket.IO Server\n    participant AI as OpenAI Moderation\n    participant DB as PostgreSQL\n\n    U-\u003e\u003eC: 호선 선택 (2호선)\n    C-\u003e\u003eS: join_line(lineId: 2)\n    S--\u003e\u003eC: user_count 업데이트\n\n    U-\u003e\u003eC: 메시지 입력\n    C-\u003e\u003eS: POST /api/posts\n    S-\u003e\u003eS: 1차 Local Regex 필터링\n    S-\u003e\u003eAI: 2차 문맥 분석\n    AI--\u003e\u003eS: { safe: true }\n    S-\u003e\u003eDB: INSERT message\n    S--\u003e\u003eC: 201 Created\n    S-\u003e\u003eS: io.to(\"line_2\").emit()\n    S--\u003e\u003eC: new_message (broadcast)\n```\n\n### 데이터베이스 설계\n```mermaid\nerDiagram\n    SUBWAY_LINES ||--o{ POSTS : contains\n    SUBWAY_LINES ||--o{ DAILY_VISITS : tracks\n    SUBWAY_LINES ||--o{ HOURLY_VISITS : tracks\n    SUBWAY_LINES ||--o{ UNIQUE_VISITORS : first_visit\n\n    SUBWAY_LINES {\n        int id PK\n        string line_name\n        string color\n    }\n\n    POSTS {\n        uuid id PK\n        int subway_line_id FK\n        int reply_to FK\n        text content\n        string message_type\n        int user_id FK\n        string anonymous_id\n        timestamp created_at\n        timestamp deleted_at\n    }\n\n    DAILY_VISITS {\n        int id PK\n        date visit_date\n        int subway_line_id FK\n        int visit_count\n    }\n\n    UNIQUE_VISITORS {\n        int id PK\n        string visitor_hash\n        date visit_date\n        int first_line_id FK\n    }\n\n    FEEDBACK {\n        int id PK\n        text content\n        string user_session_id\n        timestamp created_at\n    }\n\n    POSTS ||--o{ REPORTS : has\n    POSTS ||--o{ REACTIONS : has\n    POSTS ||--o{ COMMENTS : has\n    POSTS ||--o{ PUSH_SUBSCRIPTIONS : notifies\n\n    REPORTS {\n        int id PK\n        int post_id FK\n        string reason\n        string status\n        timestamp created_at\n    }\n\n    REACTIONS {\n        int id PK\n        int post_id FK\n        string anonymous_id\n        string type\n    }\n\n    COMMENTS {\n        int id PK\n        int post_id FK\n        text content\n        string anonymous_id\n        timestamp created_at\n    }\n\n    PUSH_SUBSCRIPTIONS {\n        int id PK\n        string endpoint\n        string auth_key\n        string p256dh_key\n        timestamp created_at\n    }\n```\n\n---\n\n## 프로젝트 구조\n\n```\nsubway-board/\n├── frontend/                        # React 19 SPA (Vercel 배포, 100% TypeScript)\n│   ├── src/\n│   │   ├── components/\n│   │   │   ├── admin/               # 대시보드 컴포넌트 분리\n│   │   │   │   ├── AdminLogin.tsx\n│   │   │   │   ├── DashboardHeader.tsx\n│   │   │   │   ├── OverviewTab.tsx\n│   │   │   │   ├── HourlyTab.tsx\n│   │   │   │   ├── LinesTab.tsx\n│   │   │   │   ├── QueryBuilder.tsx\n│   │   │   │   ├── QueryTab.tsx\n│   │   │   │   ├── ReportsTab.tsx\n│   │   │   │   ├── ResultsTable.tsx\n│   │   │   │   └── SummaryCard.tsx\n│   │   │   ├── chat/                # 채팅 컴포넌트 분리\n│   │   │   │   ├── ChatComposer.tsx\n│   │   │   │   ├── ChatHeader.tsx\n│   │   │   │   ├── ChatMessageArea.tsx\n│   │   │   │   ├── DateDivider.tsx\n│   │   │   │   ├── MessageBubble.tsx\n│   │   │   │   ├── MessageList.tsx\n│   │   │   │   ├── MessageMenu.tsx\n│   │   │   │   ├── MessageReactions.tsx\n│   │   │   │   └── ScrollToBottom.tsx\n│   │   │   ├── ads/                 # Apps in Toss 광고 컴포넌트\n│   │   │   │   ├── BannerAdSlot.tsx\n│   │   │   │   └── RewardedAdButton.tsx\n│   │   │   ├── layout/              # Header, Footer, MainLayout\n│   │   │   ├── AnimatedBackground.tsx\n│   │   │   ├── ClosedAlertModal.tsx    # 운영시간 외 대기실 (잡지식/퀴즈)\n│   │   │   ├── ErrorBoundary.tsx\n│   │   │   ├── FeedbackModal.tsx\n│   │   │   ├── OperatingHoursGuard.tsx # 운영시간 가드 (홈+채팅방)\n│   │   │   ├── LinkifyText.tsx\n│   │   │   ├── RouteSkeleton.tsx\n│   │   │   ├── SessionExpiredModal.tsx\n│   │   │   └── Toast.tsx\n│   │   ├── config/                  # constants, storageKeys, adConstants\n│   │   ├── contexts/                # AuthContext, ThemeContext\n│   │   ├── hooks/                   # useChatSocket, useChatScroll, useChatState, useSwipeReply, useToast, useAitAds, useDocumentMeta, useFocusTrap, useMessageSearch 등\n│   │   ├── pages/                   # HomePage, LinePage, AdminDashboard, PrivacyPage, TermsPage, AboutPage, PreviewHome, PreviewChat\n│   │   ├── services/api/            # Axios 인스턴스 + 인터셉터 + 자동 unwrap\n│   │   ├── data/                    # subwayTrivia (잡지식/퀴즈 데이터)\n│   │   └── utils/                   # socket, temporaryUser, linkify, errorUtils, pushNotification, serviceWorker, aitDetect, analytics, notifications, routePrefetch\n│   ├── e2e/                         # Playwright E2E (9 spec files)\n│   ├── granite.config.ts            # Apps in Toss 빌드 설정\n│   └── vite.config.js\n│\n├── backend/                         # Express 5 API (Railway 배포, 100% TypeScript)\n│   ├── src/\n│   │   ├── config/                  # constants, env, patterns, rateLimiters, redis, swagger, validateEnv, middleware, socketSetup, gracefulShutdown\n│   │   ├── controllers/             # post, comment, visit, feedback, dashboard, auth, admin, reaction, topic, congestion, push, report, subwayLine\n│   │   ├── db/                      # connection.ts, migrate.ts, migrations/\n│   │   ├── middleware/              # auth, requireAuth, admin, validator, error\n│   │   ├── services/               # postService, cacheService, aiService, visitService, dashboardService, reactionService, topicService, congestionService, pushService, reportService, commentService, feedbackService, adminService, subwayLineService, analyticsQueryBuilder\n│   │   ├── socket/                  # handlers, state, index (Socket.IO 이벤트 핸들링)\n│   │   ├── routes/index.ts\n│   │   └── utils/                   # logger, scheduler, AppError, asyncHandler, errorCodes, response, httpCache, queryParser, profanityFilter, hashPassword, userHelper\n│   └── tests/                       # 59 suites, 966 tests (100% .ts)\n│\n├── scripts/                         # release_gate.sh, web_vitals_baseline.sh, slo_budget_gate.sh\n├── docs/                            # E2E, Load Testing, Admin Dashboard 가이드\n├── .github/workflows/               # ci.yml, security.yml, load-testing.yml\n└── backend/Dockerfile               # Railway 빌드용 (multi-stage)\n```\n\n---\n\n## 앱 라우트\n\n### Frontend (React Router)\n| 경로 | 페이지 | 설명 |\n|------|--------|------|\n| `/` | HomePage | 호선 선택 그리드 |\n| `/line/:lineId` | LinePage | 실시간 채팅방 |\n| `/admin` | AdminDashboard | 관리자 대시보드 (인증 필요) |\n| `/privacy` | PrivacyPage | 개인정보처리방침 |\n| `/terms` | TermsPage | 이용약관 |\n| `/about` | AboutPage | 서비스 소개 |\n| `/preview` | PreviewHome | 미리보기 홈 |\n| `/preview/chat/:lineId` | PreviewChat | 미리보기 채팅 |\n\n### Backend API (`api.gagisiro.com`)\n| 카테고리 | 주요 엔드포인트 |\n|----------|----------------|\n| 인증 | `POST /api/auth/anonymous` |\n| 지하철 | `GET /api/subway-lines`, `GET /api/congestion/:lineId` |\n| 게시글 | `GET/POST/DELETE /api/posts/*`, `POST /api/posts/join`, `POST /api/posts/leave` |\n| 댓글 | `GET/POST /api/posts/:postId/comments`, `DELETE /api/comments/:commentId` |\n| 반응 | `POST/GET /api/posts/:postId/reactions` |\n| 토픽 | `GET /api/topic/today` |\n| 피드백 | `POST /api/feedback` |\n| 신고 | `POST /api/posts/:postId/report` |\n| 푸시 | `GET /api/push/vapid-key`, `POST /api/push/subscribe`, `POST /api/push/unsubscribe` |\n| 대시보드 | `POST /api/dashboard/login`, `GET /api/dashboard/data`, `POST /api/dashboard/query` |\n| 관리자 | `GET /api/admin/reports`, `GET /api/admin/feedback`, `GET /api/admin/stats` |\n| 헬스/모니터링 | `GET /health`, `GET /metrics` (Prometheus) |\n\n---\n\n## 로컬 실행 방법\n\n### 요구 사항\n- Node.js 22.x 이상, PostgreSQL 15.x, npm 10.x\n\n### Quick Start\n```bash\n# 저장소 클론\ngit clone https://github.com/doublesilver/subway-board.git\ncd subway-board\n\n# Backend\ncd backend\ncp .env.example .env    # DATABASE_URL, JWT_SECRET 등 설정\nnpm install\nnpm run dev             # http://localhost:5001\n\n# Frontend (새 터미널)\ncd frontend\nnpm install\nnpm run dev             # http://localhost:3000\n```\n\n### Apps in Toss 빌드\n```bash\ncd frontend\nnpm run build:ait           # .ait 파일 생성 (granite build)\nnpm run deploy:ait          # 토스 개발자 콘솔 배포 (ait deploy)\n```\n\n### Railway 배포 (Production)\n```bash\n# Railway CLI 설치 후\ncd backend\nrailway login\nrailway link\nrailway up\n```\n\n| 서비스 | 설명 |\n|--------|------|\n| subway-board-backend | Express API + Socket.IO (Railway) |\n| PostgreSQL | Railway 내장 DB |\n| Redis | Railway 내장 캐시 |\n\n**Backend URL**: `https://subway-board-backend-production.up.railway.app`\n\n### Admin Password Hashing\n\nAdmin dashboard passwords should be stored as bcrypt hashes (12 rounds):\n\n```bash\ncd backend\nnpx ts-node src/utils/hashPassword.ts \"your-plain-password\"\n# → $2b$12$...  (paste this into ADMIN_DASHBOARD_PASSWORD env var)\n```\n\nThe server auto-detects hashed vs. plaintext passwords. Plaintext is accepted with a warning in non-production environments.\n\n---\n\n## 테스트\n\n**Coverage**: 127 Suites | 1,847 Tests (Backend 1,003 + Frontend 844) + E2E 66\n\n| Layer | Tool | Count | Coverage |\n|-------|------|-------|----------|\n| Backend Unit/Integration | Jest 30.x | 1,003 tests (63 suites) | 80%+ lines |\n| Frontend Unit/Integration | Vitest 3.1 | 844 tests (64 suites) | 80%+ lines |\n| E2E | Playwright 1.50 | 9 specs (2 projects) | 주요 유저 플로우 전체 |\n| Load | k6 | 4 scenarios | - |\n\n### Coverage Thresholds\n| 메트릭 | 백엔드 | 프론트엔드 |\n|--------|--------|-----------|\n| Lines | 80% | 80% |\n| Statements | 80% | 80% |\n| Branches | 70% | 70% |\n| Functions | 70% | 70% |\n\n```bash\n# Backend 테스트\ncd backend \u0026\u0026 npm test\ncd backend \u0026\u0026 npm run test:coverage\n\n# Frontend 테스트\ncd frontend \u0026\u0026 npm run test:run\ncd frontend \u0026\u0026 npm run test:coverage\n\n# Frontend E2E (Chromium + Mobile Chrome)\ncd frontend \u0026\u0026 npm run test:e2e\n\n# 부하 테스트 (k6 필요)\ncd backend \u0026\u0026 k6 run tests/load/load-test.js\n```\n\n### E2E 테스트 스펙\n| 파일 | 커버리지 |\n|------|----------|\n| `home.spec.ts` | 홈페이지 렌더링, 호선 선택 |\n| `navigation.spec.ts` | SPA 라우팅, 페이지 전환 |\n| `user-flow.spec.ts` | 전체 유저 시나리오 (입장→채팅→퇴장) |\n| `chat-features.spec.ts` | 메시지 전송, 답장, 반응 |\n| `admin.spec.ts` | 관리자 로그인, 권한 |\n| `admin-dashboard.spec.ts` | 대시보드 기능 전체 |\n| `accessibility.spec.ts` | 접근성 (axe-core) 검증 |\n| `reactions.spec.ts` | 리액션 이모지 기능 |\n| `session-recovery.spec.ts` | 세션 복구 시나리오 |\n\n- [E2E Testing Guide](docs/E2E_TESTING.md)\n- [Load Testing Guide](docs/LOAD_TESTING.md)\n\n---\n\n## 성능\n\n**테스트 환경**: Railway | **도구**: k6 v1.5.0\n\n| 시나리오 | 동시 사용자 | 처리량 | P95 응답 | 결과 |\n|---------|-----------|--------|---------|------|\n| Smoke | 1 | 0.73 req/s | 73ms | Pass |\n| Load | 50 | 9.87 req/s | 28ms | Pass |\n| Stress | 200 | 235 req/s | 77ms | Pass |\n| Spike | 200 | 363 req/s | 81ms | Pass |\n\n- **최대 동시 사용자**: 200명 안정 처리\n- **캐시 효율**: 98.64% hit rate (Redis)\n- **API 응답**: 평균 16~35ms\n\n\u003e 상세 결과: [Load Testing Results](docs/LOAD_TESTING_RESULTS.md)\n\n---\n\n## 보안\n\n### 애플리케이션 (OWASP Top 10 대응)\n| 영역 | 구현 |\n|------|------|\n| HTTP 헤더 | Helmet.js 보안 헤더 (CSP, X-Frame-Options 등) |\n| Rate Limiting | 쓰기 50/15min, 읽기 100/min; Redis-backed store (분산 환경 지원, memory fallback) |\n| CORS | 허용 도메인만 접근 (gagisiro.com, tossmini.com) |\n| Input Validation | XSS 필터링, 길이 검증, 위험 프로토콜 차단 |\n| SQL Injection | Parameterized Query |\n| 콘텐츠 필터링 | Local Regex + OpenAI AI 필터 (Fail-Open) |\n| URL Sanitization | `javascript:`, `data:`, `vbscript:` 프로토콜 차단 |\n| Admin Auth | bcrypt 12-round hashing for dashboard password; plaintext fallback w/ warning |\n| DB Connection | SSL enforced in production (`DB_SSL_REJECT_UNAUTHORIZED=true` default) |\n\n### 인프라 (Railway)\n| 영역 | 구현 |\n|------|------|\n| Railway | 자동 HTTPS, 도메인 관리 |\n| Docker Multi-stage Build | 최소 이미지, 보안 비root 사용자 |\n| Health Check | `/health` 엔드포인트 (DB/Redis 상태 포함) |\n\n---\n\n## 기술적 도전과 해결\n\n| 도전 | 해결 |\n|------|------|\n| Express 5 마이그레이션 | 와일드카드 라우팅 `/{*path}`, async 에러 네이티브 지원 |\n| CRA → Vite | 개발 서버 10초→1초, HMR 2초→50ms |\n| 모바일 키보드 대응 | visualViewport API로 입력창 위치 동적 조정 |\n| 연속 메시지 전송 UX | preventDefault로 textarea 포커스 유지 |\n| 운영 시간 제한 | 평일 07:00~09:00 서버 사이드 검증 |\n| AI 필터링 비용 최적화 | Local Regex 1차 → OpenAI 2차, Fail-Open |\n| Redis Fail-Safe | 장애 시 자동 DB fallback, 가용성 100% |\n| 낙관적 업데이트 | 즉시 UI 반영 + 실패 시 롤백 + WebSocket 중복 제거 |\n| JSX→TSX 전체 마이그레이션 | Frontend/Backend 100% TypeScript 달성 |\n| 컴포넌트 분리 | LinePage 405줄 → chat/ 모듈, AdminDashboard → admin/ 모듈 |\n| 테스트 커버리지 달성 | Backend 40%→80%+, Frontend 25%→80%+ (1,762 tests) |\n| 프론트엔드/백엔드 분리 배포 | Vercel(SPA) + Railway(API) 아키텍처 |\n| Apps in Toss WebView 적응 | safe-area, 44px 터치 타겟, CSP 화이트리스트, overscroll 제어, 조건부 레이아웃 |\n| 인앱 광고 3종 통합 | 배너/전면/보상형, 쿨다운 시스템, 기존 웹 영향 없는 isAppsInToss() 분기 설계 |\n| API 응답 포맷 통합 | 29개 엔드포인트를 `{ success, data/error }` 단일 envelope으로 표준화; Axios 인터셉터 자동 unwrap |\n| 모놀리식 파일 분해 | backend/index.ts 468줄 → middleware.ts + socketSetup.ts + gracefulShutdown.ts; socket/ 디렉토리 통합 |\n| Redis rate-limit store | distributed rate limiting (graceful memory fallback); comments/reactions/dashboard 캐시 추가 |\n| 번들 최적화 | recharts separate chunk (AdminDashboard 409KB → 23KB); gzip + Brotli (vite-plugin-compression2) |\n| 접근성 개선 | FeedbackModal 중복 h1 제거, Header 비상호작용 h1 수정, a11y 회귀 테스트 추가 |\n| 서비스 전체 운영시간 차단 | OperatingHoursGuard로 홈+채팅방 통합 차단, optional onClose 패턴으로 context별 UI 분기 |\n| 대기실 잡지식/퀴즈 | 10개 지하철 잡지식 + 5개 퀴즈, 탭 전환, 4지선다 정답/오답 피드백 UX |\n| Request ID tracing | 요청별 고유 ID 부여, 로그 추적성 향상, Prometheus `/metrics` 엔드포인트 |\n| CI/CD 파이프라인 최적화 | Playwright 캐시, 중복 트리거 제거, E2E strict mode 위반 수정 |\n\n\n---\n\n## 개발 타임라인\n\n```mermaid\ngantt\n    title 개발 일정\n    dateFormat YYYY-MM-DD\n    section 기획/개발\n    서비스 기획 + 와이어프레임   :done, 2025-12-01, 14d\n    Frontend + Backend 구축     :done, 2025-12-15, 14d\n    Socket.IO 실시간 채팅       :done, 2025-12-22, 7d\n    section 최적화\n    UI/UX + Vite + Express 5    :done, 2026-01-01, 11d\n    section v2.x\n    Hooks 리팩토링 + 통계       :done, 2026-01-14, 3d\n    CI/CD + Swagger 문서화      :done, 2026-01-21, 1d\n    TypeScript + Redis 캐싱     :done, 2026-01-28, 5d\n    section v3.x\n    E2E + 부하 테스트           :done, 2026-02-05, 2d\n    70% 커버리지 달성           :done, 2026-02-06, 1d\n    Prometheus + Grafana        :done, 2026-02-07, 1d\n    section v4.0\n    Full TypeScript Migration   :done, 2026-02-07, 1d\n    컴포넌트 분리              :done, 2026-02-07, 1d\n    레거시 문서 정리            :done, 2026-02-07, 1d\n    section v4.2\n    검색 + 신고 + 혼잡도 API    :done, 2026-02-08, 2d\n    푸시 알림 운영 안정화        :done, 2026-02-10, 2d\n    Docker 배포 최적화          :done, 2026-02-12, 1d\n    section v4.3\n    백엔드 커버리지 80%+ 달성   :done, 2026-02-20, 1d\n    프론트엔드 커버리지 80%+ 달성 :done, 2026-02-20, 1d\n    E2E 시나리오 확장           :done, 2026-02-20, 1d\n    OWASP Top 10 보안 강화      :done, 2026-02-20, 1d\n    성능 최적화 + 접근성        :done, 2026-02-20, 1d\n    section v4.4\n    Apps in Toss 광고 통합      :done, 2026-02-24, 1d\n    토스 WebView UX 적응        :done, 2026-02-24, 1d\n    .ait 빌드 + 배포 설정       :done, 2026-02-24, 1d\n    section v4.5 (Phase 7-8)\n    모놀리식 분해 + 코드 정리   :done, 2026-02-25, 1d\n    API 응답 포맷 통합          :done, 2026-02-26, 1d\n    Redis rate-limit + 캐시     :done, 2026-02-26, 1d\n    번들 최적화 + a11y          :done, 2026-02-26, 1d\n    bcrypt + DB SSL + Sentry    :done, 2026-02-26, 1d\n    section v4.6 (Phase 9)\n    운영시간 전체 차단 + 대기실  :done, 2026-02-27, 1d\n    잡지식/퀴즈 탭 구현         :done, 2026-02-27, 1d\n    Request ID + Prometheus     :done, 2026-02-28, 1d\n    CI 파이프라인 최적화         :done, 2026-02-28, 1d\n```\n\n---\n\n## 운영 스크립트\n\n| 스크립트 | 설명 |\n|---------|------|\n| `release_gate.sh` | 릴리스 게이트 검증 |\n| `web_vitals_baseline.sh` | Web Vitals 베이스라인 측정 |\n| `slo_budget_gate.sh` | SLO 예산 게이트 |\n\n---\n\n## 문서\n\n| 문서 | 설명 |\n|------|------|\n| [CHANGELOG.md](CHANGELOG.md) | 전체 버전 히스토리 |\n| [HANDOFF.md](HANDOFF.md) | 프로덕션 품질 업그레이드 + Apps in Toss 배포 상세 |\n| [관리자 대시보드 가이드](docs/ADMIN_DASHBOARD_GUIDE.md) | 대시보드 사용법 + SQL 쿼리 모음 |\n| [E2E Testing Guide](docs/E2E_TESTING.md) | Playwright E2E 테스트 |\n| [Load Testing Guide](docs/LOAD_TESTING.md) | k6 부하 테스트 |\n| [Load Testing Results](docs/LOAD_TESTING_RESULTS.md) | 부하 테스트 상세 결과 |\n| [Web Vitals Runbook](docs/WEB_VITALS_BASELINE_RUNBOOK.md) | Web Vitals 베이스라인 런북 |\n\n---\n\n## License\n\nMIT License - 자유롭게 사용, 수정, 배포 가능합니다.\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\n*이 프로젝트는 개인 포트폴리오 목적으로 제작되었으며, 실제 지하철 운영 주체와는 무관합니다.*\n\n**Made with care by a developer who also hates Monday mornings**\n\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoublesilver%2Fsubway-board","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdoublesilver%2Fsubway-board","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoublesilver%2Fsubway-board/lists"}