Files
habit-tracker/README.md
T
shinalokandClaude Sonnet 5 d212451fe0 add habit goal-period difficulty and habit formation research notes
Ground the 21-day habit myth in actual research (Lally et al. 2010) and let
users pick a target period (21/66/254 days or unlimited) matching that
study's easy/median/hard automaticity timelines when creating a habit,
with progress shown on the habits list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 10:12:17 +09:00

132 lines
9.1 KiB
Markdown

# 습관 트래커
개인용 습관 관리 PWA. 아이폰과 PC에서 같은 서버(MariaDB)에 접속해 습관을 관리합니다.
참고: 습관이 실제로 어떻게 형성되는지에 대한 연구 정리는 [habit-formation-research.md](./habit-formation-research.md) 참고.
## 설치
```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://<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://tailscale.com)로 별도 인증서 관리 없이 신뢰된 HTTPS를 무료로 얻을 수 있습니다. (앞단에 리버스 프록시가 이미 있다면 이 섹션은 건너뛰세요.)
1. 집 PC와 아이폰 모두에 Tailscale 앱을 설치하고 같은 계정으로 로그인합니다 (같은 tailnet에 연결됨).
2. 집 PC에서 서버가 실행 중인 상태(포트 8000)에서 아래 명령을 실행합니다:
```powershell
tailscale serve --bg 8000
```
3. 발급된 주소를 확인합니다:
```powershell
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/`에 남깁니다.
```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. 다듬기 & 상시 실행 설정 (현재 단계)