-- ============================================================================= -- Vignette · migration 24 — 학습자·교수자용 속마음 요약 (jev-client-affect-v2 §8.2) -- ============================================================================= -- app.client_affect_trace(23)는 관리자 전용이다. 이 테이블은 같은 client 턴에 -- 학습자·교수자에게 노출 가능한 고정 문구 요약만 담는다(숫자·확률·영문 코드 없음). -- 앱 런타임은 client turn INSERT · client_affect_trace INSERT와 같은 트랜잭션에 -- 이 테이블 INSERT를 둔다. CREATE TABLE IF NOT EXISTS app.client_inner_reaction ( turn_id UUID PRIMARY KEY REFERENCES app.turns(id) ON DELETE CASCADE, session_id UUID NOT NULL REFERENCES app.sessions(id) ON DELETE CASCADE, reaction JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_client_inner_reaction_session_created ON app.client_inner_reaction (session_id, created_at); ALTER TABLE app.client_inner_reaction ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS p_client_inner_reaction_select ON app.client_inner_reaction; DROP POLICY IF EXISTS p_client_inner_reaction_insert_learner ON app.client_inner_reaction; -- AI 경로(app.is_ai_context())는 절대 읽지 못한다: 속마음 문구가 프롬프트로 -- 역류하면 v2가 막으려는 겉과 속 차이 설계를 스스로 깨뜨린다. CREATE POLICY p_client_inner_reaction_select ON app.client_inner_reaction FOR SELECT USING ( NOT app.is_ai_context() AND EXISTS ( SELECT 1 FROM app.sessions AS s WHERE s.id = app.client_inner_reaction.session_id ) AND ( app.current_role_name() IN ('admin', 'instructor') OR EXISTS ( SELECT 1 FROM app.sessions AS s WHERE s.id = app.client_inner_reaction.session_id AND s.learner_id = app.current_uid() ) ) ); CREATE POLICY p_client_inner_reaction_insert_learner ON app.client_inner_reaction FOR INSERT WITH CHECK ( app.current_role_name() = 'learner' AND EXISTS ( SELECT 1 FROM app.turns AS turn_row JOIN app.sessions AS session_row ON session_row.id = turn_row.session_id WHERE turn_row.id = app.client_inner_reaction.turn_id AND turn_row.session_id = app.client_inner_reaction.session_id AND turn_row.speaker = 'client' AND session_row.learner_id = app.current_uid() ) );