Google OAuth branding review rejected the app twice with "no description of the app purpose on the homepage" and "app name does not match the homepage", even though both were present in Korean. Two likely causes, both addressed: - The h1 wrapped an alt="" logo image before the text, so the app name could not be extracted from it. The image now sits outside the h1, leaving the heading as plain text. - The page was Korean-only. The app name is now "해빗랩 (HabitLab)" (matching the console exactly), and the title, meta description and a new About section carry an English description of the app purpose and its use of Google account data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
해빗랩
개인용 습관 관리 PWA. 아이폰과 PC에서 같은 서버(MariaDB)에 접속해 습관을 관리합니다.
참고: 습관이 실제로 어떻게 형성되는지에 대한 연구 정리는 habit-formation-research.md 참고.
설치
conda activate py_web
pip install -e .
환경 설정
.env.example을 복사해.env로 저장합니다.DATABASE_URL에 원격 MariaDB 접속 정보를 입력합니다 (mysql+pymysql://user:password@host:3306/habit_tracker).SECRET_KEY를 임의의 무작위 문자열로 채웁니다 (python -c "import secrets; print(secrets.token_hex(32))").- 구글 로그인을 설정합니다: Google Cloud Console에서 "OAuth 클라이언트 ID"(웹 애플리케이션)를 만들고, "승인된 리디렉션 URI"에
GOOGLE_REDIRECT_URI와 정확히 같은 값(로컬은http://localhost:8000/auth/google/callback)을 등록한 뒤 발급된 클라이언트 ID/시크릿을.env의GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET에 붙여넣습니다. - 알림(Web Push)을 쓰려면
python scripts/generate_vapid_keys.py실행 후 출력된VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY를.env에 붙여넣습니다.
DB 마이그레이션
alembic upgrade head
기존 데이터 이관 (PIN 로그인 시절 데이터가 있는 경우)
구글 로그인 도입 전 PIN으로 쓰던 습관 데이터는 마이그레이션 후에도 소유자가 없는 상태로 남아있습니다. 아래 순서로 한 번만 연결해주면 됩니다.
- 마이그레이션을 적용하고 서버를 띄운 뒤, 구글 계정으로 한 번 로그인합니다(계정이 자동 생성됩니다).
python scripts/claim_orphan_habits.py <로그인한 이메일>을 실행합니다 — 소유자 없는 습관을 전부 그 계정에 연결합니다.
실행
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
PC 브라우저에서 http://localhost:8000, 아이폰에서는 같은 Wi-Fi 내에서 http://<PC의 로컬 IP>:8000으로 접속합니다.
아이폰에서는 Safari로 접속한 뒤 공유 버튼 → "홈 화면에 추가"를 해야 앱처럼(standalone) 실행되고, 푸시 알림도 이 상태에서만 받을 수 있습니다. 홈 화면에 추가한 뒤 앱을 열어서 "오늘" 화면 상단의 "알림 켜기" 버튼을 눌러 알림 권한을 허용해야 리마인더를 받을 수 있습니다.
주의: 서비스워커(오프라인 캐싱)와 Web Push는 브라우저 보안 정책상 HTTPS 또는 localhost에서만 동작합니다. http://<PC의 로컬 IP>:8000처럼 평문 HTTP로 접속하면 습관 체크/조회는 문제없지만 아이폰에서 오프라인 캐싱과 푸시 알림은 동작하지 않습니다.
- 집 PC를 직접 서버로 쓴다면 → 아래 "HTTPS로 접속하기 (Tailscale)" 참고.
- 이미 앞단에 리버스 프록시(nginx 등)가 있는 서버에 배포한다면 → Tailscale은 필요 없습니다. 프록시가 도메인에 대한 TLS를 처리하고 내부적으로 이 앱의 8000번 포트로 평문 HTTP 프록시하면 됩니다 — 브라우저 입장에서는 프록시가 내준 도메인이
https://이기만 하면 서비스워커/푸시가 정상 동작합니다. Docker 배포 시 참고사항은 아래 "Docker로 배포하기" 절에 있습니다.
HTTPS로 접속하기 (Tailscale, 집 PC를 직접 서버로 쓰는 경우)
리버스 프록시 없이 집 PC를 그대로 서버로 쓴다면, Tailscale로 별도 인증서 관리 없이 신뢰된 HTTPS를 무료로 얻을 수 있습니다. (앞단에 리버스 프록시가 이미 있다면 이 섹션은 건너뛰세요.)
- 집 PC와 아이폰 모두에 Tailscale 앱을 설치하고 같은 계정으로 로그인합니다 (같은 tailnet에 연결됨).
- 집 PC에서 서버가 실행 중인 상태(포트 8000)에서 아래 명령을 실행합니다:
tailscale serve --bg 8000 - 발급된 주소를 확인합니다:
tailscale serve statushttps://<PC-이름>.<tailnet-이름>.ts.net형태의 주소가 표시됩니다. - 아이폰에서 Tailscale 앱을 켠 상태로 Safari에서 위 주소로 접속합니다. 인증서 경고 없이 정상적으로 HTTPS 연결이 됩니다.
- 이 상태로 "홈 화면에 추가" 후 "알림 켜기"를 누르면 서비스워커와 푸시가 모두 정상 동작합니다.
tailscale serve 설정은 재부팅 후에도 유지되므로 최초 1회만 실행하면 됩니다. 외부(인터넷)에는 노출되지 않고 같은 tailnet에 연결된 기기끼리만 접속할 수 있습니다.
상시 실행 (Windows)
PC를 켤 때마다 수동으로 서버를 실행하지 않으려면 Windows 작업 스케줄러에 등록합니다. scripts/run_server.ps1이 conda 환경의 python.exe를 직접 호출해 --reload 없이(운영용) 서버를 실행하고 로그를 logs/에 남깁니다.
schtasks /create /tn "HabitTrackerServer" /tr "powershell.exe -ExecutionPolicy Bypass -File \"C:\workspace\habit-tracker\scripts\run_server.ps1\"" /sc onlogon /rl highest /f
등록 후 로그온 시 자동으로 서버가 시작됩니다. 확인/삭제는 다음 명령으로 합니다:
schtasks /query /tn "HabitTrackerServer"
schtasks /delete /tn "HabitTrackerServer" /f
Docker로 배포하기
Docker가 설치된 서버라면 scripts/run_server.ps1 + 작업 스케줄러 조합 대신 Docker의 재시작 정책으로 상시 실행을 훨씬 간단하게 처리할 수 있습니다.
- 배포 서버에 저장소를 올리고,
.env.example을 복사해.env로 저장한 뒤 값을 채웁니다 (DATABASE_URL,SECRET_KEY,GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET/GOOGLE_REDIRECT_URI,VAPID_*— 로컬 설치 때와 동일하게 "환경 설정" 절 참고).GOOGLE_REDIRECT_URI는 배포 서버가 실제로 응답하는 도메인 기준으로 채우고 Google Cloud Console에도 동일하게 등록해야 합니다..env는 이미지에 포함되지 않고 컨테이너 실행 시점에 주입되므로,.env안의 값은 배포 서버 기준으로 채워야 합니다 — 특히DATABASE_URL이 원격 MariaDB를 가리킨다면 배포 서버에서 그 주소로 접속 가능한지 먼저 확인하세요. - 빌드 후 실행합니다:
내부적으로 컨테이너가 시작될 때마다
docker compose up -d --buildalembic upgrade head를 먼저 실행한 뒤uvicorn을 띄웁니다(scripts/docker-entrypoint.sh) — 이미 적용된 마이그레이션은 건너뛰므로 재시작할 때마다 실행돼도 안전합니다. docker-compose.yml의restart: unless-stopped가 서버 재부팅/컨테이너 크래시 시 자동 재시작을 담당합니다 — Windows 작업 스케줄러 등록이 더 이상 필요 없습니다.- 로그 확인:
docker compose logs -f
주의:
- 이 앱은 알림 스케줄러(APScheduler)를 프로세스 안에서 직접 돌립니다(
app/services/scheduler_service.py) — 반드시 컨테이너를 1개만 실행하세요. 여러 개(replica)를 띄우면 각자 스케줄러가 따로 돌아서 같은 알림을 중복 시도하게 됩니다(경합 자체는_claim_notification_slot이 방어하지만, 굳이 여러 개 띄울 이유가 없습니다). - 이미 리버스 프록시가 있는 서버라면 Tailscale은 필요 없습니다. 프록시가 도메인을 HTTPS로 받아서 컨테이너의 8000번 포트로 평문 HTTP 프록시하도록 설정하면 됩니다(예: nginx
proxy_pass http://127.0.0.1:8000;). 이 앱은 별도 설정 없이 8000번에서 평문 HTTP로만 응답하므로 그대로 붙이면 됩니다.docker-compose.yml의ports: ["8000:8000"]는 모든 인터페이스(0.0.0.0)에 노출하는데, 프록시가 같은 호스트에서 돈다면"127.0.0.1:8000:8000"으로 바꿔서 컨테이너가 프록시를 거치지 않고 외부에 직접 노출되지 않게 하는 걸 권장합니다.
아이콘 재생성
pip install -e ".[dev]"
python scripts/generate_icons.py
테스트
pytest는 dev 의존성이라 기본 설치(pip install -e .)에는 포함되지 않습니다. 먼저 아래 명령으로 설치합니다.
pip install -e ".[dev]"
이후 테스트는 원격 MariaDB가 아니라 임시 SQLite 인메모리 DB로 실행되므로(tests/conftest.py), .env의 DATABASE_URL이나 실제 DB 접속 여부와 무관하게 돌아갑니다.
pytest # 전체 테스트
pytest tests/test_habit_service.py # 파일 단위
pytest tests/test_log_service.py::test_streak_breaks_on_past_miss # 단일 테스트
개발 단계
- ✅ 기반 셋업 + 습관 CRUD
- ✅ 데일리 체크 & 오늘 화면
- ✅ 기록 확인 화면 (월별/주별)
- ✅ PWA 기본 (manifest, 서비스워커, 아이콘, iOS 홈 화면 추가 안내)
- ✅ Web Push 알림 (VAPID, 구독 흐름, APScheduler 매분 tick)
- 다듬기 & 상시 실행 설정 (현재 단계)