o2o-negosium-original/negodata/backend/services/notification.py
Mina Choi 1d111a031e [feat] negodata: 알림함 — 견적 마감 결과(낙찰/재생성/결렬) 자동 통지
- company.notifications 테이블 + NotificationType(SUCCESS/REGENERATED/FAILURE)
- 백엔드 알림 조회/읽음 API + close_and_decide 결과 분기마다 알림 생성
- 헤더 알림 벨(안읽음 배지) + 알림 페이지(읽음·딥링크)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:24:59 +09:00

89 lines
3.4 KiB
Python

import uuid
from fastapi import Depends
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import notifications
from common.enums import DBWRType, ErrorType, NotificationType
from common.logger import LOG
from common.models.gmodel import PageParams
from common.utils.gtime import GTime
from crud.notification_crud import INotificationCRUD, NotificationCRUD
from router.v1.notification.protocol import NotificationData, Res_NotificationList, Res_NotificationRead
def _as_uuid(v):
return uuid.UUID(v) if isinstance(v, str) else v
async def create_notification(
user_id,
ntype: NotificationType,
data: dict,
ref_qt_id=None,
ref_session_id=None,
) -> None:
"""알림 1건 기록(인박스). 수신자=user_id(견적 작성자), data=렌더 스냅샷(유형별 필드).
부가 효과라 실패해도 본 흐름을 막지 않는다(raise 안 함, 로그만)."""
notif = notifications(
user_id=_as_uuid(user_id),
type=ntype.value,
ref_qt_id=_as_uuid(ref_qt_id) if ref_qt_id else None,
ref_session_id=_as_uuid(ref_session_id) if ref_session_id else None,
data=data,
)
err = await DB_SESSION_MNG.execute_lambda_run(
[notifications.DBType()],
[lambda s: DB_SESSION_MNG.insert(s, notif, raise_error=False)],
)
if err != ErrorType.SUCCESS:
LOG.e_no_callstack(f"[notify] 알림 기록 실패 user={user_id} type={ntype.name}")
class NotificationService:
"""알림 조회/읽음 처리(헤더 벨·알림 페이지). 항상 로그인 유저(user_id)로 스코프."""
def __init__(self, crud: INotificationCRUD = Depends(NotificationCRUD)):
self.crud = crud
async def list_notifications(self, user_id: str, pg: PageParams) -> Res_NotificationList:
res = Res_NotificationList(page=pg.page, size=pg.size)
uid = uuid.UUID(user_id)
err_type, rows, total = await DB_SESSION_MNG.execute_lambda(
notifications.DBType(),
DBWRType.DB_READ.value,
lambda s: self.crud.list_for_user(s, uid, pg.skip, pg.size),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.notifications = [NotificationData.model_validate(r) for r in rows]
res.total = total
_e, unread = await DB_SESSION_MNG.execute_lambda(
notifications.DBType(),
DBWRType.DB_READ.value,
lambda s: self.crud.count_unread(s, uid),
)
res.unread = unread if _e == ErrorType.SUCCESS else 0
return res
async def mark_read(self, user_id: str, notification_id: str) -> Res_NotificationRead:
res = Res_NotificationRead()
err_type = await DB_SESSION_MNG.execute_lambda_run(
[notifications.DBType()],
[lambda s: self.crud.mark_read(s, uuid.UUID(user_id), uuid.UUID(notification_id), GTime.UTC())],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
async def mark_all_read(self, user_id: str) -> Res_NotificationRead:
res = Res_NotificationRead()
err_type = await DB_SESSION_MNG.execute_lambda_run(
[notifications.DBType()],
[lambda s: self.crud.mark_all_read(s, uuid.UUID(user_id), GTime.UTC())],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res