prepare app for public app store distribution

- rebrand from 습관 트래커 to 해빗랩 across templates, manifest, service worker
- add HTTPS redirect middleware for reverse-proxied deployments
- add public landing page (/) and privacy policy page with real data
  handling disclosures
- add in-app account deletion (Apple review requirement)
- add Android TWA Digital Asset Links support (/.well-known/assetlinks.json)
- add SFTP deployment script for the Synology-hosted server

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:41:50 +09:00
co-authored by Claude Sonnet 5
parent 34a128a79b
commit ef2a3ea082
26 changed files with 534 additions and 25 deletions
+48 -4
View File
@@ -8,12 +8,13 @@ from fastapi.templating import Jinja2Templates
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.config import settings
from app.database import get_db
from app.models.habit import HabitDifficulty, HabitStatus, HabitType
from app.models.user import User
from app.schemas.habit import HabitCreate, HabitUpdate
from app.security import get_current_user_optional
from app.services import habit_service, log_service
from app.services import habit_service, log_service, user_service
from app.template_utils import (
difficulty_label,
goal_progress,
@@ -46,14 +47,57 @@ def _current_user_or_redirect(request: Request, db: Session) -> User | RedirectR
def index(request: Request, db: Session = Depends(get_db)):
if get_current_user_optional(request, db) is not None:
return RedirectResponse(url="/today", status_code=303)
return RedirectResponse(url="/login", status_code=303)
# 로그인 없이도 앱 목적을 설명하는 페이지가 있어야 한다(구글 OAuth 동의 화면 "홈페이지" 요건).
return templates.TemplateResponse(request, "home.html", {"logged_in": False, "current_user": None})
@router.get("/login")
def login_page(request: Request, db: Session = Depends(get_db)):
def login_page(request: Request, deleted: bool = False, db: Session = Depends(get_db)):
if get_current_user_optional(request, db) is not None:
return RedirectResponse(url="/today", status_code=303)
return templates.TemplateResponse(request, "login.html", {"logged_in": False, "current_user": None})
return templates.TemplateResponse(
request, "login.html", {"logged_in": False, "current_user": None, "deleted": deleted}
)
@router.get("/privacy")
def privacy_page(request: Request, db: Session = Depends(get_db)):
user = get_current_user_optional(request, db)
return templates.TemplateResponse(
request, "privacy.html", {"logged_in": user is not None, "current_user": user}
)
@router.get("/account")
def account_page(request: Request, db: Session = Depends(get_db)):
current = _current_user_or_redirect(request, db)
if isinstance(current, RedirectResponse):
return current
return templates.TemplateResponse(request, "account.html", {"logged_in": True, "current_user": current})
@router.post("/account/delete")
def delete_account_page(request: Request, confirm_email: str = Form(""), db: Session = Depends(get_db)):
current = _current_user_or_redirect(request, db)
if isinstance(current, RedirectResponse):
return current
if confirm_email.strip().lower() != current.email.lower():
return templates.TemplateResponse(
request,
"account.html",
{
"logged_in": True,
"current_user": current,
"delete_error": "입력한 이메일이 계정 이메일과 일치하지 않습니다.",
},
status_code=400,
)
user_service.delete_account(db, current)
response = RedirectResponse(url="/login?deleted=1", status_code=303)
response.delete_cookie(settings.session_cookie_name)
return response
def _today_context(db: Session, user_id: int, **extra) -> dict:
+1 -1
View File
@@ -42,5 +42,5 @@ def unsubscribe(
@router.post("/test")
def send_test(db: Session = Depends(get_db), current_user: User = Depends(require_login)):
sent = push_service.send_to_user(db, current_user.id, title="습관 트래커", body="테스트 알림입니다.")
sent = push_service.send_to_user(db, current_user.id, title="해빗랩", body="테스트 알림입니다.")
return {"sent": sent}