Stabilize runtime auth and E2E coverage
This commit is contained in:
parent
6a3e3b541c
commit
188e899394
133 changed files with 55987 additions and 6775 deletions
|
|
@ -36,6 +36,7 @@ docker compose up -d
|
|||
|---|---|
|
||||
| `POSTGRES_PASSWORD` | DB 비밀번호(필수) |
|
||||
| `SESSION_SECRET` | 세션 서명키(필수, 랜덤) |
|
||||
| `AUTH_ALLOWED_EMAIL_DOMAINS` | Google 로그인 허용 이메일 도메인(JSON 배열). 기본 `["hs.ac.kr","twentyoz.kr"]`. Google Console authorized domains가 아니라 서버에서 `email`/`hd` claim으로 강제 |
|
||||
| `OPENAI_API_KEY` | 음성(STT/TTS) |
|
||||
| `ANTHROPIC_API_KEY` | 엔진 폴백/클라우드 모드 |
|
||||
| `ENGINE_MODE` / `ENGINE_URL` | 엔진 위치(위 표) |
|
||||
|
|
|
|||
311
docs/HANDOFF.md
311
docs/HANDOFF.md
|
|
@ -1,63 +1,270 @@
|
|||
# Vignette 핸드오프 노트
|
||||
# Vignette Handoff
|
||||
|
||||
> 2026-06-26 02:0x KST · 세션 b00bd7f3 → 다음 세션. **새 세션은 이 파일부터 읽고 이어받는다.**
|
||||
> Updated: 2026-06-26 14:43 KST. 새 세션은 이 문서와 `docs/DESIGN_CONCEPT.md`를 먼저 읽고 이어가면 된다.
|
||||
|
||||
## 프로젝트
|
||||
한신대 SW중심대학 산학협력 — 생성형 AI 심리상담 시뮬레이션 훈련 플랫폼 **Vignette**.
|
||||
- repo: `D:\workspace\vignette` · github.com/yunchan8804-blip/vignette (**master, f869d25**)
|
||||
- 학습자가 가상 내담자(AI 페르소나)와 상담 연습 → 백그라운드 평가 AI가 기법 라벨링·피드백. 3역할(학습자/교수자/관리자).
|
||||
## 현재 상태
|
||||
|
||||
## 라이브 URL
|
||||
- **프론트(공개)**: https://vignette.chanpaca.net (= vignette-b1q.pages.dev, Cloudflare Pages)
|
||||
- **API(터널)**: https://api-vignette.chanpaca.net (cloudflared named tunnel → localhost:8000)
|
||||
- 라이브 경로: 로그인(학습자) → 학습자홈 → **연습 시작** → 상담세션 → **회기 시작** → 입력창 말걸기 → 서연(P1) 실응답
|
||||
- Workspace: `D:\workspace\vignette`
|
||||
- 로컬 웹 기본 포트: `http://localhost:5173`
|
||||
- 로컬 API 기본 포트: `http://127.0.0.1:8000`
|
||||
- 로컬 engine gateway 기본 포트: `http://127.0.0.1:9099`
|
||||
- 공개 웹: `https://vignette.chanpaca.net`
|
||||
- 공개 API: `https://api-vignette.chanpaca.net`
|
||||
- Google OAuth 허용 이메일 도메인: `hs.ac.kr`, `twentyoz.kr`
|
||||
- `frontenddesign` 스킬은 현재 세션의 사용 가능 스킬 목록에 없었다. 대신 `docs/DESIGN_CONCEPT.md`를 SSOT로 사용했다.
|
||||
- 공개 API 터널은 현재 `C:\Users\encep\.cloudflared\vignette-config.yml`에서 `http://127.0.0.1:8001`을 본다.
|
||||
- 공개용 API 프로세스는 `127.0.0.1:8001`에서 `ENVIRONMENT=prod`로 떠 있다. 로컬 개발 API `127.0.0.1:8000`은 `dev`로 유지했다.
|
||||
- `GET /personas`는 이제 인증 필요다. 로컬 `http://127.0.0.1:8000/personas`와 공개 `https://api-vignette.chanpaca.net/personas` 모두 비로그인 401 확인 완료.
|
||||
- 로컬 웹이 `http://127.0.0.1:5175`처럼 다른 Vite 포트로 떠도 로그인은 `로컬 테스트 계정으로 계속` 버튼을 사용한다. Google OAuth 버튼은 로컬에서는 disabled로 둔다. 현재 OAuth callback이 공개 API로 돌아가기 때문에 로컬 Google OAuth는 로컬 세션에 붙을 수 없다.
|
||||
- 중복 Vite/API/gateway/cloudflared 프로세스는 정리했다. 현재 확인용 프로세스는 `127.0.0.1:8000` dev API, `127.0.0.1:9099` engine gateway, `127.0.0.1:8001` prod public API, `cloudflared` tunnel 1개다. Playwright 전체 테스트는 자체 Vite web server를 `127.0.0.1:5173`에 띄워 검증했다.
|
||||
- 공개 런타임 재기동 스크립트:
|
||||
- start: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1`
|
||||
- stop: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\stop-public-runtime.ps1`
|
||||
|
||||
## ⚠️ 상주 인프라 (윤찬 PC, 세션 넘겨도 살아있음 — 안 끄면)
|
||||
| 프로세스 | 포트 | 재시작 |
|
||||
|---|---|---|
|
||||
| 엔진 게이트웨이 (claude -p 상주풀) | 9099 | `cd apps/api && C:/Users/encep/AppData/Local/Programs/Python/Python311/python.exe -m uvicorn engine_gateway.gateway:app --host 127.0.0.1 --port 9099` |
|
||||
| API 서버 (FastAPI) | 8000 | 아래 env로 `python -m uvicorn app.main:app --host 127.0.0.1 --port 8000` (WorkingDir apps/api) |
|
||||
| cloudflared (vignette tunnel ca8c5be0) | — | `cloudflared tunnel --config C:\Users\encep\.cloudflared\vignette-config.yml run` |
|
||||
## 이번 세션 완료
|
||||
|
||||
API env(필수): `ENGINE_URL=http://127.0.0.1:9099` `ENGINE_MODE=claude_cli` `ENVIRONMENT=dev` `CORS_ORIGINS=["https://vignette.chanpaca.net","https://vignette-b1q.pages.dev","http://localhost:5173"]`
|
||||
- health 확인: `curl https://api-vignette.chanpaca.net/health` → 200 / `curl -X POST .../sessions -d '{"persona_code":"P1","theory_mode":"humanistic"}'` → 201
|
||||
- 남아 있던 E2E seed persona 결합을 제거했다.
|
||||
- `session-persistence`, `teacher`, `voice` 스펙은 `/api/personas`에서 실제 DB persona를 가져와 사용한다.
|
||||
- `voice-success`만 DB-offline fixture라 `ALLOW_SEED_PERSONA_FALLBACK=true`와 `P1`을 의도적으로 유지하고 주석을 남겼다.
|
||||
- 운영 설정 fail-closed를 강화했다.
|
||||
- `ENVIRONMENT != dev`에서 dev login, seed fallback, 기본 session secret, OAuth 누락, localhost frontend/CORS를 설정 검증에서 거부한다.
|
||||
- API lifespan은 DB 초기화 실패 시 dev에서만 degraded fallback을 허용하고, staging/prod에서는 시작 실패로 둔다.
|
||||
- `SettingsConfigDict(populate_by_name=True)`를 추가해 테스트/코드에서 field name 기반 설정 생성이 가능하게 했다.
|
||||
- `infra/docker-compose.yml`을 깨끗한 ASCII compose로 재작성했다.
|
||||
- `ENVIRONMENT`, Google OAuth client id/secret, `OAUTH_REDIRECT_URI`, `CORS_ORIGINS`, `OPENAI_BASE_URL`을 API 컨테이너에 명시 주입한다.
|
||||
- 기존 손상된 줄 때문에 `OPENAI_API_KEY`가 주석/중복으로 처리되던 문제를 제거했다.
|
||||
- 기본 `FRONTEND_BASE_URL`은 `https://vignette.chanpaca.net`로 수정했다.
|
||||
- `infra/.env.example`에 운영 필수 값을 보강했다.
|
||||
- `ENVIRONMENT=prod`, `OAUTH_GOOGLE_CLIENT_ID`, `OAUTH_GOOGLE_CLIENT_SECRET`
|
||||
- 기본 CORS에서 localhost 제거. 로컬 개발은 주석대로 `ENVIRONMENT=dev`와 localhost CORS를 별도 추가.
|
||||
- admin UI 안정화.
|
||||
- health card의 긴 engine readiness JSON이 모바일 폭을 밀어내지 않도록 줄바꿈 처리.
|
||||
- backend admin health는 동적인 raw JSON detail 대신 안정적인 `Engine readiness failed` 문구를 내려준다.
|
||||
- 전역 engine config를 바꾸는 `db-persistence` E2E는 `@single-run`으로 분리했다.
|
||||
- 사용자 생성 후 UI가 서버 목록 reload를 끝내기 전에 카드를 찾던 E2E 타이밍을 안정화했다. 생성한 이메일로 검색한 뒤 실제 서버 응답 카드만 검증한다.
|
||||
- 사용자 관리 UI는 DB 사용자 저장소가 durable하지 않으면 생성/수정/비활성화를 막는다.
|
||||
- AI 운영 라벨은 `Claude CLI 게이트웨이`, `Anthropic API`, `OpenAI 호환`, `Solar`처럼 실제 어댑터명으로 표시한다.
|
||||
- 설정의 AI 운영 패널은 `/admin/health` 기반 응답 생성 상태를 함께 보여주되, health 지연이 설정 폼 로딩을 막지 않도록 분리했다.
|
||||
- 비개발 환경에서 `admin_engine_config` singleton row가 없으면 runtime default로 내려가지 않고 fail-closed 한다.
|
||||
- 비개발 DB 장애 상태는 `임시 기록`이 아니라 저장소 중단으로 표시한다.
|
||||
- public Google OAuth smoke 문서를 보강했다.
|
||||
- `E2E_PUBLIC_AUTH=1`에서는 로컬 Vite 서버를 띄우지 않고 공개 웹을 대상으로 한다.
|
||||
- storageState는 인증 쿠키를 포함하므로 `node_modules/.tmp` 아래 민감 파일로 취급한다.
|
||||
- API session TTL은 현재 8시간이다.
|
||||
- 공개 API prod 전환을 완료했다.
|
||||
- `cloudflared` ingress를 `127.0.0.1:8000`에서 `127.0.0.1:8001`로 변경했다.
|
||||
- `https://api-vignette.chanpaca.net/health`가 `environment:"prod"`, `db:true`, `engine:true`를 반환한다.
|
||||
- public production-safe Playwright 게이트가 통과한다.
|
||||
- 공개 런타임 start/stop 스크립트를 추가했다.
|
||||
- `scripts/start-public-runtime.ps1`는 prod-safe env override로 API 8001을 띄우고 health를 검증한다.
|
||||
- 기본 실행 시 `cloudflared`도 vignette config 기준으로 재시작한다.
|
||||
- `scripts/stop-public-runtime.ps1`는 8001 API를 정리한다. `-StopCloudflared`를 붙이면 터널도 같이 정리한다.
|
||||
- 공개 API 보안 경계를 보강했다.
|
||||
- `apps/api/app/routes/personas.py`에 `CurrentPrincipal`을 붙여 persona catalog를 authenticated-only로 전환했다.
|
||||
- 로컬 E2E `auth.spec.ts`에 비로그인 `/api/personas => 401` 회귀를 추가했다.
|
||||
- public readiness gate는 빈 API request context로 비로그인 `/personas => 401`을 확인한다. storageState가 붙은 request fixture를 쓰지 않도록 처리했다.
|
||||
- 공개 웹 API base fallback을 보강했다.
|
||||
- `apps/web/src/lib/api.ts`는 `VITE_API_BASE`가 없고 host가 `vignette.chanpaca.net` 또는 `*.pages.dev`이면 `https://api-vignette.chanpaca.net`을 기본 API origin으로 사용한다.
|
||||
- 이 변경으로 public web의 `/api/auth/config`가 SPA HTML로 떨어져 Google 버튼이 disabled 되는 문제를 피한다.
|
||||
- 로컬 로그인 UX를 보강했다.
|
||||
- `apps/web/src/pages/Login.tsx`는 로컬 origin에서 OAuth redirect URI가 공개 API이면 Google 버튼을 disabled 처리하고 `로컬 테스트 계정으로 로그인` 안내를 보여준다.
|
||||
- `auth.spec.ts`에 로컬 테스트 계정 로그인 후 `/learn` redirect 회귀를 추가했다.
|
||||
- 학습자 홈 기록 UX를 보강했다.
|
||||
- `apps/web/src/pages/LearnerHome.tsx`는 세션이 0개인 사용자에게도 실제 서버 세션 0개를 기반으로 `기존 회기` 빈 상태와 0 카운트를 보여준다.
|
||||
- 기록이 있을 때만 `has-session-records` 압축 레이아웃을 쓰도록 분리했다. 0회 사용자 모바일에서도 선택 내담자 정보, 새 회기 CTA, 기록 빈 상태가 문서 스크롤 없이 보인다.
|
||||
- `apps/web/e2e/readiness.spec.ts`는 fake 12회/최근 8회 부재뿐 아니라 서버 기반 0회 빈 상태를 기대하도록 갱신했다.
|
||||
- history의 반복 CTA는 `다시 시도` 대신 `다시 연습`으로 바꿔 기록 회고와 새 회기 시작 의미를 분리했다.
|
||||
- 직접 세션 URL의 persona 우회 경로를 막았다.
|
||||
- `LearnerHome`에서만 막던 `degraded` 또는 non-DB persona 차단을 `Session` 직접 진입 경로에도 적용했다.
|
||||
- `/learn/session/:personaCode`로 직접 들어와도 DB 원본 persona가 아니면 `회기 시작` 버튼이 비활성화된다.
|
||||
- desktop/mobile E2E에 degraded `seed_fallback` persona route mock 회귀를 추가했다.
|
||||
- active session dense viewport를 보정했다.
|
||||
- `apps/web/src/pages/session/session.css`에서 380px 이하/낮은 화면의 stage padding과 avatar 크기를 줄여 stage 내부 clipping을 없앴다.
|
||||
- `session-layout.spec.ts` dense viewport desktop/mobile 회귀와 전체 Playwright에서 확인했다.
|
||||
- active session 텍스트 턴 저장/표시 흐름을 실제 SSE 계약에 맞췄다.
|
||||
- 프론트 `sessionApi.stream`은 실제 백엔드 계약인 `POST /sessions/{id}/stream`을 fetch stream으로 소비한다.
|
||||
- `/stream` 요청이 거절되거나 SSE가 열린 뒤 error 이벤트로 끝나면 학습자 발화와 부분 내담자 응답을 transcript에 남기지 않고 입력값을 복원한다.
|
||||
- 내담자 자막은 로컬 typewriter가 아니라 서버 token 이벤트로만 쌓는다.
|
||||
- 백엔드 `/turn`과 `/stream`은 엔진 성공 전 learner turn/state를 append하지 않도록 순서를 바꿨고, `app.test_session_turn_persistence` 회귀로 검증했다.
|
||||
- 음성 회기 실패 턴 저장/표시도 같은 기준으로 맞췄다.
|
||||
- voice route는 엔진 응답 생성 성공 후에만 learner/client turn을 저장한다.
|
||||
- 프론트는 음성 transcript를 pending으로 표시하고, 엔진 실패 시 pending transcript를 제거한다.
|
||||
- 음성 degraded reason의 내부 `seed_fallback`/runtime store 표현은 사용자용 한국어 상태 문구로 바꿨다.
|
||||
- admin health의 dev DB fallback 문구를 `임시 기록`이 아니라 `비영구 런타임 기록`으로 정리했다.
|
||||
- Live2D demo fallback을 제거했다.
|
||||
- Mao/Haru 샘플 모델은 로컬 통합 확인용 자산으로만 남기고 모든 페르소나의 공용 fallback으로 쓰지 않는다.
|
||||
- `AvatarPersona.live2dModelUrl`이 명시된 실제 페르소나 모델이 있을 때만 Live2D를 렌더링하며, 없거나 실패하면 SVG persona avatar를 유지한다.
|
||||
|
||||
## ✅ 완료 (대시보드 구현·인프라)
|
||||
- 프론트 7화면(로그인/학습자홈/세션/리뷰/교수자/관리자/설정) 공개배포 + 라이트테마
|
||||
- 백엔드 풀 라이브: 게이트웨이(/v1/generate·/v1/stream 어댑터) + API(orchestrator: 가드레일→상태머신→페르소나→엔진→평가) + **DB 없이 degraded(store 인메모리 폴백)**
|
||||
- named tunnel + CORS + puppeteer 실화면 검증(서연 실응답)
|
||||
- 커스텀도메인 vignette.chanpaca.net **SSL active**
|
||||
- DB 마이그레이션 35테이블+pgvector (검증 후 Docker 내림. 운영 DB는 NAS PostgreSQL 설계)
|
||||
- 합성 골든셋 62발화(P4-P7 비위기, `data/golden/`+`data/personas/`) + 평가AI few-shot 연결(`evaluator.py` GOLDEN_DIR)
|
||||
## 검증 결과
|
||||
|
||||
## 🔴 다음 세션이 이어받을 작업 (윤찬 최신 지시 2건)
|
||||
### 1. 학습자홈 목업 제거 (최우선) — `apps/web/src/pages/LearnerHome.tsx`
|
||||
윤찬: "신규 사용자인데 **12회 연습/수련님/이미 있는 회기 목록**이 가짜로 차있으면 안 돼. 애당초 목업이 없어야."
|
||||
- 빼야 할 mock: `MOCK_WEEK`(doneCount/reviewPending/weeklyGoal), `MOCK_SESSIONS`(회기 목록), `MOCK_GROWTH`(성장 추세), `MOCK_NEXT`(추천), 253줄 `"지금까지 12회 연습했어요"`, `MOCK_LEARNER_NAME_FALLBACK`("도윤"/"수련님")
|
||||
- 목표: 신규 사용자 **빈 상태(empty state)** — 0회, 빈 회기, "아직 연습 기록이 없어요 → 첫 상담 연습을 시작해보세요" CTA 중심. 또는 API(`GET /sessions` 류)에서 실제 사용자 데이터 로드(없으면 빈).
|
||||
- 주의: "연습 시작" 라우팅은 이미 고침(navigate(`/learn/session/:personaCode`)) — 빈 상태에서도 CTA는 살려야.
|
||||
- Web typecheck:
|
||||
- `cd apps/web; fnm env --use-on-cd | Out-String | Invoke-Expression; fnm use 22.22.3; npm run typecheck`
|
||||
- Passed
|
||||
- Backend unit/compile:
|
||||
- `cd apps/api; C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m unittest app.test_runtime_policy app.test_session_turn_persistence engine_gateway.test_gateway_model`
|
||||
- `20 tests OK`
|
||||
- `python -m compileall app engine_gateway`
|
||||
- Passed
|
||||
- Web production build:
|
||||
- `cd apps/web; fnm use 22.22.3; npm run build`
|
||||
- Passed
|
||||
- Docker compose config:
|
||||
- `cd infra`
|
||||
- With dummy required env vars, `docker compose --env-file .env.example config --quiet`
|
||||
- Passed
|
||||
- Focused Playwright after fixes:
|
||||
- `npx playwright test e2e/admin.spec.ts e2e/db-persistence.spec.ts e2e/voice-success.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run`
|
||||
- `10 passed`
|
||||
- Admin manage-users focused rerun:
|
||||
- `PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/admin.spec.ts --project=chromium-desktop --project=chromium-mobile --grep "manage real server-known users"`
|
||||
- `2 passed`
|
||||
- Full Playwright:
|
||||
- `cd apps/web; PLAYWRIGHT_HOST=127.0.0.1 npx playwright test`
|
||||
- `72 passed` after public API/cloudflared was restored
|
||||
- 참고: tunnel을 내린 직후 첫 전체 실행은 public login gate 2개만 `Cloudflare 1033`으로 실패하고 나머지 `70 passed`였다. `scripts\start-public-runtime.ps1`로 prod public API와 cloudflared를 복구한 뒤 전체 재실행에서 `72 passed`.
|
||||
- Public auth project discovery:
|
||||
- `E2E_PUBLIC_AUTH=1 npx playwright test --list --project=chromium-public-auth`
|
||||
- `2 tests`: public production-safe config + real OAuth `/turn`
|
||||
- Public production-safe gate:
|
||||
- `E2E_PUBLIC_AUTH=1 npx playwright test e2e/public-auth-turn.spec.ts --project=chromium-public-auth --grep "production-safe"`
|
||||
- `1 passed` on rerun
|
||||
- Admin/settings focused E2E:
|
||||
- `npx playwright test e2e/admin.spec.ts e2e/settings.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run`
|
||||
- Latest focused reruns: `admin.spec.ts` `8 passed`, `settings.spec.ts` `9 passed`
|
||||
- Auth/readiness focused E2E:
|
||||
- `npx playwright test e2e/auth.spec.ts e2e/readiness.spec.ts --project=chromium-desktop --project=chromium-mobile`
|
||||
- `14 passed`
|
||||
- Persona auth boundary:
|
||||
- local unauth `http://127.0.0.1:8000/personas` -> 401
|
||||
- public unauth `https://api-vignette.chanpaca.net/personas` -> 401
|
||||
- Public runtime script:
|
||||
- `powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1`
|
||||
- API 8001 health and public `https://api-vignette.chanpaca.net/health` both returned `environment:"prod"`, `db:true`, `engine:true`
|
||||
- Public login screen:
|
||||
- `npx playwright test e2e/auth.spec.ts --project=chromium-desktop --grep "public login"`
|
||||
- `1 passed`
|
||||
- Local login on `127.0.0.1:5175`:
|
||||
- `PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/auth.spec.ts --project=chromium-desktop`
|
||||
- `6 passed`
|
||||
- `PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/auth.spec.ts --project=chromium-mobile`
|
||||
- `6 passed`
|
||||
- focused local dev-login redirect: `1 passed`
|
||||
- Learner home/history focused verification:
|
||||
- `cd apps/web; npm run typecheck`
|
||||
- Passed
|
||||
- `PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/learner.spec.ts e2e/readiness.spec.ts --project=chromium-desktop --project=chromium-mobile`
|
||||
- `14 passed`
|
||||
- Visual audit screenshots: `apps/web/node_modules/.tmp/ui-audit/learn-empty-desktop.png`, `apps/web/node_modules/.tmp/ui-audit/learn-empty-mobile-compact.png`
|
||||
- Screenshot metrics confirmed desktop/mobile document overflow `x:0, y:0`, empty history visible, and real stats area present.
|
||||
- Session dense viewport verification:
|
||||
- `PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile`
|
||||
- `8 passed`
|
||||
- Latest voice/session focused E2E:
|
||||
- `PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/voice.spec.ts e2e/voice-success.spec.ts e2e/session-layout.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run`
|
||||
- `15 passed`
|
||||
- Latest learner/session/settings/admin focused E2E:
|
||||
- `PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 PLAYWRIGHT_SKIP_WEB_SERVER=1 npx playwright test e2e/learner.spec.ts e2e/session-layout.spec.ts e2e/settings.spec.ts e2e/admin.spec.ts --project=chromium-desktop --project=chromium-mobile --project=chromium-single-run`
|
||||
- `37 passed`
|
||||
|
||||
### 2. 음성 입력 — `services/voice.py` + `routes/voice.py`(WSS) + `Session.tsx` 마이크
|
||||
윤찬: "크롬으로 하니까 내 음성이 안 들어감. 브라우저 어떻게 해야하는지 해야함."
|
||||
- (a) 크롬 **마이크 권한** — HTTPS(vignette.chanpaca.net)는 OK, getUserMedia 권한 요청 UI 필요
|
||||
- (b) **음성 파이프라인 실연동** — STT(OpenAI whisper) → orchestrator turn → TTS. WSS `/api/voice/ws`(Caddyfile passthrough 있음). Session.tsx 마이크 버튼이 현재 UI만 — WSS 연동 필요.
|
||||
- ⚠️ **OpenAI 키 필요**: 현재 `.env`/API env의 `OPENAI_API_KEY`가 dummy. 윤찬에게 실키 받아야 STT/TTS 동작. 키 없으면 services/voice.py가 degraded(501/503).
|
||||
## 아직 못 끝낸 것
|
||||
|
||||
## 외부 블로커 (자율 불가)
|
||||
- **n=1 실 임상 축어록**: 한신대 교수 회신 필요. 요청 메일초안 = `Projects/영업/2026/한신대학교 산학협력 프로젝트/축어록_추가요청_메일초안.md` (윤찬 발송 대기). 합성 데이터로 개발 단계는 완화함.
|
||||
- 공개 Google OAuth 실제 `/turn` proof는 아직 없다.
|
||||
- 이유: `E2E_PUBLIC_STORAGE_STATE=apps/web/node_modules/.tmp/public-auth.json`가 아직 확보되지 않았다.
|
||||
- 사용자가 Playwright codegen 브라우저에서 Google 로그인 후 storageState를 저장해야 한다.
|
||||
- 2026-06-26 14:43 KST 기준 `apps/web/node_modules/.tmp/public-auth.json`은 없다. codegen 창/프로세스가 남아 있으면 로그인 완료 후 창을 닫아 저장을 완료하면 된다.
|
||||
- 공개 API `environment:"dev"` 문제는 이번 세션에서 해결했다. cloudflared가 prod API인 `127.0.0.1:8001`을 보도록 변경했고 public gate로 확인했다.
|
||||
- Google Console 설정은 브라우저 로그인 세션이 필요하다. 막히면 사용자가 로그인해 주기로 했다.
|
||||
- worktree는 원래부터 매우 dirty이고, 이전 세션/사용자 변경이 많다. 관련 없는 변경을 되돌리지 말 것.
|
||||
|
||||
## 핵심 함정/패턴 (꼭 지킬 것)
|
||||
- **빌드는 반드시 node22** — node23.4.0은 vite **segfault**(출력 0줄). PowerShell: `fnm env --use-on-cd | Out-String | Invoke-Expression; fnm use 22.22.3`
|
||||
- 프론트 빌드: `$env:VITE_API_BASE="https://api-vignette.chanpaca.net"; npm run build`
|
||||
- 배포: `wrangler pages deploy dist --project-name vignette --branch main --commit-dirty=true` (먼저 `dist/_redirects`에 `/* /index.html 200`)
|
||||
- **deployment 미리보기 URL은 CORS 불일치** → 검증은 production(vignette.chanpaca.net) URL로
|
||||
- 라이브 검증 도구: `apps/web/_pptr_flow.cjs` (puppeteer-core + 시스템 Chrome, mock auth 주입 후 풀흐름 클릭) — `node _pptr_flow.cjs "https://vignette.chanpaca.net" <out.png>`
|
||||
- dev 인증 우회: `ENVIRONMENT=dev`면 쿠키 없어도 통과(deps.py). 프론트 mock auth = localStorage `vignette.dev-auth`
|
||||
- 한글 JSON은 **python httpx로** 보낼 것(curl -d 한글 인코딩 깨짐)
|
||||
- Cloudflare 토큰: wrangler oauth(`%APPDATA%\xdg.config\.wrangler\config\default.toml`) / DNS 권한은 cloudflared `cert.pem`의 ARGO TOKEN apiToken
|
||||
## 다음 세션 우선순위
|
||||
|
||||
## 윤찬 작업방식 (위반 주의)
|
||||
- **완료보고 불신** — 실화면 인터랙션(puppeteer 클릭)까지 검증 후 보고. CORS/API 200만으론 "완료" 금지.
|
||||
- **목업 싫어함** — 가짜 데이터로 화면 채우지 말 것. 빈 상태/실데이터.
|
||||
- **디코로 답변 필수** — chat_id 1485472826118115338, inbound는 reply로.
|
||||
- 위험은 한 번 알리고 주저말고 진행(확인 반복 금지). 단 Docker 같은 명시 가드는 존중.
|
||||
1. Google OAuth Console을 확인한다.
|
||||
- Authorized redirect URI: `https://api-vignette.chanpaca.net/auth/callback`
|
||||
- Authorized JavaScript origin: `https://vignette.chanpaca.net`
|
||||
- 앱/도메인 설정은 `hs.ac.kr`, `twentyoz.kr` 계정만 서버 claim 검증으로 허용한다. Google Console의 authorized domains와 이메일 도메인 제한은 다른 개념이다.
|
||||
2. storageState를 캡처하고 public `/turn` smoke를 실행한다.
|
||||
- `cd D:\workspace\vignette\apps\web`
|
||||
- `npx playwright codegen https://vignette.chanpaca.net/login --save-storage=.\node_modules\.tmp\public-auth.json`
|
||||
- `$env:E2E_PUBLIC_AUTH="1"`
|
||||
- `$env:E2E_PUBLIC_STORAGE_STATE=".\node_modules\.tmp\public-auth.json"`
|
||||
- `.\node_modules\.bin\playwright.cmd test e2e/public-auth-turn.spec.ts --project=chromium-public-auth`
|
||||
3. UI/UX는 계속 `docs/DESIGN_CONCEPT.md`를 따른다.
|
||||
- learner home은 빈 사용자에게 fake 통계/기록을 보여주지 않는다. 0회 상태는 실제 서버 기록 0개로 `기존 회기` 빈 상태를 보여주는 현재 구현을 유지한다.
|
||||
- active learning session은 전체 화면, 문서 스크롤 없음이 원칙이다.
|
||||
- 실제 session history는 `기존 회기`, `이어하기`, `기록/리뷰`, `다시 연습`, `새 회기 시작` 흐름으로 유지한다.
|
||||
4. 다음 배포 전 `admin/engine-config`를 다시 확인한다.
|
||||
- 의도된 현재값이 `claude_cli / http://127.0.0.1:9099 / gateway-default`인지 확인.
|
||||
- E2E가 engine config를 건드리므로 배포 전 실제 운영값을 반드시 재확인한다.
|
||||
|
||||
## 로컬 실행 명령
|
||||
|
||||
Engine gateway:
|
||||
|
||||
```powershell
|
||||
cd D:\workspace\vignette\apps\api
|
||||
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m uvicorn engine_gateway.gateway:app --host 127.0.0.1 --port 9099
|
||||
```
|
||||
|
||||
API:
|
||||
|
||||
```powershell
|
||||
cd D:\workspace\vignette\apps\api
|
||||
$env:ENGINE_URL="http://127.0.0.1:9099"
|
||||
$env:ENGINE_MODE="claude_cli"
|
||||
$env:ENVIRONMENT="dev"
|
||||
$env:AUTH_DEV_LOGIN_ENABLED="true"
|
||||
C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
Public API process:
|
||||
|
||||
```powershell
|
||||
cd D:\workspace\vignette
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1
|
||||
```
|
||||
|
||||
Web:
|
||||
|
||||
```powershell
|
||||
cd D:\workspace\vignette\apps\web
|
||||
fnm env --use-on-cd | Out-String | Invoke-Expression
|
||||
fnm use 22.22.3
|
||||
npm run dev -- --host localhost --port 5173
|
||||
```
|
||||
|
||||
## 최근 주요 변경 파일
|
||||
|
||||
- `apps/web/e2e/support.ts`
|
||||
- `apps/web/e2e/auth.spec.ts`
|
||||
- `apps/web/e2e/learner.spec.ts`
|
||||
- `apps/web/e2e/session-layout.spec.ts`
|
||||
- `apps/web/e2e/session-review.spec.ts`
|
||||
- `apps/web/e2e/session-persistence.spec.ts`
|
||||
- `apps/web/e2e/teacher.spec.ts`
|
||||
- `apps/web/e2e/voice.spec.ts`
|
||||
- `apps/web/e2e/voice-success.spec.ts`
|
||||
- `apps/web/e2e/public-auth-turn.spec.ts`
|
||||
- `apps/web/e2e/db-persistence.spec.ts`
|
||||
- `apps/web/e2e/README.md`
|
||||
- `apps/web/playwright.config.ts`
|
||||
- `apps/web/src/pages/Admin.tsx`
|
||||
- `apps/web/src/pages/Login.tsx`
|
||||
- `apps/web/src/pages/Session.tsx`
|
||||
- `apps/web/src/pages/Settings.tsx`
|
||||
- `apps/web/src/pages/session/session.css`
|
||||
- `apps/web/src/components/avatar/ClientAvatar.tsx`
|
||||
- `apps/web/src/components/avatar/persona.ts`
|
||||
- `apps/web/public/live2d/README.md`
|
||||
- `apps/api/app/config.py`
|
||||
- `apps/api/app/main.py`
|
||||
- `apps/api/app/routes/admin.py`
|
||||
- `apps/api/app/routes/personas.py`
|
||||
- `apps/api/app/routes/users.py`
|
||||
- `apps/api/app/routes/voice.py`
|
||||
- `apps/api/app/test_session_turn_persistence.py`
|
||||
- `apps/api/app/test_runtime_policy.py`
|
||||
- `infra/docker-compose.yml`
|
||||
- `infra/.env.example`
|
||||
- `docs/HANDOFF.md`
|
||||
- `docs/dev_dashboard.html`
|
||||
- `apps/web/src/lib/api.ts`
|
||||
|
|
|
|||
|
|
@ -1,602 +1,378 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vignette — 개발 오버뷰 (PM 보드)</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable.min.css">
|
||||
<style>
|
||||
/* =====================================================================
|
||||
Vignette — 개발 오버뷰 대시보드 (내부 PM 보드, 앱 화면 아님)
|
||||
토큰: DESIGN_CONCEPT.md §3 그대로 계승. accent = 세이지-틸(학습자 톤).
|
||||
철칙: border-left 0 · 이모지 0(아이콘 inline SVG) · 카드덤프 0 ·
|
||||
순흑/순백 금지 · 강조는 weight/tint/kicker/dot로만 · 한 화면 한 목적.
|
||||
빨강은 리스크에만.
|
||||
===================================================================== */
|
||||
:root{
|
||||
/* 중성 베이스 (따뜻한 종이 + 잉크 네이비-그린) */
|
||||
--paper:#FBFAF8; --paper-2:#F4F2EE; --surface:#FFFFFF;
|
||||
--neutral-100:#ECEFF1; --neutral-150:#E5E1DA; --neutral-200:#D5DBDE; --neutral-400:#98A4A9;
|
||||
--ink:#1C2A2A; --ink-2:#5A6663; --ink-3:#93A09C; --hair:#E5E1DA;
|
||||
/* 악센트: 세이지-틸 (프로젝트 톤) */
|
||||
--accent:#3E7A6E; --accent-deep:#316257; --accent-tint:#EEF4F2; --accent-bright:#5F968B;
|
||||
/* 보조 온기 (테라코타) — 화면당 절제 */
|
||||
--clay:#B0735C; --clay-deep:#94604C; --clay-tint:#F6F0EC;
|
||||
/* 의미색 */
|
||||
--pos-text:#2C6E49; --pos-solid:#3B8C5E; --pos-tint:#E8F3EC;
|
||||
--warn-text:#8A5A00; --warn-solid:#C28A2C; --warn-tint:#FAF1E0;
|
||||
--crit-text:#A8352C; --crit-solid:#C4453A; --crit-tint:#FAEAE8;
|
||||
--info-text:#2E5A7A; --info-solid:#3C7299; --info-tint:#EAF1F6;
|
||||
--neutral-sig:#8895A2;
|
||||
/* 별칭 */
|
||||
--text-on-accent:#FBFAF8;
|
||||
--focus-ring:rgba(62,122,110,.45);
|
||||
/* 폰트 */
|
||||
--font-sans:"Pretendard Variable",Pretendard,-apple-system,"Apple SD Gothic Neo","Noto Sans KR",system-ui,sans-serif;
|
||||
--font-num:"Pretendard Variable","JetBrains Mono",ui-monospace,monospace;
|
||||
/* spacing */
|
||||
--sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:24px; --sp-6:32px; --sp-7:48px; --sp-8:64px;
|
||||
--radius:8px; --radius-sm:6px; --radius-lg:12px;
|
||||
--maxw:1200px;
|
||||
--shadow-sm:0 1px 2px rgba(28,42,42,.05);
|
||||
--shadow-md:0 4px 14px rgba(28,42,42,.07);
|
||||
--ease-out:cubic-bezier(.22,.61,.36,1);
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box;}
|
||||
html,body{height:100%;}
|
||||
body{
|
||||
font-family:var(--font-sans); background:var(--paper); color:var(--ink);
|
||||
-webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
|
||||
letter-spacing:-0.01em; line-height:1.6;
|
||||
}
|
||||
.tab-num{font-variant-numeric:tabular-nums; letter-spacing:-0.01em;}
|
||||
|
||||
/* ============ 공통 앱 셸: 톱바 ============ */
|
||||
.topbar{
|
||||
height:56px; background:var(--surface); border-bottom:1px solid var(--hair);
|
||||
display:flex; align-items:center; gap:var(--sp-6); padding:0 var(--sp-6);
|
||||
position:sticky; top:0; z-index:20;
|
||||
}
|
||||
.brand{display:flex; align-items:center; gap:10px;}
|
||||
.brand .mark{display:flex; align-items:center; justify-content:center; width:26px; height:26px;}
|
||||
.brand .mark svg{width:26px; height:26px;}
|
||||
.brand .wm{font-size:17px; font-weight:700; letter-spacing:-0.02em; color:var(--ink);}
|
||||
.brand .wm .v{color:var(--accent);}
|
||||
/* 컨텍스트 라벨 (mono accent, 조용한 신호) */
|
||||
.rolelabel{
|
||||
font-family:var(--font-num); font-size:13px; font-weight:600;
|
||||
letter-spacing:0.04em; color:var(--accent);
|
||||
padding-left:var(--sp-4); margin-left:var(--sp-2);
|
||||
border-left:1px solid var(--hair); /* 셸 구획 디바이더(강조선 아님) */
|
||||
}
|
||||
.topbar .spacer{flex:1;}
|
||||
.repo{
|
||||
font-family:var(--font-num); font-size:12.5px; color:var(--ink-3);
|
||||
display:inline-flex; align-items:center; gap:7px;
|
||||
}
|
||||
.repo svg{width:14px; height:14px; stroke:var(--ink-3); fill:none;}
|
||||
.repo .priv{
|
||||
font-size:11px; font-weight:600; color:var(--ink-2); background:var(--paper-2);
|
||||
padding:2px 8px; border-radius:100px; letter-spacing:0.02em;
|
||||
}
|
||||
.topbar .when{
|
||||
display:flex; align-items:center; gap:8px; font-size:13px; color:var(--ink-2);
|
||||
font-weight:500; margin-left:var(--sp-2);
|
||||
}
|
||||
.topbar .when .dot{width:7px; height:7px; border-radius:50%; background:var(--accent);}
|
||||
.topbar .when .d{font-family:var(--font-num); font-variant-numeric:tabular-nums; color:var(--ink); font-weight:600;}
|
||||
|
||||
/* ============ 본문 래퍼 ============ */
|
||||
.wrap{max-width:var(--maxw); margin:0 auto; padding:var(--sp-7) var(--sp-6) var(--sp-8);}
|
||||
.kicker{
|
||||
font-family:var(--font-num); font-size:12px; font-weight:600;
|
||||
letter-spacing:0.08em; color:var(--accent);
|
||||
display:flex; align-items:center; gap:8px;
|
||||
}
|
||||
.kicker .dot{width:6px; height:6px; border-radius:50%; background:var(--accent-bright);}
|
||||
|
||||
/* 페이지 헤드라인 (한 화면 한 메시지) */
|
||||
.head{margin-bottom:var(--sp-7);}
|
||||
.head .kicker{margin-bottom:var(--sp-3);}
|
||||
.head h1{font-size:30px; font-weight:700; letter-spacing:-0.02em; color:var(--ink); line-height:1.3;}
|
||||
.head h1 em{font-style:normal; color:var(--accent-deep);}
|
||||
.head .sub{font-size:15px; color:var(--ink-3); margin-top:8px; max-width:680px;}
|
||||
|
||||
/* 헤드라인 옆 요약 스탯 라인 */
|
||||
.headstats{
|
||||
display:flex; align-items:center; flex-wrap:wrap; gap:var(--sp-5);
|
||||
margin-top:var(--sp-5); padding-top:var(--sp-5); border-top:1px solid var(--hair);
|
||||
}
|
||||
.headstats .stat{display:inline-flex; align-items:baseline; gap:7px;}
|
||||
.headstats .stat .num{font-size:20px; font-weight:700; color:var(--ink); font-variant-numeric:tabular-nums; letter-spacing:-0.02em;}
|
||||
.headstats .stat .lab{font-size:13.5px; color:var(--ink-3);}
|
||||
.headstats .vsep{width:1px; height:18px; background:var(--hair);}
|
||||
|
||||
/* 섹션 헤더 */
|
||||
.sechead{margin-bottom:var(--sp-5);}
|
||||
.sechead .kicker{margin-bottom:6px;}
|
||||
.sechead .desc{font-size:13px; color:var(--ink-3);}
|
||||
|
||||
.block{margin-bottom:var(--sp-8);}
|
||||
|
||||
/* ============ 1. 로드맵 4단계 진행 막대 ============ */
|
||||
.roadmap{
|
||||
background:var(--surface); border:1px solid var(--hair); border-radius:var(--radius-lg);
|
||||
box-shadow:var(--shadow-sm); padding:var(--sp-6) var(--sp-6) var(--sp-5);
|
||||
}
|
||||
.phase-grid{display:grid; grid-template-columns:repeat(4,1fr); gap:var(--sp-5);}
|
||||
.phase .ph-top{display:flex; align-items:baseline; justify-content:space-between; margin-bottom:9px;}
|
||||
.phase .ph-id{font-family:var(--font-num); font-size:12px; font-weight:700; letter-spacing:0.04em; color:var(--ink-3);}
|
||||
.phase .ph-pct{font-family:var(--font-num); font-size:13px; font-weight:700; font-variant-numeric:tabular-nums; color:var(--ink-2);}
|
||||
.phase.is-active .ph-pct{color:var(--accent-deep);}
|
||||
.phase .ph-name{font-size:15px; font-weight:600; color:var(--ink); letter-spacing:-0.01em; line-height:1.4; margin-bottom:3px;}
|
||||
.phase .ph-weeks{font-size:12px; color:var(--ink-3); font-variant-numeric:tabular-nums; margin-bottom:11px;}
|
||||
.phase .track{height:8px; background:var(--paper-2); border-radius:4px; overflow:hidden;}
|
||||
.phase .track .fill{height:100%; border-radius:4px; background:var(--neutral-150);}
|
||||
.phase.is-active .track .fill{background:var(--accent);}
|
||||
.phase .ph-state{
|
||||
margin-top:11px; display:inline-flex; align-items:center; gap:7px;
|
||||
font-size:12.5px; font-weight:500;
|
||||
}
|
||||
.phase .ph-state .d{width:7px; height:7px; border-radius:50%; background:var(--neutral-200);}
|
||||
.phase .ph-state.active{color:var(--accent-deep); font-weight:600;}
|
||||
.phase .ph-state.active .d{background:var(--accent);}
|
||||
.phase .ph-state.wait{color:var(--ink-3);}
|
||||
.phase .ph-note{font-size:11.5px; color:var(--ink-3); margin-top:7px; line-height:1.5;}
|
||||
/* 단계 사이 연결 디바이더 (시각 흐름) */
|
||||
.phase{position:relative;}
|
||||
.phase:not(:first-child)::before{
|
||||
content:""; position:absolute; left:calc(-1 * var(--sp-5) / 2 - 0.5px); top:7px; bottom:7px;
|
||||
width:1px; background:var(--hair);
|
||||
}
|
||||
|
||||
/* ============ 2단 본문 (좌: 완료/다음 · 우: 결정/리스크) ============ */
|
||||
.cols{display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-7); align-items:start;}
|
||||
|
||||
/* 완료 체크리스트 */
|
||||
.panel{
|
||||
background:var(--surface); border:1px solid var(--hair); border-radius:var(--radius-lg);
|
||||
box-shadow:var(--shadow-sm); padding:var(--sp-6);
|
||||
}
|
||||
.panel + .panel{margin-top:var(--sp-6);}
|
||||
.panel .ph{font-size:16px; font-weight:600; color:var(--ink); letter-spacing:-0.01em; margin-bottom:var(--sp-2);}
|
||||
.panel .pd{font-size:12.5px; color:var(--ink-3); margin-bottom:var(--sp-5);}
|
||||
|
||||
.checks{display:flex; flex-direction:column;}
|
||||
.chk-group + .chk-group{margin-top:var(--sp-5);}
|
||||
.chk-glabel{
|
||||
font-family:var(--font-num); font-size:11px; font-weight:600; letter-spacing:0.06em;
|
||||
color:var(--ink-3); text-transform:uppercase; margin-bottom:10px;
|
||||
}
|
||||
.chk{display:flex; align-items:flex-start; gap:11px; padding:7px 0;}
|
||||
.chk + .chk{border-top:1px solid var(--paper-2);}
|
||||
.chk .mk{
|
||||
flex:none; width:18px; height:18px; border-radius:50%; margin-top:1px;
|
||||
background:var(--pos-tint); display:flex; align-items:center; justify-content:center;
|
||||
}
|
||||
.chk .mk svg{width:11px; height:11px; stroke:var(--pos-solid); fill:none; stroke-width:2.6;}
|
||||
.chk .ct{font-size:13.5px; color:var(--ink); line-height:1.5;}
|
||||
.chk .ct b{font-weight:600;}
|
||||
.chk .ct .tag{
|
||||
display:inline-block; margin-left:6px; font-family:var(--font-num); font-size:10.5px; font-weight:600;
|
||||
letter-spacing:0.02em; color:var(--accent-deep); background:var(--accent-tint);
|
||||
padding:1px 7px; border-radius:100px; vertical-align:middle;
|
||||
}
|
||||
.chk .ct .verified{color:var(--pos-text); background:var(--pos-tint);}
|
||||
|
||||
/* 진행중 / 다음 (도트 리스트) */
|
||||
.nextlist{display:flex; flex-direction:column;}
|
||||
.nx{display:flex; align-items:flex-start; gap:11px; padding:10px 0;}
|
||||
.nx + .nx{border-top:1px solid var(--paper-2);}
|
||||
.nx .nd{flex:none; width:8px; height:8px; border-radius:50%; margin-top:7px;}
|
||||
.nx .nd.doing{background:var(--accent);}
|
||||
.nx .nd.queued{background:var(--neutral-200);}
|
||||
.nx .nt{font-size:13.5px; color:var(--ink); line-height:1.5;}
|
||||
.nx .nt b{font-weight:600;}
|
||||
.nx .nt .st{
|
||||
display:inline-block; margin-left:6px; font-size:11px; font-weight:600;
|
||||
padding:1px 8px; border-radius:100px; vertical-align:middle;
|
||||
}
|
||||
.nx .nt .st.doing{color:var(--accent-deep); background:var(--accent-tint);}
|
||||
.nx .nt .st.blocked{color:var(--crit-text); background:var(--crit-tint);}
|
||||
.nx .nt .st.queued{color:var(--ink-3); background:var(--paper-2);}
|
||||
.nx .nt .why{display:block; font-size:12px; color:var(--ink-3); margin-top:3px; line-height:1.5;}
|
||||
|
||||
/* 확정 결정 */
|
||||
.decisions{display:flex; flex-direction:column;}
|
||||
.dc{display:grid; grid-template-columns:96px 1fr; align-items:baseline; gap:var(--sp-4); padding:11px 0;}
|
||||
.dc + .dc{border-top:1px solid var(--paper-2);}
|
||||
.dc .dk{font-size:12.5px; font-weight:600; color:var(--ink-3); letter-spacing:-0.01em;}
|
||||
.dc .dv{font-size:13.5px; color:var(--ink); line-height:1.5;}
|
||||
.dc .dv b{font-weight:600; color:var(--accent-deep);}
|
||||
.dc.span{grid-template-columns:1fr;}
|
||||
.dc.span .dk{
|
||||
font-family:var(--font-num); font-size:11px; letter-spacing:0.06em; text-transform:uppercase;
|
||||
color:var(--ink-3); margin-bottom:8px;
|
||||
}
|
||||
.dc .stack-chips{display:flex; flex-wrap:wrap; gap:7px; margin-top:3px;}
|
||||
.dc .stack-chips .c{
|
||||
font-family:var(--font-num); font-size:11.5px; font-weight:500; color:var(--ink-2);
|
||||
background:var(--paper-2); padding:4px 10px; border-radius:100px;
|
||||
}
|
||||
.dc .stack-chips .c.key{color:var(--accent-deep); background:var(--accent-tint); font-weight:600;}
|
||||
|
||||
/* 리스크 · 블로커 (빨강은 여기에만) */
|
||||
.risks{display:flex; flex-direction:column;}
|
||||
.rk{display:flex; align-items:flex-start; gap:12px; padding:13px 0;}
|
||||
.rk + .rk{border-top:1px solid var(--paper-2);}
|
||||
.rk .sev{
|
||||
flex:none; margin-top:3px; display:inline-flex; align-items:center; gap:7px;
|
||||
font-family:var(--font-num); font-size:10.5px; font-weight:700; letter-spacing:0.04em;
|
||||
width:64px;
|
||||
}
|
||||
.rk .sev .d{width:9px; height:9px; border-radius:50%;}
|
||||
.rk .sev.high{color:var(--crit-text);} .rk .sev.high .d{background:var(--crit-solid);}
|
||||
.rk .sev.mid{color:var(--warn-text);} .rk .sev.mid .d{background:var(--warn-solid);}
|
||||
.rk .rbody .rt{font-size:13.5px; font-weight:600; color:var(--ink); line-height:1.45;}
|
||||
.rk .rbody .rd{font-size:12.5px; color:var(--ink-2); line-height:1.55; margin-top:3px;}
|
||||
.rk .rbody .rd .ext{
|
||||
font-size:11px; font-weight:600; color:var(--clay-deep); background:var(--clay-tint);
|
||||
padding:1px 7px; border-radius:100px; margin-left:4px;
|
||||
}
|
||||
|
||||
/* ============ 3. 산출물 인벤토리 (테이블) ============ */
|
||||
.inv-tbl{width:100%; border-collapse:collapse; font-size:14px;}
|
||||
.inv-tbl thead th{
|
||||
font-size:12px; font-weight:600; color:var(--ink-3); letter-spacing:0.02em;
|
||||
text-align:left; padding:0 var(--sp-3) var(--sp-3); border-bottom:1px solid var(--neutral-200);
|
||||
white-space:nowrap;
|
||||
}
|
||||
.inv-tbl tbody td{padding:var(--sp-4) var(--sp-3); border-bottom:1px solid var(--paper-2); vertical-align:top;}
|
||||
.inv-tbl tbody tr:last-child td{border-bottom:none;}
|
||||
.inv-tbl .area{font-weight:600; color:var(--ink); white-space:nowrap;}
|
||||
.inv-tbl .area .kd{
|
||||
display:inline-flex; align-items:center; gap:8px;
|
||||
}
|
||||
.inv-tbl .area .kd .d{width:7px; height:7px; border-radius:50%; background:var(--accent-bright);}
|
||||
.inv-tbl .what{color:var(--ink-2); line-height:1.55;}
|
||||
.inv-tbl .path{
|
||||
font-family:var(--font-num); font-size:12px; color:var(--ink-3); line-height:1.7;
|
||||
white-space:nowrap;
|
||||
}
|
||||
.inv-tbl .path code{
|
||||
background:var(--paper-2); color:var(--ink-2); padding:2px 7px; border-radius:var(--radius-sm);
|
||||
font-family:var(--font-num); display:inline-block; margin:1px 4px 1px 0;
|
||||
}
|
||||
.inv-tbl .stt{white-space:nowrap;}
|
||||
.invtag{display:inline-flex; align-items:center; gap:7px; font-size:12.5px; font-weight:500;}
|
||||
.invtag .d{width:7px; height:7px; border-radius:50%;}
|
||||
.invtag.done{color:var(--pos-text);} .invtag.done .d{background:var(--pos-solid);}
|
||||
.invtag.partial{color:var(--warn-text);} .invtag.partial .d{background:var(--warn-solid);}
|
||||
|
||||
/* 푸터 메타 */
|
||||
.foot{
|
||||
margin-top:var(--sp-6); display:flex; align-items:center; flex-wrap:wrap; gap:var(--sp-4);
|
||||
font-size:12px; color:var(--ink-3); font-variant-numeric:tabular-nums;
|
||||
}
|
||||
.foot .fdot{width:5px; height:5px; border-radius:50%; background:var(--neutral-200);}
|
||||
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
*,*::before,*::after{animation-duration:.01ms!important; transition-duration:.01ms!important;}
|
||||
}
|
||||
@media (max-width:980px){
|
||||
.phase-grid{grid-template-columns:repeat(2,1fr); gap:var(--sp-6);}
|
||||
.phase:not(:first-child)::before{display:none;}
|
||||
.cols{grid-template-columns:1fr; gap:var(--sp-6);}
|
||||
.repo,.topbar .when{display:none;}
|
||||
.inv-tbl .path{white-space:normal;}
|
||||
}
|
||||
@media (max-width:560px){
|
||||
.phase-grid{grid-template-columns:1fr;}
|
||||
}
|
||||
</style>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Vignette 개발 대시보드</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--paper:#FBFAF8;
|
||||
--paper-2:#F4F2EE;
|
||||
--surface:#FFFFFF;
|
||||
--hair:#E5E1DA;
|
||||
--ink:#1C2A2A;
|
||||
--ink-2:#5A6663;
|
||||
--ink-3:#93A09C;
|
||||
--accent:#3E7A6E;
|
||||
--accent-deep:#316257;
|
||||
--accent-tint:#EEF4F2;
|
||||
--clay:#B0735C;
|
||||
--pos:#2C6E49;
|
||||
--warn:#8A5A00;
|
||||
--crit:#A8352C;
|
||||
--pos-bg:#E8F3EC;
|
||||
--warn-bg:#FAF1E0;
|
||||
--crit-bg:#FAEAE8;
|
||||
--info-bg:#EAF1F6;
|
||||
--font:"Pretendard Variable", Pretendard, -apple-system, "Apple SD Gothic Neo", "Noto Sans KR", system-ui, sans-serif;
|
||||
--num:"Pretendard Variable", "JetBrains Mono", ui-monospace, monospace;
|
||||
--radius:8px;
|
||||
}
|
||||
* { box-sizing:border-box; }
|
||||
body {
|
||||
margin:0;
|
||||
background:var(--paper);
|
||||
color:var(--ink);
|
||||
font-family:var(--font);
|
||||
line-height:1.55;
|
||||
letter-spacing:0;
|
||||
}
|
||||
header {
|
||||
position:sticky;
|
||||
top:0;
|
||||
z-index:2;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:24px;
|
||||
min-height:58px;
|
||||
padding:0 32px;
|
||||
background:rgba(255,255,255,.92);
|
||||
border-bottom:1px solid var(--hair);
|
||||
backdrop-filter:blur(10px);
|
||||
}
|
||||
.brand { display:flex; align-items:center; gap:10px; font-weight:760; }
|
||||
.mark { width:24px; height:24px; border:2px solid var(--accent); border-radius:50%; position:relative; }
|
||||
.mark:after { content:""; width:6px; height:6px; border-radius:50%; background:var(--clay); position:absolute; left:7px; top:6px; }
|
||||
.stamp { color:var(--ink-3); font:600 12px/1 var(--num); }
|
||||
main { width:min(1180px, calc(100% - 40px)); margin:0 auto; padding:42px 0 64px; }
|
||||
.hero { display:grid; grid-template-columns:minmax(0,1.2fr) minmax(280px,.8fr); gap:28px; align-items:start; margin-bottom:34px; }
|
||||
.kicker { display:flex; align-items:center; gap:8px; color:var(--accent); font:700 12px/1.3 var(--num); letter-spacing:.08em; text-transform:uppercase; }
|
||||
.kicker:before { content:""; width:6px; height:6px; border-radius:50%; background:var(--accent); }
|
||||
h1 { margin:12px 0 0; font-size:30px; line-height:1.28; letter-spacing:0; }
|
||||
h1 em { color:var(--accent-deep); font-style:normal; }
|
||||
.lead { margin:12px 0 0; color:var(--ink-2); max-width:760px; font-size:15px; }
|
||||
.summary {
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2,minmax(0,1fr));
|
||||
gap:1px;
|
||||
overflow:hidden;
|
||||
border:1px solid var(--hair);
|
||||
border-radius:12px;
|
||||
background:var(--hair);
|
||||
}
|
||||
.metric { min-width:0; padding:18px; background:var(--surface); }
|
||||
.metric span { display:block; color:var(--ink-3); font-size:12px; font-weight:650; }
|
||||
.metric b { display:block; margin-top:6px; color:var(--ink); font:760 28px/1 var(--num); }
|
||||
section { margin-top:30px; }
|
||||
.section-head { display:flex; align-items:end; justify-content:space-between; gap:18px; margin-bottom:14px; }
|
||||
.section-head h2 { margin:0; font-size:20px; line-height:1.3; }
|
||||
.section-head p { margin:4px 0 0; color:var(--ink-3); font-size:13px; }
|
||||
.grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:16px; }
|
||||
.panel {
|
||||
min-width:0;
|
||||
background:var(--surface);
|
||||
border:1px solid var(--hair);
|
||||
border-radius:12px;
|
||||
padding:20px;
|
||||
box-shadow:0 1px 2px rgba(28,42,42,.05);
|
||||
}
|
||||
.panel h3 { margin:0 0 10px; font-size:16px; line-height:1.35; }
|
||||
.panel p { margin:0; color:var(--ink-2); font-size:13px; }
|
||||
.tag { display:inline-flex; align-items:center; gap:6px; margin-bottom:12px; padding:3px 9px; border-radius:999px; font-size:12px; font-weight:680; }
|
||||
.tag:before { content:""; width:7px; height:7px; border-radius:50%; }
|
||||
.done { color:var(--pos); background:var(--pos-bg); }
|
||||
.done:before { background:var(--pos); }
|
||||
.doing { color:var(--warn); background:var(--warn-bg); }
|
||||
.doing:before { background:var(--warn); }
|
||||
.risk { color:var(--crit); background:var(--crit-bg); }
|
||||
.risk:before { background:var(--crit); }
|
||||
.info { color:#2E5A7A; background:var(--info-bg); }
|
||||
.info:before { background:#3C7299; }
|
||||
.list { display:grid; gap:10px; margin:0; padding:0; list-style:none; }
|
||||
.list li { display:grid; grid-template-columns:10px minmax(0,1fr); gap:10px; color:var(--ink-2); font-size:13px; }
|
||||
.list li:before { content:""; width:6px; height:6px; margin-top:8px; border-radius:50%; background:var(--accent); }
|
||||
code { font-family:var(--num); font-size:12px; background:var(--paper-2); padding:2px 6px; border-radius:6px; }
|
||||
.wide { grid-column:1 / -1; }
|
||||
.commands { display:grid; gap:10px; }
|
||||
pre { margin:0; overflow:auto; padding:14px; border-radius:8px; background:#1E2724; color:#F6F0EC; font:12px/1.6 var(--num); }
|
||||
table { width:100%; border-collapse:collapse; background:var(--surface); border:1px solid var(--hair); border-radius:12px; overflow:hidden; display:table; }
|
||||
th,td { padding:12px 14px; border-bottom:1px solid var(--hair); text-align:left; vertical-align:top; font-size:13px; }
|
||||
th { color:var(--ink-3); font-weight:700; background:var(--paper-2); }
|
||||
tr:last-child td { border-bottom:0; }
|
||||
@media (max-width:900px) {
|
||||
header { padding:0 20px; }
|
||||
main { width:min(100% - 28px, 1180px); padding-top:28px; }
|
||||
.hero { grid-template-columns:1fr; }
|
||||
.grid { grid-template-columns:1fr; }
|
||||
}
|
||||
@media (max-width:520px) {
|
||||
.summary { grid-template-columns:1fr; }
|
||||
h1 { font-size:24px; }
|
||||
.stamp { display:none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="brand"><span class="mark" aria-hidden="true"></span><span>Vignette 개발 대시보드</span></div>
|
||||
<div class="stamp">2026-06-26 14:43 KST</div>
|
||||
</header>
|
||||
|
||||
<!-- ============ 공통 앱 셸: 톱바 ============ -->
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<span class="mark">
|
||||
<!-- 워드마크: 비네트(조리개) — inline SVG -->
|
||||
<svg viewBox="0 0 26 26" fill="none" aria-hidden="true">
|
||||
<circle cx="13" cy="13" r="11" stroke="#3E7A6E" stroke-width="2"/>
|
||||
<path d="M8 14.5c1.4 1.7 3 2.5 5 2.5s3.6-.8 5-2.5" stroke="#3E7A6E" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="13" cy="9" r="1.6" fill="#B0735C"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="wm"><span class="v">Vignette</span></span>
|
||||
</div>
|
||||
<span class="rolelabel">개발 오버뷰</span>
|
||||
<span class="spacer"></span>
|
||||
<span class="repo">
|
||||
<svg viewBox="0 0 24 24" stroke-width="1.7"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
yunchan8804-blip/vignette
|
||||
<span class="priv">private</span>
|
||||
</span>
|
||||
<span class="when"><span class="dot"></span>오늘 <span class="d">2026-06-25</span></span>
|
||||
</header>
|
||||
|
||||
<!-- ============ 본문 ============ -->
|
||||
<div class="wrap">
|
||||
|
||||
<!-- 페이지 헤드라인 -->
|
||||
<div class="head">
|
||||
<div class="kicker"><span class="dot"></span>한신대 산학협력 · 20주 · 매칭금 500만</div>
|
||||
<h1>Phase 0 기반정렬 막바지, <em>엔진 실동작까지 검증</em>됐습니다.</h1>
|
||||
<div class="sub">설계 4종·모노레포·디자인 7종·P0 코드 골격이 오늘 자로 완료됐고, 남은 블로커는 Docker 기동 후 DB 실마이그레이션과 외부 데이터 수급입니다.</div>
|
||||
<div class="headstats">
|
||||
<span class="stat"><span class="num">4</span><span class="lab">로드맵 단계</span></span>
|
||||
<span class="vsep"></span>
|
||||
<span class="stat"><span class="num">1</span><span class="lab">진행중 (P0)</span></span>
|
||||
<span class="vsep"></span>
|
||||
<span class="stat"><span class="num">6</span><span class="lab">확정 결정</span></span>
|
||||
<span class="vsep"></span>
|
||||
<span class="stat"><span class="num">3</span><span class="lab">열린 리스크</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. 로드맵 4단계 진행 막대 -->
|
||||
<section class="block">
|
||||
<div class="sechead">
|
||||
<div class="kicker"><span class="dot"></span>로드맵 · 20주</div>
|
||||
<div class="desc">단계별 진행률과 상태. 진행중인 단계만 색으로 강조, 대기 단계는 무채로 둡니다.</div>
|
||||
</div>
|
||||
|
||||
<div class="roadmap">
|
||||
<div class="phase-grid">
|
||||
<!-- Phase 0 -->
|
||||
<div class="phase is-active">
|
||||
<div class="ph-top"><span class="ph-id">PHASE 0</span><span class="ph-pct">70%</span></div>
|
||||
<div class="ph-name">기반정렬</div>
|
||||
<div class="ph-weeks">1~2주차</div>
|
||||
<div class="track"><div class="fill" style="width:70%"></div></div>
|
||||
<div class="ph-state active"><span class="d"></span>진행중</div>
|
||||
<div class="ph-note">스캐폴드·DB 스키마·엔진 PoC 거의 완료</div>
|
||||
</div>
|
||||
<!-- Phase 1 -->
|
||||
<div class="phase">
|
||||
<div class="ph-top"><span class="ph-id">PHASE 1</span><span class="ph-pct">0%</span></div>
|
||||
<div class="ph-name">텍스트 상담 MVP</div>
|
||||
<div class="ph-weeks">3~8주차</div>
|
||||
<div class="track"><div class="fill" style="width:0%"></div></div>
|
||||
<div class="ph-state wait"><span class="d"></span>대기</div>
|
||||
<div class="ph-note">상담 루프 + 상태머신 + 실시간 피드백</div>
|
||||
</div>
|
||||
<!-- Phase 2 -->
|
||||
<div class="phase">
|
||||
<div class="ph-top"><span class="ph-id">PHASE 2</span><span class="ph-pct">0%</span></div>
|
||||
<div class="ph-name">음성 + 3역할 + 페르소나</div>
|
||||
<div class="ph-weeks">9~15주차</div>
|
||||
<div class="track"><div class="fill" style="width:0%"></div></div>
|
||||
<div class="ph-state wait"><span class="d"></span>대기</div>
|
||||
<div class="ph-note">음성 캐스케이드 · 슈퍼바이저/내담자/평가</div>
|
||||
</div>
|
||||
<!-- Phase 3 -->
|
||||
<div class="phase">
|
||||
<div class="ph-top"><span class="ph-id">PHASE 3</span><span class="ph-pct">0%</span></div>
|
||||
<div class="ph-name">효과검증 파일럿</div>
|
||||
<div class="ph-weeks">16~20주차</div>
|
||||
<div class="track"><div class="fill" style="width:0%"></div></div>
|
||||
<div class="ph-state wait"><span class="d"></span>대기</div>
|
||||
<div class="ph-note">IRB 면제 확정 (교육용)</div>
|
||||
</div>
|
||||
<main>
|
||||
<div class="hero">
|
||||
<div>
|
||||
<div class="kicker">team lead review</div>
|
||||
<h1>Phase 0는 <em>검증 가능한 운영 안전장치와 E2E 안정화</em>까지 마무리됐다.</h1>
|
||||
<p class="lead">
|
||||
이번 세션은 seed persona 결합 제거, 비개발 fail-closed 설정, compose OAuth 주입,
|
||||
admin 모바일 overflow 수정, persona 인증 경계 보강, 전체 Playwright 재통과, 공개 API prod 전환까지 완료했다.
|
||||
이후 로컬 5175 런타임 재기동, admin 사용자 관리 E2E 안정화, 학습자 홈 0회 기록 UX,
|
||||
active session 실제 SSE 스트림 전환, 텍스트/음성 실패 턴 미저장, 직접 URL persona 우회 차단, Live2D demo fallback 제거,
|
||||
AI 운영 fail-closed 보강까지 전체 Playwright로 확인했다.
|
||||
2026-06-26 14:43 KST 기준 공개 API 터널을 복구한 뒤 전체 Playwright 72개가 desktop/mobile/single-run 모두 통과했다.
|
||||
공개 Google OAuth 실제 `/turn` 증명만 storageState 캡처 후 남아 있다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="summary" aria-label="현재 검증 요약">
|
||||
<div class="metric"><span>Full Playwright</span><b>72 passed</b></div>
|
||||
<div class="metric"><span>Public API</span><b>prod OK</b></div>
|
||||
<div class="metric"><span>Backend unittest</span><b>20 OK</b></div>
|
||||
<div class="metric"><span>Decision</span><b>조건부 GO</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2단 본문 -->
|
||||
<div class="cols">
|
||||
|
||||
<!-- 좌측: 오늘 완료 + 진행중/다음 -->
|
||||
<div>
|
||||
<section class="block">
|
||||
<div class="sechead">
|
||||
<div class="kicker"><span class="dot"></span>오늘 완료 · 2026-06-25</div>
|
||||
<div class="desc">P0 기반정렬에서 오늘 자로 닫은 항목들입니다.</div>
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>팀장 판정</h2>
|
||||
<p>이 HTML만 보고 판단해야 하는 항목</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<article class="panel">
|
||||
<span class="tag done">go</span>
|
||||
<h3>로컬 회귀와 공개 prod-safe 게이트 통과</h3>
|
||||
<p>백엔드 unittest 20개, Python compile, 웹 production build, 전체 Playwright 72개가 통과했다. 공개 API도 `environment:"prod"`, `db:true`, `engine:true`로 응답한다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag doing">conditional</span>
|
||||
<h3>운영 반영 전 남은 증거 1개</h3>
|
||||
<p>실제 Google 로그인 쿠키가 필요한 공개 `/turn` smoke는 아직 storageState가 없어 미완이다. 로그인 가능한 계정으로 한 번 캡처하면 바로 검증 가능하다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag info">mock audit</span>
|
||||
<h3>런타임 목업 제거 기준</h3>
|
||||
<p>프론트 페이지는 실제 API/DB 경로를 탄다. 남은 `mock` 표기는 테스트 코드의 `unittest.mock` 또는 의도적 fixture이며, seed fallback은 prod/staging에서 fail-closed 된다.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="panel">
|
||||
<div class="checks">
|
||||
<!-- 설계 -->
|
||||
<div class="chk-group">
|
||||
<div class="chk-glabel">설계 4종</div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">마스터플랜</span></div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">적대검증 — red team <b>40개 결함</b> 도출 후 재설계</span></div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">메모리 · 페르소나 설계</span></div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">디자인 컨셉</span></div>
|
||||
</div>
|
||||
<!-- 인프라 -->
|
||||
<div class="chk-group">
|
||||
<div class="chk-glabel">인프라</div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">모노레포 repo 생성</span></div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">Docker 이식 구성 <span class="ct" style="color:var(--ink-3)">— compose + Caddy + 엔진 어댑터 분리</span></span></div>
|
||||
</div>
|
||||
<!-- 디자인 -->
|
||||
<div class="chk-group">
|
||||
<div class="chk-glabel">디자인</div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">화면 <b>7종</b> 시안 <span class="tag">login · learner-home · session · session-review · professor · admin · settings</span></span></div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">아바타 <b>Rive</b> 채택</span></div>
|
||||
</div>
|
||||
<!-- 엔진 -->
|
||||
<div class="chk-group">
|
||||
<div class="chk-glabel">엔진</div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct"><code style="font-family:var(--font-num);font-size:12px;background:var(--paper-2);padding:1px 6px;border-radius:5px;">claude -p</code> 상주 멀티턴 게이트웨이 <span class="tag verified">실동작 검증 완료</span><br><span style="font-size:12px;color:var(--ink-3);">컨텍스트 유지 + 캐시 재사용 확인</span></span></div>
|
||||
</div>
|
||||
<!-- P0 코드 -->
|
||||
<div class="chk-group">
|
||||
<div class="chk-glabel">P0 코드 · 구문검증 통과</div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">DB 스키마 <b>35테이블</b> <span style="color:var(--ink-3);font-variant-numeric:tabular-nums;">(727줄)</span></span></div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">FastAPI 골격</span></div>
|
||||
<div class="chk"><span class="mk"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span class="ct">taxonomy <span style="color:var(--ink-3);font-variant-numeric:tabular-nums;">(285줄)</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>완료</h2>
|
||||
<p>새 세션에서 반복하지 않아도 되는 작업</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<article class="panel">
|
||||
<span class="tag done">E2E persona decoupled</span>
|
||||
<h3>DB persona 기반 테스트</h3>
|
||||
<p>`session-persistence`, `teacher`, `voice`는 `/api/personas`에서 실제 DB persona를 가져온다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">fail-closed</span>
|
||||
<h3>비개발 런타임 보호</h3>
|
||||
<p>prod/staging에서 dev login, seed fallback, OAuth 누락, 기본 secret, localhost CORS를 거부한다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">infra</span>
|
||||
<h3>compose OAuth 주입</h3>
|
||||
<p>`ENVIRONMENT`, Google OAuth vars, `CORS_ORIGINS`, `OPENAI_BASE_URL`을 API 컨테이너에 명시했다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">admin UX</span>
|
||||
<h3>모바일 overflow 수정</h3>
|
||||
<p>긴 readiness detail을 안정화했고, 사용자 변경은 DB 저장소가 durable할 때만 허용한다. AI 운영은 실제 어댑터명과 health 상태를 표시한다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">test isolation</span>
|
||||
<h3>global engine config 분리</h3>
|
||||
<p>`db-persistence`는 전역 engine config를 바꾸므로 `@single-run`으로 분리했다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">docs</span>
|
||||
<h3>handoff 갱신</h3>
|
||||
<p>`docs/HANDOFF.md`, `docs/dev_dashboard.html`, `apps/web/e2e/README.md`를 최신 상태로 맞췄다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">public prod</span>
|
||||
<h3>공개 API prod 전환</h3>
|
||||
<p>`cloudflared` ingress를 `127.0.0.1:8001` prod API로 돌렸고 public readiness gate가 통과했다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">ops script</span>
|
||||
<h3>공개 런타임 재기동</h3>
|
||||
<p>`scripts/start-public-runtime.ps1`와 `scripts/stop-public-runtime.ps1`로 API 8001과 cloudflared를 재현 가능하게 만들었다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">auth boundary</span>
|
||||
<h3>persona catalog 인증화</h3>
|
||||
<p>`/personas`는 이제 비로그인 401이다. 로컬과 공개 API 모두 직접 확인했고 E2E 회귀를 추가했다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">public web</span>
|
||||
<h3>API base fallback 보강</h3>
|
||||
<p>public 웹에서 `/api/auth/config`가 SPA HTML로 떨어지지 않도록 public API origin fallback을 추가했다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">learner UX</span>
|
||||
<h3>0회 기록 상태 보강</h3>
|
||||
<p>학습자 홈은 이제 fake 기록 없이 실제 서버 세션 0개를 `기존 회기` 빈 상태와 0 카운트로 표시한다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">local login</span>
|
||||
<h3>5175 로컬 재기동 검증</h3>
|
||||
<p>`127.0.0.1:5175` web, `8000` API, `9099` engine을 재기동하고 dev-login 서버 세션과 `/learn` redirect를 확인했다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">session UX</span>
|
||||
<h3>dense viewport 보정</h3>
|
||||
<p>375px급 active session에서 stage 내부가 1px 넘게 잘리던 문제를 avatar/padding 조정으로 제거했다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">real stream</span>
|
||||
<h3>텍스트 턴 SSE 전환</h3>
|
||||
<p>`POST /sessions/{id}/stream` token 이벤트로만 내담자 자막을 쌓고, HTTP 거절과 SSE error 모두 실패 턴을 transcript와 저장소에 남기지 않는다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">voice turn</span>
|
||||
<h3>음성 실패 턴 미저장</h3>
|
||||
<p>음성 회기도 엔진 응답 생성 성공 후에만 learner/client turn을 저장한다. 실패한 transcript는 pending 상태에서 제거한다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">ops copy</span>
|
||||
<h3>비영구 상태 문구 정리</h3>
|
||||
<p>admin health와 voice degraded reason은 내부 fallback/seed 표현 대신 비영구 런타임 상태를 사용자용 문구로 표시한다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">route guard</span>
|
||||
<h3>직접 URL persona 차단</h3>
|
||||
<p>`/learn/session/:code`로 직접 들어와도 DB 원본 persona가 아니면 회기 시작을 막는다. degraded/seed fallback 회귀를 추가했다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag done">avatar</span>
|
||||
<h3>demo Live2D fallback 제거</h3>
|
||||
<p>Mao/Haru 샘플은 공용 fallback으로 쓰지 않는다. 실제 persona별 `live2dModelUrl`이 있을 때만 Live2D를 렌더링한다.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<div class="sechead">
|
||||
<div class="kicker"><span class="dot"></span>진행중 · 다음</div>
|
||||
<div class="desc">P0 마무리와 P1 진입 작업.</div>
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>검증</h2>
|
||||
<p>마지막으로 직접 실행한 결과</p>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>범위</th><th>명령</th><th>결과</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Web typecheck</td><td><code>npm run typecheck</code></td><td>Passed</td></tr>
|
||||
<tr><td>Backend</td><td><code>python -m unittest app.test_runtime_policy app.test_session_turn_persistence engine_gateway.test_gateway_model</code></td><td>20 tests OK</td></tr>
|
||||
<tr><td>Compile</td><td><code>python -m compileall app engine_gateway</code></td><td>Passed</td></tr>
|
||||
<tr><td>Web build</td><td><code>npm run build</code></td><td>Passed</td></tr>
|
||||
<tr><td>Compose</td><td><code>docker compose --env-file .env.example config --quiet</code></td><td>Passed with dummy required env</td></tr>
|
||||
<tr><td>Focused E2E</td><td><code>admin + db-persistence + voice-success</code></td><td>10 passed</td></tr>
|
||||
<tr><td>Admin manage-users</td><td><code>admin.spec.ts --grep manage real server-known users</code></td><td>desktop/mobile 2 passed</td></tr>
|
||||
<tr><td>Full E2E</td><td><code>PLAYWRIGHT_HOST=127.0.0.1 npx playwright test</code></td><td>72 passed</td></tr>
|
||||
<tr><td>Public auth discovery</td><td><code>E2E_PUBLIC_AUTH=1 npx playwright test --list --project=chromium-public-auth</code></td><td>2 tests listed</td></tr>
|
||||
<tr><td>Public readiness</td><td><code>public-auth-turn.spec.ts --grep production-safe</code></td><td>1 passed</td></tr>
|
||||
<tr><td>Persona auth boundary</td><td><code>GET /personas</code></td><td>local/public unauth 401</td></tr>
|
||||
<tr><td>Public login</td><td><code>auth.spec.ts --grep public login</code></td><td>1 passed</td></tr>
|
||||
<tr><td>Public runtime script</td><td><code>scripts\start-public-runtime.ps1</code></td><td>API 8001 + cloudflared running</td></tr>
|
||||
<tr><td>Public API health</td><td><code>https://api-vignette.chanpaca.net/health</code></td><td>prod, db true, engine true</td></tr>
|
||||
<tr><td>Local 5175 login</td><td><code>PLAYWRIGHT_BASE_URL=http://127.0.0.1:5175 auth.spec.ts</code></td><td>desktop/mobile passed</td></tr>
|
||||
<tr><td>Learner/readiness E2E</td><td><code>learner.spec.ts + readiness.spec.ts desktop/mobile</code></td><td>14 passed</td></tr>
|
||||
<tr><td>Learner screenshots</td><td><code>learn-empty-desktop.png / learn-empty-mobile-compact.png</code></td><td>document overflow 0, empty history visible</td></tr>
|
||||
<tr><td>Session layout/turn</td><td><code>session-layout.spec.ts desktop/mobile</code></td><td>8 passed</td></tr>
|
||||
<tr><td>Voice/session focused</td><td><code>voice + voice-success + session-layout</code></td><td>15 passed</td></tr>
|
||||
<tr><td>Latest focused E2E</td><td><code>learner + session-layout + settings + admin</code></td><td>37 passed</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="panel">
|
||||
<div class="nextlist">
|
||||
<div class="nx">
|
||||
<span class="nd doing"></span>
|
||||
<span class="nt">DB 실제 마이그레이션 <span class="st blocked">블로커</span>
|
||||
<span class="why">Docker Desktop 띄워 psql 적용 — Docker 꺼져 있어 미완</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="nx">
|
||||
<span class="nd queued"></span>
|
||||
<span class="nt">상담 루프 연결 <span class="st queued">P1</span>
|
||||
<span class="why">엔진 + 상태머신 + 실시간 피드백</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="nx">
|
||||
<span class="nd queued"></span>
|
||||
<span class="nt">Rive 아바타 통합 <span class="st queued">P1</span></span>
|
||||
</div>
|
||||
<div class="nx">
|
||||
<span class="nd queued"></span>
|
||||
<span class="nt">OpenAI 음성 캐스케이드 <span class="st queued">P1</span></span>
|
||||
</div>
|
||||
<div class="nx">
|
||||
<span class="nd queued"></span>
|
||||
<span class="nt">React 프론트 연결 <span class="st queued">P1</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>남은 리스크</h2>
|
||||
<p>다음 세션 첫 작업</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<article class="panel">
|
||||
<span class="tag risk">blocker</span>
|
||||
<h3>public `/turn` proof 없음</h3>
|
||||
<p>2026-06-26 14:43 KST 기준 Google 로그인 storageState가 없어 공개 API `/turn` 실제 성공 증거가 아직 없다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag doing">ops</span>
|
||||
<h3>공개 API 프로세스 유지</h3>
|
||||
<p>터널은 `127.0.0.1:8001` prod API를 본다. 재부팅 후에는 prod API와 cloudflared를 같이 재기동해야 한다.</p>
|
||||
</article>
|
||||
<article class="panel">
|
||||
<span class="tag doing">check before deploy</span>
|
||||
<h3>engine config 재확인</h3>
|
||||
<p>E2E가 admin engine config를 건드린다. 배포 전 의도한 model/url/mode인지 다시 확인해야 한다.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 우측: 확정 결정 + 리스크/블로커 -->
|
||||
<div>
|
||||
<section class="block">
|
||||
<div class="sechead">
|
||||
<div class="kicker"><span class="dot"></span>확정 결정</div>
|
||||
<div class="desc">되돌리지 않는 아키텍처·운영 기준선.</div>
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>다음 명령</h2>
|
||||
<p>공개 OAuth smoke</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="commands">
|
||||
<pre>cd D:\workspace\vignette\apps\web
|
||||
npx playwright codegen https://vignette.chanpaca.net/login --save-storage=.\node_modules\.tmp\public-auth.json
|
||||
|
||||
<div class="panel">
|
||||
<div class="decisions">
|
||||
<div class="dc">
|
||||
<span class="dk">엔진</span>
|
||||
<span class="dv">로컬 <b>claude -p</b> 상주풀 <span style="color:var(--ink-3);">(API 폴백)</span></span>
|
||||
</div>
|
||||
<div class="dc">
|
||||
<span class="dk">아바타</span>
|
||||
<span class="dv"><b>Rive</b></span>
|
||||
</div>
|
||||
<div class="dc span">
|
||||
<span class="dk">기술 스택</span>
|
||||
<span class="dv">
|
||||
<span class="stack-chips">
|
||||
<span class="c key">React 19</span>
|
||||
<span class="c key">FastAPI</span>
|
||||
<span class="c key">PG 16 + pgvector</span>
|
||||
<span class="c">Docker</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="dc span">
|
||||
<span class="dk">운영 디폴트 6종</span>
|
||||
<span class="dv" style="font-size:13px;color:var(--ink-2);line-height:1.65;">
|
||||
s2s 2차 · 평가 AI 로컬 · 재귀학습 few-shot · 자유연습 ON (교수 토글) · 스케일업 compose
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
$env:E2E_PUBLIC_AUTH="1"
|
||||
$env:E2E_PUBLIC_STORAGE_STATE=".\node_modules\.tmp\public-auth.json"
|
||||
.\node_modules\.bin\playwright.cmd test e2e/public-auth-turn.spec.ts --project=chromium-public-auth</pre>
|
||||
<pre>cd D:\workspace\vignette
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\start-public-runtime.ps1</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2>주의</h2>
|
||||
<p>작업 방식</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<div class="sechead">
|
||||
<div class="kicker"><span class="dot"></span>리스크 · 블로커</div>
|
||||
<div class="desc">빨강은 데이터·외부 의존, 주황은 검증 미완.</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="risks">
|
||||
<div class="rk">
|
||||
<span class="sev high"><span class="d"></span>HIGH</span>
|
||||
<span class="rbody">
|
||||
<span class="rt">데이터 n=1</span>
|
||||
<span class="rd">0615 단일 축어록뿐. 추가 사례 수급은 한신대 협의 필요 <span class="ext">외부 블로커</span></span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="rk">
|
||||
<span class="sev mid"><span class="d"></span>MID</span>
|
||||
<span class="rbody">
|
||||
<span class="rt">DB 실마이그레이션 미검증</span>
|
||||
<span class="rd">Docker Desktop 기동 후 psql 적용 필요 — 현재 Docker 꺼짐</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="rk">
|
||||
<span class="sev mid"><span class="d"></span>MID</span>
|
||||
<span class="rbody">
|
||||
<span class="rt">미성년 사례데이터 활용동의</span>
|
||||
<span class="rd">한신대 공문 필요 <span class="ext">외부 협의</span></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 산출물 인벤토리 -->
|
||||
<section class="block">
|
||||
<div class="sechead">
|
||||
<div class="kicker"><span class="dot"></span>산출물 인벤토리</div>
|
||||
<div class="desc">repo 경로 기준. 카드가 아니라 표로 — 전체를 한 번에 훑습니다.</div>
|
||||
</div>
|
||||
|
||||
<table class="inv-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:120px;">영역</th>
|
||||
<th>내용</th>
|
||||
<th>repo 경로</th>
|
||||
<th style="width:88px;">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="area"><span class="kd"><span class="d"></span>설계 문서</span></td>
|
||||
<td class="what">마스터플랜 · 적대검증(red team) · 메모리·페르소나 설계 · 디자인 컨셉</td>
|
||||
<td class="path"><code>docs/MASTERPLAN.md</code><code>docs/redteam/</code><code>docs/MEMORY_KNOWLEDGE_PERSONA_DESIGN.md</code><code>docs/DESIGN_CONCEPT.md</code></td>
|
||||
<td class="stt"><span class="invtag done"><span class="d"></span>완료</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area"><span class="kd"><span class="d"></span>디자인 시안</span></td>
|
||||
<td class="what">화면 7종 목업 (login · learner-home · session · session-review · professor · admin · settings)</td>
|
||||
<td class="path"><code>docs/mockups/</code> <span style="color:var(--ink-3);font-family:var(--font-num);">(7)</span></td>
|
||||
<td class="stt"><span class="invtag done"><span class="d"></span>완료</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area"><span class="kd"><span class="d"></span>인프라</span></td>
|
||||
<td class="what">Docker 이식 구성 — compose + Caddy + 엔진 어댑터 분리</td>
|
||||
<td class="path"><code>infra/</code></td>
|
||||
<td class="stt"><span class="invtag partial"><span class="d"></span>구성 완료 · 마이그레이션 미완</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area"><span class="kd"><span class="d"></span>엔진 · 백엔드</span></td>
|
||||
<td class="what">claude -p 상주 게이트웨이 (실동작 검증) + FastAPI 골격 + DB 스키마 35테이블 + taxonomy</td>
|
||||
<td class="path"><code>apps/api/</code></td>
|
||||
<td class="stt"><span class="invtag partial"><span class="d"></span>골격 · 구문검증 통과</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="foot">
|
||||
<span>repo · github.com/yunchan8804-blip/vignette (private)</span>
|
||||
<span class="fdot"></span>
|
||||
<span>기준일 2026-06-25</span>
|
||||
<span class="fdot"></span>
|
||||
<span>Vignette 개발팀 내부 PM 보드</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<ul class="list">
|
||||
<li>`frontenddesign` 스킬은 현재 세션에 없었다. 다음 세션에서 보이면 먼저 사용하고, 없으면 `docs/DESIGN_CONCEPT.md`를 따른다.</li>
|
||||
<li>학습자 홈과 active session UX는 사용자가 강하게 불만을 말한 영역이다. fake 기록/통계 금지, 0회 기록은 실제 서버 세션 0개 기반 빈 상태로 유지, active session 전체화면/무문서스크롤 유지.</li>
|
||||
<li>worktree는 원래부터 dirty다. 관련 없는 변경을 되돌리지 말 것.</li>
|
||||
<li>OAuth secret은 출력하지 말 것. 실제 secret은 환경 파일/인프라 설정에만 둔다.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue