메일 알림 시스템 추가

This commit is contained in:
Yun Chan 2026-06-29 17:07:01 +09:00
parent ddf12a851c
commit 3bf38c50df
22 changed files with 1769 additions and 31 deletions

View file

@ -89,6 +89,8 @@ vignette/
`GET /health`는 liveness + DB readiness(`db.healthcheck()`) + 엔진 게이트웨이 readiness
(`engine_client.health_detail()`)를 합쳐 `{"status": "ok|degraded", db, engine, engine_mode, ...}`를 반환한다.
DB readiness는 auth/admin 테이블뿐 아니라 세션 read-model 핵심 테이블·컬럼
(`app.sessions`, `app.turns.provider_events`, `app.session_review_status` worksheet 컬럼)을 함께 확인한다.
### 2.2 턴 오케스트레이터 — `app/services/orchestrator.py`
@ -378,7 +380,27 @@ session lifecycle을 유지하며, future Node read API는 이 read-model contra
- `/teach/session/:sessionId/review` 화면은 같은 `GET /sessions/{id}/review` 자료를 교수자 읽기 전용으로 표시하고,
검토 메모 저장은 위 teacher endpoint로 분리한다.
### 2.9.2 공개 공유·검색 메타 — `app/routes/share.py`
### 2.9.2 운영 메일 알림 — `app/services/notifications.py`
- 가입 승인 알림: Google/SAML 신규 사용자가 `account_status=pending`으로 세션을 만들면
`account_pending_approval:{user_id}` idempotency key로 `app.notification_event`를 만들고,
슈퍼 관리자·관리자 콘솔 접근권자 중 `account_approval` 알림을 켠 수신자에게 메일 delivery를 큐잉한다.
- 회기 검토 알림: 회기 종료 후 `app.session_evaluation` 저장이 완료되면
`session_review_ready:{session_id}` idempotency key로 담당 코호트 교수자와 관리자에게
`/teach/session/{sessionId}/review` 딥링크 메일을 큐잉한다. 평가 생성이 실패해도 error record가 저장되면
교수자 수동 검토가 필요하므로 알림은 생성된다.
- 메일은 업무 상태의 원본이 아니다. 승인 상태는 `app.app_user.account_status`, 교수자 검토 상태는
`app.session_review_status`, 전송 상태는 `app.notification_delivery`가 각각 원본이다.
- SMTP 발송은 `NOTIFICATION_EMAIL_PROVIDER=smtp``SMTP_*` env가 있을 때만 수행한다. provider가
`disabled`면 이벤트/큐 구조는 유지되고 실제 발송은 worker 또는 관리자 처리 API 실행 시 skipped로 남는다.
- 메일 HTML은 Vignette 토큰 톤(종이 배경, 세이지-틸 CTA, 8px radius)을 inline style로 재현한다. 메일 본문에는
축어록, 평가 전문, 민감한 심리 상태를 넣지 않고, 로그인 후 앱 화면에서만 확인하게 한다.
- 운영 API: `GET /admin/notifications`는 최근 delivery와 queued/failed/sent/skipped 카운트를 반환하고,
`POST /admin/notifications/process` 또는 `scripts/run-notification-worker.py`는 큐를 한 번 drain한다.
`POST /admin/notifications/test`는 관리자 수신자에게 `admin_test_email:{uuid}` 테스트 이벤트를 만들고
같은 발송 큐로 즉시 처리한다.
### 2.9.3 공개 공유·검색 메타 — `app/routes/share.py`
- `GET /share/session/{token}` — 인증 없이 접근 가능한 unfurl HTML. Open Graph/Twitter Card/JSON-LD를 서버에서
직접 내려 URL만 전달해도 카카오톡·Slack·메일·AI 브라우저가 제목/요약/썸네일을 읽을 수 있게 한다.
@ -512,15 +534,15 @@ React 19 + Vite. 라우팅은 `apps/web/src/App.tsx`(react-router-dom).
|---|---|---|
| `/login` | Login (dev-login 경로 포함) | 공개 |
| `/pending` | PendingApproval(승인 대기/보류 안내) | 인증됨, approved 전용 제한 화면 |
| `/learn` | LearnerHome(대시보드: 학습 요약, 최근 회기 리캡, AI 코치) | learner |
| `/learn/practice` | LearnerHome(연습 대상 선택·새 회기 시작) | learner |
| `/learn/history` | LearnerHome(회기 기록·보관/복원·리뷰 진입) | learner |
| `/learn/session/:sessionId` | Session(상담 화면) | learner |
| `/learn/session/:sessionId/review` | SessionReview(회기 리뷰) | learner |
| `/learn/avatar-expressions` | AvatarExpressionLab | learner |
| `/teach` | Professor(교수자 대시보드) | teacher |
| `/learn` | LearnerHome(대시보드: 학습 요약, 최근 회기 리캡, AI 코치) | learner/admin(learner 관점) |
| `/learn/practice` | LearnerHome(연습 대상 선택·새 회기 시작) | learner/admin(learner 관점) |
| `/learn/history` | LearnerHome(회기 기록·보관/복원·리뷰 진입) | learner/admin(learner 관점) |
| `/learn/session/:sessionId` | Session(상담 화면) | learner/admin(learner 관점) |
| `/learn/session/:sessionId/review` | SessionReview(회기 리뷰) | learner/admin(learner 관점) |
| `/learn/avatar-expressions` | AvatarExpressionLab | learner/admin(learner 관점) |
| `/teach` | Professor(교수자 대시보드) | teacher/admin |
| `/teach/personas` | PersonaStudio(페르소나 저작·검수) | teacher/admin |
| `/teach/session/:sessionId/review` | SessionReview(교수자 읽기 전용 회기 검토) | teacher |
| `/teach/session/:sessionId/review` | SessionReview(교수자 읽기 전용 회기 검토) | teacher/admin |
| `/admin` | Admin(운영 홈) | admin |
| `/admin/users` | Admin(사용자 관리) | admin |
| `/admin/access` | Admin(접근 권한) | admin |
@ -600,6 +622,10 @@ DB는 PostgreSQL 16 + pgvector(단일 SoR). 초기화 SQL은 `infra/db/init/`에
- **공개 공유 카드** `app.session_share_link``session_id` 단위 공개 토큰 해시와 sanitized preview payload.
RLS는 학습자 본인 생성/폐기와 teacher/admin 열람, public route의 AI 컨텍스트 조회만 허용한다. 원문 축어록을
저장하지 않는다.
- **운영 메일 알림** `app.notification_event` / `app.notification_delivery` — 가입 승인 요청과 회기 검토 요청을
이벤트와 수신자별 delivery로 분리해 저장한다. `idempotency_key`가 중복 메일을 막고, delivery는
`queued/sending/sent/failed/skipped` 상태와 시도 횟수, provider message id, 마지막 오류만 저장한다.
메일 본문 HTML이나 회기 축어록은 DB에 복제하지 않는다. RLS는 관리자 전체 처리만 허용한다.
- **운영 콘솔** `app.admin_health_event` / `app.admin_health_daily_rollup` — 관리자 `/admin/health`
조회 시점 또는 `scripts/record-admin-health-sample.py` synthetic sampler 실행 시점의 서비스별 원시
헬스 샘플은 `app.admin_health_event`에 남긴다. `scripts/maintain-admin-health-events.py`는 명시
@ -668,8 +694,10 @@ DB 레벨 이중강제(`04_audit_eval_rls.sql` §5, `app/db.py` `acquire()`):
서버 로그에 남긴다. 프론트는 `token_exchange_failed`, `invalid_state`, provider error(`access_denied`/`provider_error`),
identity claim 실패를 구분하고 실패 reason code를 화면에 함께 표시한다.
- 역할은 `AUTH_TEACHER_EMAILS`/`AUTH_ADMIN_EMAILS` email allowlist로 1차 판정한다.
`admin_access`는 기본 역할과 별도인 관리자 콘솔 진입 권한이며, 슈퍼 관리자만 `/admin/users`에서
부여·회수할 수 있다. `AUTH_SUPER_ADMIN_EMAILS`는 항상 관리자 콘솔 접근, 학습자·교수자 공간 접근,
실제 `admin` 역할 사용자는 관리자 콘솔, 교수자 공간, 학습자 공간에 모두 접근할 수 있다.
`admin_access`는 기본 역할과 별도인 관리자 콘솔 진입 권한이며, 비관리자 계정에 학습자·교수자
공간 접근권을 추가하지 않는다. 슈퍼 관리자만 `/admin/users`에서 `admin_access`를 부여·회수할 수 있다.
`AUTH_SUPER_ADMIN_EMAILS`는 항상 관리자 콘솔 접근, 학습자·교수자 공간 접근,
approved 상태를 부여하는 신뢰 루트다(기본 `yunchan@twentyoz.kr`, `hoonjungkoo@hs.ac.kr`). 코호트는
`AUTH_EMAIL_COHORT_MAP``AUTH_DOMAIN_COHORT_MAP` 설정, SAML fixture의 `cohort` claim을 합쳐
`cohort_ids`로 세션에 저장한다. 관리 사용자 `app_user.external_id`는 provider subject 기반

View file

@ -142,6 +142,8 @@ npm install
| `EVALUATOR_SEMANTIC_CACHE_ENABLED` | `true` | fast/deep evaluator structured 결과 인메모리 캐시 활성화. 원문 prompt/completion은 저장하지 않음 |
| `EVALUATOR_SEMANTIC_CACHE_TTL_SECONDS` | `900` | evaluator cache TTL(초). 0 이하면 비활성 |
| `EVALUATOR_SEMANTIC_CACHE_MAX_ENTRIES` | `256` | evaluator cache LRU 최대 엔트리 수. 0 이하면 비활성 |
| `NOTIFICATION_EMAIL_PROVIDER` | `disabled` | 운영 메일 provider. 실제 발송은 `smtp``SMTP_*` 설정이 있을 때만 수행 |
| `SMTP_HOST` / `SMTP_FROM_EMAIL` | 빈 값 | `NOTIFICATION_EMAIL_PROVIDER=smtp`일 때 필요한 SMTP 호스트와 발신 주소 |
> 참고: 프로세스 환경변수(`$env:KEY`)는 `.env`보다 우선한다. 일회성 오버라이드에 쓸 수 있다.
@ -266,6 +268,8 @@ C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\mainta
- dev-login 사용자는 로컬/E2E 흐름 유지를 위해 `account_status=approved`로 생성된다.
- Google/SAML 신규 사용자는 기본적으로 `account_status=pending`이며, 승인 전에는 `/pending` 화면만 볼 수 있다.
관리자 콘솔 `/admin/users`의 가입 승인 탭에서 `approved`로 바꾸면 역할 홈에 접근한다.
DB가 연결되어 있으면 pending 생성 시 `app.notification_event`/`app.notification_delivery`에 가입 승인
메일 큐가 생긴다. `NOTIFICATION_EMAIL_PROVIDER=disabled`인 로컬 기본값에서는 실제 메일은 발송하지 않는다.
- 신규 사용자 또는 온보딩 미완료 사용자는 로그인 직후 `/onboarding`에서 닉네임, 자기소개,
선택 아바타 이미지, 이름, 소속, 학과, 학년/직위, 전화번호, 주소/수령지와 약관·개인정보
동의를 저장해야 역할 홈으로 이동한다. 학습자 회기 시작은 온보딩 완료와 동의가 모두 있어야 한다.
@ -277,7 +281,24 @@ C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe scripts\mainta
> 단, 관리자가 `/admin/users`에 미리 등록한 정확한 이메일은 도메인 밖이어도 로그인할 수 있다.
> 미등록 외부 도메인은 계속 `403 email domain is not allowed`.
### 3.1 PowerShell(권장) — Invoke-RestMethod + 세션 쿠키
### 3.1 메일 알림 큐 확인/처리
가입 승인과 교수자 회기 검토 알림은 상태 원본이 아니라 보조 알림이다. 원본 상태는 각각
`app.app_user.account_status`, `app.session_review_status`이고, 메일 전송 상태만
`app.notification_delivery`에 남는다.
```powershell
cd D:\workspace\vignette
# SMTP 설정이 준비된 환경에서 큐를 한 번 처리
python scripts\run-notification-worker.py --limit 25
```
관리자 API에서도 `GET /admin/notifications`로 최근 delivery를 보고,
`POST /admin/notifications/process`로 한 번 처리할 수 있다. `POST /admin/notifications/test`는 관리자
수신자에게 테스트 메일 이벤트를 만들고 같은 큐로 즉시 처리한다. 메일 본문에는 축어록이나 평가 전문을 넣지 않고,
`/admin/users`, `/teach/session/:sessionId/review`, `/admin` 링크만 제공한다.
### 3.2 PowerShell(권장) — Invoke-RestMethod + 세션 쿠키
PowerShell의 `curl``Invoke-WebRequest` 별칭이라 JSON 본문·쿠키 다루기가 번거롭다.
PowerShell에서는 `Invoke-RestMethod`가 가장 깔끔하다.
@ -291,7 +312,7 @@ Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8000/auth/dev-login `
Invoke-RestMethod -Uri http://127.0.0.1:8000/auth/me -WebSession $s
```
### 3.2 curl.exe(셸 무관) — JSON 본문은 파일로
### 3.3 curl.exe(셸 무관) — JSON 본문은 파일로
Windows에서 따옴표 이스케이프 사고를 피하려면 본문을 파일에 넣고 `--data-binary @file`로 보낸다.
(PowerShell에서는 반드시 `curl.exe`라고 적어 별칭이 아닌 실제 curl을 호출한다.)
@ -309,7 +330,7 @@ curl.exe -i -X POST http://127.0.0.1:8000/auth/dev-login `
curl.exe http://127.0.0.1:8000/auth/me -b cookies.txt
```
### 3.3 웹 UI
### 3.4 웹 UI
`apps/web`의 로그인 화면(`Login.tsx`)에 dev-login 경로가 있다. 웹을 띄운 상태(5173)에서
프록시를 통해 `/api/auth/dev-login`으로 동일하게 동작한다.
@ -317,7 +338,7 @@ curl.exe http://127.0.0.1:8000/auth/me -b cookies.txt
> 로컬/Tailnet 테스트는 dev-login을 사용한다. Google OAuth는 공개 도메인
> `https://vignette.chanpaca.net`에서만 실제 계정 흐름으로 검증한다.
### 3.4 라이브 코칭 source pack RAG 색인
### 3.5 라이브 코칭 source pack RAG 색인
`data/kb/live_coaching_workbook_0615.json``data/kb/live_coaching_sources/*.json`는 라이브 코칭의
기본 근거 source pack이다. API가 DB와 연결된 상태라면 관리자 dev-login 쿠키로 같은 자료를 RAG KB에도