iPadOS 13+ sends a desktop Safari UA by default, which fools CodeMirror's mobile detection into using the more fragile textarea input mode. That, combined with CodeMirror repainting the line mid-composition, breaks Korean IME composition and leaves jamo unmerged. inputStyle can't be changed after the editor is created, so it must be set in the EasyMDE constructor options. Co-Authored-By: Claude Sonnet 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)
- 다듬기 & 상시 실행 설정 (현재 단계)