개선관리 요구사항과 Google 로그인을 완료

This commit is contained in:
Yun Chan 2026-08-28 16:07:09 +09:00
parent cc0a15b7c6
commit 2a39636163
112 changed files with 10166 additions and 527 deletions

View file

@ -56,6 +56,10 @@ class DeliberatePracticeConflictError(RuntimeError):
pass
class DeliberatePracticeFeedbackDisabledError(PermissionError):
"""처방/연습 원천 회기의 학습자 피드백 스냅샷이 비활성이다."""
def _value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
try:
return row[key]
@ -63,6 +67,14 @@ def _value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
return default
def _public_row(row: Mapping[str, Any]) -> dict[str, Any]:
"""API 응답에서 정책 판정 전용 내부 열을 제거한다."""
payload = dict(row)
payload.pop("source_learner_feedback_enabled", None)
return payload
def _canonical_hash(payload: Mapping[str, Any]) -> str:
serialized = json.dumps(
payload,
@ -202,11 +214,15 @@ async def _latest_snapshot(
) -> Mapping[str, Any] | None:
return await conn.fetchrow(
"""
SELECT snapshot_id, session_id, snapshot_no, content_hash, graph_payload,
evidence_turn_ids, created_at
FROM app.competency_graph_snapshot
WHERE learner_id = $1
ORDER BY snapshot_no DESC
SELECT snapshot.snapshot_id, snapshot.session_id, snapshot.snapshot_no,
snapshot.content_hash, snapshot.graph_payload,
snapshot.evidence_turn_ids, snapshot.created_at,
source_session.learner_feedback_enabled
AS source_learner_feedback_enabled
FROM app.competency_graph_snapshot snapshot
JOIN app.sessions source_session ON source_session.id = snapshot.session_id
WHERE snapshot.learner_id = $1
ORDER BY snapshot.snapshot_no DESC
LIMIT 1
""",
learner_id,
@ -595,6 +611,37 @@ async def append_learner_attempt_submission(
"SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))",
f"practice-attempt:{submission_id}",
)
prescription_row = await conn.fetchrow(
"""
SELECT prescription.prescription_record_id,
prescription.session_id,
prescription.prescription_payload,
prescription.created_at,
source_session.learner_feedback_enabled
AS source_learner_feedback_enabled
FROM app.practice_prescription prescription
JOIN app.sessions source_session
ON source_session.id = prescription.session_id
WHERE prescription.learner_id = $1
AND prescription.prescription_key = $2
""",
learner_id,
prescription_id,
)
if prescription_row is None:
raise DeliberatePracticeNotFoundError(
"practice prescription not found or not visible"
)
if not bool(
_value(
prescription_row,
"source_learner_feedback_enabled",
True,
)
):
raise DeliberatePracticeFeedbackDisabledError(
"learner feedback was disabled for the prescription source session"
)
existing = await _existing_submission(
conn,
table="app.practice_episode_submission",
@ -608,19 +655,6 @@ async def append_learner_attempt_submission(
"SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))",
f"practice-graph:{learner_id}",
)
prescription_row = await conn.fetchrow(
"""
SELECT prescription_record_id, session_id, prescription_payload, created_at
FROM app.practice_prescription
WHERE learner_id = $1 AND prescription_key = $2
""",
learner_id,
prescription_id,
)
if prescription_row is None:
raise DeliberatePracticeNotFoundError(
"practice prescription not found or not visible"
)
source_session_id = UUID(str(_value(prescription_row, "session_id")))
session_id = practice_session_id or source_session_id
practice_session = await _visible_session(conn, session_id)
@ -1218,10 +1252,13 @@ async def read_deliberate_practice(
p.activity_mode, p.scenario_variant_id, p.scenario_novelty,
p.difficulty_level, p.prescription_payload, p.created_at,
c.card_key, c.coach_claim, c.evidence_turn_ids, c.source_refs,
c.uncertainty, c.counterevidence
c.uncertainty, c.counterevidence,
source_session.learner_feedback_enabled
AS source_learner_feedback_enabled
FROM app.practice_prescription p
JOIN app.practice_coaching_card c
ON c.coaching_card_record_id = p.coaching_card_record_id
JOIN app.sessions source_session ON source_session.id = p.session_id
WHERE p.learner_id = $1
ORDER BY p.created_at, p.prescription_record_id
""",
@ -1231,12 +1268,19 @@ async def read_deliberate_practice(
episodes = list(
await conn.fetch(
"""
SELECT episode_submission_id, episode_key, session_id,
progress, mastery_allowed, mastery_blockers, uncertainty,
evidence_turn_ids, counterevidence, assessment_payload, created_at
FROM app.practice_episode_submission
WHERE learner_id = $1
ORDER BY created_at, episode_submission_id
SELECT episode.episode_submission_id, episode.episode_key,
episode.session_id, episode.progress,
episode.mastery_allowed, episode.mastery_blockers,
episode.uncertainty, episode.evidence_turn_ids,
episode.counterevidence, episode.assessment_payload,
episode.created_at,
source_session.learner_feedback_enabled
AS source_learner_feedback_enabled
FROM app.practice_episode_submission episode
JOIN app.sessions source_session
ON source_session.id = episode.session_id
WHERE episode.learner_id = $1
ORDER BY episode.created_at, episode.episode_submission_id
""",
target_learner_id,
)
@ -1285,6 +1329,19 @@ async def read_deliberate_practice(
else []
)
snapshot = await _latest_snapshot(conn, target_learner_id)
if principal.role == Role.LEARNER and any(
not bool(
_value(
item,
"source_learner_feedback_enabled",
True,
)
)
for item in (*prescriptions, *episodes, *([snapshot] if snapshot else []))
):
raise DeliberatePracticeFeedbackDisabledError(
"learner feedback was disabled for a practice source session"
)
decision = (
await conn.fetchrow(
"""
@ -1313,7 +1370,7 @@ async def read_deliberate_practice(
).append(payload)
episode_payloads: list[dict[str, Any]] = []
for row in episodes:
payload = dict(row)
payload = _public_row(row)
payload["attempts"] = attempts_by_episode.get(
UUID(str(_value(row, "episode_submission_id"))), []
)
@ -1321,7 +1378,7 @@ async def read_deliberate_practice(
return {
"learner_id": target_learner_id,
"clinical_claim_allowed": False,
"prescriptions": [dict(item) for item in prescriptions],
"prescriptions": [_public_row(item) for item in prescriptions],
"episodes": episode_payloads,
"competency_graph": (
_value(snapshot, "graph_payload") if snapshot is not None else None
@ -1335,6 +1392,7 @@ async def read_deliberate_practice(
__all__ = [
"DeliberatePracticeConflictError",
"DeliberatePracticeFeedbackDisabledError",
"DeliberatePracticeNotFoundError",
"DeliberatePracticeStateError",
"append_learner_attempt_submission",