# 습관 트래커 개인용 습관 관리 PWA. 아이폰과 PC에서 같은 서버(MariaDB)에 접속해 습관을 관리합니다. ## 설치 ```bash 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](https://console.cloud.google.com/apis/credentials)에서 "OAuth 클라이언트 ID"(웹 애플리케이션)를 만들고, "승인된 리디렉션 URI"에 `GOOGLE_REDIRECT_URI`와 정확히 같은 값(로컬은 `http://localhost:8000/auth/google/callback`)을 등록한 뒤 발급된 클라이언트 ID/시크릿을 `.env`의 `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET`에 붙여넣습니다. 5. 알림(Web Push)을 쓰려면 `python scripts/generate_vapid_keys.py` 실행 후 출력된 `VAPID_PUBLIC_KEY`/`VAPID_PRIVATE_KEY`를 `.env`에 붙여넣습니다. ## DB 마이그레이션 ```bash alembic upgrade head ``` ### 기존 데이터 이관 (PIN 로그인 시절 데이터가 있는 경우) 구글 로그인 도입 전 PIN으로 쓰던 습관 데이터는 마이그레이션 후에도 소유자가 없는 상태로 남아있습니다. 아래 순서로 한 번만 연결해주면 됩니다. 1. 마이그레이션을 적용하고 서버를 띄운 뒤, 구글 계정으로 한 번 로그인합니다(계정이 자동 생성됩니다). 2. `python scripts/claim_orphan_habits.py <로그인한 이메일>`을 실행합니다 — 소유자 없는 습관을 전부 그 계정에 연결합니다. ## 실행 ```bash uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 ``` PC 브라우저에서 `http://localhost:8000`, 아이폰에서는 같은 Wi-Fi 내에서 `http://:8000`으로 접속합니다. 아이폰에서는 Safari로 접속한 뒤 공유 버튼 → "홈 화면에 추가"를 해야 앱처럼(standalone) 실행되고, 푸시 알림도 이 상태에서만 받을 수 있습니다. 홈 화면에 추가한 뒤 앱을 열어서 "오늘" 화면 상단의 "알림 켜기" 버튼을 눌러 알림 권한을 허용해야 리마인더를 받을 수 있습니다. **주의**: 서비스워커(오프라인 캐싱)와 Web Push는 브라우저 보안 정책상 HTTPS 또는 `localhost`에서만 동작합니다. `http://:8000`처럼 평문 HTTP로 접속하면 습관 체크/조회는 문제없지만 아이폰에서 오프라인 캐싱과 푸시 알림은 동작하지 않습니다. - **집 PC를 직접 서버로 쓴다면** → 아래 "HTTPS로 접속하기 (Tailscale)" 참고. - **이미 앞단에 리버스 프록시(nginx 등)가 있는 서버에 배포한다면** → Tailscale은 필요 없습니다. 프록시가 도메인에 대한 TLS를 처리하고 내부적으로 이 앱의 8000번 포트로 평문 HTTP 프록시하면 됩니다 — 브라우저 입장에서는 프록시가 내준 도메인이 `https://`이기만 하면 서비스워커/푸시가 정상 동작합니다. Docker 배포 시 참고사항은 아래 "Docker로 배포하기" 절에 있습니다. ## HTTPS로 접속하기 (Tailscale, 집 PC를 직접 서버로 쓰는 경우) 리버스 프록시 없이 집 PC를 그대로 서버로 쓴다면, [Tailscale](https://tailscale.com)로 별도 인증서 관리 없이 신뢰된 HTTPS를 무료로 얻을 수 있습니다. (앞단에 리버스 프록시가 이미 있다면 이 섹션은 건너뛰세요.) 1. 집 PC와 아이폰 모두에 Tailscale 앱을 설치하고 같은 계정으로 로그인합니다 (같은 tailnet에 연결됨). 2. 집 PC에서 서버가 실행 중인 상태(포트 8000)에서 아래 명령을 실행합니다: ```powershell tailscale serve --bg 8000 ``` 3. 발급된 주소를 확인합니다: ```powershell tailscale serve status ``` `https://..ts.net` 형태의 주소가 표시됩니다. 4. 아이폰에서 Tailscale 앱을 켠 상태로 Safari에서 위 주소로 접속합니다. 인증서 경고 없이 정상적으로 HTTPS 연결이 됩니다. 5. 이 상태로 "홈 화면에 추가" 후 "알림 켜기"를 누르면 서비스워커와 푸시가 모두 정상 동작합니다. `tailscale serve` 설정은 재부팅 후에도 유지되므로 최초 1회만 실행하면 됩니다. 외부(인터넷)에는 노출되지 않고 같은 tailnet에 연결된 기기끼리만 접속할 수 있습니다. ## 상시 실행 (Windows) PC를 켤 때마다 수동으로 서버를 실행하지 않으려면 Windows 작업 스케줄러에 등록합니다. `scripts/run_server.ps1`이 conda 환경의 `python.exe`를 직접 호출해 `--reload` 없이(운영용) 서버를 실행하고 로그를 `logs/`에 남깁니다. ```powershell schtasks /create /tn "HabitTrackerServer" /tr "powershell.exe -ExecutionPolicy Bypass -File \"C:\workspace\habit-tracker\scripts\run_server.ps1\"" /sc onlogon /rl highest /f ``` 등록 후 로그온 시 자동으로 서버가 시작됩니다. 확인/삭제는 다음 명령으로 합니다: ```powershell 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. 빌드 후 실행합니다: ```bash docker compose up -d --build ``` 내부적으로 컨테이너가 시작될 때마다 `alembic upgrade head`를 먼저 실행한 뒤 `uvicorn`을 띄웁니다(`scripts/docker-entrypoint.sh`) — 이미 적용된 마이그레이션은 건너뛰므로 재시작할 때마다 실행돼도 안전합니다. 3. `docker-compose.yml`의 `restart: 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.yml`의 `ports: ["8000:8000"]`는 모든 인터페이스(`0.0.0.0`)에 노출하는데, 프록시가 같은 호스트에서 돈다면 `"127.0.0.1:8000:8000"`으로 바꿔서 컨테이너가 프록시를 거치지 않고 외부에 직접 노출되지 않게 하는 걸 권장합니다. ## 아이콘 재생성 ```bash pip install -e ".[dev]" python scripts/generate_icons.py ``` ## 테스트 `pytest`는 dev 의존성이라 기본 설치(`pip install -e .`)에는 포함되지 않습니다. 먼저 아래 명령으로 설치합니다. ```bash pip install -e ".[dev]" ``` 이후 테스트는 원격 MariaDB가 아니라 임시 SQLite 인메모리 DB로 실행되므로(`tests/conftest.py`), `.env`의 `DATABASE_URL`이나 실제 DB 접속 여부와 무관하게 돌아갑니다. ```bash 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. 다듬기 & 상시 실행 설정 (현재 단계)