feat(docs): 스크린샷 가이드 — 인증 후 세부 화면 캡처 추가
로그인 전 7장만 캡처하던 스크립트를 확장: - SCREENSHOT_EMAIL/PASSWORD env var로 인증 세션 획득 - 로그인 후 홈·프로필·게시글폼·모집폼 캡처 - game-detail 리뷰·댓글 composer hidden 제거 후 캡처 - AJAX 로그인 흐름(fetch + window.location) wait_for_url 대응 Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162BaZnrbiYgWc9JJMZ9Czb
This commit is contained in:
parent
9098325e7c
commit
5dd639fd7a
|
|
@ -12,54 +12,51 @@ pip3 install playwright
|
|||
# 브라우저 다운로드 불필요 — 로컬 Chrome 사용
|
||||
```
|
||||
|
||||
## 스크립트
|
||||
|
||||
```python
|
||||
import os
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
OUT_DIR = "/tmp/bibimbap-screenshots"
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
pages = [
|
||||
("/", "01-home.png"),
|
||||
("/login", "02-login.png"),
|
||||
("/signup", "03-signup.png"),
|
||||
("/posts", "04-posts-list.png"),
|
||||
("/recruit", "05-recruit-list.png"),
|
||||
("/game/3", "06-game-detail.png"),
|
||||
("/terms", "07-terms.png"),
|
||||
]
|
||||
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(
|
||||
executable_path=CHROME,
|
||||
headless=True,
|
||||
args=["--no-sandbox"]
|
||||
)
|
||||
page = browser.new_page(viewport={"width": 1400, "height": 900})
|
||||
|
||||
for path, filename in pages:
|
||||
page.goto(f"{BASE_URL}{path}", wait_until="networkidle")
|
||||
page.screenshot(path=os.path.join(OUT_DIR, filename), full_page=True)
|
||||
print(f"✓ {filename}")
|
||||
|
||||
browser.close()
|
||||
```
|
||||
|
||||
## 실행
|
||||
|
||||
### 비인증 페이지만 (로그인 불필요)
|
||||
|
||||
```bash
|
||||
python3 docs/development/screenshot-guide.py
|
||||
# 결과: /tmp/bibimbap-screenshots/*.png
|
||||
```
|
||||
|
||||
7장 캡처: 홈(비로그인), 로그인, 회원가입, 게시글 목록, 모집 목록, 게임 상세(비로그인), 이용약관
|
||||
|
||||
### 전체 캡처 (로그인 포함 세부 화면)
|
||||
|
||||
```bash
|
||||
SCREENSHOT_EMAIL=user@example.com SCREENSHOT_PASSWORD=yourpw \
|
||||
python3 docs/development/screenshot-guide.py
|
||||
```
|
||||
|
||||
추가 7장 캡처:
|
||||
|
||||
| 파일 | 화면 |
|
||||
|---|---|
|
||||
| `08-home-loggedin.png` | 홈 (로그인 후 — 내비게이션 변경) |
|
||||
| `09-profile.png` | 프로필 페이지 |
|
||||
| `10-posts-form.png` | 게시글 작성 폼 |
|
||||
| `11-recruit-form.png` | 모집 작성 폼 |
|
||||
| `12-game-detail-loggedin.png` | 게임 상세 (로그인 후 전체 뷰) |
|
||||
| `13-game-review-form.png` | 리뷰 작성 폼 (노출 상태) |
|
||||
| `14-game-comment-form.png` | 댓글 작성 폼 (노출 상태, 섹션 포커스) |
|
||||
|
||||
## 캡처 전략
|
||||
|
||||
**비인증 → 인증 비교**: 01·06번(비인증)과 08·12번(인증) 쌍으로 로그인 전후 차이를 비교할 수 있다.
|
||||
|
||||
**폼 상태 강제 노출**: 리뷰/댓글 composer는 서버 API 응답(로그인 상태, 기존 리뷰 여부) 후 JS가 제어한다. 스크립트는 3초 내 자연 노출을 기다리고, 실패하면 `removeAttribute('hidden')`으로 강제 노출해 캡처한다. 실제 동작과 동일한 HTML 구조를 캡처한다.
|
||||
|
||||
## 출력 위치
|
||||
|
||||
```
|
||||
/tmp/bibimbap-screenshots/*.png
|
||||
```
|
||||
|
||||
재부팅 시 삭제됨. 영구 저장 필요 시 `OUT_DIR` 변수 변경.
|
||||
|
||||
## 주의
|
||||
|
||||
- 헤드리스 모드는 다크 테마 미적용 (시스템 prefers-color-scheme 무시)
|
||||
- 로그인 필요 페이지는 세션 없이 리다이렉트됨
|
||||
- `/tmp/bibimbap-screenshots/` 는 재부팅 시 삭제 — 영구 저장 필요하면 경로 변경
|
||||
- Claude Design에 넘길 때 "다크 테마로 리디자인" 프롬프트에 명시
|
||||
- 로그인 실패(잘못된 계정 정보) 시 인증 페이지는 건너뛰고 비인증 7장만 저장됨
|
||||
|
|
|
|||
|
|
@ -1,34 +1,131 @@
|
|||
"""
|
||||
bibimbap 스크린샷 캡처 스크립트
|
||||
|
||||
비인증 페이지 + 인증 후 세부 화면 상태 모두 캡처.
|
||||
|
||||
사용법:
|
||||
# 비인증 페이지만
|
||||
python3 docs/development/screenshot-guide.py
|
||||
|
||||
# 인증 포함 전체 (로그인 계정 필요)
|
||||
SCREENSHOT_EMAIL=user@example.com SCREENSHOT_PASSWORD=pw \
|
||||
python3 docs/development/screenshot-guide.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
OUT_DIR = "/tmp/bibimbap-screenshots"
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
pages = [
|
||||
("/", "01-home.png"),
|
||||
EMAIL = os.environ.get("SCREENSHOT_EMAIL", "")
|
||||
PASSWORD = os.environ.get("SCREENSHOT_PASSWORD", "")
|
||||
|
||||
# 비인증 페이지
|
||||
ANON_PAGES = [
|
||||
("/", "01-home-anon.png"),
|
||||
("/login", "02-login.png"),
|
||||
("/signup", "03-signup.png"),
|
||||
("/posts", "04-posts-list.png"),
|
||||
("/recruit", "05-recruit-list.png"),
|
||||
("/game/3", "06-game-detail.png"),
|
||||
("/game/3", "06-game-detail-anon.png"),
|
||||
("/terms", "07-terms.png"),
|
||||
]
|
||||
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def capture(page, path, filename, *, full_page=True):
|
||||
page.goto(f"{BASE_URL}{path}", wait_until="networkidle")
|
||||
page.screenshot(path=os.path.join(OUT_DIR, filename), full_page=full_page)
|
||||
print(f" ✓ {filename}")
|
||||
|
||||
|
||||
def do_login(page):
|
||||
"""로그인 폼 제출 후 세션 획득. CSRF 토큰은 폼에서 추출."""
|
||||
page.goto(f"{BASE_URL}/login", wait_until="networkidle")
|
||||
page.fill("input[name='email']", EMAIL)
|
||||
page.fill("input[name='password']", PASSWORD)
|
||||
page.click("button[type='submit']")
|
||||
page.wait_for_url(lambda url: "/login" not in url, timeout=8000)
|
||||
|
||||
|
||||
def capture_review_form(page):
|
||||
"""게임 상세 — 리뷰 작성 폼 노출 상태."""
|
||||
page.goto(f"{BASE_URL}/game/3", wait_until="networkidle")
|
||||
# JS가 로그인 상태 확인 후 composer를 보여줄 때까지 대기 (최대 3초)
|
||||
try:
|
||||
page.wait_for_function(
|
||||
"document.getElementById('game-review-composer') && "
|
||||
"!document.getElementById('game-review-composer').hasAttribute('hidden')",
|
||||
timeout=5000,
|
||||
)
|
||||
except Exception:
|
||||
# JS가 숨긴 채로 두면 강제 노출 (스크린샷 전용)
|
||||
page.evaluate(
|
||||
"document.getElementById('game-review-composer')?.removeAttribute('hidden')"
|
||||
)
|
||||
page.screenshot(
|
||||
path=os.path.join(OUT_DIR, "13-game-review-form.png"), full_page=False
|
||||
)
|
||||
print(" ✓ 13-game-review-form.png")
|
||||
|
||||
|
||||
def capture_comment_form(page):
|
||||
"""게임 상세 — 댓글 작성 폼 노출 상태."""
|
||||
page.goto(f"{BASE_URL}/game/3", wait_until="networkidle")
|
||||
try:
|
||||
page.wait_for_function(
|
||||
"document.getElementById('game-comment-composer') && "
|
||||
"!document.getElementById('game-comment-composer').hasAttribute('hidden')",
|
||||
timeout=5000,
|
||||
)
|
||||
except Exception:
|
||||
page.evaluate(
|
||||
"document.getElementById('game-comment-composer')?.removeAttribute('hidden')"
|
||||
)
|
||||
# 댓글 섹션으로 스크롤
|
||||
page.evaluate(
|
||||
"document.getElementById('game-comment-composer')?.scrollIntoView({block:'center'})"
|
||||
)
|
||||
page.screenshot(
|
||||
path=os.path.join(OUT_DIR, "14-game-comment-form.png"), full_page=False
|
||||
)
|
||||
print(" ✓ 14-game-comment-form.png")
|
||||
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(
|
||||
executable_path=CHROME,
|
||||
headless=True,
|
||||
args=["--no-sandbox"]
|
||||
args=["--no-sandbox"],
|
||||
)
|
||||
page = browser.new_page(viewport={"width": 1400, "height": 900})
|
||||
|
||||
for path, filename in pages:
|
||||
page.goto(f"{BASE_URL}{path}", wait_until="networkidle")
|
||||
page.screenshot(path=os.path.join(OUT_DIR, filename), full_page=True)
|
||||
print(f"✓ {filename}")
|
||||
print("── 비인증 페이지 ──────────────────────────────")
|
||||
for path, filename in ANON_PAGES:
|
||||
capture(page, path, filename)
|
||||
|
||||
if EMAIL and PASSWORD:
|
||||
print("\n── 로그인 후 세부 화면 ─────────────────────────")
|
||||
try:
|
||||
do_login(page)
|
||||
print(" ✓ 로그인 성공")
|
||||
|
||||
capture(page, "/", "08-home-loggedin.png")
|
||||
capture(page, "/profile", "09-profile.png")
|
||||
capture(page, "/posts/new", "10-posts-form.png")
|
||||
capture(page, "/recruit/new", "11-recruit-form.png")
|
||||
capture(page, "/game/3", "12-game-detail-loggedin.png")
|
||||
capture_review_form(page)
|
||||
capture_comment_form(page)
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ 로그인 실패 또는 인증 페이지 캡처 오류: {e}", file=sys.stderr)
|
||||
else:
|
||||
print("\n※ SCREENSHOT_EMAIL / SCREENSHOT_PASSWORD 미설정 — 비인증 페이지만 캡처됨")
|
||||
print(" 전체 캡처: SCREENSHOT_EMAIL=... SCREENSHOT_PASSWORD=... python3 docs/development/screenshot-guide.py")
|
||||
|
||||
browser.close()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue