o2o-negosium-original/negodata/backend/services/card_service.py
Mina Choi e7660b10ac [feat] negodata/backend: 카드 도메인 CRUD 추가 + 견적·auth 보강
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:00:15 +09:00

205 lines
8.5 KiB
Python

import uuid
from fastapi import Depends
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import nego_cards, wild_cards
from common.enums import CardStatus, DBWRType, ErrorType
from common.models.gmodel import PageParams
from crud.card_crud import ICardCRUD, CardCRUD
from router.v1.card.protocol import CardData, Res_Card, Res_CardList, Res_DeleteCard
class CardService:
"""협상카드 비즈니스 로직. nego_cards/wild_cards 두 테이블을 user_id 로 스코프하고
프론트용 단일 모델(CardData, is_wildcard 플래그)로 합친다."""
def __init__(self, card_crud: ICardCRUD = Depends(CardCRUD)):
self.card_crud = card_crud
# ---- 행 → 통합 CardData --------------------------------------------------
@staticmethod
def _nego_to_data(row) -> CardData:
# 일반 협상카드는 상태 개념이 없다(상시 적용) → ACTIVE 고정.
return CardData(
nego_card_id=row.nego_card_id,
user_id=row.user_id,
is_wildcard=False,
name=row.name,
number=row.number,
script=row.script,
edit_script=row.edit_script,
status=CardStatus.ACTIVE.value,
created_at=row.created_at,
updated_at=row.updated_at,
)
@staticmethod
def _wild_to_data(row) -> CardData:
# 와일드카드 적용 여부(available) → status(ACTIVE/INACTIVE) 매핑.
return CardData(
nego_card_id=row.wild_card_id,
user_id=row.user_id,
is_wildcard=True,
name=row.name,
number=row.number,
script=row.script,
edit_script=row.edit_script,
status=CardStatus.ACTIVE.value if row.available else CardStatus.INACTIVE.value,
condition=row.condition,
memo=row.memo,
created_at=row.created_at,
updated_at=row.updated_at,
)
# ---- 소유 카드 탐색(어느 테이블인지 모를 때) ------------------------------
async def _find_owned(self, user_uuid: uuid.UUID, card_id: uuid.UUID):
"""card_id 를 nego_cards → wild_cards 순으로 찾고 소유권 확인.
(ErrorType, model, pk_col, row, is_wildcard) 반환."""
err, row = await DB_SESSION_MNG.execute_lambda(
nego_cards.DBType(),
DBWRType.DB_READ.value,
lambda s: self.card_crud.get_by_id(s, nego_cards, nego_cards.nego_card_id, card_id),
)
if err == ErrorType.SUCCESS and row is not None:
if row.user_id != user_uuid:
return ErrorType.CARD_NOT_FOUND, None, None, None, False
return ErrorType.SUCCESS, nego_cards, nego_cards.nego_card_id, row, False
err, row = await DB_SESSION_MNG.execute_lambda(
wild_cards.DBType(),
DBWRType.DB_READ.value,
lambda s: self.card_crud.get_by_id(s, wild_cards, wild_cards.wild_card_id, card_id),
)
if err == ErrorType.SUCCESS and row is not None:
if row.user_id != user_uuid:
return ErrorType.CARD_NOT_FOUND, None, None, None, True
return ErrorType.SUCCESS, wild_cards, wild_cards.wild_card_id, row, True
return ErrorType.CARD_NOT_FOUND, None, None, None, False
# ---- 목록 ----------------------------------------------------------------
async def list_cards(self, user_id: str, search, pg: PageParams) -> Res_CardList:
res = Res_CardList(page=pg.page, size=pg.size)
if not user_id:
return res
user_uuid = uuid.UUID(user_id)
# 합쳐서 정렬/페이징하므로 각 테이블에서 skip+limit 까지 받아온다(카드 수가 적어 충분).
fetch = pg.skip + pg.size
err_n, nego_rows, total_n = await DB_SESSION_MNG.execute_lambda(
nego_cards.DBType(),
DBWRType.DB_READ.value,
lambda s: self.card_crud.search(s, nego_cards, user_uuid, search, 0, fetch),
)
if err_n != ErrorType.SUCCESS:
res.result.SetResult(err_n)
return res
err_w, wild_rows, total_w = await DB_SESSION_MNG.execute_lambda(
wild_cards.DBType(),
DBWRType.DB_READ.value,
lambda s: self.card_crud.search(s, wild_cards, user_uuid, search, 0, fetch),
)
if err_w != ErrorType.SUCCESS:
res.result.SetResult(err_w)
return res
merged = [self._nego_to_data(r) for r in nego_rows] + [self._wild_to_data(r) for r in wild_rows]
merged.sort(key=lambda c: c.created_at or "", reverse=True)
res.cards = merged[pg.skip : pg.skip + pg.size]
res.total = total_n + total_w
return res
# ---- 단건 조회 -----------------------------------------------------------
async def get_card(self, user_id: str, card_id: str) -> Res_Card:
res = Res_Card()
err, _model, _pk, row, is_wild = await self._find_owned(uuid.UUID(user_id), uuid.UUID(card_id))
if err != ErrorType.SUCCESS:
res.result.SetResult(err)
return res
res.card = self._wild_to_data(row) if is_wild else self._nego_to_data(row)
return res
# ---- 등록 ----------------------------------------------------------------
async def create_card(self, user_id: str, data: dict) -> Res_Card:
res = Res_Card()
user_uuid = uuid.UUID(user_id)
is_wildcard = bool(data.get("is_wildcard", False))
common = dict(
user_id=user_uuid,
name=data.get("name"),
number=data.get("number"),
script=data.get("script"),
edit_script=data.get("edit_script"),
)
if is_wildcard:
card = wild_cards(
**common,
condition=data.get("condition"),
available=(data.get("status", CardStatus.ACTIVE.value) == CardStatus.ACTIVE.value),
memo=data.get("memo"),
)
model, pk_attr = wild_cards, "wild_card_id"
else:
card = nego_cards(**common)
model, pk_attr = nego_cards, "nego_card_id"
err = await DB_SESSION_MNG.execute_lambda_run(
[model.DBType()],
[lambda s: self.card_crud.add(s, card)],
)
if err != ErrorType.SUCCESS:
res.result.SetResult(err)
return res
# 서버 기본값(created_at 등)은 insert 후 객체에 실리지 않으므로 재조회.
return await self.get_card(user_id, str(getattr(card, pk_attr)))
# ---- 수정 ----------------------------------------------------------------
async def update_card(self, user_id: str, card_id: str, data: dict) -> Res_Card:
res = Res_Card()
user_uuid = uuid.UUID(user_id)
card_uuid = uuid.UUID(card_id)
err, model, pk_col, _row, is_wild = await self._find_owned(user_uuid, card_uuid)
if err != ErrorType.SUCCESS:
res.result.SetResult(err)
return res
# 해당 테이블에 있는 컬럼만 추린다(없는 필드는 무시). status → available(와일드 전용).
allowed = {"name", "number", "script", "edit_script"}
if is_wild:
allowed |= {"condition", "memo"}
payload = {k: v for k, v in data.items() if k in allowed}
if is_wild and "status" in data and data["status"] is not None:
payload["available"] = data["status"] == CardStatus.ACTIVE.value
err = await DB_SESSION_MNG.execute_lambda_run(
[model.DBType()],
[lambda s: self.card_crud.update(s, model, pk_col, card_uuid, payload)],
)
if err != ErrorType.SUCCESS:
res.result.SetResult(err)
return res
return await self.get_card(user_id, card_id)
# ---- 삭제(soft) ----------------------------------------------------------
async def delete_card(self, user_id: str, card_id: str) -> Res_DeleteCard:
res = Res_DeleteCard()
user_uuid = uuid.UUID(user_id)
card_uuid = uuid.UUID(card_id)
err, model, pk_col, _row, _is_wild = await self._find_owned(user_uuid, card_uuid)
if err != ErrorType.SUCCESS:
res.result.SetResult(err)
return res
err = await DB_SESSION_MNG.execute_lambda_run(
[model.DBType()],
[lambda s: self.card_crud.soft_delete(s, model, pk_col, card_uuid)],
)
if err != ErrorType.SUCCESS:
res.result.SetResult(err)
return res