Vignette session review
{title}
{description}
강점 요약
- {_list_items(share.goodMoments)}
개선 요약
- {_list_items(share.growthPoints)}
사례개념화 핵심값
- {_worksheet_items(share.worksheetHighlights)}
{html.escape(share.privacy)}
Vignette 열기"""Public share/unfurl routes. 공개 공유 URL은 세션 권한을 우회하지 않는다. 학습자가 명시적으로 생성한 토큰으로 app.session_share_link의 sanitized payload만 읽고, 원문 축어록은 조회하지 않는다. """ from __future__ import annotations import html import json import re from typing import Any from fastapi import APIRouter, HTTPException, Request, Response, status from fastapi.responses import HTMLResponse, PlainTextResponse from pydantic import BaseModel, Field from .. import session_persistence router = APIRouter(tags=["share"]) _TOKEN_RE = re.compile(r"^[A-Za-z0-9_-]{32,160}$") _SHARE_HEADERS = { "cache-control": "public, max-age=300", "x-robots-tag": "noindex, noarchive, max-snippet:160", } class PublicSessionShareResponse(BaseModel): title: str description: str summary: str imageUrl: str appUrl: str clientName: str persona: str date: str durationLabel: str reachedPhase: str sessionSignal: str reviewReady: bool = False goodMoments: list[str] = Field(default_factory=list) growthPoints: list[str] = Field(default_factory=list) worksheetHighlights: list[dict[str, str]] = Field(default_factory=list) privacy: str = "" def _safe_payload(payload: dict[str, Any]) -> PublicSessionShareResponse: return PublicSessionShareResponse( title=str(payload.get("title") or "Vignette 회기 리뷰"), description=str(payload.get("description") or "AI 심리상담 시뮬레이션 회기 리뷰 요약"), summary=str(payload.get("summary") or ""), imageUrl=str(payload.get("imageUrl") or ""), appUrl=str(payload.get("appUrl") or ""), clientName=str(payload.get("clientName") or "내담자"), persona=str(payload.get("persona") or ""), date=str(payload.get("date") or ""), durationLabel=str(payload.get("durationLabel") or ""), reachedPhase=str(payload.get("reachedPhase") or ""), sessionSignal=str(payload.get("sessionSignal") or ""), reviewReady=bool(payload.get("reviewReady")), goodMoments=[str(item) for item in payload.get("goodMoments") or []][:3], growthPoints=[str(item) for item in payload.get("growthPoints") or []][:3], worksheetHighlights=[ { "section": str(item.get("section") or ""), "label": str(item.get("label") or ""), "value": str(item.get("value") or ""), } for item in (payload.get("worksheetHighlights") or []) if isinstance(item, dict) ][:4], privacy=str(payload.get("privacy") or ""), ) async def _load_share_or_404(token: str) -> PublicSessionShareResponse: if not _TOKEN_RE.match(token): raise HTTPException(status.HTTP_404_NOT_FOUND, detail="share not found") record = await session_persistence.load_public_session_share( session_persistence.share_token_hash(token) ) if record is None: raise HTTPException(status.HTTP_404_NOT_FOUND, detail="share not found") return _safe_payload(dict(record.get("payload") or {})) def _request_url(request: Request) -> str: return str(request.url) def _json_ld(share: PublicSessionShareResponse, url: str) -> str: payload = { "@context": "https://schema.org", "@type": "CreativeWork", "name": share.title, "description": share.description, "url": url, "image": share.imageUrl, "inLanguage": "ko-KR", "educationalUse": "AI counseling simulation review", "isAccessibleForFree": True, "provider": { "@type": "Organization", "name": "Vignette", }, } return json.dumps(payload, ensure_ascii=False) def _meta(name: str, content: str, *, prop: bool = False) -> str: attr = "property" if prop else "name" return f'' def _list_items(values: list[str]) -> str: if not values: return "
Vignette session review
{description}
{html.escape(share.privacy)}
Vignette 열기