Spaces:
Runtime error
Runtime error
Claude commited on
feat: 작업 2 - 최종 입장 표현 구현
Browse files[토론] 토론 전 초기 입장(찬성/반대/중립) 선택 UI 추가
[토론] 토론 후 최종 입장 투표 UI 추가
[토론] 교사 대시보드 전후 입장 변화율 도넛 차트 시각화
[토의] 토의 종료 후 '가장 중요한 결론은?' 한 줄 입력 UI 추가
[토의] 교사 대시보드 결론 워드클라우드 시각화
DB: opinion_changes에 initial_stance, final_stance, discussion_conclusion 컬럼 추가 필요 (SQL 별도 안내)
https://claude.ai/code/session_016DE3uHLsVmA5Uv7gZGGaKT
- app.py +1 -1
- components/opinion_change.py +52 -5
- components/teacher_dashboard.py +68 -2
- db.py +20 -16
app.py
CHANGED
|
@@ -209,7 +209,7 @@ if user_role == "학생" and opinion_changes_available():
|
|
| 209 |
row = fetch_opinion_change(supabase, room_name, student_name)
|
| 210 |
has_pre_opinion = bool((row or {}).get("pre_opinion"))
|
| 211 |
if not has_pre_opinion:
|
| 212 |
-
render_pre_opinion_form(supabase, room_name, student_name, current_topic)
|
| 213 |
st.caption("💡 위에서 토론 전 생각을 제출하면 의견 작성이 활성화됩니다.")
|
| 214 |
else:
|
| 215 |
_render_opinion_input(supabase, room_name, user_role, student_name, student_number, current_mode)
|
|
|
|
| 209 |
row = fetch_opinion_change(supabase, room_name, student_name)
|
| 210 |
has_pre_opinion = bool((row or {}).get("pre_opinion"))
|
| 211 |
if not has_pre_opinion:
|
| 212 |
+
render_pre_opinion_form(supabase, room_name, student_name, current_topic, act_type)
|
| 213 |
st.caption("💡 위에서 토론 전 생각을 제출하면 의견 작성이 활성화됩니다.")
|
| 214 |
else:
|
| 215 |
_render_opinion_input(supabase, room_name, user_role, student_name, student_number, current_mode)
|
components/opinion_change.py
CHANGED
|
@@ -7,6 +7,7 @@ from db import (
|
|
| 7 |
fetch_opinion_change,
|
| 8 |
opinion_changes_available,
|
| 9 |
save_opinion_analysis,
|
|
|
|
| 10 |
upsert_post_opinion,
|
| 11 |
upsert_pre_opinion,
|
| 12 |
)
|
|
@@ -14,9 +15,22 @@ from env import get_secret
|
|
| 14 |
from services.ai import build_opinion_change_prompt, generate_ai_response
|
| 15 |
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
| 18 |
"""토론 전 생각 입력 폼. 제출 완료 시 전체 앱 재실행."""
|
| 19 |
-
st.info(f"💬 **토론 전 내 생각 먼저 기록하기**\n\n'{current_topic}' 주제에 대한 나의 생각을 적어주세요. 제출 후 토론에 참여할 수 있습니다.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
pre_input = st.text_area(
|
| 21 |
"이 주제에 대한 내 생각은?",
|
| 22 |
height=100,
|
|
@@ -28,7 +42,7 @@ def render_pre_opinion_form(supabase, room_name, student_name, current_topic):
|
|
| 28 |
if not pre_input.strip():
|
| 29 |
st.warning("생각을 입력해 주세요.")
|
| 30 |
return
|
| 31 |
-
res = upsert_pre_opinion(supabase, room_name, student_name, pre_input.strip())
|
| 32 |
if res is not None:
|
| 33 |
st.toast("✅ 내 생각이 기록되었습니다. 이제 토론에 참여할 수 있습니다!", icon="🎉")
|
| 34 |
st.rerun()
|
|
@@ -54,7 +68,30 @@ def render_post_opinion_section(supabase, room_name, student_name, act_type, cur
|
|
| 54 |
st.caption("📌 토론 전 생각 기록 없음")
|
| 55 |
|
| 56 |
if not post_opinion:
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
post_input = st.text_area(
|
| 59 |
"토론 후 생각 변화",
|
| 60 |
height=100,
|
|
@@ -67,13 +104,23 @@ def render_post_opinion_section(supabase, room_name, student_name, act_type, cur
|
|
| 67 |
if not post_input.strip():
|
| 68 |
st.warning("생각을 입력해 주세요.")
|
| 69 |
return
|
| 70 |
-
res = upsert_post_opinion(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
if res is not None:
|
| 72 |
_trigger_analysis(supabase, room_name, student_name, act_type, current_topic, pre_opinion, post_input.strip())
|
| 73 |
st.rerun()
|
| 74 |
else:
|
| 75 |
st.error("저장에 실패했습니다. 다시 시도해 주세요.")
|
| 76 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
st.success(f"✅ **토론 후 내 생각:** {post_opinion}")
|
| 78 |
st.divider()
|
| 79 |
if ai_analysis:
|
|
|
|
| 7 |
fetch_opinion_change,
|
| 8 |
opinion_changes_available,
|
| 9 |
save_opinion_analysis,
|
| 10 |
+
stance_available,
|
| 11 |
upsert_post_opinion,
|
| 12 |
upsert_pre_opinion,
|
| 13 |
)
|
|
|
|
| 15 |
from services.ai import build_opinion_change_prompt, generate_ai_response
|
| 16 |
|
| 17 |
|
| 18 |
+
_STANCE_OPTIONS = ["🔵 찬성", "🔴 반대", "⚪ 중립"]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def render_pre_opinion_form(supabase, room_name, student_name, current_topic, act_type="토론"):
|
| 22 |
"""토론 전 생각 입력 폼. 제출 완료 시 전체 앱 재실행."""
|
| 23 |
+
st.info(f"💬 **{'토론' if act_type == '토론' else '토의'} 전 내 생각 먼저 기록하기**\n\n'{current_topic}' 주제에 대한 나의 생각을 적어주세요. 제출 후 {'토론' if act_type == '토론' else '토의'}에 참여할 수 있습니다.")
|
| 24 |
+
|
| 25 |
+
initial_stance = None
|
| 26 |
+
if act_type == "토론" and stance_available():
|
| 27 |
+
initial_stance = st.radio(
|
| 28 |
+
"📌 토론 전 나의 초기 입장",
|
| 29 |
+
_STANCE_OPTIONS,
|
| 30 |
+
horizontal=True,
|
| 31 |
+
key="pre_stance_radio",
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
pre_input = st.text_area(
|
| 35 |
"이 주제에 대한 내 생각은?",
|
| 36 |
height=100,
|
|
|
|
| 42 |
if not pre_input.strip():
|
| 43 |
st.warning("생각을 입력해 주세요.")
|
| 44 |
return
|
| 45 |
+
res = upsert_pre_opinion(supabase, room_name, student_name, pre_input.strip(), initial_stance=initial_stance)
|
| 46 |
if res is not None:
|
| 47 |
st.toast("✅ 내 생각이 기록되었습니다. 이제 토론에 참여할 수 있습니다!", icon="🎉")
|
| 48 |
st.rerun()
|
|
|
|
| 68 |
st.caption("📌 토론 전 생각 기록 없음")
|
| 69 |
|
| 70 |
if not post_opinion:
|
| 71 |
+
end_label = "토론" if act_type == "토론" else "토의"
|
| 72 |
+
st.info(f"{end_label}이 종료되었습니다. {end_label} 후 생각이 어떻게 바뀌었는지 기록해 주세요.")
|
| 73 |
+
|
| 74 |
+
final_stance = None
|
| 75 |
+
discussion_conclusion = None
|
| 76 |
+
|
| 77 |
+
if act_type == "토론" and stance_available():
|
| 78 |
+
initial_stance_val = (row or {}).get("initial_stance") or ""
|
| 79 |
+
if initial_stance_val:
|
| 80 |
+
st.caption(f"📌 **토론 전 나의 입장:** {initial_stance_val}")
|
| 81 |
+
final_stance = st.radio(
|
| 82 |
+
"🗳️ 토론 후 최종 입장",
|
| 83 |
+
_STANCE_OPTIONS,
|
| 84 |
+
horizontal=True,
|
| 85 |
+
key="post_stance_radio",
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
if act_type == "토의" and stance_available():
|
| 89 |
+
discussion_conclusion = st.text_input(
|
| 90 |
+
"💡 가장 중요한 결론은?",
|
| 91 |
+
max_chars=200,
|
| 92 |
+
placeholder="이번 토의에서 얻은 가장 중요한 결론을 한 줄로 써보세요.",
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
post_input = st.text_area(
|
| 96 |
"토론 후 생각 변화",
|
| 97 |
height=100,
|
|
|
|
| 104 |
if not post_input.strip():
|
| 105 |
st.warning("생각을 입력해 주세요.")
|
| 106 |
return
|
| 107 |
+
res = upsert_post_opinion(
|
| 108 |
+
supabase, room_name, student_name, post_input.strip(),
|
| 109 |
+
final_stance=final_stance,
|
| 110 |
+
discussion_conclusion=discussion_conclusion.strip() if discussion_conclusion else None,
|
| 111 |
+
)
|
| 112 |
if res is not None:
|
| 113 |
_trigger_analysis(supabase, room_name, student_name, act_type, current_topic, pre_opinion, post_input.strip())
|
| 114 |
st.rerun()
|
| 115 |
else:
|
| 116 |
st.error("저장에 실패했습니다. 다시 시도해 주세요.")
|
| 117 |
else:
|
| 118 |
+
final_stance_val = (row or {}).get("final_stance") or ""
|
| 119 |
+
discussion_conclusion_val = (row or {}).get("discussion_conclusion") or ""
|
| 120 |
+
if act_type == "토론" and final_stance_val:
|
| 121 |
+
st.success(f"✅ **최종 입장:** {final_stance_val}")
|
| 122 |
+
if act_type == "토의" and discussion_conclusion_val:
|
| 123 |
+
st.success(f"✅ **나의 결론:** {discussion_conclusion_val}")
|
| 124 |
st.success(f"✅ **토론 후 내 생각:** {post_opinion}")
|
| 125 |
st.divider()
|
| 126 |
if ai_analysis:
|
components/teacher_dashboard.py
CHANGED
|
@@ -3,9 +3,10 @@ import logging
|
|
| 3 |
import plotly.express as px
|
| 4 |
import streamlit as st
|
| 5 |
|
| 6 |
-
from db import destroy_room_data, fetch_all_opinion_changes, fetch_debate_status, fetch_live_messages, opinion_changes_available, session_control_available, set_debate_status
|
| 7 |
from utils import create_analysis_image
|
| 8 |
-
from components.opinion_change import _render_image_download
|
|
|
|
| 9 |
from validators import with_fallback_author_role
|
| 10 |
from utils import log_audit
|
| 11 |
from config import DASHBOARD_FETCH_LIMIT, ROOM_DESTROY_ENABLED, UI_FONT_FAMILY
|
|
@@ -60,6 +61,19 @@ def render_teacher_dashboard(supabase, room_name, user_role, student_name, curre
|
|
| 60 |
pre = row.get("pre_opinion") or "(없음)"
|
| 61 |
post = row.get("post_opinion") or "(없음)"
|
| 62 |
ai = row.get("ai_analysis") or ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
col_pre, col_post = st.columns(2)
|
| 64 |
with col_pre:
|
| 65 |
st.caption("📌 토론 전 생각")
|
|
@@ -78,6 +92,58 @@ def render_teacher_dashboard(supabase, room_name, user_role, student_name, curre
|
|
| 78 |
else:
|
| 79 |
st.caption("AI 분석이 아직 없습니다.")
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
if session_control_available():
|
| 82 |
st.divider()
|
| 83 |
st.subheader("🎛️ 토론 진행 제어")
|
|
|
|
| 3 |
import plotly.express as px
|
| 4 |
import streamlit as st
|
| 5 |
|
| 6 |
+
from db import destroy_room_data, fetch_all_opinion_changes, fetch_debate_status, fetch_live_messages, opinion_changes_available, session_control_available, set_debate_status, stance_available
|
| 7 |
from utils import create_analysis_image
|
| 8 |
+
from components.opinion_change import _render_image_download, _STANCE_OPTIONS
|
| 9 |
+
from wordcloud import build_word_frequencies, build_circular_wordcloud_html
|
| 10 |
from validators import with_fallback_author_role
|
| 11 |
from utils import log_audit
|
| 12 |
from config import DASHBOARD_FETCH_LIMIT, ROOM_DESTROY_ENABLED, UI_FONT_FAMILY
|
|
|
|
| 61 |
pre = row.get("pre_opinion") or "(없음)"
|
| 62 |
post = row.get("post_opinion") or "(없음)"
|
| 63 |
ai = row.get("ai_analysis") or ""
|
| 64 |
+
|
| 65 |
+
if stance_available() and act_type == "토론":
|
| 66 |
+
init_s = row.get("initial_stance") or ""
|
| 67 |
+
final_s = row.get("final_stance") or ""
|
| 68 |
+
if init_s or final_s:
|
| 69 |
+
col_is, col_fs = st.columns(2)
|
| 70 |
+
with col_is:
|
| 71 |
+
st.caption("📌 토론 전 입장")
|
| 72 |
+
st.info(init_s or "(미입력)")
|
| 73 |
+
with col_fs:
|
| 74 |
+
st.caption("🗳️ 토론 후 최종 입장")
|
| 75 |
+
st.info(final_s or "(미입력)")
|
| 76 |
+
|
| 77 |
col_pre, col_post = st.columns(2)
|
| 78 |
with col_pre:
|
| 79 |
st.caption("📌 토론 전 생각")
|
|
|
|
| 92 |
else:
|
| 93 |
st.caption("AI 분석이 아직 없습니다.")
|
| 94 |
|
| 95 |
+
# 입장 변화 도넛 차트 (토론) / 워드클라우드 (토의)
|
| 96 |
+
if stance_available():
|
| 97 |
+
st.divider()
|
| 98 |
+
if act_type == "토론":
|
| 99 |
+
st.subheader("📊 입장 변화 현황")
|
| 100 |
+
col_d1, col_d2 = st.columns(2)
|
| 101 |
+
for col, col_name, label in [
|
| 102 |
+
(col_d1, "initial_stance", "토론 전 초기 입장"),
|
| 103 |
+
(col_d2, "final_stance", "토론 후 최종 입장"),
|
| 104 |
+
]:
|
| 105 |
+
if col_name in df_oc.columns:
|
| 106 |
+
counts = (
|
| 107 |
+
df_oc[col_name]
|
| 108 |
+
.dropna()
|
| 109 |
+
.value_counts()
|
| 110 |
+
.reindex(_STANCE_OPTIONS, fill_value=0)
|
| 111 |
+
.reset_index()
|
| 112 |
+
)
|
| 113 |
+
counts.columns = ["입장", "인원"]
|
| 114 |
+
with col:
|
| 115 |
+
st.caption(label)
|
| 116 |
+
if counts["인원"].sum() > 0:
|
| 117 |
+
fig = px.pie(
|
| 118 |
+
counts, names="입장", values="인원",
|
| 119 |
+
hole=0.45,
|
| 120 |
+
color="입장",
|
| 121 |
+
color_discrete_map={
|
| 122 |
+
"🔵 찬성": "#1558a0",
|
| 123 |
+
"🔴 반대": "#d62728",
|
| 124 |
+
"⚪ 중립": "#aaaaaa",
|
| 125 |
+
},
|
| 126 |
+
)
|
| 127 |
+
fig.update_layout(
|
| 128 |
+
margin=dict(t=10, b=10, l=10, r=10),
|
| 129 |
+
font={"family": UI_FONT_FAMILY},
|
| 130 |
+
showlegend=True,
|
| 131 |
+
legend=dict(orientation="h"),
|
| 132 |
+
)
|
| 133 |
+
st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False})
|
| 134 |
+
else:
|
| 135 |
+
st.info("아직 입력된 입장이 없습니다.")
|
| 136 |
+
elif act_type == "토의":
|
| 137 |
+
if "discussion_conclusion" in df_oc.columns:
|
| 138 |
+
conclusions = df_oc["discussion_conclusion"].dropna()
|
| 139 |
+
if not conclusions.empty:
|
| 140 |
+
st.subheader("☁️ 결론 워드클라우드")
|
| 141 |
+
freq = build_word_frequencies(conclusions)
|
| 142 |
+
if freq:
|
| 143 |
+
st.markdown(build_circular_wordcloud_html(freq), unsafe_allow_html=True)
|
| 144 |
+
else:
|
| 145 |
+
st.info("아직 제출된 결론이 없습니다.")
|
| 146 |
+
|
| 147 |
if session_control_available():
|
| 148 |
st.divider()
|
| 149 |
st.subheader("🎛️ 토론 진행 제어")
|
db.py
CHANGED
|
@@ -118,6 +118,7 @@ def check_schema_columns() -> dict:
|
|
| 118 |
("topic.created_by", lambda: supabase.table("topic").select("created_by").limit(1).execute()),
|
| 119 |
("teacher_accounts.is_admin", lambda: supabase.table("teacher_accounts").select("is_admin").limit(1).execute()),
|
| 120 |
("opinion_changes.pre_opinion", lambda: supabase.table("opinion_changes").select("pre_opinion").limit(1).execute()),
|
|
|
|
| 121 |
("session_control.status", lambda: supabase.table("session_control").select("status").limit(1).execute()),
|
| 122 |
]
|
| 123 |
|
|
@@ -153,6 +154,9 @@ def topic_owner_column_available() -> bool:
|
|
| 153 |
def opinion_changes_available() -> bool:
|
| 154 |
return check_schema_columns().get("opinion_changes.pre_opinion", False)
|
| 155 |
|
|
|
|
|
|
|
|
|
|
| 156 |
def session_control_available() -> bool:
|
| 157 |
return check_schema_columns().get("session_control.status", False)
|
| 158 |
|
|
@@ -380,7 +384,7 @@ def fetch_opinion_change(supabase: Client, room_name: str, student_name: str):
|
|
| 380 |
return None
|
| 381 |
res = execute_query(
|
| 382 |
supabase.table("opinion_changes")
|
| 383 |
-
.select("
|
| 384 |
.eq("room_name", room_name)
|
| 385 |
.eq("student_name", student_name)
|
| 386 |
.limit(1),
|
|
@@ -391,40 +395,40 @@ def fetch_opinion_change(supabase: Client, room_name: str, student_name: str):
|
|
| 391 |
return res.data[0]
|
| 392 |
|
| 393 |
|
| 394 |
-
def upsert_pre_opinion(supabase: Client, room_name: str, student_name: str, pre_opinion: str):
|
| 395 |
if not opinion_changes_available():
|
| 396 |
return None
|
|
|
|
|
|
|
|
|
|
| 397 |
existing = fetch_opinion_change(supabase, room_name, student_name)
|
| 398 |
if existing is not None:
|
| 399 |
return execute_query(
|
| 400 |
-
supabase.table("opinion_changes")
|
| 401 |
-
.update({"pre_opinion": pre_opinion})
|
| 402 |
-
.eq("room_name", room_name)
|
| 403 |
-
.eq("student_name", student_name),
|
| 404 |
fail_message="토론 전 생각 저장 실패",
|
| 405 |
)
|
| 406 |
return execute_query(
|
| 407 |
-
supabase.table("opinion_changes")
|
| 408 |
-
.insert({"room_name": room_name, "student_name": student_name, "pre_opinion": pre_opinion}),
|
| 409 |
fail_message="토론 전 생각 저장 실패",
|
| 410 |
)
|
| 411 |
|
| 412 |
|
| 413 |
-
def upsert_post_opinion(supabase: Client, room_name: str, student_name: str, post_opinion: str):
|
| 414 |
if not opinion_changes_available():
|
| 415 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
existing = fetch_opinion_change(supabase, room_name, student_name)
|
| 417 |
if existing is not None:
|
| 418 |
return execute_query(
|
| 419 |
-
supabase.table("opinion_changes")
|
| 420 |
-
.update({"post_opinion": post_opinion})
|
| 421 |
-
.eq("room_name", room_name)
|
| 422 |
-
.eq("student_name", student_name),
|
| 423 |
fail_message="토론 후 생각 저장 실패",
|
| 424 |
)
|
| 425 |
return execute_query(
|
| 426 |
-
supabase.table("opinion_changes")
|
| 427 |
-
.insert({"room_name": room_name, "student_name": student_name, "post_opinion": post_opinion}),
|
| 428 |
fail_message="토론 후 생각 저장 실패",
|
| 429 |
)
|
| 430 |
|
|
@@ -434,7 +438,7 @@ def fetch_all_opinion_changes(supabase: Client, room_name: str):
|
|
| 434 |
return pd.DataFrame()
|
| 435 |
res = execute_query(
|
| 436 |
supabase.table("opinion_changes")
|
| 437 |
-
.select("
|
| 438 |
.eq("room_name", room_name)
|
| 439 |
.order("student_name"),
|
| 440 |
fail_message="학생별 생각 변화 조회 실패",
|
|
|
|
| 118 |
("topic.created_by", lambda: supabase.table("topic").select("created_by").limit(1).execute()),
|
| 119 |
("teacher_accounts.is_admin", lambda: supabase.table("teacher_accounts").select("is_admin").limit(1).execute()),
|
| 120 |
("opinion_changes.pre_opinion", lambda: supabase.table("opinion_changes").select("pre_opinion").limit(1).execute()),
|
| 121 |
+
("opinion_changes.initial_stance", lambda: supabase.table("opinion_changes").select("initial_stance").limit(1).execute()),
|
| 122 |
("session_control.status", lambda: supabase.table("session_control").select("status").limit(1).execute()),
|
| 123 |
]
|
| 124 |
|
|
|
|
| 154 |
def opinion_changes_available() -> bool:
|
| 155 |
return check_schema_columns().get("opinion_changes.pre_opinion", False)
|
| 156 |
|
| 157 |
+
def stance_available() -> bool:
|
| 158 |
+
return check_schema_columns().get("opinion_changes.initial_stance", False)
|
| 159 |
+
|
| 160 |
def session_control_available() -> bool:
|
| 161 |
return check_schema_columns().get("session_control.status", False)
|
| 162 |
|
|
|
|
| 384 |
return None
|
| 385 |
res = execute_query(
|
| 386 |
supabase.table("opinion_changes")
|
| 387 |
+
.select("*")
|
| 388 |
.eq("room_name", room_name)
|
| 389 |
.eq("student_name", student_name)
|
| 390 |
.limit(1),
|
|
|
|
| 395 |
return res.data[0]
|
| 396 |
|
| 397 |
|
| 398 |
+
def upsert_pre_opinion(supabase: Client, room_name: str, student_name: str, pre_opinion: str, initial_stance: str = None):
|
| 399 |
if not opinion_changes_available():
|
| 400 |
return None
|
| 401 |
+
payload = {"pre_opinion": pre_opinion}
|
| 402 |
+
if initial_stance and stance_available():
|
| 403 |
+
payload["initial_stance"] = initial_stance
|
| 404 |
existing = fetch_opinion_change(supabase, room_name, student_name)
|
| 405 |
if existing is not None:
|
| 406 |
return execute_query(
|
| 407 |
+
supabase.table("opinion_changes").update(payload).eq("room_name", room_name).eq("student_name", student_name),
|
|
|
|
|
|
|
|
|
|
| 408 |
fail_message="토론 전 생각 저장 실패",
|
| 409 |
)
|
| 410 |
return execute_query(
|
| 411 |
+
supabase.table("opinion_changes").insert({"room_name": room_name, "student_name": student_name, **payload}),
|
|
|
|
| 412 |
fail_message="토론 전 생각 저장 실패",
|
| 413 |
)
|
| 414 |
|
| 415 |
|
| 416 |
+
def upsert_post_opinion(supabase: Client, room_name: str, student_name: str, post_opinion: str, final_stance: str = None, discussion_conclusion: str = None):
|
| 417 |
if not opinion_changes_available():
|
| 418 |
return None
|
| 419 |
+
payload = {"post_opinion": post_opinion}
|
| 420 |
+
if final_stance and stance_available():
|
| 421 |
+
payload["final_stance"] = final_stance
|
| 422 |
+
if discussion_conclusion and stance_available():
|
| 423 |
+
payload["discussion_conclusion"] = discussion_conclusion
|
| 424 |
existing = fetch_opinion_change(supabase, room_name, student_name)
|
| 425 |
if existing is not None:
|
| 426 |
return execute_query(
|
| 427 |
+
supabase.table("opinion_changes").update(payload).eq("room_name", room_name).eq("student_name", student_name),
|
|
|
|
|
|
|
|
|
|
| 428 |
fail_message="토론 후 생각 저장 실패",
|
| 429 |
)
|
| 430 |
return execute_query(
|
| 431 |
+
supabase.table("opinion_changes").insert({"room_name": room_name, "student_name": student_name, **payload}),
|
|
|
|
| 432 |
fail_message="토론 후 생각 저장 실패",
|
| 433 |
)
|
| 434 |
|
|
|
|
| 438 |
return pd.DataFrame()
|
| 439 |
res = execute_query(
|
| 440 |
supabase.table("opinion_changes")
|
| 441 |
+
.select("*")
|
| 442 |
.eq("room_name", room_name)
|
| 443 |
.order("student_name"),
|
| 444 |
fail_message="학생별 생각 변화 조회 실패",
|