add terms page, improve landing content for branding review, and handle HTTP HEAD requests
CI/CD / test (push) Successful in 46s
CI/CD / deploy (push) Successful in 1m44s

This commit is contained in:
2026-08-08 20:40:47 +09:00
parent e752367f79
commit be8a4e6908
5 changed files with 107 additions and 3 deletions
+21
View File
@@ -42,6 +42,27 @@ async def redirect_http_to_https(request: Request, call_next):
return await call_next(request) return await call_next(request)
@app.middleware("http")
async def support_head_requests(request: Request, call_next):
# 이 Starlette 버전은 GET 라우트에 HEAD를 자동으로 열어주지 않아 크롤러의 HEAD 요청이
# 전부 405로 막힌다(구글 OAuth 브랜딩 인증 크롤러가 홈페이지를 HEAD로 먼저 확인하면서
# "콘텐츠 없음"으로 오판하는 원인이 됐음). GET과 동일하게 처리한 뒤 본문만 비워서 응답한다.
if request.method != "HEAD":
return await call_next(request)
request.scope["method"] = "GET"
response = await call_next(request)
async def _empty_body():
return
yield b"" # pragma: no cover - 제너레이터로 만들기 위한 도달 불가 코드
response.body_iterator = _empty_body()
# uvicorn은 실제 전송 바이트와 Content-Length가 다르면 예외를 던지므로 0으로 맞춘다.
response.headers["content-length"] = "0"
return response
app.mount("/static", StaticFiles(directory="app/static"), name="static") app.mount("/static", StaticFiles(directory="app/static"), name="static")
app.include_router(auth.router) app.include_router(auth.router)
+8
View File
@@ -68,6 +68,14 @@ def privacy_page(request: Request, db: Session = Depends(get_db)):
) )
@router.get("/terms")
def terms_page(request: Request, db: Session = Depends(get_db)):
user = get_current_user_optional(request, db)
return templates.TemplateResponse(
request, "terms.html", {"logged_in": user is not None, "current_user": user}
)
@router.get("/account") @router.get("/account")
def account_page(request: Request, db: Session = Depends(get_db)): def account_page(request: Request, db: Session = Depends(get_db)):
current = _current_user_or_redirect(request, db) current = _current_user_or_redirect(request, db)
+45 -1
View File
@@ -121,6 +121,11 @@ p {
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.app-footer a + a::before {
content: "·";
margin: 0 6px;
}
/* iOS 홈 화면 추가 안내 배너 */ /* iOS 홈 화면 추가 안내 배너 */
.ios-install-banner { .ios-install-banner {
display: none; display: none;
@@ -1131,10 +1136,12 @@ label {
text-align: center; text-align: center;
} }
/* 앱 이름을 담은 h1 — 시각적으로는 작은 브랜드 라벨이지만 문서 구조상 최상위 제목이다. */
.landing-brand { .landing-brand {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin: 0;
font-size: 15px; font-size: 15px;
font-weight: 700; font-weight: 700;
color: var(--color-accent); color: var(--color-accent);
@@ -1147,7 +1154,7 @@ label {
border-radius: 6px; border-radius: 6px;
} }
.landing-hero h1 { .landing-hero-title {
font-size: 32px; font-size: 32px;
font-weight: 700; font-weight: 700;
letter-spacing: -0.02em; letter-spacing: -0.02em;
@@ -1263,3 +1270,40 @@ label {
font-size: 14.5px; font-size: 14.5px;
margin-bottom: 2px; margin-bottom: 2px;
} }
/* 기능 설명 / 계정 안내 — 구글 브랜딩 심사가 홈페이지에서 확인하는 "앱의 목적" 설명 영역. */
.landing-section h2 {
font-size: 17px;
font-weight: 700;
margin: 0 0 var(--space-2);
}
.landing-list {
margin: 0;
padding-left: 1.1em;
display: flex;
flex-direction: column;
gap: 10px;
font-size: 14.5px;
line-height: 1.65;
color: var(--color-text);
}
.landing-list strong {
color: var(--color-accent);
}
.landing-note {
margin: 0 0 var(--space-2);
font-size: 13.5px;
line-height: 1.7;
color: var(--color-text-muted);
}
.landing-note:last-child {
margin-bottom: 0;
}
.landing-note strong {
color: var(--color-text);
}
+3
View File
@@ -4,6 +4,8 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>{% block title %}해빗랩{% endblock %}</title> <title>{% block title %}해빗랩{% endblock %}</title>
{# 구글 OAuth 브랜딩 심사는 홈페이지에서 앱 이름과 목적을 확인한다 — 설명 메타 태그가 없으면 "앱의 목적에 관한 설명이 없다"로 반려된다. #}
<meta name="description" content="{% block meta_description %}해빗랩은 만들고 싶은 습관과 끊고 싶은 습관을 요일 단위로 관리하는 습관 기록 앱입니다.{% endblock %}" />
<link rel="manifest" href="/static/manifest.json" /> <link rel="manifest" href="/static/manifest.json" />
<meta name="theme-color" content="#d97757" media="(prefers-color-scheme: light)" /> <meta name="theme-color" content="#d97757" media="(prefers-color-scheme: light)" />
@@ -74,6 +76,7 @@
{% block content %}{% endblock %} {% block content %}{% endblock %}
<footer class="app-footer"> <footer class="app-footer">
<a href="/privacy">개인정보처리방침</a> <a href="/privacy">개인정보처리방침</a>
<a href="/terms">서비스 약관</a>
</footer> </footer>
</div> </div>
</body> </body>
+30 -2
View File
@@ -1,10 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}해빗랩 · 습관이 만들어지는 진짜 시간{% endblock %} {% block title %}해빗랩 · 습관이 만들어지는 진짜 시간{% endblock %}
{% block meta_description %}해빗랩은 만들고 싶은 습관과 끊고 싶은 습관을 요일 단위로 관리하는 습관 기록 앱입니다. 요일별 습관 등록과 데일리 체크, 월별·주별 기록과 완료율·연속 달성일 통계, 설정한 시각의 습관 알림, 하루를 남기는 일기를 제공합니다.{% endblock %}
{% block content %} {% block content %}
<div class="landing"> <div class="landing">
<div class="landing-hero"> <div class="landing-hero">
<div class="landing-brand"><img src="/static/icons/icon-192.png" alt="" /> 해빗랩</div> {# h1은 반드시 앱 이름이어야 한다 — 구글 브랜딩 심사가 OAuth 동의 화면의 앱 이름과 홈페이지 이름의 일치를 확인한다. #}
<h1>습관이 만들어지는<br />진짜 시간</h1> <h1 class="landing-brand"><img src="/static/icons/icon-192.png" alt="" /> 해빗랩</h1>
<p class="landing-hero-title">습관이 만들어지는<br />진짜 시간</p>
<p class="landing-tagline">21일의 법칙 대신, 습관마다 다른 진짜 목표 기간을 알려드려요</p> <p class="landing-tagline">21일의 법칙 대신, 습관마다 다른 진짜 목표 기간을 알려드려요</p>
<div class="landing-streak" aria-hidden="true"> <div class="landing-streak" aria-hidden="true">
@@ -40,6 +42,17 @@
</p> </p>
</div> </div>
<div class="landing-section">
<h2>해빗랩이 하는 일</h2>
<ul class="landing-list">
<li><strong>요일별 습관 등록</strong> — 만들고 싶은 습관과 끊고 싶은 습관을 요일 단위로 등록하고, 습관마다 목표 기간과 달성 조건을 정합니다.</li>
<li><strong>데일리 체크</strong> — 오늘 예정된 습관을 한 화면에서 체크하고, 놓친 습관은 실패로 표시해 솔직하게 기록합니다.</li>
<li><strong>기록과 통계</strong> — 월별 달력과 주별 매트릭스로 지나온 기록을 돌아보고, 습관별 완료율과 연속 달성일을 확인합니다.</li>
<li><strong>습관 알림</strong> — 습관마다 설정한 시각에 브라우저 푸시 알림을 보내드립니다. 알림을 켠 경우에만 동작합니다.</li>
<li><strong>일기</strong> — 하루의 회고나 습관과 무관한 자유 일기를 카테고리·태그·사진과 함께 남길 수 있습니다.</li>
</ul>
</div>
<div class="landing-features"> <div class="landing-features">
<div class="landing-feature"> <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> <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>
@@ -56,5 +69,20 @@
</div> </div>
<a href="/auth/google/login" class="btn btn-primary btn-block">Google로 시작하기</a> <a href="/auth/google/login" class="btn btn-primary btn-block">Google로 시작하기</a>
<div class="landing-section">
<h2>계정과 로그인</h2>
<p class="landing-note">
해빗랩은 구글 계정으로만 로그인합니다. 로그인 시 구글로부터 <strong>이름, 이메일 주소, 프로필 사진</strong>을 받아
계정을 식별하고 화면에 표시하는 용도로만 사용하며, 별도의 비밀번호는 수집하지 않습니다.
Gmail, 드라이브, 캘린더 등 다른 구글 서비스의 데이터에는 접근하지 않습니다.
직접 입력한 습관·기록·일기는 본인 계정에만 저장되고 다른 이용자에게 공개되지 않으며,
로그인 후 계정 페이지에서 언제든 계정과 모든 데이터를 직접 삭제할 수 있습니다.
</p>
<p class="landing-note">
자세한 내용은 <a href="/privacy">개인정보처리방침</a><a href="/terms">서비스 약관</a>을 확인해주세요.
문의: <a href="mailto:shinalok357@gmail.com">shinalok357@gmail.com</a>
</p>
</div>
</div> </div>
{% endblock %} {% endblock %}