메일 알림 시스템 추가
This commit is contained in:
parent
ddf12a851c
commit
3bf38c50df
22 changed files with 1769 additions and 31 deletions
|
|
@ -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 기반
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue