shinalok bf0225d53b
CI/CD / test (push) Successful in 40s
CI/CD / deploy (push) Successful in 1m57s
add SSH debug connection test to CI/CD workflow and enable batch mode for rsync
2026-08-08 07:25:51 +09:00

해빗랩

개인용 습관 관리 PWA. 아이폰과 PC에서 같은 서버(MariaDB)에 접속해 습관을 관리합니다.

참고: 습관이 실제로 어떻게 형성되는지에 대한 연구 정리는 habit-formation-research.md 참고.

설치

conda activate py_web
pip install -e .

환경 설정

  1. .env.example을 복사해 .env로 저장합니다.
  2. DATABASE_URL에 원격 MariaDB 접속 정보를 입력합니다 (mysql+pymysql://user:password@host:3306/habit_tracker).
  3. SECRET_KEY를 임의의 무작위 문자열로 채웁니다 (python -c "import secrets; print(secrets.token_hex(32))").
  4. 구글 로그인을 설정합니다: Google Cloud Console에서 "OAuth 클라이언트 ID"(웹 애플리케이션)를 만들고, "승인된 리디렉션 URI"에 GOOGLE_REDIRECT_URI와 정확히 같은 값(로컬은 http://localhost:8000/auth/google/callback)을 등록한 뒤 발급된 클라이언트 ID/시크릿을 .envGOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET에 붙여넣습니다.
  5. 알림(Web Push)을 쓰려면 python scripts/generate_vapid_keys.py 실행 후 출력된 VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY.env에 붙여넣습니다.

DB 마이그레이션

alembic upgrade head

기존 데이터 이관 (PIN 로그인 시절 데이터가 있는 경우)

구글 로그인 도입 전 PIN으로 쓰던 습관 데이터는 마이그레이션 후에도 소유자가 없는 상태로 남아있습니다. 아래 순서로 한 번만 연결해주면 됩니다.

  1. 마이그레이션을 적용하고 서버를 띄운 뒤, 구글 계정으로 한 번 로그인합니다(계정이 자동 생성됩니다).
  2. 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를 무료로 얻을 수 있습니다. (앞단에 리버스 프록시가 이미 있다면 이 섹션은 건너뛰세요.)

  1. 집 PC와 아이폰 모두에 Tailscale 앱을 설치하고 같은 계정으로 로그인합니다 (같은 tailnet에 연결됨).
  2. 집 PC에서 서버가 실행 중인 상태(포트 8000)에서 아래 명령을 실행합니다:
    tailscale serve --bg 8000
    
  3. 발급된 주소를 확인합니다:
    tailscale serve status
    
    https://<PC-이름>.<tailnet-이름>.ts.net 형태의 주소가 표시됩니다.
  4. 아이폰에서 Tailscale 앱을 켠 상태로 Safari에서 위 주소로 접속합니다. 인증서 경고 없이 정상적으로 HTTPS 연결이 됩니다.
  5. 이 상태로 "홈 화면에 추가" 후 "알림 켜기"를 누르면 서비스워커와 푸시가 모두 정상 동작합니다.

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의 재시작 정책으로 상시 실행을 훨씬 간단하게 처리할 수 있습니다.

  1. 배포 서버에 저장소를 올리고, .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를 가리킨다면 배포 서버에서 그 주소로 접속 가능한지 먼저 확인하세요.
  2. 빌드 후 실행합니다:
    docker compose up -d --build
    
    내부적으로 컨테이너가 시작될 때마다 alembic upgrade head를 먼저 실행한 뒤 uvicorn을 띄웁니다(scripts/docker-entrypoint.sh) — 이미 적용된 마이그레이션은 건너뛰므로 재시작할 때마다 실행돼도 안전합니다.
  3. docker-compose.ymlrestart: unless-stopped가 서버 재부팅/컨테이너 크래시 시 자동 재시작을 담당합니다 — Windows 작업 스케줄러 등록이 더 이상 필요 없습니다.
  4. 로그 확인: 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.ymlports: ["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), .envDATABASE_URL이나 실제 DB 접속 여부와 무관하게 돌아갑니다.

pytest                                   # 전체 테스트
pytest tests/test_habit_service.py       # 파일 단위
pytest tests/test_log_service.py::test_streak_breaks_on_past_miss   # 단일 테스트

개발 단계

  1. 기반 셋업 + 습관 CRUD
  2. 데일리 체크 & 오늘 화면
  3. 기록 확인 화면 (월별/주별)
  4. PWA 기본 (manifest, 서비스워커, 아이콘, iOS 홈 화면 추가 안내)
  5. Web Push 알림 (VAPID, 구독 흐름, APScheduler 매분 tick)
  6. 다듬기 & 상시 실행 설정 (현재 단계)
S
Description
No description provided
Readme
446 KiB
Languages
Python 68.8%
HTML 17.3%
CSS 7.3%
JavaScript 5.9%
PowerShell 0.3%
Other 0.3%