메일 알림 시스템 추가

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

@ -27,6 +27,17 @@ FRONTEND_BASE_URL=https://vignette.chanpaca.net
FRONTEND_ORIGIN_MAP={"api-vignette.chanpaca.net":"https://vignette.chanpaca.net","api-vnet.18ka.net":"https://vnet.18ka.net"}
CORS_ORIGINS=["https://vignette.chanpaca.net","https://vnet.18ka.net","https://vignette-b1q.pages.dev","http://localhost:5170","http://localhost:5171","http://localhost:5172","http://localhost:5173","http://localhost:5174","http://localhost:5175","http://localhost:5176","http://localhost:5177","http://localhost:5178","http://localhost:5179","http://localhost:5180","http://127.0.0.1:5170","http://127.0.0.1:5171","http://127.0.0.1:5172","http://127.0.0.1:5173","http://127.0.0.1:5174","http://127.0.0.1:5175","http://127.0.0.1:5176","http://127.0.0.1:5177","http://127.0.0.1:5178","http://127.0.0.1:5179","http://127.0.0.1:5180"]
# For local-only compose development, set ENVIRONMENT=dev and add http://localhost:5173 to CORS_ORIGINS.
NOTIFICATION_EMAIL_PROVIDER=disabled
NOTIFICATION_EMAIL_MAX_ATTEMPTS=3
NOTIFICATION_EMAIL_RETRY_SECONDS=900
SMTP_HOST=
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=
SMTP_FROM_NAME=Vignette
SMTP_STARTTLS=true
SMTP_SSL=false
OPENAI_API_KEY=
OPENAI_BASE_URL=https://api.openai.com/v1
VIGNETTE_VOICE_POC_SAMPLE_TTS=false

View file

@ -161,6 +161,49 @@ CREATE INDEX IF NOT EXISTS idx_support_ticket_parent
ON app.support_ticket(parent_ticket_id)
WHERE parent_ticket_id IS NOT NULL;
CREATE TABLE IF NOT EXISTS app.notification_event (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
kind TEXT NOT NULL CHECK (
kind IN ('account_pending_approval','session_review_ready','admin_test_email')
),
actor_user_id UUID REFERENCES app.app_user(user_id) ON DELETE SET NULL,
subject_user_id UUID REFERENCES app.app_user(user_id) ON DELETE SET NULL,
session_id UUID REFERENCES app.sessions(id) ON DELETE CASCADE,
idempotency_key TEXT NOT NULL UNIQUE,
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS app.notification_delivery (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_id UUID NOT NULL REFERENCES app.notification_event(id) ON DELETE CASCADE,
channel TEXT NOT NULL DEFAULT 'email' CHECK (channel IN ('email')),
recipient_user_id UUID REFERENCES app.app_user(user_id) ON DELETE SET NULL,
recipient_email TEXT NOT NULL,
recipient_name TEXT NOT NULL DEFAULT '',
recipient_role TEXT NOT NULL DEFAULT '',
subject TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'queued' CHECK (
status IN ('queued','sending','sent','failed','skipped')
),
attempts INTEGER NOT NULL DEFAULT 0,
next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
sent_at TIMESTAMPTZ,
last_error TEXT,
provider_message_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (event_id, channel, recipient_email)
);
CREATE INDEX IF NOT EXISTS idx_notification_event_created
ON app.notification_event(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_notification_delivery_queue
ON app.notification_delivery(status, next_attempt_at, created_at)
WHERE status IN ('queued','failed');
CREATE INDEX IF NOT EXISTS idx_notification_delivery_event
ON app.notification_delivery(event_id, created_at);
CREATE TABLE IF NOT EXISTS app.learner_prepost_measure (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
learner_id UUID NOT NULL REFERENCES app.app_user(user_id) ON DELETE CASCADE,
@ -241,6 +284,19 @@ CREATE POLICY p_support_ticket_delete
ON app.support_ticket FOR DELETE
USING (app.current_role_name() = 'admin');
ALTER TABLE app.notification_event ENABLE ROW LEVEL SECURITY;
ALTER TABLE app.notification_delivery ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS p_notification_event_admin_all ON app.notification_event;
DROP POLICY IF EXISTS p_notification_delivery_admin_all ON app.notification_delivery;
CREATE POLICY p_notification_event_admin_all
ON app.notification_event FOR ALL
USING (app.current_role_name() = 'admin')
WITH CHECK (app.current_role_name() = 'admin');
CREATE POLICY p_notification_delivery_admin_all
ON app.notification_delivery FOR ALL
USING (app.current_role_name() = 'admin')
WITH CHECK (app.current_role_name() = 'admin');
ALTER TABLE app.learner_prepost_measure ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS p_learner_prepost_measure_select ON app.learner_prepost_measure;
DROP POLICY IF EXISTS p_learner_prepost_measure_insert ON app.learner_prepost_measure;

View file

@ -54,6 +54,17 @@ services:
FRONTEND_ORIGIN_MAP: '${FRONTEND_ORIGIN_MAP:-{"api-vignette.chanpaca.net":"https://vignette.chanpaca.net","api-vnet.18ka.net":"https://vnet.18ka.net"}}'
CORS_ORIGINS: '${CORS_ORIGINS:-["https://vignette.chanpaca.net","https://vnet.18ka.net","https://vignette-b1q.pages.dev"]}'
USER_UPLOAD_DIR: ${USER_UPLOAD_DIR:-/app/uploads}
NOTIFICATION_EMAIL_PROVIDER: ${NOTIFICATION_EMAIL_PROVIDER:-disabled}
NOTIFICATION_EMAIL_MAX_ATTEMPTS: ${NOTIFICATION_EMAIL_MAX_ATTEMPTS:-3}
NOTIFICATION_EMAIL_RETRY_SECONDS: ${NOTIFICATION_EMAIL_RETRY_SECONDS:-900}
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_USERNAME: ${SMTP_USERNAME:-}
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-}
SMTP_FROM_NAME: ${SMTP_FROM_NAME:-Vignette}
SMTP_STARTTLS: ${SMTP_STARTTLS:-true}
SMTP_SSL: ${SMTP_SSL:-false}
extra_hosts:
- "host.docker.internal:host-gateway"
volumes: