전 저장소 리팩터링과 SSOT 정비
This commit is contained in:
parent
14ecbd4e7d
commit
3dfddcac6f
173 changed files with 19679 additions and 6952 deletions
|
|
@ -22,6 +22,7 @@ from ..config import settings
|
|||
from ..db import acquire, get_pool
|
||||
from ..deps import CurrentPrincipal, Role
|
||||
from ..runtime_policy import require_runtime_fallback_allowed
|
||||
from ..services.phase3_kpi_contract import PREPOST_MEASURE_NAMES, PREPOST_TIMEPOINTS
|
||||
from ..services.support_tickets import support_ticket_fingerprint
|
||||
from ..services.voice import PRESET_RATE, PRESET_TO_OPENAI_VOICE
|
||||
|
||||
|
|
@ -37,13 +38,6 @@ AVATAR_CONTENT_TYPES = {
|
|||
}
|
||||
DEFAULT_PREPOST_PILOT_ID = "phase3-pilot-draft"
|
||||
DEFAULT_PREPOST_INSTRUMENT_VERSION = "pilot-prepost-scaffold-2026-06-28"
|
||||
PREPOST_MEASURE_NAMES = (
|
||||
"self_efficacy",
|
||||
"skill_proficiency",
|
||||
"training_satisfaction",
|
||||
)
|
||||
PREPOST_TIMEPOINTS = ("pre", "post")
|
||||
|
||||
TERMS_BODY = """Vignette 서비스 이용약관 초안
|
||||
|
||||
1. 목적
|
||||
|
|
@ -193,7 +187,9 @@ class UserPreferencesResponse(BaseModel):
|
|||
theme: str = "system"
|
||||
voice_preset_id: str = "soft-young-fem"
|
||||
voice_rate: float = 1.0
|
||||
notifications: NotificationPreferences = Field(default_factory=NotificationPreferences)
|
||||
notifications: NotificationPreferences = Field(
|
||||
default_factory=NotificationPreferences
|
||||
)
|
||||
|
||||
|
||||
class UserPreferencesPatch(BaseModel):
|
||||
|
|
@ -221,7 +217,9 @@ TicketCategory = Literal[
|
|||
]
|
||||
TicketPriority = Literal["low", "normal", "high", "urgent"]
|
||||
TicketStatus = Literal["open", "triaged", "in_progress", "resolved", "closed"]
|
||||
PrepostMeasureName = Literal["self_efficacy", "skill_proficiency", "training_satisfaction"]
|
||||
PrepostMeasureName = Literal[
|
||||
"self_efficacy", "skill_proficiency", "training_satisfaction"
|
||||
]
|
||||
PrepostTimepoint = Literal["pre", "post"]
|
||||
|
||||
|
||||
|
|
@ -386,7 +384,9 @@ def _preferences_from_row(row) -> UserPreferencesResponse:
|
|||
theme=row["theme"],
|
||||
voice_preset_id=_normalize_voice_preset(row["voice_preset_id"]),
|
||||
voice_rate=float(row["voice_rate"]),
|
||||
notifications=NotificationPreferences.model_validate(row["notifications"] or {}),
|
||||
notifications=NotificationPreferences.model_validate(
|
||||
row["notifications"] or {}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -420,7 +420,9 @@ def _validated_avatar_extension(content_type: str, content: bytes) -> str:
|
|||
ext, magic = AVATAR_CONTENT_TYPES[normalized]
|
||||
if normalized == "image/webp":
|
||||
if not (content.startswith(magic) and content[8:12] == b"WEBP"):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="invalid_avatar_file")
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST, detail="invalid_avatar_file"
|
||||
)
|
||||
elif not content.startswith(magic):
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="invalid_avatar_file")
|
||||
return ext
|
||||
|
|
@ -484,16 +486,28 @@ async def get_me(principal: CurrentPrincipal) -> UserProfileResponse:
|
|||
|
||||
|
||||
@router.patch("/me", response_model=UserProfileResponse)
|
||||
async def patch_me(body: UserProfilePatch, principal: CurrentPrincipal) -> UserProfileResponse:
|
||||
async def patch_me(
|
||||
body: UserProfilePatch, principal: CurrentPrincipal
|
||||
) -> UserProfileResponse:
|
||||
profile = await _profile_for(principal)
|
||||
updated = await update_managed_user(
|
||||
principal.user_id,
|
||||
ManagedUserPatch(
|
||||
display_name=body.display_name if body.display_name is not None else profile.display_name,
|
||||
affiliation=body.affiliation if body.affiliation is not None else profile.affiliation,
|
||||
legal_name=body.legal_name if body.legal_name is not None else profile.legal_name,
|
||||
department=body.department if body.department is not None else profile.department,
|
||||
grade_level=body.grade_level if body.grade_level is not None else profile.grade_level,
|
||||
display_name=body.display_name
|
||||
if body.display_name is not None
|
||||
else profile.display_name,
|
||||
affiliation=body.affiliation
|
||||
if body.affiliation is not None
|
||||
else profile.affiliation,
|
||||
legal_name=body.legal_name
|
||||
if body.legal_name is not None
|
||||
else profile.legal_name,
|
||||
department=body.department
|
||||
if body.department is not None
|
||||
else profile.department,
|
||||
grade_level=body.grade_level
|
||||
if body.grade_level is not None
|
||||
else profile.grade_level,
|
||||
phone=body.phone if body.phone is not None else profile.phone,
|
||||
contact_address=(
|
||||
body.contact_address
|
||||
|
|
@ -506,7 +520,9 @@ async def patch_me(body: UserProfilePatch, principal: CurrentPrincipal) -> UserP
|
|||
if body.self_introduction is not None
|
||||
else profile.self_introduction
|
||||
),
|
||||
avatar_url=body.avatar_url if body.avatar_url is not None else profile.avatar_url,
|
||||
avatar_url=body.avatar_url
|
||||
if body.avatar_url is not None
|
||||
else profile.avatar_url,
|
||||
),
|
||||
)
|
||||
if updated is None:
|
||||
|
|
@ -525,7 +541,9 @@ async def upload_my_avatar(
|
|||
if not content:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="empty_avatar_file")
|
||||
if len(content) > AVATAR_MAX_BYTES:
|
||||
raise HTTPException(status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="avatar_too_large")
|
||||
raise HTTPException(
|
||||
status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="avatar_too_large"
|
||||
)
|
||||
ext = _validated_avatar_extension(content_type, content)
|
||||
|
||||
root = _upload_root()
|
||||
|
|
@ -597,7 +615,9 @@ async def create_support_ticket(
|
|||
) -> UserSupportTicketResponse:
|
||||
profile = await _profile_for(principal)
|
||||
try:
|
||||
async with acquire(role=principal.role.value, user_id=principal.user_id) as conn:
|
||||
async with acquire(
|
||||
role=principal.role.value, user_id=principal.user_id
|
||||
) as conn:
|
||||
subject = body.subject.strip()
|
||||
ticket_body = body.body.strip()
|
||||
source_path = body.source_path.strip()
|
||||
|
|
@ -725,7 +745,9 @@ async def _support_tickets_for_user(
|
|||
resolution_note=row["resolution_note"] or "",
|
||||
created_at=float(row["created_at"] or 0.0),
|
||||
updated_at=float(row["updated_at"] or 0.0),
|
||||
resolved_at=float(row["resolved_at"]) if row["resolved_at"] is not None else None,
|
||||
resolved_at=float(row["resolved_at"])
|
||||
if row["resolved_at"] is not None
|
||||
else None,
|
||||
)
|
||||
for row in rows
|
||||
],
|
||||
|
|
@ -741,11 +763,13 @@ async def list_my_support_tickets(
|
|||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="support ticket persistence unavailable",
|
||||
detail="support ticket persistence unavailable",
|
||||
) from exc
|
||||
|
||||
|
||||
def _normalized_prepost_score(raw_score: float, min_score: float, max_score: float) -> float:
|
||||
def _normalized_prepost_score(
|
||||
raw_score: float, min_score: float, max_score: float
|
||||
) -> float:
|
||||
if max_score <= min_score:
|
||||
return 0.0
|
||||
return round(((raw_score - min_score) / (max_score - min_score)) * 100.0, 3)
|
||||
|
|
@ -803,7 +827,8 @@ async def _prepost_measures_for_user(
|
|||
pairs = {
|
||||
item.measure_name
|
||||
for item in measures
|
||||
if {m.timepoint for m in measures if m.measure_name == item.measure_name} == {"pre", "post"}
|
||||
if {m.timepoint for m in measures if m.measure_name == item.measure_name}
|
||||
== {"pre", "post"}
|
||||
}
|
||||
return UserPrepostMeasuresResponse(
|
||||
source="database",
|
||||
|
|
@ -820,7 +845,9 @@ async def _prepost_measures_for_user(
|
|||
@router.get("/me/prepost-measures", response_model=UserPrepostMeasuresResponse)
|
||||
async def list_my_prepost_measures(
|
||||
principal: CurrentPrincipal,
|
||||
pilot_id: str = Query(default=DEFAULT_PREPOST_PILOT_ID, min_length=1, max_length=80),
|
||||
pilot_id: str = Query(
|
||||
default=DEFAULT_PREPOST_PILOT_ID, min_length=1, max_length=80
|
||||
),
|
||||
) -> UserPrepostMeasuresResponse:
|
||||
try:
|
||||
return await _prepost_measures_for_user(principal, pilot_id=pilot_id)
|
||||
|
|
@ -837,9 +864,13 @@ async def upsert_my_prepost_measure(
|
|||
principal: CurrentPrincipal,
|
||||
) -> UserPrepostMeasureItem:
|
||||
pilot_id = body.pilot_id.strip() or DEFAULT_PREPOST_PILOT_ID
|
||||
instrument_version = body.instrument_version.strip() or DEFAULT_PREPOST_INSTRUMENT_VERSION
|
||||
instrument_version = (
|
||||
body.instrument_version.strip() or DEFAULT_PREPOST_INSTRUMENT_VERSION
|
||||
)
|
||||
try:
|
||||
async with acquire(role=principal.role.value, user_id=principal.user_id) as conn:
|
||||
async with acquire(
|
||||
role=principal.role.value, user_id=principal.user_id
|
||||
) as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO app.learner_prepost_measure (
|
||||
|
|
@ -982,7 +1013,9 @@ async def patch_preferences(
|
|||
if body.voice_preset_id is not None
|
||||
else current_prefs.voice_preset_id
|
||||
),
|
||||
voice_rate=body.voice_rate if body.voice_rate is not None else current_prefs.voice_rate,
|
||||
voice_rate=body.voice_rate
|
||||
if body.voice_rate is not None
|
||||
else current_prefs.voice_rate,
|
||||
notifications=(
|
||||
body.notifications
|
||||
if body.notifications is not None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue