운영 지표와 지원 요청 저장소 추가

This commit is contained in:
Yun Chan 2026-06-28 20:13:06 +09:00
parent 50fa4ad432
commit e7ebb38177
20 changed files with 3038 additions and 39 deletions

View file

@ -343,15 +343,46 @@ async def _runtime_tables_ready(conn) -> bool:
WHERE stage_code IN ('라포','탐색','개입','정리')
) AS has_stage_defs,
to_regclass('app.admin_health_event') IS NOT NULL AS has_admin_health_event,
to_regclass('app.admin_health_daily_rollup') IS NOT NULL AS has_admin_health_daily_rollup,
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'app'
AND table_name = 'admin_health_daily_rollup'
AND column_name = 'last_down_at'
) AS has_admin_health_daily_rollup_columns,
to_regclass('app.support_ticket') IS NOT NULL AS has_support_ticket,
EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'app'
AND table_name = 'support_ticket'
AND column_name IN ('fingerprint', 'parent_ticket_id')
GROUP BY table_schema, table_name
HAVING count(*) = 2
) AS has_support_ticket_duplicate_columns,
EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = 'app'
AND tablename = 'admin_health_event'
AND policyname IN ('p_admin_health_event_select','p_admin_health_event_insert')
AND policyname IN (
'p_admin_health_event_select',
'p_admin_health_event_insert',
'p_admin_health_event_delete'
)
GROUP BY schemaname, tablename
HAVING count(*) = 2
HAVING count(*) = 3
) AS has_admin_health_event_policies,
EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = 'app'
AND tablename = 'admin_health_daily_rollup'
AND policyname IN (
'p_admin_health_daily_rollup_select',
'p_admin_health_daily_rollup_insert',
'p_admin_health_daily_rollup_update'
)
GROUP BY schemaname, tablename
HAVING count(*) = 3
) AS has_admin_health_daily_rollup_policies,
EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = 'app'
@ -365,6 +396,19 @@ async def _runtime_tables_ready(conn) -> bool:
GROUP BY schemaname, tablename
HAVING count(*) = 4
) AS has_support_ticket_policies,
to_regclass('app.learner_prepost_measure') IS NOT NULL AS has_learner_prepost_measure,
EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = 'app'
AND tablename = 'learner_prepost_measure'
AND policyname IN (
'p_learner_prepost_measure_select',
'p_learner_prepost_measure_insert',
'p_learner_prepost_measure_update'
)
GROUP BY schemaname, tablename
HAVING count(*) = 3
) AS has_learner_prepost_measure_policies,
EXISTS (
SELECT 1 FROM pg_policies
WHERE schemaname = 'app'
@ -407,9 +451,15 @@ async def _runtime_tables_ready(conn) -> bool:
and row["has_state_columns"]
and row["has_stage_defs"]
and row["has_admin_health_event"]
and row["has_admin_health_daily_rollup"]
and row["has_admin_health_daily_rollup_columns"]
and row["has_support_ticket"]
and row["has_support_ticket_duplicate_columns"]
and row["has_admin_health_event_policies"]
and row["has_admin_health_daily_rollup_policies"]
and row["has_support_ticket_policies"]
and row["has_learner_prepost_measure"]
and row["has_learner_prepost_measure_policies"]
and row["has_session_write_policies"]
and row["removed_old_session_policy"]
and row["has_turn_write_policies"]
@ -574,6 +624,34 @@ async def ensure_runtime_tables() -> None:
ON app.admin_health_event(observed_at DESC, service_key)
"""
)
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS app.admin_health_daily_rollup (
rollup_date DATE NOT NULL,
environment TEXT NOT NULL,
engine_mode TEXT NOT NULL,
service_key TEXT NOT NULL,
service_name TEXT NOT NULL,
sample_count INTEGER NOT NULL DEFAULT 0 CHECK (sample_count >= 0),
ok_samples INTEGER NOT NULL DEFAULT 0 CHECK (ok_samples >= 0),
degraded_samples INTEGER NOT NULL DEFAULT 0 CHECK (degraded_samples >= 0),
down_samples INTEGER NOT NULL DEFAULT 0 CHECK (down_samples >= 0),
first_observed_at TIMESTAMPTZ NOT NULL,
last_observed_at TIMESTAMPTZ NOT NULL,
latest_status TEXT NOT NULL CHECK (latest_status IN ('ok','degraded','down')),
last_down_at TIMESTAMPTZ,
max_load REAL NOT NULL DEFAULT 0.0 CHECK (max_load >= 0.0 AND max_load <= 1.0),
generated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (rollup_date, environment, engine_mode, service_key)
)
"""
)
await conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_admin_health_daily_rollup_latest
ON app.admin_health_daily_rollup(last_observed_at DESC, service_key)
"""
)
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS app.support_ticket (
@ -599,6 +677,8 @@ async def ensure_runtime_tables() -> None:
subject TEXT NOT NULL,
body TEXT NOT NULL,
source_path TEXT NOT NULL DEFAULT '',
fingerprint TEXT NOT NULL DEFAULT '',
parent_ticket_id UUID REFERENCES app.support_ticket(id) ON DELETE SET NULL,
assigned_group TEXT NOT NULL DEFAULT '',
resolution_note TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
@ -620,17 +700,95 @@ async def ensure_runtime_tables() -> None:
ON app.support_ticket(reporter_id, created_at DESC)
"""
)
await conn.execute(
"""
ALTER TABLE app.support_ticket
ADD COLUMN IF NOT EXISTS fingerprint TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS parent_ticket_id UUID REFERENCES app.support_ticket(id) ON DELETE SET NULL
"""
)
await conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_support_ticket_fingerprint
ON app.support_ticket(fingerprint, created_at DESC)
WHERE fingerprint <> ''
"""
)
await conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_support_ticket_parent
ON app.support_ticket(parent_ticket_id)
WHERE parent_ticket_id IS NOT NULL
"""
)
await conn.execute(
"""
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,
pilot_id TEXT NOT NULL DEFAULT 'phase3-pilot-draft',
measure_name TEXT NOT NULL CHECK (
measure_name IN ('self_efficacy','skill_proficiency','training_satisfaction')
),
timepoint TEXT NOT NULL CHECK (timepoint IN ('pre','post')),
raw_score NUMERIC(8,3) NOT NULL,
min_score NUMERIC(8,3) NOT NULL DEFAULT 1.0,
max_score NUMERIC(8,3) NOT NULL DEFAULT 5.0,
instrument_version TEXT NOT NULL DEFAULT 'pilot-prepost-scaffold-2026-06-28',
item_count INTEGER NOT NULL DEFAULT 1 CHECK (item_count >= 1 AND item_count <= 80),
collected_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (max_score > min_score),
CHECK (raw_score >= min_score AND raw_score <= max_score),
UNIQUE (learner_id, pilot_id, measure_name, timepoint, instrument_version)
)
"""
)
await conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_learner_prepost_measure_pilot
ON app.learner_prepost_measure(pilot_id, measure_name, timepoint)
"""
)
await conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_learner_prepost_measure_learner
ON app.learner_prepost_measure(learner_id, pilot_id)
"""
)
await conn.execute(
"""
ALTER TABLE app.admin_health_event ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS p_admin_health_event_select ON app.admin_health_event;
DROP POLICY IF EXISTS p_admin_health_event_insert ON app.admin_health_event;
DROP POLICY IF EXISTS p_admin_health_event_delete ON app.admin_health_event;
CREATE POLICY p_admin_health_event_select
ON app.admin_health_event FOR SELECT
USING (app.current_role_name() = 'admin');
CREATE POLICY p_admin_health_event_insert
ON app.admin_health_event FOR INSERT
WITH CHECK (app.current_role_name() = 'admin');
CREATE POLICY p_admin_health_event_delete
ON app.admin_health_event FOR DELETE
USING (app.current_role_name() = 'admin');
ALTER TABLE app.admin_health_daily_rollup ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS p_admin_health_daily_rollup_select ON app.admin_health_daily_rollup;
DROP POLICY IF EXISTS p_admin_health_daily_rollup_insert ON app.admin_health_daily_rollup;
DROP POLICY IF EXISTS p_admin_health_daily_rollup_update ON app.admin_health_daily_rollup;
DROP POLICY IF EXISTS p_admin_health_daily_rollup_delete ON app.admin_health_daily_rollup;
ALTER TABLE app.admin_health_daily_rollup
ADD COLUMN IF NOT EXISTS last_down_at TIMESTAMPTZ;
CREATE POLICY p_admin_health_daily_rollup_select
ON app.admin_health_daily_rollup FOR SELECT
USING (app.current_role_name() = 'admin');
CREATE POLICY p_admin_health_daily_rollup_insert
ON app.admin_health_daily_rollup FOR INSERT
WITH CHECK (app.current_role_name() = 'admin');
CREATE POLICY p_admin_health_daily_rollup_update
ON app.admin_health_daily_rollup FOR UPDATE
USING (app.current_role_name() = 'admin')
WITH CHECK (app.current_role_name() = 'admin');
ALTER TABLE app.support_ticket ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS p_support_ticket_select ON app.support_ticket;
@ -656,6 +814,41 @@ async def ensure_runtime_tables() -> None:
CREATE POLICY p_support_ticket_delete
ON app.support_ticket FOR DELETE
USING (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;
DROP POLICY IF EXISTS p_learner_prepost_measure_update ON app.learner_prepost_measure;
CREATE POLICY p_learner_prepost_measure_select
ON app.learner_prepost_measure FOR SELECT
USING (
app.current_role_name() = 'admin'
OR learner_id = app.current_uid()
OR (
app.current_role_name() = 'instructor'
AND EXISTS (
SELECT 1 FROM app.app_user u
WHERE u.user_id = app.learner_prepost_measure.learner_id
AND u.cohort = current_setting('app.current_cohort', true)
)
)
);
CREATE POLICY p_learner_prepost_measure_insert
ON app.learner_prepost_measure FOR INSERT
WITH CHECK (
app.current_role_name() = 'admin'
OR learner_id = app.current_uid()
);
CREATE POLICY p_learner_prepost_measure_update
ON app.learner_prepost_measure FOR UPDATE
USING (
app.current_role_name() = 'admin'
OR learner_id = app.current_uid()
)
WITH CHECK (
app.current_role_name() = 'admin'
OR learner_id = app.current_uid()
);
"""
)
await conn.execute(
@ -674,6 +867,12 @@ async def ensure_runtime_tables() -> None:
ADD COLUMN IF NOT EXISTS turns_in_stage INT NOT NULL DEFAULT 0
"""
)
await conn.execute(
"""
ALTER TABLE app.turns
ADD COLUMN IF NOT EXISTS provider_events JSONB NOT NULL DEFAULT '[]'::jsonb
"""
)
await conn.execute(
"""
INSERT INTO app.stage_def (stage_code, display_name, seq, base_openness)
@ -730,6 +929,37 @@ async def ensure_runtime_tables() -> None:
)
"""
)
await conn.execute(
"""
ALTER TABLE app.pinned_fact ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS p_pinned_insert ON app.pinned_fact;
DROP POLICY IF EXISTS p_pinned_update ON app.pinned_fact;
CREATE POLICY p_pinned_insert ON app.pinned_fact FOR INSERT WITH CHECK (
app.current_role_name() IN ('admin','instructor')
OR EXISTS (
SELECT 1 FROM app.case_profile cp
WHERE cp.case_id = app.pinned_fact.case_id
AND cp.learner_id = app.current_uid()
)
);
CREATE POLICY p_pinned_update ON app.pinned_fact FOR UPDATE USING (
app.current_role_name() IN ('admin','instructor')
OR EXISTS (
SELECT 1 FROM app.case_profile cp
WHERE cp.case_id = app.pinned_fact.case_id
AND cp.learner_id = app.current_uid()
)
) WITH CHECK (
app.current_role_name() IN ('admin','instructor')
OR EXISTS (
SELECT 1 FROM app.case_profile cp
WHERE cp.case_id = app.pinned_fact.case_id
AND cp.learner_id = app.current_uid()
)
)
"""
)
await conn.execute(
"""
DROP POLICY IF EXISTS p_turns_modify ON app.turns;