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
+18 -2
View File
@@ -2,7 +2,7 @@ import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, JSONResponse
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.sessions import SessionMiddleware
@@ -24,12 +24,22 @@ async def lifespan(app: FastAPI):
scheduler_service.shutdown_scheduler()
app = FastAPI(title="습관 트래커", lifespan=lifespan)
app = FastAPI(title="해빗랩", lifespan=lifespan)
# 구글 OAuth 핸드셰이크 중 state/nonce를 임시로 저장하는 데만 쓰는 세션이다.
# 로그인 유지용 쿠키(habit_session)와는 별개.
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key, session_cookie="oauth_session", max_age=600)
@app.middleware("http")
async def redirect_http_to_https(request: Request, call_next):
# 리버스 프록시가 X-Forwarded-Proto로 원래 스킴을 알려줄 때만 동작한다
# (로컬 uvicorn 직접 실행 시에는 이 헤더가 없어 그냥 통과).
if request.headers.get("x-forwarded-proto") == "http":
return RedirectResponse(str(request.url.replace(scheme="https")), status_code=301)
return await call_next(request)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
app.include_router(auth.router)
@@ -75,3 +85,9 @@ async def unhandled_exception_handler(request: Request, exc: Exception):
def service_worker():
# 서비스워커 scope가 앱 전체를 커버하려면 /static/ 하위가 아닌 루트 경로로 서빙해야 한다.
return FileResponse("app/static/service-worker.js", media_type="application/javascript")
@app.get("/.well-known/assetlinks.json")
def assetlinks():
# Android TWA(kr.co.alphalok.habit.twa)의 도메인 소유권 증명용 — 반드시 /.well-known/ 루트 경로여야 한다.
return FileResponse("app/static/.well-known/assetlinks.json", media_type="application/json")
+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}
+14
View File
@@ -0,0 +1,14 @@
from sqlalchemy.orm import Session
from app.models.user import User
def delete_account(db: Session, user: User) -> None:
"""계정과 그에 딸린 모든 데이터를 삭제한다.
Habit/PushSubscription/HabitLog/HabitNotificationLog/SummaryNotificationLog는 전부
user.id 기준 DB 레벨 ON DELETE CASCADE로 연결돼 있어(마이그레이션 0004/0005 참고),
User 행만 지우면 나머지는 MariaDB가 알아서 정리한다.
"""
db.delete(user)
db.commit()
+8
View File
@@ -0,0 +1,8 @@
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "kr.co.alphalok.habit.twa",
"sha256_cert_fingerprints": ["2A:BC:19:7D:91:65:3A:88:68:02:8C:3B:1D:58:42:C7:17:02:FA:B9:C9:4F:01:02:3D:60:B7:C9:CE:6F:05:20"]
}
}]
+161
View File
@@ -111,6 +111,16 @@ p {
color: var(--color-text-muted);
}
.app-footer {
text-align: center;
padding: var(--space-4) 0 var(--space-2);
font-size: 13px;
}
.app-footer a {
color: var(--color-text-muted);
}
/* iOS 홈 화면 추가 안내 배너 */
.ios-install-banner {
display: none;
@@ -799,3 +809,154 @@ label {
.wm-dash {
color: var(--color-border);
}
/* 랜딩 페이지 (/) */
.landing {
max-width: 480px;
margin: 0 auto;
padding: var(--space-4) var(--space-2) var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.landing-hero {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
text-align: center;
}
.landing-brand {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 700;
color: var(--color-accent);
letter-spacing: -0.01em;
}
.landing-brand img {
width: 24px;
height: 24px;
border-radius: 6px;
}
.landing-hero h1 {
font-size: 32px;
font-weight: 700;
letter-spacing: -0.02em;
text-wrap: balance;
margin: 0;
}
.landing-tagline {
font-size: 16px;
color: var(--color-text-muted);
margin: 0;
}
/* 요일 스트릭 시연: 실제 습관 체크 인터랙션을 그대로 축소해 보여준다 */
.landing-streak {
display: flex;
gap: 8px;
}
.landing-day {
width: 34px;
height: 34px;
border-radius: 999px;
border: 1px solid var(--color-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: var(--color-text-muted);
background: var(--color-surface);
}
.landing-day.done {
background: var(--color-accent);
border-color: var(--color-accent);
color: #fff;
opacity: 0;
animation: landing-day-in 0.35s ease-out forwards;
}
.landing-day.done svg {
width: 16px;
height: 16px;
}
.landing-day:nth-of-type(1).done { animation-delay: 0.1s; }
.landing-day:nth-of-type(2).done { animation-delay: 0.25s; }
.landing-day:nth-of-type(3).done { animation-delay: 0.4s; }
.landing-day:nth-of-type(4).done { animation-delay: 0.55s; }
.landing-day:nth-of-type(5).done { animation-delay: 0.7s; }
@keyframes landing-day-in {
from {
opacity: 0;
transform: scale(0.6);
}
to {
opacity: 1;
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.landing-day.done {
opacity: 1;
animation: none;
}
}
.landing-copy {
font-size: 15.5px;
line-height: 1.75;
color: var(--color-text);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.landing-copy strong {
color: var(--color-accent);
}
.landing-features {
display: grid;
gap: var(--space-2);
}
.landing-feature {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-card);
}
.landing-feature svg {
flex-shrink: 0;
width: 20px;
height: 20px;
color: var(--color-accent);
}
.landing-feature p {
margin: 0;
font-size: 14px;
color: var(--color-text);
}
.landing-feature p strong {
display: block;
font-size: 14.5px;
margin-bottom: 2px;
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "습관 트래커",
"short_name": "습관 트래커",
"name": "해빗랩",
"short_name": "해빗랩",
"description": "형성하고 싶은 습관과 끊고 싶은 습관을 요일별로 관리하고 매일 체크하는 개인용 습관 관리 앱",
"start_url": "/today",
"scope": "/",
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>오프라인 - 습관 트래커</title>
<title>오프라인 - 해빗랩</title>
<link rel="icon" href="/static/icons/icon-192.png" />
<link rel="stylesheet" href="/static/css/style.css" />
</head>
+1 -1
View File
@@ -78,7 +78,7 @@ self.addEventListener("push", (event) => {
if (!event.data) return;
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title || "습관 트래커", {
self.registration.showNotification(data.title || "해빗랩", {
body: data.body || "",
icon: "/static/icons/icon-192.png",
badge: "/static/icons/icon-192.png",
+1 -1
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}페이지를 찾을 수 없어요 · 습관 트래커{% endblock %}
{% block title %}페이지를 찾을 수 없어요 · 해빗랩{% endblock %}
{% block content %}
<div style="min-height: 60vh; display:flex; align-items:center; justify-content:center; text-align:center;">
<div>
+1 -1
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}오류가 발생했어요 · 습관 트래커{% endblock %}
{% block title %}오류가 발생했어요 · 해빗랩{% endblock %}
{% block content %}
<div style="min-height: 60vh; display:flex; align-items:center; justify-content:center; text-align:center;">
<div>
+24
View File
@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}계정 · 해빗랩{% endblock %}
{% block content %}
<h1>계정</h1>
<div class="card">
<p><strong>이메일</strong><br />{{ current_user.email }}</p>
{% if current_user.name %}<p><strong>이름</strong><br />{{ current_user.name }}</p>{% endif %}
<p><strong>가입일</strong><br />{{ current_user.created_at.strftime('%Y-%m-%d') }}</p>
</div>
<div class="card">
<h2>계정 삭제</h2>
<p>
계정을 삭제하면 등록한 모든 습관, 체크 기록, 알림 구독 정보가 즉시 영구적으로 삭제되며 되돌릴 수 없습니다.
</p>
{% if delete_error %}<p style="color: var(--color-danger);">{{ delete_error }}</p>{% endif %}
<form method="post" action="/account/delete">
<label for="confirm_email">확인을 위해 본인 이메일({{ current_user.email }})을 입력하세요</label>
<input type="text" id="confirm_email" name="confirm_email" autocomplete="off" />
<button type="submit" class="btn btn-danger-ghost btn-block" style="margin-top: var(--space-2);">계정 영구 삭제</button>
</form>
</div>
{% endblock %}
+7 -4
View File
@@ -3,14 +3,14 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>{% block title %}습관 트래커{% endblock %}</title>
<title>{% block title %}해빗랩{% endblock %}</title>
<link rel="manifest" href="/static/manifest.json" />
<meta name="theme-color" content="#d97757" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#e08962" media="(prefers-color-scheme: dark)" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="습관 트래커" />
<meta name="apple-mobile-web-app-title" content="해빗랩" />
<link rel="apple-touch-icon" sizes="180x180" href="/static/icons/icon-apple-180.png" />
<link rel="apple-touch-icon" href="/static/icons/icon-192.png" />
<link rel="icon" href="/static/icons/icon-192.png" />
@@ -31,7 +31,7 @@
<button type="button" onclick="dismissIosInstallBanner()" aria-label="닫기">&times;</button>
</div>
<nav class="top-nav">
<span class="brand">습관 트래커</span>
<span class="brand">해빗랩</span>
<div class="nav-links">
<a href="/today" class="{% block nav_today %}{% endblock %}">오늘</a>
<a href="/habits" class="{% block nav_habits %}{% endblock %}">습관 관리</a>
@@ -40,7 +40,7 @@
{% if current_user %}
<div class="nav-user">
{% if account_level %}<span class="nav-level-pill">Lv.{{ account_level.level }}</span>{% endif %}
<span>{{ current_user.name or current_user.email }}</span>
<a href="/account">{{ current_user.name or current_user.email }}</a>
<form method="post" action="/auth/logout">
<button type="submit" class="btn-link">로그아웃</button>
</form>
@@ -63,6 +63,9 @@
</nav>
{% endif %}
{% block content %}{% endblock %}
<footer class="app-footer">
<a href="/privacy">개인정보처리방침</a>
</footer>
</div>
</body>
</html>
+1 -1
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}습관 관리 · 습관 트래커{% endblock %}
{% block title %}습관 관리 · 해빗랩{% endblock %}
{% block nav_habits %}active{% endblock %}
{% block content %}
<h1>습관 관리</h1>
+1 -1
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}기록 · 습관 트래커{% endblock %}
{% block title %}기록 · 해빗랩{% endblock %}
{% block nav_history %}active{% endblock %}
{% block shell_class %} wide{% endblock %}
{% block content %}
+60
View File
@@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block title %}해빗랩 · 습관이 만들어지는 진짜 시간{% endblock %}
{% block content %}
<div class="landing">
<div class="landing-hero">
<div class="landing-brand"><img src="/static/icons/icon-192.png" alt="" /> 해빗랩</div>
<h1>습관이 만들어지는<br />진짜 시간</h1>
<p class="landing-tagline">21일의 법칙 대신, 습관마다 다른 진짜 목표 기간을 알려드려요</p>
<div class="landing-streak" aria-hidden="true">
<div class="landing-day done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5 9-9" /></svg>
</div>
<div class="landing-day done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5 9-9" /></svg>
</div>
<div class="landing-day done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5 9-9" /></svg>
</div>
<div class="landing-day done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5 9-9" /></svg>
</div>
<div class="landing-day done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5 9-9" /></svg>
</div>
<div class="landing-day"></div>
<div class="landing-day"></div>
</div>
</div>
<div class="landing-copy">
<p>
"습관은 21일이면 만들어진다"는 말, 많이 들어보셨을 거예요. 사실 근거가 약한 통설이에요.
실증 연구(<strong>Lally et al., 2010, UCL</strong>)에 따르면 습관이 몸에 붙기까지 걸리는 시간은
사람마다, 습관마다 달라서 짧게는 3주, 길게는 8개월 넘게 걸리기도 해요.
</p>
<p>
<strong>해빗랩</strong>은 여러분이 만들고 싶은 습관도, 멈추고 싶은 습관도 함께 만들어가는 동반자예요.
검증된 연구를 시스템에 그대로 담아 목표 기간과 진행 상황을 짚어드리고, 그 여정을 끝까지 응원할게요.
</p>
</div>
<div class="landing-features">
<div class="landing-feature">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="5" /><circle cx="12" cy="12" r="1" /></svg>
<p><strong>연구 기반 목표 기간</strong>속설이 아니라 실제 연구값(21·66·254일)으로 목표를 잡아요</p>
</div>
<div class="landing-feature">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="12" r="6" /><line x1="8" y1="9" x2="8" y2="15" /><line x1="5" y1="12" x2="11" y2="12" /><circle cx="17" cy="12" r="6" /><line x1="14" y1="12" x2="20" y2="12" /></svg>
<p><strong>만들기와 끊기, 동시에</strong>새 습관을 만드는 것과 나쁜 습관을 끊는 것을 똑같은 무게로 다뤄요</p>
</div>
<div class="landing-feature">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
<p><strong>광고·구독 없음</strong>습관관리에만 집중해요, 그 외엔 아무것도 없어요</p>
</div>
</div>
<a href="/auth/google/login" class="btn btn-primary btn-block">Google로 시작하기</a>
</div>
{% endblock %}
+3 -2
View File
@@ -1,9 +1,10 @@
{% extends "base.html" %}
{% block title %}로그인 · 습관 트래커{% endblock %}
{% block title %}로그인 · 해빗랩{% endblock %}
{% block content %}
<div style="min-height: 70vh; display: flex; align-items: center; justify-content: center;">
<div class="card" style="width: 100%; max-width: 320px;">
<h1 style="text-align:center;">습관 트래커</h1>
<h1 style="text-align:center;">해빗랩</h1>
{% if deleted %}<p style="text-align:center; color: var(--color-success);">계정이 삭제되었습니다.</p>{% endif %}
<p style="text-align:center;">구글 계정으로 로그인하세요</p>
<a href="/auth/google/login" class="btn btn-primary btn-block">Google로 로그인</a>
</div>
+45
View File
@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block title %}개인정보처리방침 · 해빗랩{% endblock %}
{% block content %}
<div style="max-width: 640px; margin: 0 auto; padding: 24px 16px 48px;">
<div class="card">
<h1>개인정보처리방침</h1>
<p>시행일자: 2026년 7월 22일</p>
<p>해빗랩(이하 "서비스")는 이용자의 개인정보를 소중히 다루며, 아래와 같이 개인정보를 수집·이용·보관합니다.</p>
<h2>1. 수집하는 개인정보 항목</h2>
<p><strong>가. 구글 로그인 시 (필수)</strong><br />
이메일 주소, 이름, 프로필 사진 URL, 구글 계정 고유 식별자(sub). 서비스는 구글 계정 정보를 이용해 자동으로 계정을 생성하며, 비밀번호는 별도로 수집·저장하지 않습니다.</p>
<p><strong>나. 서비스 이용 중 직접 입력하는 정보</strong><br />
등록한 습관의 이름, 유형(만들기/끊기), 요일 스케줄, 목표 난이도, 달성 조건, 알림 시각, 날짜별 체크·실패 기록.</p>
<p><strong>다. 알림(Web Push)을 켠 경우에만</strong><br />
브라우저가 발급하는 푸시 구독 정보(endpoint, 암호화 키)와 브라우저 종류(User-Agent). 알림 중복 발송을 막기 위한 발송 이력도 함께 기록됩니다.</p>
<p><strong>라. 자동 수집 정보</strong><br />
로그인 유지를 위한 서명된 세션 쿠키. 별도의 광고·분석·트래킹 쿠키는 사용하지 않습니다.</p>
<h2>2. 개인정보의 수집 및 이용 목적</h2>
<p>구글 계정 인증 및 로그인 유지, 습관 등록·체크·통계(완료율, 연속 달성일 등) 제공, 설정한 시각에 맞춘 습관 알림(Web Push) 발송을 위해서만 이용합니다. 광고, 마케팅, 프로필링 목적으로는 이용하지 않습니다.</p>
<h2>3. 개인정보의 보유 및 이용 기간</h2>
<p>회원 탈퇴 시까지 보관하며, 탈퇴 시 계정 정보와 습관·체크 기록, 푸시 구독 정보를 지체 없이 삭제합니다. 관계 법령에 따라 보존이 필요한 경우가 아니면 별도 보관하지 않습니다.</p>
<h2>4. 개인정보의 제3자 제공</h2>
<p>서비스는 이용자의 개인정보를 원칙적으로 외부에 제공하지 않습니다. 다만 구글 로그인 인증 과정에서 구글(Google LLC)과 통신이 발생하며, 이는 이용자 인증을 위한 목적에 한정됩니다.</p>
<h2>5. 이용자의 권리와 행사 방법</h2>
<p>이용자는 언제든지 자신의 개인정보 열람·정정·삭제를 요청할 수 있습니다. 로그인 후 <a href="/account">계정</a> 페이지에서 본인 이메일 확인 후 즉시 계정과 모든 데이터를 직접 삭제할 수 있으며, 앱을 이용하기 어려운 경우 아래 문의처로 요청하셔도 확인 후 지체 없이 처리해드립니다.</p>
<h2>6. 보안을 위한 조치</h2>
<p>비밀번호를 직접 저장하지 않는 구글 OAuth 인증 방식을 사용하며, 로그인 세션은 서명된 쿠키로 관리합니다. 모든 통신은 HTTPS로 암호화되며, 각 이용자의 데이터는 계정 단위로 분리되어 다른 이용자가 접근할 수 없습니다.</p>
<h2>7. 문의처</h2>
<p>개인정보 관련 문의, 열람·정정·삭제 요청은 아래로 연락해주세요.</p>
<p>해빗랩 운영자<br />
이메일: <a href="mailto:shinalok357@gmail.com">shinalok357@gmail.com</a></p>
<h2>8. 고지의 의무</h2>
<p>이 개인정보처리방침은 법령이나 서비스 변경사항을 반영하기 위해 수정될 수 있으며, 변경 시 이 페이지를 통해 고지합니다.</p>
</div>
</div>
{% endblock %}
+1 -1
View File
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}오늘 · 습관 트래커{% endblock %}
{% block title %}오늘 · 해빗랩{% endblock %}
{% block nav_today %}active{% endblock %}
{% block content %}
<div id="today-content">