Merge remote-tracking branch 'origin/main' into feature/backend

This commit is contained in:
민헌 2026-06-18 14:26:06 +09:00
commit 16c5fde284
132 changed files with 6370 additions and 1227 deletions

View File

@ -38,9 +38,10 @@ PostgreSQL (외부, 5432)
## 빠른 시작
```bash
# 1) DB 준비 (최초 1회) — 사용할 PostgreSQL 에 스키마 적용
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/01-schema.sql
# (negosium_db / negodata_db + tbl_account 생성)
# 1) DB 준비 (최초 1회) — 사용할 PostgreSQL 에 스키마 + 시드 적용
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/01-schema.sql # negosium_db + 도메인 schema
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/02-learning-schema.sql # agent learning 스키마
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/03-seed-negodata.sql # negodata 전용 시드
# 2) 백엔드 기동
docker compose up -d # 두 backend (DB 는 config 대로 외부 연결)

View File

@ -9,8 +9,9 @@
# agent 서버: http://localhost:9500/docs
#
# DB 준비(최초 1회): postgres-init 의 SQL 을 대상 DB 에 적용한다.
# psql -h <host> -p <port> -U <user> -f postgres-init/01-schema.sql (negosium_db/negodata_db + tbl_account)
# psql -h <host> -p <port> -U <user> -f postgres-init/01-schema.sql (단일 negosium_db + 도메인별 schema)
# psql -h <host> -p <port> -U <user> -f postgres-init/02-learning-schema.sql (agent learning 스키마)
# psql -h <host> -p <port> -U <user> -f postgres-init/03-seed-negodata.sql (negodata 전용 시드: admin / admin1234, company.users)
services:
negosium-backend:
@ -38,6 +39,17 @@ services:
- "host.docker.internal:host-gateway"
restart: unless-stopped
# negodata 프론트 (Vite dev 서버).
negodata-front:
build: ./negodata/front
container_name: negodata-front
ports:
- "3000:3000"
volumes:
- ./negodata/front:/app
- /app/node_modules
restart: unless-stopped
# 협상 에이전트 (negosium_db 공유, learning 스키마 사용).
agent:
build: ./agent

View File

@ -1,26 +1,207 @@
import uuid
from sqlalchemy.orm import declarative_base
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy import Column, Integer, SmallInteger, BigInteger, Numeric, Float, String, Boolean, DateTime
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.sql import text
from common.enums import DBType
from common.enums import DBType, UserStatus, UserRole, CompanyStatus
# 모든 ORM 모델의 베이스. insert 시 isinstance 체크에도 사용된다.
MAIN_BASE = declarative_base()
class tbl_account(MAIN_BASE):
# 모델이 자신이 속한 논리 DB 를 알려준다 (람다 실행 시 DBType 으로 세션 선택).
# 공통 mixin
# DB 계약(_DBTypeMixin)과 ERD 공통 컬럼(MainTableMixin)을 분리해 둔다.
def _utc_now_sql():
return text("(now() AT TIME ZONE 'utc')")
class _DBTypeMixin:
"""모델이 자신이 속한 논리 DB 를 알려준다 (람다 실행 시 DBType 으로 세션 선택)."""
@staticmethod
def DBType():
return DBType.MAIN.value
__tablename__ = "tbl_account"
uid = Column(Integer, primary_key=True, autoincrement=True)
id = Column(String(45), nullable=False, unique=True) # 로그인 ID. 중복 가입 방지 위해 unique.
pw = Column(String(255), nullable=False, default="") # bcrypt 해시 저장
nickname = Column(String(45), nullable=False, default="")
is_blocked = Column(Boolean, nullable=False, default=False)
# PostgreSQL UTC now: now() 는 timestamptz 이므로 utc 로 변환해 timestamp 로 저장.
last_login_at = Column(DateTime, nullable=False, server_default=text("(now() AT TIME ZONE 'utc')"))
create_at = Column(DateTime, server_default=text("(now() AT TIME ZONE 'utc')"))
# ERD 공통 컬럼
class MainTableMixin(_DBTypeMixin):
created_at = Column(DateTime, nullable=False, server_default=_utc_now_sql())
updated_at = Column(DateTime, nullable=False, server_default=_utc_now_sql(), onupdate=_utc_now_sql())
deleted = Column(Boolean, nullable=False, server_default=text("false"), default=False)
# ERD 도메인 모델
class companies(MainTableMixin, MAIN_BASE):
__tablename__ = "companies"
company_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(100), nullable=False)
business_number = Column(String(30), nullable=True)
code = Column(Integer, nullable=True) # 내부 인덱스용
representative_name = Column(String(50), nullable=True)
email = Column(String(255), nullable=True)
contact_number = Column(String(20), nullable=True)
website_url = Column(String(255), nullable=True)
industry = Column(SmallInteger, nullable=True) # 업종 코드 (스키마 SMALLINT)
status = Column(SmallInteger, nullable=False, default=CompanyStatus.ACTIVE.value) # CompanyStatus
class users(MainTableMixin, MAIN_BASE):
__tablename__ = "users"
user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
company_id = Column(UUID(as_uuid=True), nullable=False, index=True)
id = Column(String(20), nullable=False, unique=True, index=True) # 로그인 아이디
password = Column(String(255), nullable=False) # bcrypt 해시 (ERD VARCHAR(30)→255 확장)
name = Column(String(50), nullable=True)
email = Column(String(255), nullable=True)
contact_number = Column(String(20), nullable=True)
last_accessed_at = Column(DateTime, nullable=False, server_default=_utc_now_sql())
status = Column(SmallInteger, nullable=False, default=UserStatus.ACTIVE.value) # UserStatus
role = Column(SmallInteger, nullable=False, default=UserRole.USER.value) # UserRole
class items(MainTableMixin, MAIN_BASE):
__tablename__ = "items"
item_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
company_id = Column(UUID(as_uuid=True), nullable=False, index=True)
user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 등록한 유저
name = Column(String(100), nullable=False)
code = Column(String(30), nullable=True)
category = Column(String(255), nullable=True)
category_type = Column(Integer, nullable=False, default=1) # 카테고리 탐색용 자동 증가 숫자
image_url = Column(String(255), nullable=True)
model_name = Column(String(100), nullable=True)
spec = Column(String(255), nullable=True)
manufacturer = Column(String(50), nullable=True)
made_in = Column(String(100), nullable=True)
price = Column(BigInteger, nullable=True) # 금액(원), 스키마 BIGINT
internet_lowest_price_yn = Column(Boolean, nullable=False, default=False) # 최저가 솔루션 원자성 보존용
moq = Column(String(50), nullable=True) # 최소 주문 수량
lead_time = Column(SmallInteger, nullable=True) # 주문 후 배송 도착까지 시간
quantity_unit = Column(String(50), nullable=True) # 단위 라벨(자유입력): EA/BOX/SET/ROLL ...
delivery_type = Column(SmallInteger, nullable=True) # 배송 유형 코드
vat_yn = Column(Boolean, nullable=True)
delivery_fee_yn = Column(Boolean, nullable=True)
class suppliers(MainTableMixin, MAIN_BASE):
__tablename__ = "suppliers"
supplier_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
company_id = Column(UUID(as_uuid=True), nullable=False, index=True)
user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 등록한 유저
name = Column(String(100), nullable=False)
code = Column(String(20), nullable=True)
manager_name = Column(String(50), nullable=True)
manager_email = Column(String(255), nullable=True)
manager_contact_number = Column(String(20), nullable=True) # ERD 오타(manger) 교정
priority = Column(String(10), nullable=True) # True/False 가 아닌 string value 가능
class nego_cards(MainTableMixin, MAIN_BASE):
__tablename__ = "nego_cards"
nego_card_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=True, index=True) # 등록 유저(o2o 기본 카드는 NULL)
name = Column(String(20), nullable=True) # 카드명
number = Column(String(10), nullable=True) # 식별번호(카드코드)
script = Column(String(255), nullable=True) # 협상 스크립트(평문 미리보기)
edit_script = Column(JSONB, nullable=True) # 편집된 스크립트(Slate JSON)
class wild_cards(MainTableMixin, MAIN_BASE):
__tablename__ = "wild_cards"
wild_card_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=True, index=True) # 등록 유저(o2o 기본 카드는 NULL)
name = Column(String(20), nullable=True) # 카드명
number = Column(String(10), nullable=True) # 식별번호(카드코드)
script = Column(String(255), nullable=True) # 협상 스크립트(평문 미리보기)
edit_script = Column(JSONB, nullable=True) # 편집된 스크립트(Slate JSON)
condition = Column(String(255), nullable=True) # 사용 조건(트리거)
available = Column(Boolean, nullable=False, default=False) # 수동 협상 적용 여부(ACTIVE/INACTIVE 매핑)
memo = Column(String(255), nullable=True) # 자유 메모
class quotation_settings(MainTableMixin, MAIN_BASE):
__tablename__ = "quotation_settings"
qt_setting_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=True, index=True) # 설정 소유 유저
target_margin_rate = Column(Numeric(8, 6), nullable=False)
anchoring_value = Column(Numeric(8, 6), nullable=False, default=0.01)
card_count = Column(Integer, nullable=False, default=3) # 한 협상 내 협상카드 사용 횟수
class quotations(MainTableMixin, MAIN_BASE):
__tablename__ = "quotations"
qt_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), nullable=False, index=True)
qt_setting_id = Column(UUID(as_uuid=True), nullable=False, index=True)
version_id = Column(UUID(as_uuid=True), nullable=False, index=True)
name = Column(String(50), nullable=False)
number = Column(String(30), nullable=False)
type = Column(SmallInteger, nullable=False) # QuotationType: 1=renego(1:1) / 2=requote(1:N)
round = Column(Integer, nullable=False, default=1) # 재견적 진행 시 증가
status = Column(SmallInteger, nullable=False) # 진행 상태 코드
start_time = Column(DateTime, nullable=False)
end_time = Column(DateTime, nullable=False)
manager_name = Column(String(50), nullable=True)
manager_email = Column(String(255), nullable=True)
manager_contact_number = Column(String(20), nullable=True)
memo = Column(String(100), nullable=True)
iteration = Column(Integer, nullable=False, default=0)
preferred_sp_yn = Column(Boolean, nullable=True)
preferred_sp_id = Column(UUID(as_uuid=True), nullable=True)
preferred_sp_name = Column(String(20), nullable=True)
equal_bid_yn = Column(Boolean, nullable=True)
equal_bid_data = Column(JSONB, nullable=True)
class sessions(MainTableMixin, MAIN_BASE):
__tablename__ = "sessions"
session_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
quotation_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 소속 견적(quotations.qt_id)
item_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 대상 상품(items.item_id)
supplier_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 대상 공급사(suppliers.supplier_id)
qt_number = Column(String(30), nullable=False) # 견적번호 스냅샷
qt_round = Column(Integer, nullable=False) # 견적 라운드 스냅샷
qt_type = Column(SmallInteger, nullable=False) # QuotationType 스냅샷
target_price = Column(BigInteger, nullable=False) # 목표가(원)
status = Column(SmallInteger, nullable=False) # SessionStatus 코드
bid_price = Column(BigInteger, nullable=True) # 입찰가(원)
bid_at = Column(DateTime, nullable=True) # 입찰 시각
end_time = Column(DateTime, nullable=False) # 세션 종료 시각
reject_reason = Column(String(255), nullable=True)
reject_price = Column(BigInteger, nullable=True)
reject_delivery_type = Column(SmallInteger, nullable=True) # DeliveryType 코드
class chats(MainTableMixin, MAIN_BASE):
__tablename__ = "chats"
chat_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
session_id = Column(UUID(as_uuid=True), nullable=False, index=True) # 소속 세션(sessions.session_id)
card_id = Column(UUID(as_uuid=True), nullable=True) # 사용된 카드(nego_cards.nego_card_id)
seq = Column(Integer, nullable=False, default=1) # 세션 내 메시지 순번(프로토콜 index)
sender = Column(SmallInteger, nullable=False) # ChatSender 코드(1=bot, 2=partner)
target_price = Column(BigInteger, nullable=False) # 제시 목표가(원)
card_used_yn = Column(Boolean, nullable=True) # 카드 사용 여부
indicator_value = Column(Numeric(8, 6), nullable=True)
card_type = Column(SmallInteger, nullable=True) # 1=nego_card, 2=wild_card

View File

@ -36,6 +36,23 @@ class ErrorType(Enum):
ACCOUNT_ALREADY_EXIST = auto()
ACCOUNT_BLOCKED_USER = auto()
# 상품 관련 에러
ITEM_NOT_FOUND = 1300
ITEM_CODE_DUPLICATE = auto()
# 협력사 관련 에러
SUPPLIER_NOT_FOUND = 1400
SUPPLIER_CODE_DUPLICATE = auto()
# 견적 관련 에러
QUOTATION_NOT_FOUND = 1500
# 견적 설정 관련 에러
QUOTATION_SETTING_NOT_FOUND = 1600
# 협상카드 관련 에러
CARD_NOT_FOUND = 1700
# ErrorType 의 HTTP_* 값과 status_code 를 맞춰 router 단에서 raise 한다.
EXCEPTION_INVALID_CLIENT_REQUEST = HTTPException(status_code=ErrorType.HTTP_INVALID_CLIENT_REQUEST.value, detail=ErrorType.HTTP_INVALID_CLIENT_REQUEST.name)
@ -59,3 +76,111 @@ class DBWRType(Enum):
DB_READ = 1
DB_WRITE = 2
# 도메인 코드값
class UserStatus(Enum):
"""users.status 코드값."""
ACTIVE = 1
INACTIVE = 2
class UserRole(Enum):
"""users.role 코드값."""
USER = 1
MANAGER = 2
class CompanyStatus(Enum):
"""companies.status 코드값."""
ACTIVE = 1
INACTIVE = 2
class QuotationType(Enum):
"""quotations.type 코드값. 1=renego(재협상 1:1), 2=requote(재견적 1:N)."""
RENEGO = 1
REQUOTE = 2
class QuotationStatus(Enum):
"""quotations.status 코드값(SMALLINT). 프론트 견적상태 뱃지와 매핑된다."""
CREATED = 1 # 견적생성
ACTIVE = 2 # 견적진행중
CLOSED = 3 # 견적마감
ON_HOLD = 4 # 협상보류
class SessionStatus(Enum):
"""negotiation.sessions.status 코드값. 협력사별 협상 세션 진행 상태."""
NEGOTIATING = 1 # 협상중
COMPLETED = 2 # 협상종료
REJECTED = 3 # 협상거부
class ChatSender(Enum):
"""negotiation.chats.sender 코드값. 채팅 발신 주체."""
BOT = 1 # 구매대행 봇
PARTNER = 2 # 협력사
class DeliveryType(Enum):
"""items.delivery_type 코드값. 협상 채팅의 배송형태 선택지와 동일 집합."""
PARTNER = 1 # 협력사배송
COURIER = 2 # 지정택배배송
PICKUP = 3 # 픽업배송
class CardStatus(Enum):
"""nego_cards.status 코드값. 와일드카드의 협상 적용 여부(수동 승인). 일반 협상카드는 상시 ACTIVE."""
ACTIVE = 1
INACTIVE = 2
# 도메인 enum 한글 라벨. 프론트 드롭다운 표시는 이 라벨을 쓴다(값=코드).
ENUM_LABELS = {
UserStatus.ACTIVE: "활성",
UserStatus.INACTIVE: "비활성",
UserRole.USER: "일반",
UserRole.MANAGER: "관리자",
CompanyStatus.ACTIVE: "활성",
CompanyStatus.INACTIVE: "비활성",
QuotationType.RENEGO: "재협상",
QuotationType.REQUOTE: "재견적",
QuotationStatus.CREATED: "견적생성",
QuotationStatus.ACTIVE: "견적진행중",
QuotationStatus.CLOSED: "견적마감",
QuotationStatus.ON_HOLD: "협상보류",
SessionStatus.NEGOTIATING: "협상중",
SessionStatus.COMPLETED: "협상종료",
SessionStatus.REJECTED: "협상거부",
ChatSender.BOT: "봇",
ChatSender.PARTNER: "협력사",
DeliveryType.PARTNER: "협력사배송",
DeliveryType.COURIER: "지정택배배송",
DeliveryType.PICKUP: "픽업배송",
CardStatus.ACTIVE: "적용",
CardStatus.INACTIVE: "대기",
}
# 프론트로 내려주는 도메인 코드 enum 모음. 새 코드 enum 추가 시 여기에 등록한다.
DOMAIN_ENUMS = {
"user_status": UserStatus,
"user_role": UserRole,
"company_status": CompanyStatus,
"quotation_type": QuotationType,
"quotation_status": QuotationStatus,
"session_status": SessionStatus,
"chat_sender": ChatSender,
"delivery_type": DeliveryType,
"card_status": CardStatus,
}

View File

@ -1,6 +1,7 @@
import json
from typing import Optional
from fastapi import Query
from pydantic import BaseModel, Field
from common.enums import ErrorType
@ -45,12 +46,30 @@ class Res_WebPacketProtocol(WebPacketProtocol):
msg: Optional[str] = None
class Res_PageProtocol(Res_WebPacketProtocol):
# 목록 응답 공용 페이지 메타.
total: int = 0
page: int = 0
size: int = 0
class PageParams:
# 목록 엔드포인트 공용 쿼리 파라미터. 라우터에서 Depends() 로 주입한다.
def __init__(self, page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100)):
self.page = page
self.size = size
@property
def skip(self) -> int:
return (self.page - 1) * self.size
class UserInfo(StructModel):
"""JWT subject 로 인코딩되는 유저 식별 정보."""
uid: int
id: str
nickname: str
user_id: str # users.user_id (uuid) — 데이터 스코프 키
id: str # users.id (로그인 아이디) — get_me 재조회 키
company_id: str # users.company_id (uuid) — 멀티테넌트 스코프 키
def __init__(self, *args, **kwargs) -> None:
super().__init__()

View File

@ -7,6 +7,7 @@ port = 9400
process_count = 1
is_ssl = false
is_test = true
client_url = "http://localhost:3000" # CORS 허용
[LogConfig]
print_console = true
@ -16,7 +17,7 @@ log_level = "debug"
# 관리형 DB(RDS/Aurora/Azure)는 host 에 엔드포인트, sslmode="require".
[MainDBConfig]
db_type = "postgresql"
name = "negodata_db"
name = "negosium_db"
write_host = "127.0.0.1"
write_port = 5432
write_id = "<DB_USER>"

View File

@ -7,6 +7,7 @@ class WebServerConfig(ConfigModel):
process_count: int = 1
is_ssl: bool = False
is_test: bool = False
client_url: str = ""
class LogConfig(ConfigModel):

View File

@ -4,6 +4,8 @@ import os
os.environ.setdefault("APP_ENV", "local")
import uuid
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy import text
@ -28,11 +30,31 @@ async def db_engine():
engine = create_async_engine(_write_url(main_db_config))
async with engine.begin() as conn:
await conn.run_sync(MAIN_BASE.metadata.create_all) # 이미 있으면 skip
await conn.execute(text("TRUNCATE TABLE tbl_account"))
# negodata 도메인 테이블 전부 비워 격리 (CASCADE: FK 미설정이라 안전망)
await conn.execute(
text(
"TRUNCATE TABLE tbl_account, users, companies, items, suppliers, "
"quotation_settings, quotations RESTART IDENTITY CASCADE"
)
)
yield engine
await engine.dispose()
@pytest_asyncio.fixture
async def company_id(db_engine) -> str:
"""테스트용 소속사 1개를 시드하고 company_id(uuid str)를 돌려준다.
users 는 company_id 를 요구하므로 계정 생성 테스트의 선행 조건이다.
"""
cid = uuid.uuid4()
async with db_engine.begin() as conn:
await conn.execute(
text("INSERT INTO companies (company_id, name) VALUES (:cid, :name)"),
{"cid": cid, "name": "테스트사"},
)
return str(cid)
@pytest_asyncio.fixture(scope="session", autouse=True)
async def _dispose_app_engines():
"""테스트 세션이 끝날 때 앱 싱글톤 엔진을 정리한다.

View File

@ -0,0 +1,104 @@
from abc import ABC, abstractmethod
from typing import Optional, Tuple
from sqlalchemy import select, func, and_, or_, update
from sqlalchemy.ext.asyncio import AsyncSession
from common.database.db_session_manager import DB_SESSION_MNG
from common.enums import ErrorType
from common.logger import LOG
from common.utils.gtime import GTime
# 협상카드 CRUD. nego_cards/wild_cards 두 테이블에 공통으로 쓰는 제네릭 구현.
class ICardCRUD(ABC):
@abstractmethod
async def search(self, cdb: AsyncSession, model, user_id, search, skip, limit) -> Tuple[ErrorType, list, int]:
pass
@abstractmethod
async def get_by_id(self, cdb: AsyncSession, model, pk_col, card_id) -> Tuple[ErrorType, object]:
pass
@abstractmethod
async def add(self, cdb: AsyncSession, card) -> ErrorType:
pass
@abstractmethod
async def update(self, cdb: AsyncSession, model, pk_col, card_id, data: dict) -> ErrorType:
pass
@abstractmethod
async def soft_delete(self, cdb: AsyncSession, model, pk_col, card_id) -> ErrorType:
pass
class CardCRUD(ICardCRUD):
async def search(
self, cdb: AsyncSession, model, user_id, search: Optional[str], skip: int, limit: int
) -> Tuple[ErrorType, list, int]:
try:
conditions = [model.deleted == False, model.user_id == user_id] # noqa: E712
if search:
conditions.append(
or_(
model.name.ilike(f"%{search}%"),
model.number.ilike(f"%{search}%"),
model.script.ilike(f"%{search}%"),
)
)
where = and_(*conditions)
cnt_err, cnt_rows = await DB_SESSION_MNG.execute(cdb, select(func.count()).select_from(model).where(where))
if cnt_err != ErrorType.SUCCESS:
return cnt_err, [], 0
total = int(cnt_rows[0] or 0) if cnt_rows else 0
list_err, rows = await DB_SESSION_MNG.execute(
cdb,
select(model).where(where).order_by(model.created_at.desc()).offset(skip).limit(limit),
)
if list_err != ErrorType.SUCCESS:
return list_err, [], 0
return ErrorType.SUCCESS, list(rows), total
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, [], 0
async def get_by_id(self, cdb: AsyncSession, model, pk_col, card_id) -> Tuple[ErrorType, object]:
try:
query = select(model).where(pk_col == card_id, model.deleted == False).limit(1) # noqa: E712
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, None
if len(row_list) != 1:
return ErrorType.DB_INVALID_KEY, None
return ErrorType.SUCCESS, row_list[0]
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, None
async def add(self, cdb: AsyncSession, card) -> ErrorType:
try:
return await DB_SESSION_MNG.insert(cdb, card)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def update(self, cdb: AsyncSession, model, pk_col, card_id, data: dict) -> ErrorType:
try:
if not data:
return ErrorType.SUCCESS
query = update(model).where(pk_col == card_id).values(**data)
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def soft_delete(self, cdb: AsyncSession, model, pk_col, card_id) -> ErrorType:
try:
query = update(model).where(pk_col == card_id).values(deleted=True, updated_at=GTime.UTC())
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED

View File

@ -0,0 +1,168 @@
from abc import ABC, abstractmethod
from typing import Optional, Tuple
from sqlalchemy import select, func, and_, or_, update
from sqlalchemy.ext.asyncio import AsyncSession
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import items
from common.enums import ErrorType
from common.logger import LOG
from common.utils.gtime import GTime
# 상품 CRUD. 모든 조회/변경은 company_id 로 스코프된다(멀티테넌트).
class IItemCRUD(ABC):
@abstractmethod
async def search(self, cdb: AsyncSession, company_id, search, category, skip, limit) -> Tuple[ErrorType, list, int]:
pass
@abstractmethod
async def distinct_categories(self, cdb: AsyncSession, company_id) -> Tuple[ErrorType, list]:
pass
@abstractmethod
async def code_exists(self, cdb: AsyncSession, company_id, code) -> Tuple[ErrorType, bool]:
pass
@abstractmethod
async def existing_codes(self, cdb: AsyncSession, company_id, codes: list) -> Tuple[ErrorType, list]:
pass
@abstractmethod
async def get_by_id(self, cdb: AsyncSession, item_id) -> Tuple[ErrorType, items]:
pass
@abstractmethod
async def add_item(self, cdb: AsyncSession, item: items) -> ErrorType:
pass
@abstractmethod
async def update_item(self, cdb: AsyncSession, item_id, data: dict) -> ErrorType:
pass
@abstractmethod
async def soft_delete(self, cdb: AsyncSession, item_id) -> ErrorType:
pass
class ItemCRUD(IItemCRUD):
async def search(
self, cdb: AsyncSession, company_id, search: Optional[str], category: Optional[str], skip: int, limit: int
) -> Tuple[ErrorType, list, int]:
try:
conditions = [items.deleted == False, items.company_id == company_id] # noqa: E712
if search:
conditions.append(or_(items.name.ilike(f"%{search}%"), items.code.ilike(f"%{search}%")))
if category:
conditions.append(items.category == category)
where = and_(*conditions)
cnt_err, cnt_rows = await DB_SESSION_MNG.execute(cdb, select(func.count()).select_from(items).where(where))
if cnt_err != ErrorType.SUCCESS:
return cnt_err, [], 0
total = int(cnt_rows[0] or 0) if cnt_rows else 0
list_err, rows = await DB_SESSION_MNG.execute(
cdb,
select(items).where(where).order_by(items.created_at.desc()).offset(skip).limit(limit),
)
if list_err != ErrorType.SUCCESS:
return list_err, [], 0
return ErrorType.SUCCESS, list(rows), total
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, [], 0
# 회사의 모든 상품에서 카테고리를 "이름 단위"로 중복 제거해 (이름, 대표 타입) 목록을 돌려준다.
async def distinct_categories(self, cdb: AsyncSession, company_id) -> Tuple[ErrorType, list]:
try:
query = (
select(items.category, func.min(items.category_type))
.where(
items.company_id == company_id,
items.deleted == False, # noqa: E712
items.category.isnot(None),
items.category != "",
)
.group_by(items.category)
.order_by(items.category)
)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, []
return ErrorType.SUCCESS, list(rows)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, []
async def code_exists(self, cdb: AsyncSession, company_id, code) -> Tuple[ErrorType, bool]:
try:
query = select(func.count()).select_from(items).where(
items.company_id == company_id,
items.code == code,
items.deleted == False, # noqa: E712
)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, False
cnt = int(rows[0] or 0) if rows else 0
return ErrorType.SUCCESS, cnt > 0
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, False
async def existing_codes(self, cdb: AsyncSession, company_id, codes: list) -> Tuple[ErrorType, list]:
try:
if not codes:
return ErrorType.SUCCESS, []
query = select(items.code).where(
items.company_id == company_id,
items.code.in_(codes),
items.deleted == False, # noqa: E712
)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, []
return ErrorType.SUCCESS, [c for c in rows if c is not None]
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, []
async def get_by_id(self, cdb: AsyncSession, item_id) -> Tuple[ErrorType, items]:
try:
query = select(items).where(items.item_id == item_id, items.deleted == False).limit(1) # noqa: E712
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, None
if len(row_list) != 1:
return ErrorType.DB_INVALID_KEY, None
return ErrorType.SUCCESS, row_list[0]
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, None
async def add_item(self, cdb: AsyncSession, item: items) -> ErrorType:
try:
return await DB_SESSION_MNG.insert(cdb, item)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def update_item(self, cdb: AsyncSession, item_id, data: dict) -> ErrorType:
try:
if not data:
return ErrorType.SUCCESS
query = update(items).where(items.item_id == item_id).values(**data)
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def soft_delete(self, cdb: AsyncSession, item_id) -> ErrorType:
try:
query = update(items).where(items.item_id == item_id).values(deleted=True, updated_at=GTime.UTC())
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED

View File

@ -0,0 +1,190 @@
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Optional, Tuple
from sqlalchemy import select, func, and_, update
from sqlalchemy.ext.asyncio import AsyncSession
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import quotations, sessions, chats, nego_cards, wild_cards
from common.enums import ErrorType
from common.logger import LOG
from common.utils.gtime import GTime
# 견적 CRUD.
class IQuotationCRUD(ABC):
@abstractmethod
async def search(
self, cdb: AsyncSession, status, type_, start_from, start_to, skip, limit
) -> Tuple[ErrorType, list, int]:
pass
@abstractmethod
async def get_by_id(self, cdb: AsyncSession, qt_id) -> Tuple[ErrorType, quotations]:
pass
@abstractmethod
async def add_quotation(self, cdb: AsyncSession, quotation: quotations) -> ErrorType:
pass
@abstractmethod
async def update_quotation(self, cdb: AsyncSession, qt_id, data: dict) -> ErrorType:
pass
@abstractmethod
async def soft_delete(self, cdb: AsyncSession, qt_id) -> ErrorType:
pass
@abstractmethod
async def list_sessions(self, cdb: AsyncSession, qt_id) -> Tuple[ErrorType, list]:
pass
@abstractmethod
async def list_chats(self, cdb: AsyncSession, session_id) -> Tuple[ErrorType, list]:
pass
@abstractmethod
async def list_used_cards(self, cdb: AsyncSession, qt_id) -> Tuple[ErrorType, list]:
pass
class QuotationCRUD(IQuotationCRUD):
async def search(
self,
cdb: AsyncSession,
status: Optional[str],
type_: Optional[str],
start_from: Optional[datetime],
start_to: Optional[datetime],
skip: int,
limit: int,
) -> Tuple[ErrorType, list, int]:
try:
conditions = [quotations.deleted == False] # noqa: E712
if status:
conditions.append(quotations.status == status)
if type_:
conditions.append(quotations.type == type_)
if start_from:
conditions.append(quotations.start_time >= start_from)
if start_to:
conditions.append(quotations.start_time <= start_to)
where = and_(*conditions)
cnt_err, cnt_rows = await DB_SESSION_MNG.execute(cdb, select(func.count()).select_from(quotations).where(where))
if cnt_err != ErrorType.SUCCESS:
return cnt_err, [], 0
total = int(cnt_rows[0] or 0) if cnt_rows else 0
list_err, rows = await DB_SESSION_MNG.execute(
cdb,
select(quotations).where(where).order_by(quotations.created_at.desc()).offset(skip).limit(limit),
)
if list_err != ErrorType.SUCCESS:
return list_err, [], 0
return ErrorType.SUCCESS, list(rows), total
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, [], 0
async def get_by_id(self, cdb: AsyncSession, qt_id) -> Tuple[ErrorType, quotations]:
try:
query = select(quotations).where(quotations.qt_id == qt_id, quotations.deleted == False).limit(1) # noqa: E712
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, None
if len(row_list) != 1:
return ErrorType.DB_INVALID_KEY, None
return ErrorType.SUCCESS, row_list[0]
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, None
async def add_quotation(self, cdb: AsyncSession, quotation: quotations) -> ErrorType:
try:
return await DB_SESSION_MNG.insert(cdb, quotation)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def update_quotation(self, cdb: AsyncSession, qt_id, data: dict) -> ErrorType:
try:
if not data:
return ErrorType.SUCCESS
query = update(quotations).where(quotations.qt_id == qt_id).values(**data)
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def soft_delete(self, cdb: AsyncSession, qt_id) -> ErrorType:
try:
query = update(quotations).where(quotations.qt_id == qt_id).values(deleted=True, updated_at=GTime.UTC())
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
# ----- 견적 상세: 세션 / 채팅 / 사용카드 (읽기 전용) -----
async def list_sessions(self, cdb: AsyncSession, qt_id) -> Tuple[ErrorType, list]:
try:
query = (
select(sessions)
.where(sessions.quotation_id == qt_id, sessions.deleted == False) # noqa: E712
.order_by(sessions.created_at.asc())
)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, []
return ErrorType.SUCCESS, list(rows)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, []
async def list_chats(self, cdb: AsyncSession, session_id) -> Tuple[ErrorType, list]:
try:
query = (
select(chats)
.where(chats.session_id == session_id, chats.deleted == False) # noqa: E712
.order_by(chats.seq.asc())
)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, []
return ErrorType.SUCCESS, list(rows)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, []
async def list_used_cards(self, cdb: AsyncSession, qt_id) -> Tuple[ErrorType, list]:
"""견적의 세션들에서 실제 사용된 카드(chats.card_used_yn)를 카드 카탈로그와 조인.
반환: [(chat_row, card_id, name, script), ...].
card_type 1=nego_cards / 2=wild_cards 양쪽을 LEFT JOIN 해서 어느 쪽이든 잡는다.
"""
try:
query = (
select(
chats,
func.coalesce(nego_cards.nego_card_id, wild_cards.wild_card_id).label("card_pk"),
func.coalesce(nego_cards.name, wild_cards.name).label("card_name"),
func.coalesce(nego_cards.script, wild_cards.script).label("card_script"),
)
.join(sessions, sessions.session_id == chats.session_id)
.outerjoin(nego_cards, and_(nego_cards.nego_card_id == chats.card_id, chats.card_type == 1))
.outerjoin(wild_cards, and_(wild_cards.wild_card_id == chats.card_id, chats.card_type == 2))
.where(
sessions.quotation_id == qt_id,
chats.card_used_yn == True, # noqa: E712
chats.deleted == False, # noqa: E712
sessions.deleted == False, # noqa: E712
)
.order_by(chats.created_at.asc())
)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, []
return ErrorType.SUCCESS, list(rows)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, []

View File

@ -0,0 +1,104 @@
from abc import ABC, abstractmethod
from typing import Tuple
from sqlalchemy import select, func, and_, update
from sqlalchemy.ext.asyncio import AsyncSession
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import quotation_settings
from common.enums import ErrorType
from common.logger import LOG
from common.utils.gtime import GTime
# 견적 설정 CRUD. 모든 조회/변경은 user_id 로 스코프된다(유저별 설정).
class IQuotationSettingCRUD(ABC):
@abstractmethod
async def list_by_user(self, cdb: AsyncSession, user_id) -> Tuple[ErrorType, list, int]:
pass
@abstractmethod
async def get_by_id(self, cdb: AsyncSession, qt_setting_id) -> Tuple[ErrorType, quotation_settings]:
pass
@abstractmethod
async def add_setting(self, cdb: AsyncSession, setting: quotation_settings) -> ErrorType:
pass
@abstractmethod
async def update_setting(self, cdb: AsyncSession, qt_setting_id, data: dict) -> ErrorType:
pass
@abstractmethod
async def soft_delete(self, cdb: AsyncSession, qt_setting_id) -> ErrorType:
pass
class QuotationSettingCRUD(IQuotationSettingCRUD):
async def list_by_user(self, cdb: AsyncSession, user_id) -> Tuple[ErrorType, list, int]:
try:
where = and_(quotation_settings.deleted == False, quotation_settings.user_id == user_id) # noqa: E712
cnt_err, cnt_rows = await DB_SESSION_MNG.execute(
cdb, select(func.count()).select_from(quotation_settings).where(where)
)
if cnt_err != ErrorType.SUCCESS:
return cnt_err, [], 0
total = int(cnt_rows[0] or 0) if cnt_rows else 0
list_err, rows = await DB_SESSION_MNG.execute(
cdb,
select(quotation_settings).where(where).order_by(quotation_settings.created_at.desc()),
)
if list_err != ErrorType.SUCCESS:
return list_err, [], 0
return ErrorType.SUCCESS, list(rows), total
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, [], 0
async def get_by_id(self, cdb: AsyncSession, qt_setting_id) -> Tuple[ErrorType, quotation_settings]:
try:
query = (
select(quotation_settings)
.where(quotation_settings.qt_setting_id == qt_setting_id, quotation_settings.deleted == False) # noqa: E712
.limit(1)
)
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, None
if len(row_list) != 1:
return ErrorType.DB_INVALID_KEY, None
return ErrorType.SUCCESS, row_list[0]
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, None
async def add_setting(self, cdb: AsyncSession, setting: quotation_settings) -> ErrorType:
try:
return await DB_SESSION_MNG.insert(cdb, setting)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def update_setting(self, cdb: AsyncSession, qt_setting_id, data: dict) -> ErrorType:
try:
if not data:
return ErrorType.SUCCESS
query = update(quotation_settings).where(quotation_settings.qt_setting_id == qt_setting_id).values(**data)
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def soft_delete(self, cdb: AsyncSession, qt_setting_id) -> ErrorType:
try:
query = (
update(quotation_settings)
.where(quotation_settings.qt_setting_id == qt_setting_id)
.values(deleted=True, updated_at=GTime.UTC())
)
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED

View File

@ -0,0 +1,148 @@
from abc import ABC, abstractmethod
from typing import Optional, Tuple
from sqlalchemy import select, func, and_, or_, update
from sqlalchemy.ext.asyncio import AsyncSession
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import suppliers
from common.enums import ErrorType
from common.logger import LOG
from common.utils.gtime import GTime
# 협력사 CRUD. 모든 조회/변경은 company_id 로 스코프된다(멀티테넌트).
class ISupplierCRUD(ABC):
@abstractmethod
async def search(self, cdb: AsyncSession, company_id, search, priority, skip, limit) -> Tuple[ErrorType, list, int]:
pass
@abstractmethod
async def code_exists(self, cdb: AsyncSession, company_id, code) -> Tuple[ErrorType, bool]:
pass
@abstractmethod
async def existing_codes(self, cdb: AsyncSession, company_id, codes: list) -> Tuple[ErrorType, list]:
pass
@abstractmethod
async def get_by_id(self, cdb: AsyncSession, supplier_id) -> Tuple[ErrorType, suppliers]:
pass
@abstractmethod
async def add_supplier(self, cdb: AsyncSession, supplier: suppliers) -> ErrorType:
pass
@abstractmethod
async def update_supplier(self, cdb: AsyncSession, supplier_id, data: dict) -> ErrorType:
pass
@abstractmethod
async def soft_delete(self, cdb: AsyncSession, supplier_id) -> ErrorType:
pass
class SupplierCRUD(ISupplierCRUD):
async def search(
self, cdb: AsyncSession, company_id, search: Optional[str], priority: Optional[str], skip: int, limit: int
) -> Tuple[ErrorType, list, int]:
try:
conditions = [suppliers.deleted == False, suppliers.company_id == company_id] # noqa: E712
if search:
conditions.append(
or_(
suppliers.name.ilike(f"%{search}%"),
suppliers.code.ilike(f"%{search}%"),
suppliers.manager_name.ilike(f"%{search}%"),
)
)
if priority:
conditions.append(suppliers.priority == priority)
where = and_(*conditions)
cnt_err, cnt_rows = await DB_SESSION_MNG.execute(cdb, select(func.count()).select_from(suppliers).where(where))
if cnt_err != ErrorType.SUCCESS:
return cnt_err, [], 0
total = int(cnt_rows[0] or 0) if cnt_rows else 0
list_err, rows = await DB_SESSION_MNG.execute(
cdb,
select(suppliers).where(where).order_by(suppliers.created_at.desc()).offset(skip).limit(limit),
)
if list_err != ErrorType.SUCCESS:
return list_err, [], 0
return ErrorType.SUCCESS, list(rows), total
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, [], 0
async def code_exists(self, cdb: AsyncSession, company_id, code) -> Tuple[ErrorType, bool]:
try:
query = select(func.count()).select_from(suppliers).where(
suppliers.company_id == company_id,
suppliers.code == code,
suppliers.deleted == False, # noqa: E712
)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, False
cnt = int(rows[0] or 0) if rows else 0
return ErrorType.SUCCESS, cnt > 0
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, False
async def existing_codes(self, cdb: AsyncSession, company_id, codes: list) -> Tuple[ErrorType, list]:
try:
if not codes:
return ErrorType.SUCCESS, []
query = select(suppliers.code).where(
suppliers.company_id == company_id,
suppliers.code.in_(codes),
suppliers.deleted == False, # noqa: E712
)
err_type, rows = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, []
return ErrorType.SUCCESS, [c for c in rows if c is not None]
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, []
async def get_by_id(self, cdb: AsyncSession, supplier_id) -> Tuple[ErrorType, suppliers]:
try:
query = select(suppliers).where(suppliers.supplier_id == supplier_id, suppliers.deleted == False).limit(1) # noqa: E712
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, None
if len(row_list) != 1:
return ErrorType.DB_INVALID_KEY, None
return ErrorType.SUCCESS, row_list[0]
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, None
async def add_supplier(self, cdb: AsyncSession, supplier: suppliers) -> ErrorType:
try:
return await DB_SESSION_MNG.insert(cdb, supplier)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def update_supplier(self, cdb: AsyncSession, supplier_id, data: dict) -> ErrorType:
try:
if not data:
return ErrorType.SUCCESS
query = update(suppliers).where(suppliers.supplier_id == supplier_id).values(**data)
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def soft_delete(self, cdb: AsyncSession, supplier_id) -> ErrorType:
try:
query = update(suppliers).where(suppliers.supplier_id == supplier_id).values(deleted=True, updated_at=GTime.UTC())
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED

View File

@ -5,7 +5,7 @@ from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import tbl_account
from common.database.model.models import users, companies
from common.enums import ErrorType
from common.logger import LOG
from common.utils.gtime import GTime
@ -16,27 +16,31 @@ from common.utils.gtime import GTime
# - 모든 메서드는 (session, ...) 을 받는다. session 은 람다 호출 시 매니저가 넘겨준다.
class IUserCRUD(ABC):
@abstractmethod
async def get_account_by_id(self, cdb: AsyncSession, user_id: str) -> Tuple[ErrorType, tbl_account]:
async def get_user_by_login_id(self, cdb: AsyncSession, login_id: str) -> Tuple[ErrorType, users]:
pass
@abstractmethod
async def is_account(self, cdb: AsyncSession, user_id: str) -> ErrorType:
async def is_user(self, cdb: AsyncSession, login_id: str) -> ErrorType:
pass
@abstractmethod
async def add_account(self, cdb: AsyncSession, account: tbl_account) -> ErrorType:
async def add_user(self, cdb: AsyncSession, user: users) -> ErrorType:
pass
@abstractmethod
async def update_last_login(self, cdb: AsyncSession, user_uid: int) -> ErrorType:
async def update_last_accessed(self, cdb: AsyncSession, user_id) -> ErrorType:
pass
@abstractmethod
async def get_company(self, cdb: AsyncSession, company_id) -> Tuple[ErrorType, companies]:
pass
class UserCRUD(IUserCRUD):
async def get_account_by_id(self, cdb: AsyncSession, user_id: str) -> Tuple[ErrorType, tbl_account]:
async def get_user_by_login_id(self, cdb: AsyncSession, login_id: str) -> Tuple[ErrorType, users]:
try:
query = select(tbl_account).where(tbl_account.id == user_id).limit(1)
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query, f"get_account_by_id(ID:{user_id}) failed.")
query = select(users).where(users.id == login_id, users.deleted == False).limit(1) # noqa: E712
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query, f"get_user_by_login_id(ID:{login_id}) failed.")
if err_type != ErrorType.SUCCESS:
return err_type, None
if len(row_list) != 1:
@ -46,9 +50,9 @@ class UserCRUD(IUserCRUD):
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, None
async def is_account(self, cdb: AsyncSession, user_id: str) -> ErrorType:
async def is_user(self, cdb: AsyncSession, login_id: str) -> ErrorType:
try:
query = select(tbl_account).where(tbl_account.id == user_id).limit(1)
query = select(users).where(users.id == login_id, users.deleted == False).limit(1) # noqa: E712
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type
@ -59,17 +63,30 @@ class UserCRUD(IUserCRUD):
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def add_account(self, cdb: AsyncSession, account: tbl_account) -> ErrorType:
async def add_user(self, cdb: AsyncSession, user: users) -> ErrorType:
try:
return await DB_SESSION_MNG.insert(cdb, account)
return await DB_SESSION_MNG.insert(cdb, user)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def update_last_login(self, cdb: AsyncSession, user_uid: int) -> ErrorType:
async def update_last_accessed(self, cdb: AsyncSession, user_id) -> ErrorType:
try:
query = update(tbl_account).where(tbl_account.uid == user_uid).values(last_login_at=GTime.UTC())
query = update(users).where(users.user_id == user_id).values(last_accessed_at=GTime.UTC())
return await DB_SESSION_MNG.add(cdb, query)
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED
async def get_company(self, cdb: AsyncSession, company_id) -> Tuple[ErrorType, companies]:
try:
query = select(companies).where(companies.company_id == company_id, companies.deleted == False).limit(1) # noqa: E712
err_type, row_list = await DB_SESSION_MNG.execute(cdb, query)
if err_type != ErrorType.SUCCESS:
return err_type, None
if len(row_list) != 1:
return ErrorType.DB_INVALID_KEY, None
return ErrorType.SUCCESS, row_list[0]
except Exception as ex:
LOG.e_no_callstack(ex)
return ErrorType.DB_RUN_FAILED, None

View File

@ -1,8 +1,11 @@
fastapi
uvicorn[standard]
sqlalchemy>=2.0
greenlet
asyncpg
python-jose[cryptography]
bcrypt
orjson
pydantic>=2.0
python-multipart
openpyxl

View File

@ -2,12 +2,20 @@ import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from common.database.db_session_manager import DB_SESSION_MNG
from common.logger import LOG
from common.utils.gtime import GTime
from config.server_configs import web_server_config
import router.v1.auth.account
import router.v1.item.item
import router.v1.supplier.supplier
import router.v1.card.card
import router.v1.quotation.quotation
import router.v1.quotation_setting.quotation_setting
import router.v1.enums.enums
API_SERVER_START_TIME = GTime.UTCStr()
@ -22,6 +30,15 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="Negodata Api Server", lifespan=lifespan)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=[web_server_config.client_url],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Accept-Encoding: gzip 요청에 대해 1000 bytes 이상 응답을 압축.
app.add_middleware(GZipMiddleware, minimum_size=1000)
@ -42,3 +59,9 @@ async def healthz():
# 각 도메인 라우터를 등록한다. 새 기능 추가 시 router.v1.<domain>.<file> 를 import 후 include.
app.include_router(router.v1.auth.account.router)
app.include_router(router.v1.item.item.router)
app.include_router(router.v1.supplier.supplier.router)
app.include_router(router.v1.card.card.router)
app.include_router(router.v1.quotation.quotation.router)
app.include_router(router.v1.quotation_setting.quotation_setting.router)
app.include_router(router.v1.enums.enums.router)

View File

@ -4,7 +4,7 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from common.models.gmodel import UserInfo
from router.v1.validator.dependencies import IsValidAccessToken, IsValidRefreshToken, RemoveNoneResponse
from services.auth_service import AuthService
from .protocol import Req_CreateAccount, Req_Login, Res_CreateAccount, Res_Login, Res_RefreshToken
from .protocol import Req_CreateAccount, Req_Login, Res_CreateAccount, Res_Login, Res_Me, Res_RefreshToken
security = HTTPBearer()
@ -14,12 +14,14 @@ router = APIRouter(prefix="/v1/auth", tags=["Auth"], responses={404: {"descripti
@router.post(path="/login", response_model=Res_Login, summary="로그인", description="id/pw 로 로그인하고 JWT 토큰을 발급한다.")
async def login(request: Request, req: Req_Login, service: AuthService = Depends()):
return RemoveNoneResponse(await service.attempt_login(req.id, req.pw, request.client.host))
return RemoveNoneResponse(await service.attempt_login(req.id, req.password, request.client.host))
@router.post(path="/create", response_model=Res_CreateAccount, summary="계정 생성", description="새 계정을 생성한다.")
async def create_account(request: Request, req: Req_CreateAccount, service: AuthService = Depends()):
return RemoveNoneResponse(await service.create_account(req.id, req.pw, req.nickname, request.client.host))
async def create_account(req: Req_CreateAccount, service: AuthService = Depends()):
return RemoveNoneResponse(
await service.create_account(req.id, req.password, req.company_id, req.name, req.email, req.contact_number, req.role)
)
@router.post(
@ -35,8 +37,9 @@ async def refresh_token(service: AuthService = Depends(), credentials: HTTPAutho
@router.get(
path="/me",
summary="내 정보 (보호된 엔드포인트 예시)",
description="유효한 access 토큰이 있어야 호출 가능. 토큰 검증 결과 UserInfo 를 주입받는다.",
response_model=Res_Me,
summary="내 정보",
description="유효한 access 토큰이 있어야 호출 가능. 토큰의 유저+회사 정보를 반환한다.",
)
async def me(user_info: UserInfo = Depends(IsValidAccessToken)):
return {"uid": user_info.uid, "id": user_info.id, "nickname": user_info.nickname}
async def me(service: AuthService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.get_me(user_info))

View File

@ -1,5 +1,8 @@
from typing import Optional
from pydantic import Field
from common.enums import UserRole
from common.models.gmodel import Res_WebPacketProtocol, WebPacketProtocol
@ -10,25 +13,45 @@ class AuthProtocol(WebPacketProtocol):
class Req_Login(AuthProtocol):
id: str = ""
pw: str = ""
password: str = ""
class Res_Login(Res_WebPacketProtocol):
uid: int = Field(0, description="user uid", json_schema_extra={"format": "int64"})
nickname: str = ""
access_token: str = ""
refresh_token: str = ""
token_type: str = "bearer"
class Req_CreateAccount(AuthProtocol):
id: str = ""
pw: str = ""
nickname: str = ""
password: str = ""
company_id: str = ""
name: str = ""
email: str = ""
contact_number: str = ""
role: int = UserRole.USER.value
class Res_CreateAccount(Res_WebPacketProtocol):
uid: int = Field(0, description="생성된 user uid", json_schema_extra={"format": "int64"})
user_id: str = ""
class Res_RefreshToken(Res_WebPacketProtocol):
access_token: str = ""
token_type: str = "bearer"
class CompanyData(WebPacketProtocol):
company_id: str = ""
name: str = ""
class Res_Me(Res_WebPacketProtocol):
user_id: str = ""
id: str = ""
name: Optional[str] = None
email: Optional[str] = None
contact_number: Optional[str] = None
role: int = UserRole.USER.value
role_label: str = ""
company: Optional[CompanyData] = Field(default=None)

View File

@ -0,0 +1,50 @@
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from common.models.gmodel import PageParams, UserInfo
from router.v1.validator.dependencies import IsValidAccessToken, RemoveNoneResponse
from services.card_service import CardService
from .protocol import (
Req_CreateCard,
Req_UpdateCard,
Res_Card,
Res_CardList,
Res_DeleteCard,
)
router = APIRouter(prefix="/v1/card", tags=["Card"], responses={404: {"description": "Not found"}})
@router.get(path="/list", response_model=Res_CardList, summary="협상카드 목록")
async def list_cards(
service: CardService = Depends(),
user_info: UserInfo = Depends(IsValidAccessToken),
search: str | None = Query(None, description="카드명/카드번호/스크립트 검색"),
pg: PageParams = Depends(),
):
return RemoveNoneResponse(await service.list_cards(user_info.user_id, search, pg))
@router.post(path="/create", response_model=Res_Card, summary="협상카드 등록")
async def create_card(req: Req_CreateCard, service: CardService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(
await service.create_card(user_info.user_id, req.model_dump(exclude_unset=True))
)
@router.get(path="/{card_id}", response_model=Res_Card, summary="협상카드 조회")
async def get_card(card_id: UUID, service: CardService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.get_card(user_info.user_id, str(card_id)))
@router.patch(path="/update/{card_id}", response_model=Res_Card, summary="협상카드 수정")
async def update_card(
card_id: UUID, req: Req_UpdateCard, service: CardService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)
):
return RemoveNoneResponse(await service.update_card(user_info.user_id, str(card_id), req.model_dump(exclude_unset=True)))
@router.delete(path="/delete/{card_id}", response_model=Res_DeleteCard, summary="협상카드 삭제")
async def delete_card(card_id: UUID, service: CardService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.delete_card(user_info.user_id, str(card_id)))

View File

@ -0,0 +1,63 @@
import uuid
from datetime import datetime
from typing import Any, Optional
from pydantic import ConfigDict
from common.enums import CardStatus
from common.models.gmodel import Res_PageProtocol, Res_WebPacketProtocol, WebPacketProtocol
class CardProtocol(WebPacketProtocol):
pass
class Req_CreateCard(CardProtocol):
is_wildcard: bool = False
name: Optional[str] = None
number: Optional[str] = None
script: Optional[str] = None
edit_script: Optional[Any] = None
status: int = CardStatus.ACTIVE.value # 와일드카드 적용 여부(available 매핑). 일반카드는 무시.
condition: Optional[str] = None # 와일드카드 전용
memo: Optional[str] = None # 와일드카드 전용
class Req_UpdateCard(CardProtocol):
name: Optional[str] = None
number: Optional[str] = None
script: Optional[str] = None
edit_script: Optional[Any] = None
status: Optional[int] = None
condition: Optional[str] = None
memo: Optional[str] = None
# 통합 카드 표현(nego_cards + wild_cards 공통). nego_card_id 는 출처 테이블의 PK 를 그대로 담는다.
class CardData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True)
nego_card_id: uuid.UUID # 통합 식별자(일반=nego_card_id / 와일드=wild_card_id)
user_id: Optional[uuid.UUID] = None
is_wildcard: bool = False
name: Optional[str] = None
number: Optional[str] = None
script: Optional[str] = None
edit_script: Optional[Any] = None
status: int = CardStatus.ACTIVE.value
condition: Optional[str] = None
memo: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Res_Card(Res_WebPacketProtocol):
card: Optional[CardData] = None
class Res_CardList(Res_PageProtocol):
cards: list[CardData] = []
class Res_DeleteCard(Res_WebPacketProtocol):
pass

View File

@ -0,0 +1,21 @@
from fastapi import APIRouter
from common.enums import DOMAIN_ENUMS, ENUM_LABELS
from router.v1.validator.dependencies import RemoveNoneResponse
from .protocol import EnumOption, Res_Enums
# 도메인 코드 enum 메타데이터(공용). 프론트가 페이지 진입 시 드롭다운을 이걸로 채운다.
router = APIRouter(prefix="/v1", tags=["Enums"], responses={404: {"description": "Not found"}})
@router.get(path="/enums", response_model=Res_Enums, summary="도메인 코드 enum 전체")
async def list_enums():
res = Res_Enums()
res.enums = {
key: [
EnumOption(value=member.value, name=member.name, label=ENUM_LABELS.get(member, member.name))
for member in enum_cls
]
for key, enum_cls in DOMAIN_ENUMS.items()
}
return RemoveNoneResponse(res)

View File

@ -0,0 +1,15 @@
from common.models.gmodel import Res_WebPacketProtocol, WebPacketProtocol
class EnumsProtocol(WebPacketProtocol):
pass
class EnumOption(WebPacketProtocol):
value: int
name: str
label: str
class Res_Enums(Res_WebPacketProtocol):
enums: dict[str, list[EnumOption]] = {}

View File

@ -0,0 +1,96 @@
from uuid import UUID
from fastapi import APIRouter, Depends, File, Query, UploadFile
from common.models.gmodel import PageParams, UserInfo
from router.v1.validator.dependencies import IsValidAccessToken, RemoveNoneResponse
from services.item_service import ItemService
from .protocol import (
Req_CheckCodes,
Req_CreateItem,
Req_UpdateItem,
Res_CheckCodes,
Res_DeleteItem,
Res_ExcelUpload,
Res_Item,
Res_ItemCategories,
Res_ItemList,
Res_LowestPriceResult,
Res_LowestPriceTrigger,
)
# 라우터(컨트롤러). 인증(Depends(IsValidAccessToken))으로 UserInfo 를 받아 company_id 로 스코프.
router = APIRouter(prefix="/v1/item", tags=["Item"], responses={404: {"description": "Not found"}})
@router.get(path="/list", response_model=Res_ItemList, summary="상품 목록")
async def list_items(
service: ItemService = Depends(),
user_info: UserInfo = Depends(IsValidAccessToken),
search: str | None = Query(None, description="상품명/상품코드 검색"),
category: str | None = Query(None, description="카테고리 필터"),
pg: PageParams = Depends(),
):
return RemoveNoneResponse(await service.list_items(user_info.company_id, search, category, pg))
@router.get(path="/categories", response_model=Res_ItemCategories, summary="상품 카테고리 목록(distinct)")
async def list_item_categories(service: ItemService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.list_categories(user_info.company_id))
@router.post(path="/create", response_model=Res_Item, summary="상품 등록")
async def create_item(req: Req_CreateItem, service: ItemService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(
await service.create_item(user_info.company_id, user_info.user_id, req.model_dump(exclude_unset=True))
)
@router.post(path="/check-codes", response_model=Res_CheckCodes, summary="코드 중복 사전검사(업로드 즉시)")
async def check_item_codes(req: Req_CheckCodes, service: ItemService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.check_codes(user_info.company_id, req.codes))
@router.post(path="/upload-excel", response_model=Res_ExcelUpload, summary="엑셀 일괄 등록(스텁)")
async def upload_items_excel(
service: ItemService = Depends(),
user_info: UserInfo = Depends(IsValidAccessToken),
file: UploadFile = File(...),
):
return RemoveNoneResponse(
Res_ExcelUpload(received_filename=file.filename, status="not_implemented", message="엑셀 일괄 등록은 추후 구현")
)
@router.get(path="/{item_id}", response_model=Res_Item, summary="상품 조회")
async def get_item(item_id: UUID, service: ItemService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.get_item(user_info.company_id, str(item_id)))
@router.patch(path="/update/{item_id}", response_model=Res_Item, summary="상품 수정")
async def update_item(
item_id: UUID, req: Req_UpdateItem, service: ItemService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)
):
return RemoveNoneResponse(await service.update_item(user_info.company_id, str(item_id), req.model_dump(exclude_unset=True)))
@router.delete(path="/delete/{item_id}", response_model=Res_DeleteItem, summary="상품 삭제")
async def delete_item(item_id: UUID, service: ItemService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.delete_item(user_info.company_id, str(item_id)))
@router.post(path="/{item_id}/lowest-price", response_model=Res_LowestPriceTrigger, summary="최저가 수집 요청(스텁)")
async def trigger_lowest_price(item_id: UUID, service: ItemService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
# 존재/소유 확인만 (없으면 result 에 ITEM_NOT_FOUND)
got = await service.get_item(user_info.company_id, str(item_id))
if got.item is None:
return RemoveNoneResponse(got)
return RemoveNoneResponse(Res_LowestPriceTrigger(item_id=str(item_id), status="queued", message="최저가 수집 요청됨(스텁)"))
@router.get(path="/{item_id}/lowest-price", response_model=Res_LowestPriceResult, summary="최저가 수집 결과(스텁)")
async def get_lowest_price(item_id: UUID, service: ItemService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
got = await service.get_item(user_info.company_id, str(item_id))
if got.item is None:
return RemoveNoneResponse(got)
return RemoveNoneResponse(Res_LowestPriceResult(item_id=str(item_id), results=[], message="최저가 수집 결과 없음(스텁)"))

View File

@ -0,0 +1,125 @@
import uuid
from datetime import datetime
from typing import Optional
from pydantic import ConfigDict
from common.models.gmodel import Res_PageProtocol, Res_WebPacketProtocol, WebPacketProtocol
class ItemProtocol(WebPacketProtocol):
pass
class Req_CreateItem(ItemProtocol):
name: str = ""
code: Optional[str] = None
category: Optional[str] = None
category_type: int = 1
image_url: Optional[str] = None
model_name: Optional[str] = None
spec: Optional[str] = None
manufacturer: Optional[str] = None
made_in: Optional[str] = None
price: Optional[int] = None
internet_lowest_price_yn: bool = False
moq: Optional[str] = None
lead_time: Optional[int] = None
quantity_unit: Optional[str] = None
delivery_type: Optional[int] = None
vat_yn: Optional[bool] = None
delivery_fee_yn: Optional[bool] = None
class Req_UpdateItem(ItemProtocol):
name: Optional[str] = None
code: Optional[str] = None
category: Optional[str] = None
category_type: Optional[int] = None
image_url: Optional[str] = None
model_name: Optional[str] = None
spec: Optional[str] = None
manufacturer: Optional[str] = None
made_in: Optional[str] = None
price: Optional[int] = None
internet_lowest_price_yn: Optional[bool] = None
moq: Optional[str] = None
lead_time: Optional[int] = None
quantity_unit: Optional[str] = None
delivery_type: Optional[int] = None
vat_yn: Optional[bool] = None
delivery_fee_yn: Optional[bool] = None
class ItemData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True)
item_id: uuid.UUID
company_id: uuid.UUID
user_id: uuid.UUID
name: str
code: Optional[str] = None
category: Optional[str] = None
category_type: int = 1
image_url: Optional[str] = None
model_name: Optional[str] = None
spec: Optional[str] = None
manufacturer: Optional[str] = None
made_in: Optional[str] = None
price: Optional[int] = None
internet_lowest_price_yn: bool = False
moq: Optional[str] = None
lead_time: Optional[int] = None
quantity_unit: Optional[str] = None
delivery_type: Optional[int] = None
vat_yn: Optional[bool] = None
delivery_fee_yn: Optional[bool] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Res_Item(Res_WebPacketProtocol):
item: Optional[ItemData] = None
class Res_ItemList(Res_PageProtocol):
items: list[ItemData] = []
class ItemCategory(WebPacketProtocol):
name: str
category_type: int = 1
class Res_ItemCategories(Res_WebPacketProtocol):
categories: list[ItemCategory] = []
class Req_CheckCodes(ItemProtocol):
codes: list[str] = []
class Res_CheckCodes(Res_WebPacketProtocol):
existing: list[str] = [] # codes 중 이미 DB(같은 회사)에 존재하는 코드들
class Res_DeleteItem(Res_WebPacketProtocol):
pass
class Res_ExcelUpload(Res_WebPacketProtocol):
received_filename: Optional[str] = None
status: str = ""
message: str = ""
class Res_LowestPriceTrigger(Res_WebPacketProtocol):
item_id: str = ""
status: str = ""
message: str = ""
class Res_LowestPriceResult(Res_WebPacketProtocol):
item_id: str = ""
results: list = []
message: str = ""

View File

@ -0,0 +1,152 @@
import uuid
from datetime import datetime
from typing import Any, Optional
from pydantic import ConfigDict
from common.models.gmodel import Res_PageProtocol, Res_WebPacketProtocol, WebPacketProtocol
class QuotationProtocol(WebPacketProtocol):
pass
class Req_CreateQuotation(QuotationProtocol):
qt_setting_id: uuid.UUID
version_id: uuid.UUID
name: str = ""
number: str = ""
type: int = 0
status: int = 0
start_time: datetime
end_time: datetime
round: int = 1
manager_name: Optional[str] = None
manager_email: Optional[str] = None
manager_contact_number: Optional[str] = None
memo: Optional[str] = None
class QuotationData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True)
qt_id: uuid.UUID
user_id: uuid.UUID
qt_setting_id: uuid.UUID
version_id: uuid.UUID
name: str
number: str
type: int
round: int = 1
status: int
start_time: datetime
end_time: datetime
manager_name: Optional[str] = None
manager_email: Optional[str] = None
manager_contact_number: Optional[str] = None
memo: Optional[str] = None
iteration: int = 0
preferred_sp_yn: Optional[bool] = None
preferred_sp_id: Optional[uuid.UUID] = None
preferred_sp_name: Optional[str] = None
equal_bid_yn: Optional[bool] = None
equal_bid_data: Optional[Any] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Res_Quotation(Res_WebPacketProtocol):
quotation: Optional[QuotationData] = None
class Res_QuotationList(Res_PageProtocol):
quotations: list[QuotationData] = []
class Res_DeleteQuotation(Res_WebPacketProtocol):
pass
class AsyncJob(WebPacketProtocol):
status: str = ""
message: str = ""
class Res_CreateQuotation(Res_WebPacketProtocol):
quotation: Optional[QuotationData] = None
async_job: Optional[AsyncJob] = None
class Res_QuotationStatus(Res_WebPacketProtocol):
qt_id: Optional[uuid.UUID] = None
job_status: int = 0
message: str = ""
class SessionData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True)
session_id: uuid.UUID
qt_id: uuid.UUID
supplier_id: uuid.UUID
item_id: uuid.UUID
qt_number: str
qt_round: int
qt_type: int
target_price: int
status: int
bid_price: Optional[int] = None
bid_at: Optional[datetime] = None
end_time: datetime
reject_reason: Optional[str] = None
reject_price: Optional[int] = None
reject_delivery_type: Optional[int] = None
class Res_QuotationSessions(Res_WebPacketProtocol):
qt_id: Optional[uuid.UUID] = None
sessions: list[SessionData] = []
total: int = 0
class ChatMessageData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True)
chat_id: uuid.UUID
session_id: uuid.UUID
card_id: Optional[uuid.UUID] = None
index: int
sender: int
target_price: int
card_used_yn: Optional[bool] = None
indicator_value: Optional[float] = None
card_type: Optional[int] = None
class Res_SessionChat(Res_WebPacketProtocol):
session_id: Optional[uuid.UUID] = None
messages: list[ChatMessageData] = []
class Res_QuotationResult(Res_WebPacketProtocol):
qt_id: Optional[uuid.UUID] = None
winner_supplier_id: Optional[uuid.UUID] = None
winner_supplier_name: Optional[str] = None
is_equal_bid: Optional[bool] = None
equal_bid_data: Optional[Any] = None
result_count: int = 0
class QuotationCardData(WebPacketProtocol):
session_card_id: uuid.UUID
qt_id: Optional[uuid.UUID] = None
nego_card_id: Optional[uuid.UUID] = None
wild_card_id: Optional[uuid.UUID] = None
type: Optional[int] = None
name: Optional[str] = None
script: Optional[str] = None
class Res_QuotationCards(Res_WebPacketProtocol):
qt_id: Optional[uuid.UUID] = None
cards: list[QuotationCardData] = []

View File

@ -0,0 +1,89 @@
from datetime import datetime
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from common.models.gmodel import PageParams, UserInfo
from router.v1.validator.dependencies import IsValidAccessToken, RemoveNoneResponse
from services.quotation_service import QuotationService
from .protocol import (
Req_CreateQuotation,
Res_CreateQuotation,
Res_DeleteQuotation,
Res_Quotation,
Res_QuotationCards,
Res_QuotationList,
Res_QuotationResult,
Res_QuotationSessions,
Res_QuotationStatus,
Res_SessionChat,
)
# 라우터(컨트롤러). 인증(Depends(IsValidAccessToken))으로 UserInfo 를 받는다.
# quotations 테이블에 company_id 가 없어 회사 스코핑은 하지 않는다(토큰 검증만).
# 라우팅 주의: 정적/하위 경로(/list, /create, /{qt_id}/status ...)를 /{qt_id} 보다 먼저 선언해야
# /{qt_id} 가 /list 등을 가로채지 않는다.
router = APIRouter(prefix="/v1/quotation", tags=["Quotation"], responses={404: {"description": "Not found"}})
# ----- 실제 엔드포인트 (DB) -----
@router.get(path="/list", response_model=Res_QuotationList, summary="견적 목록")
async def list_quotations(
service: QuotationService = Depends(),
user_info: UserInfo = Depends(IsValidAccessToken),
status: str | None = Query(None, description="상태 필터(정확히 일치)"),
type: str | None = Query(None, description="유형 필터(정확히 일치)"),
start_from: datetime | None = Query(None, description="시작일시 이후(ISO)"),
start_to: datetime | None = Query(None, description="시작일시 이전(ISO)"),
pg: PageParams = Depends(),
):
return RemoveNoneResponse(await service.list_quotations(status, type, start_from, start_to, pg))
@router.post(path="/create", response_model=Res_CreateQuotation, summary="견적 생성")
async def create_quotation(
req: Req_CreateQuotation, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)
):
return RemoveNoneResponse(await service.create_quotation(user_info.user_id, req.model_dump(exclude_unset=True)))
@router.post(path="/stop/{qt_id}", response_model=Res_Quotation, summary="견적 마감")
async def stop_quotation(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.stop_quotation(str(qt_id)))
# ----- 견적 상세 (FK로 연결된 하위 데이터 / 일부는 모델 미존재로 스텁) -----
@router.get(path="/{qt_id}/status", response_model=Res_QuotationStatus, summary="견적 상태 조회")
async def get_quotation_status(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.get_status(str(qt_id)))
@router.get(path="/{qt_id}/sessions", response_model=Res_QuotationSessions, summary="참여현황")
async def get_quotation_sessions(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.list_sessions(str(qt_id)))
@router.get(path="/session/{session_id}/chat", response_model=Res_SessionChat, summary="채팅 상세")
async def get_session_chat(session_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.list_chats(str(session_id)))
@router.get(path="/{qt_id}/result", response_model=Res_QuotationResult, summary="낙찰 결과")
async def get_quotation_result(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.get_result(str(qt_id)))
@router.get(path="/{qt_id}/cards", response_model=Res_QuotationCards, summary="견적 사용 카드")
async def get_quotation_cards(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.list_cards(str(qt_id)))
@router.delete(path="/delete/{qt_id}", response_model=Res_DeleteQuotation, summary="견적 삭제")
async def delete_quotation(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.delete_quotation(str(qt_id)))
# ----- 단건 조회 (정적/하위 경로 뒤에 선언) -----
@router.get(path="/{qt_id}", response_model=Res_Quotation, summary="견적 조회")
async def get_quotation(qt_id: UUID, service: QuotationService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.get_quotation(str(qt_id)))

View File

@ -0,0 +1,48 @@
import uuid
from datetime import datetime
from typing import Optional
from pydantic import ConfigDict
from common.models.gmodel import Res_WebPacketProtocol, WebPacketProtocol
class QuotationSettingProtocol(WebPacketProtocol):
pass
class Req_CreateQuotationSetting(QuotationSettingProtocol):
target_margin_rate: float
anchoring_value: float = 0.01
card_count: int = 3
class Req_UpdateQuotationSetting(QuotationSettingProtocol):
target_margin_rate: Optional[float] = None
anchoring_value: Optional[float] = None
card_count: Optional[int] = None
class QuotationSettingData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True)
qt_setting_id: uuid.UUID
user_id: Optional[uuid.UUID] = None
target_margin_rate: float
anchoring_value: float
card_count: int
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Res_QuotationSetting(Res_WebPacketProtocol):
setting: Optional[QuotationSettingData] = None
class Res_QuotationSettingList(Res_WebPacketProtocol):
settings: list[QuotationSettingData] = []
total: int = 0
class Res_DeleteQuotationSetting(Res_WebPacketProtocol):
pass

View File

@ -0,0 +1,52 @@
from uuid import UUID
from fastapi import APIRouter, Depends
from common.models.gmodel import UserInfo
from router.v1.validator.dependencies import IsValidAccessToken, RemoveNoneResponse
from services.quotation_setting_service import QuotationSettingService
from .protocol import (
Req_CreateQuotationSetting,
Req_UpdateQuotationSetting,
Res_DeleteQuotationSetting,
Res_QuotationSetting,
Res_QuotationSettingList,
)
# 라우터(컨트롤러). 인증(Depends(IsValidAccessToken))으로 UserInfo 를 받아 user_id 로 스코프.
router = APIRouter(
prefix="/v1/quotation-setting", tags=["QuotationSetting"], responses={404: {"description": "Not found"}}
)
@router.get(path="/list", response_model=Res_QuotationSettingList, summary="견적 설정 목록")
async def list_settings(service: QuotationSettingService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.list_settings(user_info.user_id))
@router.post(path="/create", response_model=Res_QuotationSetting, summary="견적 설정 등록")
async def create_setting(
req: Req_CreateQuotationSetting,
service: QuotationSettingService = Depends(),
user_info: UserInfo = Depends(IsValidAccessToken),
):
return RemoveNoneResponse(await service.create_setting(user_info.user_id, req.model_dump(exclude_unset=True)))
@router.patch(path="/update/{qt_setting_id}", response_model=Res_QuotationSetting, summary="견적 설정 수정")
async def update_setting(
qt_setting_id: UUID,
req: Req_UpdateQuotationSetting,
service: QuotationSettingService = Depends(),
user_info: UserInfo = Depends(IsValidAccessToken),
):
return RemoveNoneResponse(
await service.update_setting(user_info.user_id, str(qt_setting_id), req.model_dump(exclude_unset=True))
)
@router.delete(path="/delete/{qt_setting_id}", response_model=Res_DeleteQuotationSetting, summary="견적 설정 삭제")
async def delete_setting(
qt_setting_id: UUID, service: QuotationSettingService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)
):
return RemoveNoneResponse(await service.delete_setting(user_info.user_id, str(qt_setting_id)))

View File

@ -0,0 +1,71 @@
import uuid
from datetime import datetime
from typing import Optional
from pydantic import ConfigDict
from common.models.gmodel import Res_PageProtocol, Res_WebPacketProtocol, WebPacketProtocol
class SupplierProtocol(WebPacketProtocol):
pass
class Req_CreateSupplier(SupplierProtocol):
name: str = ""
code: Optional[str] = None
manager_name: Optional[str] = None
manager_email: Optional[str] = None
manager_contact_number: Optional[str] = None
priority: Optional[str] = None
class Req_UpdateSupplier(SupplierProtocol):
name: Optional[str] = None
code: Optional[str] = None
manager_name: Optional[str] = None
manager_email: Optional[str] = None
manager_contact_number: Optional[str] = None
priority: Optional[str] = None
class SupplierData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True)
supplier_id: uuid.UUID
company_id: uuid.UUID
user_id: uuid.UUID
name: str
code: Optional[str] = None
manager_name: Optional[str] = None
manager_email: Optional[str] = None
manager_contact_number: Optional[str] = None
priority: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Res_Supplier(Res_WebPacketProtocol):
supplier: Optional[SupplierData] = None
class Res_SupplierList(Res_PageProtocol):
suppliers: list[SupplierData] = []
class Req_CheckCodes(SupplierProtocol):
codes: list[str] = []
class Res_CheckCodes(Res_WebPacketProtocol):
existing: list[str] = [] # codes 중 이미 DB(같은 회사)에 존재하는 코드들
class Res_DeleteSupplier(Res_WebPacketProtocol):
pass
class Res_ExcelUpload(Res_WebPacketProtocol):
received_filename: Optional[str] = None
status: str = ""
message: str = ""

View File

@ -0,0 +1,77 @@
from uuid import UUID
from fastapi import APIRouter, Depends, File, Query, UploadFile
from common.models.gmodel import PageParams, UserInfo
from router.v1.validator.dependencies import IsValidAccessToken, RemoveNoneResponse
from services.supplier_service import SupplierService
from .protocol import (
Req_CheckCodes,
Req_CreateSupplier,
Req_UpdateSupplier,
Res_CheckCodes,
Res_DeleteSupplier,
Res_ExcelUpload,
Res_Supplier,
Res_SupplierList,
)
# 라우터(컨트롤러). 인증(Depends(IsValidAccessToken))으로 UserInfo 를 받아 company_id 로 스코프.
router = APIRouter(prefix="/v1/supplier", tags=["Supplier"], responses={404: {"description": "Not found"}})
@router.get(path="/list", response_model=Res_SupplierList, summary="협력사 목록")
async def list_suppliers(
service: SupplierService = Depends(),
user_info: UserInfo = Depends(IsValidAccessToken),
search: str | None = Query(None, description="협력사명/코드/담당자명 검색"),
priority: str | None = Query(None, description="우선순위 필터(HIGH/MEDIUM/LOW)"),
pg: PageParams = Depends(),
):
return RemoveNoneResponse(await service.list_suppliers(user_info.company_id, search, priority, pg))
@router.post(path="/create", response_model=Res_Supplier, summary="협력사 등록")
async def create_supplier(
req: Req_CreateSupplier, service: SupplierService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)
):
return RemoveNoneResponse(
await service.create_supplier(user_info.company_id, user_info.user_id, req.model_dump(exclude_unset=True))
)
@router.post(path="/check-codes", response_model=Res_CheckCodes, summary="코드 중복 사전검사(업로드 즉시)")
async def check_supplier_codes(
req: Req_CheckCodes, service: SupplierService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)
):
return RemoveNoneResponse(await service.check_codes(user_info.company_id, req.codes))
@router.post(path="/upload-excel", response_model=Res_ExcelUpload, summary="협력사 엑셀 일괄 등록(스텁)")
async def upload_suppliers_excel(
service: SupplierService = Depends(),
user_info: UserInfo = Depends(IsValidAccessToken),
file: UploadFile = File(...),
):
return RemoveNoneResponse(
Res_ExcelUpload(received_filename=file.filename, status="not_implemented", message="엑셀 일괄 등록은 추후 구현")
)
@router.get(path="/{supplier_id}", response_model=Res_Supplier, summary="협력사 조회")
async def get_supplier(supplier_id: UUID, service: SupplierService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.get_supplier(user_info.company_id, str(supplier_id)))
@router.patch(path="/update/{supplier_id}", response_model=Res_Supplier, summary="협력사 수정")
async def update_supplier(
supplier_id: UUID, req: Req_UpdateSupplier, service: SupplierService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)
):
return RemoveNoneResponse(
await service.update_supplier(user_info.company_id, str(supplier_id), req.model_dump(exclude_unset=True))
)
@router.delete(path="/delete/{supplier_id}", response_model=Res_DeleteSupplier, summary="협력사 삭제")
async def delete_supplier(supplier_id: UUID, service: SupplierService = Depends(), user_info: UserInfo = Depends(IsValidAccessToken)):
return RemoveNoneResponse(await service.delete_supplier(user_info.company_id, str(supplier_id)))

View File

@ -110,4 +110,6 @@ def RemoveNoneValues(obj: Any) -> Any:
def RemoveNoneResponse(obj) -> ORJSONResponse:
return ORJSONResponse(content=RemoveNoneValues(obj.model_dump()))
# mode="json": uuid/datetime 등 DB 네이티브 타입(asyncpg.UUID 포함)을 pydantic 단에서
# JSON 안전한 문자열로 변환한다. (python 모드면 orjson 이 asyncpg.UUID 를 직렬화 못 함)
return ORJSONResponse(content=RemoveNoneValues(obj.model_dump(mode="json")))

View File

@ -1,12 +1,14 @@
import uuid
from fastapi import Depends
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import tbl_account
from common.enums import DBWRType, ErrorType
from common.database.model.models import users
from common.enums import DBWRType, ErrorType, UserStatus, UserRole, ENUM_LABELS
from common.logger import LOG
from common.models.gmodel import UserInfo
from crud.user_crud import IUserCRUD, UserCRUD
from router.v1.auth.protocol import Res_CreateAccount, Res_Login, Res_RefreshToken
from router.v1.auth.protocol import CompanyData, Res_CreateAccount, Res_Login, Res_Me, Res_RefreshToken
from router.v1.validator.dependencies import (
CreateAccessToken,
CreateRefreshToken,
@ -29,59 +31,68 @@ class AuthService:
def __init__(self, user_crud: IUserCRUD = Depends(UserCRUD)):
self.user_crud = user_crud
async def attempt_login(self, id: str, pw: str, connect_ip: str) -> Res_Login:
LOG.i(f"LOGIN : {id=}")
@staticmethod
def _user_info(user: users) -> UserInfo:
# uuid → str (JWT json 직렬화 위해). 기능 라우터는 company_id 로 스코프한다.
return UserInfo(
user_id=str(user.user_id),
id=user.id,
company_id=str(user.company_id),
)
async def attempt_login(self, login_id: str, password: str, connect_ip: str) -> Res_Login:
LOG.i(f"LOGIN : id={login_id}")
res = Res_Login()
# 1) 계정 조회 (Read DB)
err_type, account = await DB_SESSION_MNG.execute_lambda(
tbl_account.DBType(),
err_type, user = await DB_SESSION_MNG.execute_lambda(
users.DBType(),
DBWRType.DB_READ.value,
lambda s: self.user_crud.get_account_by_id(s, id),
lambda s: self.user_crud.get_user_by_login_id(s, login_id),
)
if err_type != ErrorType.SUCCESS:
# 계정 없음/조회 실패 모두 로그인 실패로 일반화
res.result.SetResult(ErrorType.ACCOUNT_INVALID_INFO)
return res
account: tbl_account
user: users
# 2) 비밀번호 검증
if not await VerifyPW(pw, account.pw):
if not await VerifyPW(password, user.password):
res.result.SetResult(ErrorType.ACCOUNT_INVALID_INFO)
return res
# 3) 차단 여부
if account.is_blocked:
# 3) 상태 확인 (활성 아니면 차단)
if user.status != UserStatus.ACTIVE.value:
res.result.SetResult(ErrorType.ACCOUNT_BLOCKED_USER)
return res
# 4) 토큰 발급
user_info = UserInfo(uid=account.uid, id=account.id, nickname=account.nickname)
user_info = self._user_info(user)
res.access_token = CreateAccessToken(user_info)
res.refresh_token = CreateRefreshToken(user_info)
# 5) 마지막 로그인 시간 갱신 (Write DB, 트랜잭션)
# 5) 마지막 접속 시간 갱신 (Write DB, 트랜잭션)
err_type = await DB_SESSION_MNG.execute_lambda_run(
[tbl_account.DBType()],
[lambda s: self.user_crud.update_last_login(s, account.uid)],
[users.DBType()],
[lambda s: self.user_crud.update_last_accessed(s, user.user_id)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.uid = account.uid
res.nickname = account.nickname
return res
async def create_account(self, id: str, pw: str, nickname: str, connect_ip: str) -> Res_CreateAccount:
LOG.i(f"CREATE : {id=}, {nickname=}")
async def create_account(
self, login_id: str, password: str, company_id: str, name: str, email: str, contact_number: str, role: int
) -> Res_CreateAccount:
LOG.i(f"CREATE : id={login_id}, company_id={company_id}")
res = Res_CreateAccount()
# 1) 중복 ID 확인 (Read DB)
err_type = await DB_SESSION_MNG.execute_lambda(
tbl_account.DBType(),
users.DBType(),
DBWRType.DB_READ.value,
lambda s: self.user_crud.is_account(s, id),
lambda s: self.user_crud.is_user(s, login_id),
)
if err_type == ErrorType.DB_ALREADY_SAME_KEY:
res.result.SetResult(ErrorType.ACCOUNT_ALREADY_EXIST)
@ -91,10 +102,18 @@ class AuthService:
return res
# 2) 계정 생성 (비밀번호는 bcrypt 해시로 저장)
account = tbl_account(id=id, pw=await GetHashedPW(pw), nickname=nickname or id)
user = users(
company_id=uuid.UUID(company_id),
id=login_id,
password=await GetHashedPW(password),
name=name or None,
email=email or None,
contact_number=contact_number or None,
role=role or UserRole.USER.value,
)
err_type = await DB_SESSION_MNG.execute_lambda_run(
[tbl_account.DBType()],
[lambda s: self.user_crud.add_account(s, account)],
[users.DBType()],
[lambda s: self.user_crud.add_user(s, user)],
)
if err_type != ErrorType.SUCCESS:
# 사전 검사와 INSERT 사이의 경쟁 조건에서 unique 위반이 나면 동일 코드로 매핑.
@ -104,7 +123,41 @@ class AuthService:
res.result.SetResult(err_type)
return res
res.uid = account.uid
res.user_id = str(user.user_id)
return res
async def get_me(self, user_info: UserInfo) -> Res_Me:
res = Res_Me()
# 1) 유저 조회
err_type, user = await DB_SESSION_MNG.execute_lambda(
users.DBType(),
DBWRType.DB_READ.value,
lambda s: self.user_crud.get_user_by_login_id(s, user_info.id),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(ErrorType.ACCOUNT_INVALID_INFO)
return res
user: users
# 2) 소속사 조회 (없어도 치명적 아님)
company = None
c_err, company_row = await DB_SESSION_MNG.execute_lambda(
users.DBType(),
DBWRType.DB_READ.value,
lambda s: self.user_crud.get_company(s, user.company_id),
)
if c_err == ErrorType.SUCCESS and company_row is not None:
company = CompanyData(company_id=str(company_row.company_id), name=company_row.name)
res.user_id = str(user.user_id)
res.id = user.id
res.name = user.name
res.email = user.email
res.contact_number = user.contact_number
res.role = user.role
res.role_label = ENUM_LABELS.get(UserRole(user.role), str(user.role))
res.company = company
return res
async def refresh_token(self, refresh_token: str) -> Res_RefreshToken:

View File

@ -0,0 +1,204 @@
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

View File

@ -0,0 +1,164 @@
import uuid
from fastapi import Depends
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import items
from common.enums import DBWRType, ErrorType
from common.logger import LOG
from common.models.gmodel import PageParams
from crud.item_crud import IItemCRUD, ItemCRUD
from router.v1.item.protocol import (
ItemCategory,
ItemData,
Res_CheckCodes,
Res_DeleteItem,
Res_Item,
Res_ItemCategories,
Res_ItemList,
)
class ItemService:
"""상품 비즈니스 로직. company_id 로 소유권을 확인한다(멀티테넌트)."""
def __init__(self, item_crud: IItemCRUD = Depends(ItemCRUD)):
self.item_crud = item_crud
async def _fetch_owned(self, company_uuid: uuid.UUID, item_id: uuid.UUID):
"""item 조회 + 소유권 확인. (ErrorType, item|None) 반환."""
err_type, item = await DB_SESSION_MNG.execute_lambda(
items.DBType(),
DBWRType.DB_READ.value,
lambda s: self.item_crud.get_by_id(s, item_id),
)
if err_type != ErrorType.SUCCESS or item is None:
return ErrorType.ITEM_NOT_FOUND, None
if item.company_id != company_uuid:
return ErrorType.ITEM_NOT_FOUND, None
return ErrorType.SUCCESS, item
async def list_items(self, company_id: str, search, category, pg: PageParams) -> Res_ItemList:
res = Res_ItemList(page=pg.page, size=pg.size)
company_uuid = uuid.UUID(company_id)
err_type, rows, total = await DB_SESSION_MNG.execute_lambda(
items.DBType(),
DBWRType.DB_READ.value,
lambda s: self.item_crud.search(s, company_uuid, search, category, pg.skip, pg.size),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.items = [ItemData.model_validate(r) for r in rows]
res.total = total
return res
async def list_categories(self, company_id: str) -> Res_ItemCategories:
"""회사 전체 상품에서 distinct 카테고리(이름+타입)를 돌려준다. 카테고리 목록은 백엔드가 책임진다."""
res = Res_ItemCategories()
company_uuid = uuid.UUID(company_id)
err_type, rows = await DB_SESSION_MNG.execute_lambda(
items.DBType(),
DBWRType.DB_READ.value,
lambda s: self.item_crud.distinct_categories(s, company_uuid),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.categories = [ItemCategory(name=r[0], category_type=r[1] if r[1] is not None else 1) for r in rows]
return res
async def get_item(self, company_id: str, item_id: str) -> Res_Item:
res = Res_Item()
err_type, item = await self._fetch_owned(uuid.UUID(company_id), uuid.UUID(item_id))
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.item = ItemData.model_validate(item)
return res
async def check_codes(self, company_id: str, codes: list) -> Res_CheckCodes:
"""업로드 즉시 호출: codes 중 같은 회사 DB 에 이미 있는 코드를 돌려준다(미리보기 사전검사)."""
res = Res_CheckCodes()
company_uuid = uuid.UUID(company_id)
err_type, existing = await DB_SESSION_MNG.execute_lambda(
items.DBType(),
DBWRType.DB_READ.value,
lambda s: self.item_crud.existing_codes(s, company_uuid, codes),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.existing = list(existing)
return res
async def create_item(self, company_id: str, user_id: str, data: dict) -> Res_Item:
res = Res_Item()
company_uuid = uuid.UUID(company_id)
# DB 중복코드 검증: 같은 회사에 동일 code 가 이미 있으면 거부(프론트는 받아온 목록만 보므로 여기서 최종 차단).
code = data.get("code")
if code:
dup_err, exists = await DB_SESSION_MNG.execute_lambda(
items.DBType(),
DBWRType.DB_READ.value,
lambda s: self.item_crud.code_exists(s, company_uuid, code),
)
if dup_err != ErrorType.SUCCESS:
res.result.SetResult(dup_err)
return res
if exists:
res.result.SetResult(ErrorType.ITEM_CODE_DUPLICATE)
return res
item = items(**data, company_id=company_uuid, user_id=uuid.UUID(user_id))
err_type = await DB_SESSION_MNG.execute_lambda_run(
[items.DBType()],
[lambda s: self.item_crud.add_item(s, item)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# 서버 기본값(created_at/updated_at)은 insert 후 Python 객체에 실리지 않으므로 재조회한다.
return await self.get_item(company_id, str(item.item_id))
async def update_item(self, company_id: str, item_id: str, data: dict) -> Res_Item:
res = Res_Item()
company_uuid = uuid.UUID(company_id)
item_uuid = uuid.UUID(item_id)
# 소유권 확인
err_type, _ = await self._fetch_owned(company_uuid, item_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
err_type = await DB_SESSION_MNG.execute_lambda_run(
[items.DBType()],
[lambda s: self.item_crud.update_item(s, item_uuid, data)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# 갱신 후 재조회
return await self.get_item(company_id, item_id)
async def delete_item(self, company_id: str, item_id: str) -> Res_DeleteItem:
res = Res_DeleteItem()
company_uuid = uuid.UUID(company_id)
item_uuid = uuid.UUID(item_id)
err_type, _ = await self._fetch_owned(company_uuid, item_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
err_type = await DB_SESSION_MNG.execute_lambda_run(
[items.DBType()],
[lambda s: self.item_crud.soft_delete(s, item_uuid)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res

View File

@ -0,0 +1,266 @@
import uuid
from fastapi import Depends
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import quotations, sessions, chats
from common.enums import DBWRType, ErrorType
from common.models.gmodel import PageParams
from crud.quotation_crud import IQuotationCRUD, QuotationCRUD
from router.v1.quotation.protocol import (
AsyncJob,
ChatMessageData,
QuotationCardData,
QuotationData,
SessionData,
Res_CreateQuotation,
Res_DeleteQuotation,
Res_Quotation,
Res_QuotationCards,
Res_QuotationList,
Res_QuotationResult,
Res_QuotationSessions,
Res_QuotationStatus,
Res_SessionChat,
)
class QuotationService:
"""견적 비즈니스 로직.
quotations 테이블에는 company_id 가 없어 회사 스코핑은 하지 않는다(토큰 검증만).
user_id 는 생성 시 소유자로만 기록한다(조회/변경 시 소유권 필터 없음).
"""
def __init__(self, quotation_crud: IQuotationCRUD = Depends(QuotationCRUD)):
self.quotation_crud = quotation_crud
async def _fetch(self, qt_id: uuid.UUID):
"""견적 단건 조회. (ErrorType, quotation|None) 반환. (회사 스코프 없음)"""
err_type, quotation = await DB_SESSION_MNG.execute_lambda(
quotations.DBType(),
DBWRType.DB_READ.value,
lambda s: self.quotation_crud.get_by_id(s, qt_id),
)
if err_type != ErrorType.SUCCESS or quotation is None:
return ErrorType.QUOTATION_NOT_FOUND, None
return ErrorType.SUCCESS, quotation
async def list_quotations(self, status, type_, start_from, start_to, pg: PageParams) -> Res_QuotationList:
res = Res_QuotationList(page=pg.page, size=pg.size)
err_type, rows, total = await DB_SESSION_MNG.execute_lambda(
quotations.DBType(),
DBWRType.DB_READ.value,
lambda s: self.quotation_crud.search(s, status, type_, start_from, start_to, pg.skip, pg.size),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.quotations = [QuotationData.model_validate(r) for r in rows]
res.total = total
return res
async def get_quotation(self, qt_id: str) -> Res_Quotation:
res = Res_Quotation()
err_type, quotation = await self._fetch(uuid.UUID(qt_id))
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.quotation = QuotationData.model_validate(quotation)
return res
async def create_quotation(self, user_id: str, data: dict) -> Res_CreateQuotation:
res = Res_CreateQuotation()
quotation = quotations(**data, user_id=uuid.UUID(user_id))
err_type = await DB_SESSION_MNG.execute_lambda_run(
[quotations.DBType()],
[lambda s: self.quotation_crud.add_quotation(s, quotation)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# 서버 기본값(created_at/updated_at) 로드 위해 재조회 (응답 shape 은 Res_CreateQuotation 유지).
f_err, fresh = await DB_SESSION_MNG.execute_lambda(
quotations.DBType(),
DBWRType.DB_READ.value,
lambda s: self.quotation_crud.get_by_id(s, quotation.qt_id),
)
res.quotation = QuotationData.model_validate(fresh if f_err == ErrorType.SUCCESS and fresh is not None else quotation)
# 네고시움 백엔드 비동기 요청은 스텁이므로 row 만 생성한다.
res.async_job = AsyncJob(status="submitted", message="견적 생성 작업 요청됨(스텁)")
return res
async def stop_quotation(self, qt_id: str) -> Res_Quotation:
res = Res_Quotation()
qt_uuid = uuid.UUID(qt_id)
# 존재 확인
err_type, _ = await self._fetch(qt_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# 상태를 '견적마감'으로 변경(실제 DB 업데이트)
err_type = await DB_SESSION_MNG.execute_lambda_run(
[quotations.DBType()],
[lambda s: self.quotation_crud.update_quotation(s, qt_uuid, {"status": "견적마감"})],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# 갱신 후 재조회
return await self.get_quotation(qt_id)
async def delete_quotation(self, qt_id: str) -> Res_DeleteQuotation:
res = Res_DeleteQuotation()
qt_uuid = uuid.UUID(qt_id)
err_type, _ = await self._fetch(qt_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
err_type = await DB_SESSION_MNG.execute_lambda_run(
[quotations.DBType()],
[lambda s: self.quotation_crud.soft_delete(s, qt_uuid)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
async def get_status(self, qt_id: str) -> Res_QuotationStatus:
res = Res_QuotationStatus()
err_type, quotation = await self._fetch(uuid.UUID(qt_id))
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.qt_id = quotation.qt_id
res.job_status = quotation.status
res.message = "ok"
return res
async def get_result(self, qt_id: str) -> Res_QuotationResult:
res = Res_QuotationResult()
err_type, quotation = await self._fetch(uuid.UUID(qt_id))
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# 낙찰 결과는 quotations 컬럼에서 직접 노출. results 테이블 미존재로 result_count 는 0.
res.qt_id = quotation.qt_id
res.winner_supplier_id = quotation.preferred_sp_id
res.winner_supplier_name = quotation.preferred_sp_name
res.is_equal_bid = quotation.equal_bid_yn
res.equal_bid_data = quotation.equal_bid_data
res.result_count = 0
return res
async def list_sessions(self, qt_id: str) -> Res_QuotationSessions:
res = Res_QuotationSessions()
qt_uuid = uuid.UUID(qt_id)
err_type, quotation = await self._fetch(qt_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
err_type, rows = await DB_SESSION_MNG.execute_lambda(
sessions.DBType(),
DBWRType.DB_READ.value,
lambda s: self.quotation_crud.list_sessions(s, qt_uuid),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.qt_id = quotation.qt_id
# sessions.quotation_id → SessionData.qt_id 로 명시 매핑(컬럼명 불일치).
res.sessions = [
SessionData(
session_id=r.session_id,
qt_id=r.quotation_id,
supplier_id=r.supplier_id,
item_id=r.item_id,
qt_number=r.qt_number,
qt_round=r.qt_round,
qt_type=r.qt_type,
target_price=r.target_price,
status=r.status,
bid_price=r.bid_price,
bid_at=r.bid_at,
end_time=r.end_time,
reject_reason=r.reject_reason,
reject_price=r.reject_price,
reject_delivery_type=r.reject_delivery_type,
)
for r in rows
]
res.total = len(res.sessions)
return res
async def list_chats(self, session_id: str) -> Res_SessionChat:
res = Res_SessionChat()
sess_uuid = uuid.UUID(session_id)
res.session_id = sess_uuid
err_type, rows = await DB_SESSION_MNG.execute_lambda(
chats.DBType(),
DBWRType.DB_READ.value,
lambda s: self.quotation_crud.list_chats(s, sess_uuid),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# chats.seq → ChatMessageData.index 로 매핑. indicator_value(Decimal) → float.
res.messages = [
ChatMessageData(
chat_id=r.chat_id,
session_id=r.session_id,
card_id=r.card_id,
index=r.seq,
sender=r.sender,
target_price=r.target_price,
card_used_yn=r.card_used_yn,
indicator_value=float(r.indicator_value) if r.indicator_value is not None else None,
card_type=r.card_type,
)
for r in rows
]
return res
async def list_cards(self, qt_id: str) -> Res_QuotationCards:
res = Res_QuotationCards()
qt_uuid = uuid.UUID(qt_id)
err_type, quotation = await self._fetch(qt_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
err_type, rows = await DB_SESSION_MNG.execute_lambda(
chats.DBType(),
DBWRType.DB_READ.value,
lambda s: self.quotation_crud.list_used_cards(s, qt_uuid),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.qt_id = quotation.qt_id
# rows = [(chat_row, nego_card_id, name, script), ...]. nego/wild 구분은 chats.card_type.
cards = []
for chat_row, nc_id, nc_name, nc_script in rows:
is_wild = chat_row.card_type == 2
cards.append(
QuotationCardData(
session_card_id=chat_row.chat_id,
qt_id=quotation.qt_id,
nego_card_id=None if is_wild else nc_id,
wild_card_id=nc_id if is_wild else None,
type=chat_row.card_type if chat_row.card_type is not None else 1,
name=nc_name,
script=nc_script,
)
)
res.cards = cards
return res

View File

@ -0,0 +1,112 @@
import uuid
from fastapi import Depends
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import quotation_settings
from common.enums import DBWRType, ErrorType
from crud.quotation_setting_crud import IQuotationSettingCRUD, QuotationSettingCRUD
from router.v1.quotation_setting.protocol import (
QuotationSettingData,
Res_DeleteQuotationSetting,
Res_QuotationSetting,
Res_QuotationSettingList,
)
class QuotationSettingService:
"""견적 설정 비즈니스 로직. user_id 로 소유권을 확인한다(유저별 설정)."""
def __init__(self, qs_crud: IQuotationSettingCRUD = Depends(QuotationSettingCRUD)):
self.qs_crud = qs_crud
async def _fetch_owned(self, user_uuid: uuid.UUID, qt_setting_id: uuid.UUID):
"""설정 조회 + 소유권 확인. (ErrorType, setting|None) 반환."""
err_type, setting = await DB_SESSION_MNG.execute_lambda(
quotation_settings.DBType(),
DBWRType.DB_READ.value,
lambda s: self.qs_crud.get_by_id(s, qt_setting_id),
)
if err_type != ErrorType.SUCCESS or setting is None:
return ErrorType.QUOTATION_SETTING_NOT_FOUND, None
if setting.user_id != user_uuid:
return ErrorType.QUOTATION_SETTING_NOT_FOUND, None
return ErrorType.SUCCESS, setting
async def list_settings(self, user_id: str) -> Res_QuotationSettingList:
res = Res_QuotationSettingList()
user_uuid = uuid.UUID(user_id)
err_type, rows, total = await DB_SESSION_MNG.execute_lambda(
quotation_settings.DBType(),
DBWRType.DB_READ.value,
lambda s: self.qs_crud.list_by_user(s, user_uuid),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.settings = [QuotationSettingData.model_validate(r) for r in rows]
res.total = total
return res
async def get_setting(self, user_id: str, qt_setting_id: str) -> Res_QuotationSetting:
res = Res_QuotationSetting()
err_type, setting = await self._fetch_owned(uuid.UUID(user_id), uuid.UUID(qt_setting_id))
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.setting = QuotationSettingData.model_validate(setting)
return res
async def create_setting(self, user_id: str, data: dict) -> Res_QuotationSetting:
res = Res_QuotationSetting()
setting = quotation_settings(**data, user_id=uuid.UUID(user_id))
err_type = await DB_SESSION_MNG.execute_lambda_run(
[quotation_settings.DBType()],
[lambda s: self.qs_crud.add_setting(s, setting)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# 서버 기본값(created_at/updated_at)은 insert 후 객체에 실리지 않으므로 재조회한다.
return await self.get_setting(user_id, str(setting.qt_setting_id))
async def update_setting(self, user_id: str, qt_setting_id: str, data: dict) -> Res_QuotationSetting:
res = Res_QuotationSetting()
user_uuid = uuid.UUID(user_id)
setting_uuid = uuid.UUID(qt_setting_id)
# 소유권 확인
err_type, _ = await self._fetch_owned(user_uuid, setting_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
err_type = await DB_SESSION_MNG.execute_lambda_run(
[quotation_settings.DBType()],
[lambda s: self.qs_crud.update_setting(s, setting_uuid, data)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# 갱신 후 재조회
return await self.get_setting(user_id, qt_setting_id)
async def delete_setting(self, user_id: str, qt_setting_id: str) -> Res_DeleteQuotationSetting:
res = Res_DeleteQuotationSetting()
user_uuid = uuid.UUID(user_id)
setting_uuid = uuid.UUID(qt_setting_id)
err_type, _ = await self._fetch_owned(user_uuid, setting_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
err_type = await DB_SESSION_MNG.execute_lambda_run(
[quotation_settings.DBType()],
[lambda s: self.qs_crud.soft_delete(s, setting_uuid)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res

View File

@ -0,0 +1,141 @@
import uuid
from fastapi import Depends
from common.database.db_session_manager import DB_SESSION_MNG
from common.database.model.models import suppliers
from common.enums import DBWRType, ErrorType
from common.logger import LOG
from common.models.gmodel import PageParams
from crud.supplier_crud import ISupplierCRUD, SupplierCRUD
from router.v1.supplier.protocol import Res_CheckCodes, Res_DeleteSupplier, Res_Supplier, Res_SupplierList, SupplierData
class SupplierService:
"""협력사 비즈니스 로직. company_id 로 소유권을 확인한다(멀티테넌트)."""
def __init__(self, supplier_crud: ISupplierCRUD = Depends(SupplierCRUD)):
self.supplier_crud = supplier_crud
async def _fetch_owned(self, company_uuid: uuid.UUID, supplier_id: uuid.UUID):
"""supplier 조회 + 소유권 확인. (ErrorType, supplier|None) 반환."""
err_type, supplier = await DB_SESSION_MNG.execute_lambda(
suppliers.DBType(),
DBWRType.DB_READ.value,
lambda s: self.supplier_crud.get_by_id(s, supplier_id),
)
if err_type != ErrorType.SUCCESS or supplier is None:
return ErrorType.SUPPLIER_NOT_FOUND, None
if supplier.company_id != company_uuid:
return ErrorType.SUPPLIER_NOT_FOUND, None
return ErrorType.SUCCESS, supplier
async def list_suppliers(self, company_id: str, search, priority, pg: PageParams) -> Res_SupplierList:
res = Res_SupplierList(page=pg.page, size=pg.size)
company_uuid = uuid.UUID(company_id)
err_type, rows, total = await DB_SESSION_MNG.execute_lambda(
suppliers.DBType(),
DBWRType.DB_READ.value,
lambda s: self.supplier_crud.search(s, company_uuid, search, priority, pg.skip, pg.size),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.suppliers = [SupplierData.model_validate(r) for r in rows]
res.total = total
return res
async def get_supplier(self, company_id: str, supplier_id: str) -> Res_Supplier:
res = Res_Supplier()
err_type, supplier = await self._fetch_owned(uuid.UUID(company_id), uuid.UUID(supplier_id))
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.supplier = SupplierData.model_validate(supplier)
return res
async def check_codes(self, company_id: str, codes: list) -> Res_CheckCodes:
"""업로드 즉시 호출: codes 중 같은 회사 DB 에 이미 있는 코드를 돌려준다(미리보기 사전검사)."""
res = Res_CheckCodes()
company_uuid = uuid.UUID(company_id)
err_type, existing = await DB_SESSION_MNG.execute_lambda(
suppliers.DBType(),
DBWRType.DB_READ.value,
lambda s: self.supplier_crud.existing_codes(s, company_uuid, codes),
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
res.existing = list(existing)
return res
async def create_supplier(self, company_id: str, user_id: str, data: dict) -> Res_Supplier:
res = Res_Supplier()
company_uuid = uuid.UUID(company_id)
# DB 중복코드 검증: 같은 회사에 동일 code 가 이미 있으면 거부(프론트는 받아온 목록만 보므로 여기서 최종 차단).
code = data.get("code")
if code:
dup_err, exists = await DB_SESSION_MNG.execute_lambda(
suppliers.DBType(),
DBWRType.DB_READ.value,
lambda s: self.supplier_crud.code_exists(s, company_uuid, code),
)
if dup_err != ErrorType.SUCCESS:
res.result.SetResult(dup_err)
return res
if exists:
res.result.SetResult(ErrorType.SUPPLIER_CODE_DUPLICATE)
return res
supplier = suppliers(**data, company_id=company_uuid, user_id=uuid.UUID(user_id))
err_type = await DB_SESSION_MNG.execute_lambda_run(
[suppliers.DBType()],
[lambda s: self.supplier_crud.add_supplier(s, supplier)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# 서버 기본값(created_at/updated_at)은 insert 후 객체에 실리지 않으므로 재조회한다.
return await self.get_supplier(company_id, str(supplier.supplier_id))
async def update_supplier(self, company_id: str, supplier_id: str, data: dict) -> Res_Supplier:
res = Res_Supplier()
company_uuid = uuid.UUID(company_id)
supplier_uuid = uuid.UUID(supplier_id)
# 소유권 확인
err_type, _ = await self._fetch_owned(company_uuid, supplier_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
err_type = await DB_SESSION_MNG.execute_lambda_run(
[suppliers.DBType()],
[lambda s: self.supplier_crud.update_supplier(s, supplier_uuid, data)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
# 갱신 후 재조회
return await self.get_supplier(company_id, supplier_id)
async def delete_supplier(self, company_id: str, supplier_id: str) -> Res_DeleteSupplier:
res = Res_DeleteSupplier()
company_uuid = uuid.UUID(company_id)
supplier_uuid = uuid.UUID(supplier_id)
err_type, _ = await self._fetch_owned(company_uuid, supplier_uuid)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res
err_type = await DB_SESSION_MNG.execute_lambda_run(
[suppliers.DBType()],
[lambda s: self.supplier_crud.soft_delete(s, supplier_uuid)],
)
if err_type != ErrorType.SUCCESS:
res.result.SetResult(err_type)
return res

View File

@ -1,39 +1,48 @@
"""auth 도메인 e2e 테스트.
"""auth 도메인 e2e 테스트 (negodata: users/companies 기반).
실행 전제: docker-compose 로 PostgreSQL 이 떠 있어야 한다 (negodata_db 사용).
실행 전제: PostgreSQL(negodata_db)이 떠 있어야 한다.
docker compose up -d # 또는 로컬 postgres
cd negodata/backend && python -m pytest
계정 생성은 company_id 를 요구하므로 company_id 픽스처(conftest)가 소속사를 시드한다.
"""
async def test_create_and_login_flow(client):
# 1) 계정 생성
r = await client.post("/v1/auth/create", json={"id": "user1", "pw": "pw1234", "nickname": "닉네임"})
async def test_create_and_login_flow(client, company_id):
# 1) 계정 생성 (회사 하위로)
r = await client.post(
"/v1/auth/create",
json={"id": "user1", "password": "pw1234", "company_id": company_id, "name": "홍길동"},
)
assert r.status_code == 200
body = r.json()
assert body["result"]["success"] is True
assert body["uid"] > 0
assert body["user_id"]
# 2) 로그인 -> 토큰 발급
r = await client.post("/v1/auth/login", json={"id": "user1", "pw": "pw1234"})
r = await client.post("/v1/auth/login", json={"id": "user1", "password": "pw1234"})
assert r.status_code == 200
body = r.json()
assert body["result"]["success"] is True
assert body["access_token"]
assert body["refresh_token"]
assert body["nickname"] == "닉네임"
access_token = body["access_token"]
# 3) 보호된 엔드포인트 호출
# 3) 보호된 엔드포인트(/me) — 토큰의 유저 + 소속사 반환
r = await client.get("/v1/auth/me", headers={"Authorization": f"Bearer {access_token}"})
assert r.status_code == 200
assert r.json()["id"] == "user1"
me = r.json()
assert me["id"] == "user1"
assert me["name"] == "홍길동"
assert me["company"]["company_id"] == company_id
async def test_login_with_wrong_password(client):
await client.post("/v1/auth/create", json={"id": "user2", "pw": "correct", "nickname": "n"})
async def test_login_with_wrong_password(client, company_id):
await client.post(
"/v1/auth/create",
json={"id": "user2", "password": "correct", "company_id": company_id, "name": "n"},
)
r = await client.post("/v1/auth/login", json={"id": "user2", "pw": "wrong"})
r = await client.post("/v1/auth/login", json={"id": "user2", "password": "wrong"})
assert r.status_code == 200
body = r.json()
assert body["result"]["success"] is False
@ -43,15 +52,21 @@ async def test_login_with_wrong_password(client):
async def test_login_nonexistent_account(client):
r = await client.post("/v1/auth/login", json={"id": "ghost", "pw": "whatever"})
r = await client.post("/v1/auth/login", json={"id": "ghost", "password": "whatever"})
assert r.json()["result"]["success"] is False
async def test_duplicate_account_create(client):
r1 = await client.post("/v1/auth/create", json={"id": "dup", "pw": "pw1234", "nickname": "n"})
async def test_duplicate_account_create(client, company_id):
r1 = await client.post(
"/v1/auth/create",
json={"id": "dup", "password": "pw1234", "company_id": company_id, "name": "n"},
)
assert r1.json()["result"]["success"] is True
r2 = await client.post("/v1/auth/create", json={"id": "dup", "pw": "pw5678", "nickname": "n2"})
r2 = await client.post(
"/v1/auth/create",
json={"id": "dup", "password": "pw5678", "company_id": company_id, "name": "n2"},
)
body = r2.json()
assert body["result"]["success"] is False
# ACCOUNT_ALREADY_EXIST(1201)

View File

@ -0,0 +1,68 @@
"""supplier / quotation_setting / quotation 슬라이스 런타임 스모크.
create(재조회로 created_at 적재) + list + get 경로를 라이브 DB 로 확인한다.
"""
import uuid
async def _headers(client, company_id, login_id):
await client.post(
"/v1/auth/create",
json={"id": login_id, "password": "pw1234", "company_id": company_id, "name": "n"},
)
r = await client.post("/v1/auth/login", json={"id": login_id, "password": "pw1234"})
return {"Authorization": f"Bearer {r.json()['access_token']}"}
async def test_supplier_crud(client, company_id):
h = await _headers(client, company_id, "supuser")
r = await client.post("/v1/supplier/create", json={"name": "공급사A", "code": "S1"}, headers=h)
body = r.json()
assert body["result"]["success"] is True
sup = body["supplier"]
assert sup["name"] == "공급사A"
assert sup["created_at"] # 재조회 픽스: 서버 기본값 적재 확인
sid = sup["supplier_id"]
r = await client.get("/v1/supplier/list", headers=h)
assert r.json()["total"] == 1
r = await client.get(f"/v1/supplier/{sid}", headers=h)
assert r.json()["supplier"]["supplier_id"] == sid
async def test_quotation_setting_crud(client, company_id):
h = await _headers(client, company_id, "qsuser")
r = await client.post("/v1/quotation-setting/create", json={"target_margin_rate": 0.15}, headers=h)
body = r.json()
assert body["result"]["success"] is True
st = body["setting"]
assert st["target_margin_rate"] == 0.15
assert st["card_count"] == 3 # 기본값
assert st["created_at"]
r = await client.get("/v1/quotation-setting/list", headers=h)
assert r.json()["total"] >= 1
async def test_quotation_create(client, company_id):
h = await _headers(client, company_id, "qtuser")
body = {
"qt_setting_id": str(uuid.uuid4()),
"version_id": str(uuid.uuid4()),
"name": "견적A",
"number": "Q-001",
"type": "재견적",
"status": "진행중",
"start_time": "2026-06-16T00:00:00",
"end_time": "2026-06-17T00:00:00",
}
r = await client.post("/v1/quotation/create", json=body, headers=h)
res = r.json()
assert res["result"]["success"] is True
q = res["quotation"]
assert q["name"] == "견적A"
assert q["created_at"] # 재조회 픽스
r = await client.get("/v1/quotation/list", headers=h)
assert r.json()["total"] >= 1

View File

@ -0,0 +1,85 @@
"""item 도메인 e2e — CRUD + company 멀티테넌트 스코프 검증.
실행 전제: PostgreSQL(negodata_db). docker compose up -d 후 python -m pytest.
"""
import uuid
import pytest_asyncio
from sqlalchemy import text
async def _headers(client, company_id, login_id="itemuser", pw="pw1234"):
await client.post(
"/v1/auth/create",
json={"id": login_id, "password": pw, "company_id": company_id, "name": "n"},
)
r = await client.post("/v1/auth/login", json={"id": login_id, "password": pw})
return {"Authorization": f"Bearer {r.json()['access_token']}"}
@pytest_asyncio.fixture
async def other_company_id(db_engine) -> str:
cid = uuid.uuid4()
async with db_engine.begin() as conn:
await conn.execute(
text("INSERT INTO companies (company_id, name) VALUES (:cid, :name)"),
{"cid": cid, "name": "다른회사"},
)
return str(cid)
async def test_item_crud_flow(client, company_id):
h = await _headers(client, company_id)
# 등록
r = await client.post("/v1/item/create", json={"name": "상품A", "price": 1000, "code": "C1"}, headers=h)
assert r.status_code == 200
body = r.json()
assert body["result"]["success"] is True
item_id = body["item"]["item_id"]
assert body["item"]["name"] == "상품A"
# 목록
r = await client.get("/v1/item/list", headers=h)
body = r.json()
assert body["total"] == 1 and len(body["items"]) == 1
# 단건 조회
r = await client.get(f"/v1/item/{item_id}", headers=h)
assert r.json()["item"]["item_id"] == item_id
# 수정 (부분)
r = await client.patch(f"/v1/item/update/{item_id}", json={"price": 2000}, headers=h)
assert r.json()["item"]["price"] == 2000
assert r.json()["item"]["name"] == "상품A" # 미지정 필드 유지
# 삭제 (soft)
r = await client.delete(f"/v1/item/delete/{item_id}", headers=h)
assert r.json()["result"]["success"] is True
# 삭제 후 목록 0
r = await client.get("/v1/item/list", headers=h)
assert r.json()["total"] == 0
async def test_item_not_found(client, company_id):
h = await _headers(client, company_id)
r = await client.get(f"/v1/item/{uuid.uuid4()}", headers=h)
body = r.json()
assert body["result"]["success"] is False
assert body["result"]["code"] == 1300 # ITEM_NOT_FOUND
async def test_item_company_scope(client, company_id, other_company_id):
# 회사 A 가 상품 등록
ha = await _headers(client, company_id, login_id="userA")
r = await client.post("/v1/item/create", json={"name": "A상품"}, headers=ha)
a_item_id = r.json()["item"]["item_id"]
# 회사 B 유저는 A 의 상품을 목록/단건에서 볼 수 없다
hb = await _headers(client, other_company_id, login_id="userB")
r = await client.get("/v1/item/list", headers=hb)
assert r.json()["total"] == 0
r = await client.get(f"/v1/item/{a_item_id}", headers=hb)
assert r.json()["result"]["code"] == 1300 # 타사 자원은 ITEM_NOT_FOUND

View File

@ -32,27 +32,18 @@ import type {
import { customFetch } from "../../mutator/custom-fetch";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* id/pw 로 로그인하고 JWT 토큰을 발급한다.
* @summary 로그인
*/
export const login = (
reqLogin: ReqLogin,
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<ResLogin>(
{
url: `/v1/auth/login`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: reqLogin,
signal,
},
options,
);
export const login = (reqLogin: ReqLogin, signal?: AbortSignal) => {
return customFetch<ResLogin>({
url: `/v1/auth/login`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: reqLogin,
signal,
});
};
export const getLoginMutationOptions = <
@ -65,7 +56,6 @@ export const getLoginMutationOptions = <
{ data: ReqLogin },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof login>>,
TError,
@ -73,13 +63,13 @@ export const getLoginMutationOptions = <
TContext
> => {
const mutationKey = ["login"];
const { mutation: mutationOptions, request: requestOptions } = options
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof login>>,
@ -87,7 +77,7 @@ export const getLoginMutationOptions = <
> = (props) => {
const { data } = props ?? {};
return login(data, requestOptions);
return login(data);
};
return { mutationFn, ...mutationOptions };
@ -113,7 +103,6 @@ export const useLogin = <
{ data: ReqLogin },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
@ -127,24 +116,20 @@ export const useLogin = <
return useMutation(mutationOptions, queryClient);
};
/**
* company_id 하위로 유저를 생성한다(시드/관리자용).
* 새 계정을 생성한다.
* @summary 계정 생성
*/
export const createAccount = (
reqCreateAccount: ReqCreateAccount,
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<ResCreateAccount>(
{
url: `/v1/auth/create`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: reqCreateAccount,
signal,
},
options,
);
return customFetch<ResCreateAccount>({
url: `/v1/auth/create`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: reqCreateAccount,
signal,
});
};
export const getCreateAccountMutationOptions = <
@ -157,7 +142,6 @@ export const getCreateAccountMutationOptions = <
{ data: ReqCreateAccount },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createAccount>>,
TError,
@ -165,13 +149,13 @@ export const getCreateAccountMutationOptions = <
TContext
> => {
const mutationKey = ["createAccount"];
const { mutation: mutationOptions, request: requestOptions } = options
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createAccount>>,
@ -179,7 +163,7 @@ export const getCreateAccountMutationOptions = <
> = (props) => {
const { data } = props ?? {};
return createAccount(data, requestOptions);
return createAccount(data);
};
return { mutationFn, ...mutationOptions };
@ -205,7 +189,6 @@ export const useCreateAccount = <
{ data: ReqCreateAccount },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
@ -222,14 +205,12 @@ export const useCreateAccount = <
* refresh 토큰으로 access 토큰을 재발급한다.
* @summary 액세스 토큰 갱신
*/
export const refreshToken = (
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<ResRefreshToken>(
{ url: `/v1/auth/refresh_token`, method: "POST", signal },
options,
);
export const refreshToken = (signal?: AbortSignal) => {
return customFetch<ResRefreshToken>({
url: `/v1/auth/refresh_token`,
method: "POST",
signal,
});
};
export const getRefreshTokenMutationOptions = <
@ -242,7 +223,6 @@ export const getRefreshTokenMutationOptions = <
void,
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof refreshToken>>,
TError,
@ -250,19 +230,19 @@ export const getRefreshTokenMutationOptions = <
TContext
> => {
const mutationKey = ["refreshToken"];
const { mutation: mutationOptions, request: requestOptions } = options
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof refreshToken>>,
void
> = () => {
return refreshToken(requestOptions);
return refreshToken();
};
return { mutationFn, ...mutationOptions };
@ -285,7 +265,6 @@ export const useRefreshToken = <TError = void, TContext = unknown>(
void,
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
@ -299,17 +278,11 @@ export const useRefreshToken = <TError = void, TContext = unknown>(
return useMutation(mutationOptions, queryClient);
};
/**
* 유효한 access 토큰이 있어야 호출 가능. 토큰의 유저+소속사 정보를 반환한다.
* 유효한 access 토큰이 있어야 호출 가능. 토큰의 유저+회사 정보를 반환한다.
* @summary 내 정보
*/
export const me = (
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<ResMe>(
{ url: `/v1/auth/me`, method: "GET", signal },
options,
);
export const me = (signal?: AbortSignal) => {
return customFetch<ResMe>({ url: `/v1/auth/me`, method: "GET", signal });
};
export const getMeQueryKey = () => {
@ -323,14 +296,13 @@ export const getMeQueryOptions = <
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getMeQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof me>>> = ({ signal }) =>
me(requestOptions, signal);
me(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof me>>,
@ -355,7 +327,6 @@ export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>(
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
@ -374,7 +345,6 @@ export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>(
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -385,7 +355,6 @@ export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>(
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -400,7 +369,6 @@ export function useMe<TData = Awaited<ReturnType<typeof me>>, TError = void>(
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof me>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {

View File

@ -0,0 +1,551 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import { useMutation, useQuery } from "@tanstack/react-query";
import type {
DataTag,
DefinedInitialDataOptions,
DefinedUseQueryResult,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UndefinedInitialDataOptions,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
import type {
HTTPValidationError,
ListCardsParams,
ReqCreateCard,
ReqUpdateCard,
ResCard,
ResCardList,
ResDeleteCard,
} from ".././model";
import { customFetch } from "../../mutator/custom-fetch";
/**
* @summary 협상카드 목록
*/
export const listCards = (params?: ListCardsParams, signal?: AbortSignal) => {
return customFetch<ResCardList>({
url: `/v1/card/list`,
method: "GET",
params,
signal,
});
};
export const getListCardsQueryKey = (params?: ListCardsParams) => {
return [`/v1/card/list`, ...(params ? [params] : [])] as const;
};
export const getListCardsQueryOptions = <
TData = Awaited<ReturnType<typeof listCards>>,
TError = void | HTTPValidationError,
>(
params?: ListCardsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listCards>>, TError, TData>
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListCardsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listCards>>> = ({
signal,
}) => listCards(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listCards>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type ListCardsQueryResult = NonNullable<
Awaited<ReturnType<typeof listCards>>
>;
export type ListCardsQueryError = void | HTTPValidationError;
export function useListCards<
TData = Awaited<ReturnType<typeof listCards>>,
TError = void | HTTPValidationError,
>(
params: undefined | ListCardsParams,
options: {
query: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listCards>>, TError, TData>
> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listCards>>,
TError,
Awaited<ReturnType<typeof listCards>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListCards<
TData = Awaited<ReturnType<typeof listCards>>,
TError = void | HTTPValidationError,
>(
params?: ListCardsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listCards>>, TError, TData>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listCards>>,
TError,
Awaited<ReturnType<typeof listCards>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListCards<
TData = Awaited<ReturnType<typeof listCards>>,
TError = void | HTTPValidationError,
>(
params?: ListCardsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listCards>>, TError, TData>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary 협상카드 목록
*/
export function useListCards<
TData = Awaited<ReturnType<typeof listCards>>,
TError = void | HTTPValidationError,
>(
params?: ListCardsParams,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listCards>>, TError, TData>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getListCardsQueryOptions(params, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary 협상카드 등록
*/
export const createCard = (
reqCreateCard: ReqCreateCard,
signal?: AbortSignal,
) => {
return customFetch<ResCard>({
url: `/v1/card/create`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: reqCreateCard,
signal,
});
};
export const getCreateCardMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createCard>>,
TError,
{ data: ReqCreateCard },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createCard>>,
TError,
{ data: ReqCreateCard },
TContext
> => {
const mutationKey = ["createCard"];
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createCard>>,
{ data: ReqCreateCard }
> = (props) => {
const { data } = props ?? {};
return createCard(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateCardMutationResult = NonNullable<
Awaited<ReturnType<typeof createCard>>
>;
export type CreateCardMutationBody = ReqCreateCard;
export type CreateCardMutationError = void | HTTPValidationError;
/**
* @summary 협상카드 등록
*/
export const useCreateCard = <
TError = void | HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createCard>>,
TError,
{ data: ReqCreateCard },
TContext
>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof createCard>>,
TError,
{ data: ReqCreateCard },
TContext
> => {
const mutationOptions = getCreateCardMutationOptions(options);
return useMutation(mutationOptions, queryClient);
};
/**
* @summary 협상카드 조회
*/
export const getCard = (cardId: string, signal?: AbortSignal) => {
return customFetch<ResCard>({
url: `/v1/card/${cardId}`,
method: "GET",
signal,
});
};
export const getGetCardQueryKey = (cardId?: string) => {
return [`/v1/card/${cardId}`] as const;
};
export const getGetCardQueryOptions = <
TData = Awaited<ReturnType<typeof getCard>>,
TError = void | HTTPValidationError,
>(
cardId: string,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getCard>>, TError, TData>
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetCardQueryKey(cardId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getCard>>> = ({
signal,
}) => getCard(cardId, signal);
return {
queryKey,
queryFn,
enabled: !!cardId,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getCard>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type GetCardQueryResult = NonNullable<
Awaited<ReturnType<typeof getCard>>
>;
export type GetCardQueryError = void | HTTPValidationError;
export function useGetCard<
TData = Awaited<ReturnType<typeof getCard>>,
TError = void | HTTPValidationError,
>(
cardId: string,
options: {
query: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getCard>>, TError, TData>
> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getCard>>,
TError,
Awaited<ReturnType<typeof getCard>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useGetCard<
TData = Awaited<ReturnType<typeof getCard>>,
TError = void | HTTPValidationError,
>(
cardId: string,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getCard>>, TError, TData>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getCard>>,
TError,
Awaited<ReturnType<typeof getCard>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useGetCard<
TData = Awaited<ReturnType<typeof getCard>>,
TError = void | HTTPValidationError,
>(
cardId: string,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getCard>>, TError, TData>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary 협상카드 조회
*/
export function useGetCard<
TData = Awaited<ReturnType<typeof getCard>>,
TError = void | HTTPValidationError,
>(
cardId: string,
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getCard>>, TError, TData>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getGetCardQueryOptions(cardId, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary 협상카드 수정
*/
export const updateCard = (cardId: string, reqUpdateCard: ReqUpdateCard) => {
return customFetch<ResCard>({
url: `/v1/card/update/${cardId}`,
method: "PATCH",
headers: { "Content-Type": "application/json" },
data: reqUpdateCard,
});
};
export const getUpdateCardMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateCard>>,
TError,
{ cardId: string; data: ReqUpdateCard },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateCard>>,
TError,
{ cardId: string; data: ReqUpdateCard },
TContext
> => {
const mutationKey = ["updateCard"];
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateCard>>,
{ cardId: string; data: ReqUpdateCard }
> = (props) => {
const { cardId, data } = props ?? {};
return updateCard(cardId, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateCardMutationResult = NonNullable<
Awaited<ReturnType<typeof updateCard>>
>;
export type UpdateCardMutationBody = ReqUpdateCard;
export type UpdateCardMutationError = void | HTTPValidationError;
/**
* @summary 협상카드 수정
*/
export const useUpdateCard = <
TError = void | HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateCard>>,
TError,
{ cardId: string; data: ReqUpdateCard },
TContext
>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof updateCard>>,
TError,
{ cardId: string; data: ReqUpdateCard },
TContext
> => {
const mutationOptions = getUpdateCardMutationOptions(options);
return useMutation(mutationOptions, queryClient);
};
/**
* @summary 협상카드 삭제
*/
export const deleteCard = (cardId: string) => {
return customFetch<ResDeleteCard>({
url: `/v1/card/delete/${cardId}`,
method: "DELETE",
});
};
export const getDeleteCardMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteCard>>,
TError,
{ cardId: string },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteCard>>,
TError,
{ cardId: string },
TContext
> => {
const mutationKey = ["deleteCard"];
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteCard>>,
{ cardId: string }
> = (props) => {
const { cardId } = props ?? {};
return deleteCard(cardId);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteCardMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteCard>>
>;
export type DeleteCardMutationError = void | HTTPValidationError;
/**
* @summary 협상카드 삭제
*/
export const useDeleteCard = <
TError = void | HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteCard>>,
TError,
{ cardId: string },
TContext
>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof deleteCard>>,
TError,
{ cardId: string },
TContext
> => {
const mutationOptions = getDeleteCardMutationOptions(options);
return useMutation(mutationOptions, queryClient);
};

View File

@ -19,19 +19,11 @@ import type {
import { customFetch } from "../../mutator/custom-fetch";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* @summary Healthz
*/
export const healthzHealthzGet = (
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<unknown>(
{ url: `/healthz`, method: "GET", signal },
options,
);
export const healthzHealthzGet = (signal?: AbortSignal) => {
return customFetch<unknown>({ url: `/healthz`, method: "GET", signal });
};
export const getHealthzHealthzGetQueryKey = () => {
@ -49,15 +41,14 @@ export const getHealthzHealthzGetQueryOptions = <
TData
>
>;
request?: SecondParameter<typeof customFetch>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getHealthzHealthzGetQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof healthzHealthzGet>>
> = ({ signal }) => healthzHealthzGet(requestOptions, signal);
> = ({ signal }) => healthzHealthzGet(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof healthzHealthzGet>>,
@ -91,7 +82,6 @@ export function useHealthzHealthzGet<
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
@ -117,7 +107,6 @@ export function useHealthzHealthzGet<
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -135,7 +124,6 @@ export function useHealthzHealthzGet<
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -157,7 +145,6 @@ export function useHealthzHealthzGet<
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {

View File

@ -0,0 +1,145 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import { useQuery } from "@tanstack/react-query";
import type {
DataTag,
DefinedInitialDataOptions,
DefinedUseQueryResult,
QueryClient,
QueryFunction,
QueryKey,
UndefinedInitialDataOptions,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
import type { ResEnums } from ".././model";
import { customFetch } from "../../mutator/custom-fetch";
/**
* @summary 도메인 코드 enum 전체
*/
export const listEnums = (signal?: AbortSignal) => {
return customFetch<ResEnums>({ url: `/v1/enums`, method: "GET", signal });
};
export const getListEnumsQueryKey = () => {
return [`/v1/enums`] as const;
};
export const getListEnumsQueryOptions = <
TData = Awaited<ReturnType<typeof listEnums>>,
TError = void,
>(options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListEnumsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listEnums>>> = ({
signal,
}) => listEnums(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listEnums>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type ListEnumsQueryResult = NonNullable<
Awaited<ReturnType<typeof listEnums>>
>;
export type ListEnumsQueryError = void;
export function useListEnums<
TData = Awaited<ReturnType<typeof listEnums>>,
TError = void,
>(
options: {
query: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>
> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listEnums>>,
TError,
Awaited<ReturnType<typeof listEnums>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListEnums<
TData = Awaited<ReturnType<typeof listEnums>>,
TError = void,
>(
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listEnums>>,
TError,
Awaited<ReturnType<typeof listEnums>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListEnums<
TData = Awaited<ReturnType<typeof listEnums>>,
TError = void,
>(
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary 도메인 코드 enum 전체
*/
export function useListEnums<
TData = Awaited<ReturnType<typeof listEnums>>,
TError = void,
>(
options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listEnums>>, TError, TData>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getListEnumsQueryOptions(options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey;
return query;
}

View File

@ -24,11 +24,14 @@ import type {
BodyUploadItemsExcelV1ItemUploadExcelPost,
HTTPValidationError,
ListItemsParams,
ReqCheckCodes,
ReqCreateItem,
ReqUpdateItem,
ResCheckCodes,
ResDeleteItem,
ResExcelUpload,
ResItem,
ResItemCategories,
ResItemList,
ResLowestPriceResult,
ResLowestPriceTrigger,
@ -36,20 +39,16 @@ import type {
import { customFetch } from "../../mutator/custom-fetch";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* @summary 상품 목록
*/
export const listItems = (
params?: ListItemsParams,
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<ResItemList>(
{ url: `/v1/item/list`, method: "GET", params, signal },
options,
);
export const listItems = (params?: ListItemsParams, signal?: AbortSignal) => {
return customFetch<ResItemList>({
url: `/v1/item/list`,
method: "GET",
params,
signal,
});
};
export const getListItemsQueryKey = (params?: ListItemsParams) => {
@ -65,16 +64,15 @@ export const getListItemsQueryOptions = <
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listItems>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListItemsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listItems>>> = ({
signal,
}) => listItems(params, requestOptions, signal);
}) => listItems(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listItems>>,
@ -105,7 +103,6 @@ export function useListItems<
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
@ -128,7 +125,6 @@ export function useListItems<
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -143,7 +139,6 @@ export function useListItems<
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listItems>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -162,7 +157,6 @@ export function useListItems<
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listItems>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -180,24 +174,167 @@ export function useListItems<
return query;
}
/**
* @summary 상품 카테고리 목록(distinct)
*/
export const listItemCategories = (signal?: AbortSignal) => {
return customFetch<ResItemCategories>({
url: `/v1/item/categories`,
method: "GET",
signal,
});
};
export const getListItemCategoriesQueryKey = () => {
return [`/v1/item/categories`] as const;
};
export const getListItemCategoriesQueryOptions = <
TData = Awaited<ReturnType<typeof listItemCategories>>,
TError = void,
>(options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listItemCategories>>,
TError,
TData
>
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListItemCategoriesQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listItemCategories>>
> = ({ signal }) => listItemCategories(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listItemCategories>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type ListItemCategoriesQueryResult = NonNullable<
Awaited<ReturnType<typeof listItemCategories>>
>;
export type ListItemCategoriesQueryError = void;
export function useListItemCategories<
TData = Awaited<ReturnType<typeof listItemCategories>>,
TError = void,
>(
options: {
query: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listItemCategories>>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listItemCategories>>,
TError,
Awaited<ReturnType<typeof listItemCategories>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListItemCategories<
TData = Awaited<ReturnType<typeof listItemCategories>>,
TError = void,
>(
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listItemCategories>>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listItemCategories>>,
TError,
Awaited<ReturnType<typeof listItemCategories>>
>,
"initialData"
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListItemCategories<
TData = Awaited<ReturnType<typeof listItemCategories>>,
TError = void,
>(
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listItemCategories>>,
TError,
TData
>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary 상품 카테고리 목록(distinct)
*/
export function useListItemCategories<
TData = Awaited<ReturnType<typeof listItemCategories>>,
TError = void,
>(
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listItemCategories>>,
TError,
TData
>
>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getListItemCategoriesQueryOptions(options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary 상품 등록
*/
export const createItem = (
reqCreateItem: ReqCreateItem,
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<ResItem>(
{
url: `/v1/item/create`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: reqCreateItem,
signal,
},
options,
);
return customFetch<ResItem>({
url: `/v1/item/create`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: reqCreateItem,
signal,
});
};
export const getCreateItemMutationOptions = <
@ -210,7 +347,6 @@ export const getCreateItemMutationOptions = <
{ data: ReqCreateItem },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createItem>>,
TError,
@ -218,13 +354,13 @@ export const getCreateItemMutationOptions = <
TContext
> => {
const mutationKey = ["createItem"];
const { mutation: mutationOptions, request: requestOptions } = options
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createItem>>,
@ -232,7 +368,7 @@ export const getCreateItemMutationOptions = <
> = (props) => {
const { data } = props ?? {};
return createItem(data, requestOptions);
return createItem(data);
};
return { mutationFn, ...mutationOptions };
@ -258,7 +394,6 @@ export const useCreateItem = <
{ data: ReqCreateItem },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
@ -271,27 +406,108 @@ export const useCreateItem = <
return useMutation(mutationOptions, queryClient);
};
/**
* @summary 코드 중복 사전검사(업로드 즉시)
*/
export const checkItemCodes = (
reqCheckCodes: ReqCheckCodes,
signal?: AbortSignal,
) => {
return customFetch<ResCheckCodes>({
url: `/v1/item/check-codes`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: reqCheckCodes,
signal,
});
};
export const getCheckItemCodesMutationOptions = <
TError = void | HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof checkItemCodes>>,
TError,
{ data: ReqCheckCodes },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof checkItemCodes>>,
TError,
{ data: ReqCheckCodes },
TContext
> => {
const mutationKey = ["checkItemCodes"];
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof checkItemCodes>>,
{ data: ReqCheckCodes }
> = (props) => {
const { data } = props ?? {};
return checkItemCodes(data);
};
return { mutationFn, ...mutationOptions };
};
export type CheckItemCodesMutationResult = NonNullable<
Awaited<ReturnType<typeof checkItemCodes>>
>;
export type CheckItemCodesMutationBody = ReqCheckCodes;
export type CheckItemCodesMutationError = void | HTTPValidationError;
/**
* @summary 코드 중복 사전검사(업로드 즉시)
*/
export const useCheckItemCodes = <
TError = void | HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof checkItemCodes>>,
TError,
{ data: ReqCheckCodes },
TContext
>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof checkItemCodes>>,
TError,
{ data: ReqCheckCodes },
TContext
> => {
const mutationOptions = getCheckItemCodesMutationOptions(options);
return useMutation(mutationOptions, queryClient);
};
/**
* @summary 엑셀 일괄 등록(스텁)
*/
export const uploadItemsExcel = (
bodyUploadItemsExcelV1ItemUploadExcelPost: BodyUploadItemsExcelV1ItemUploadExcelPost,
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
const formData = new FormData();
formData.append(`file`, bodyUploadItemsExcelV1ItemUploadExcelPost.file);
return customFetch<ResExcelUpload>(
{
url: `/v1/item/upload-excel`,
method: "POST",
headers: { "Content-Type": "multipart/form-data" },
data: formData,
signal,
},
options,
);
return customFetch<ResExcelUpload>({
url: `/v1/item/upload-excel`,
method: "POST",
headers: { "Content-Type": "multipart/form-data" },
data: formData,
signal,
});
};
export const getUploadItemsExcelMutationOptions = <
@ -304,7 +520,6 @@ export const getUploadItemsExcelMutationOptions = <
{ data: BodyUploadItemsExcelV1ItemUploadExcelPost },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof uploadItemsExcel>>,
TError,
@ -312,13 +527,13 @@ export const getUploadItemsExcelMutationOptions = <
TContext
> => {
const mutationKey = ["uploadItemsExcel"];
const { mutation: mutationOptions, request: requestOptions } = options
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof uploadItemsExcel>>,
@ -326,7 +541,7 @@ export const getUploadItemsExcelMutationOptions = <
> = (props) => {
const { data } = props ?? {};
return uploadItemsExcel(data, requestOptions);
return uploadItemsExcel(data);
};
return { mutationFn, ...mutationOptions };
@ -353,7 +568,6 @@ export const useUploadItemsExcel = <
{ data: BodyUploadItemsExcelV1ItemUploadExcelPost },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
@ -369,15 +583,12 @@ export const useUploadItemsExcel = <
/**
* @summary 상품 조회
*/
export const getItem = (
itemId: string,
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<ResItem>(
{ url: `/v1/item/${itemId}`, method: "GET", signal },
options,
);
export const getItem = (itemId: string, signal?: AbortSignal) => {
return customFetch<ResItem>({
url: `/v1/item/${itemId}`,
method: "GET",
signal,
});
};
export const getGetItemQueryKey = (itemId?: string) => {
@ -393,16 +604,15 @@ export const getGetItemQueryOptions = <
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getItem>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetItemQueryKey(itemId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getItem>>> = ({
signal,
}) => getItem(itemId, requestOptions, signal);
}) => getItem(itemId, signal);
return {
queryKey,
@ -436,7 +646,6 @@ export function useGetItem<
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
@ -459,7 +668,6 @@ export function useGetItem<
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -474,7 +682,6 @@ export function useGetItem<
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getItem>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -493,7 +700,6 @@ export function useGetItem<
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getItem>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -514,20 +720,13 @@ export function useGetItem<
/**
* @summary 상품 수정
*/
export const updateItem = (
itemId: string,
reqUpdateItem: ReqUpdateItem,
options?: SecondParameter<typeof customFetch>,
) => {
return customFetch<ResItem>(
{
url: `/v1/item/update/${itemId}`,
method: "PATCH",
headers: { "Content-Type": "application/json" },
data: reqUpdateItem,
},
options,
);
export const updateItem = (itemId: string, reqUpdateItem: ReqUpdateItem) => {
return customFetch<ResItem>({
url: `/v1/item/update/${itemId}`,
method: "PATCH",
headers: { "Content-Type": "application/json" },
data: reqUpdateItem,
});
};
export const getUpdateItemMutationOptions = <
@ -540,7 +739,6 @@ export const getUpdateItemMutationOptions = <
{ itemId: string; data: ReqUpdateItem },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateItem>>,
TError,
@ -548,13 +746,13 @@ export const getUpdateItemMutationOptions = <
TContext
> => {
const mutationKey = ["updateItem"];
const { mutation: mutationOptions, request: requestOptions } = options
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateItem>>,
@ -562,7 +760,7 @@ export const getUpdateItemMutationOptions = <
> = (props) => {
const { itemId, data } = props ?? {};
return updateItem(itemId, data, requestOptions);
return updateItem(itemId, data);
};
return { mutationFn, ...mutationOptions };
@ -588,7 +786,6 @@ export const useUpdateItem = <
{ itemId: string; data: ReqUpdateItem },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
@ -604,14 +801,11 @@ export const useUpdateItem = <
/**
* @summary 상품 삭제
*/
export const deleteItem = (
itemId: string,
options?: SecondParameter<typeof customFetch>,
) => {
return customFetch<ResDeleteItem>(
{ url: `/v1/item/delete/${itemId}`, method: "DELETE" },
options,
);
export const deleteItem = (itemId: string) => {
return customFetch<ResDeleteItem>({
url: `/v1/item/delete/${itemId}`,
method: "DELETE",
});
};
export const getDeleteItemMutationOptions = <
@ -624,7 +818,6 @@ export const getDeleteItemMutationOptions = <
{ itemId: string },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteItem>>,
TError,
@ -632,13 +825,13 @@ export const getDeleteItemMutationOptions = <
TContext
> => {
const mutationKey = ["deleteItem"];
const { mutation: mutationOptions, request: requestOptions } = options
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteItem>>,
@ -646,7 +839,7 @@ export const getDeleteItemMutationOptions = <
> = (props) => {
const { itemId } = props ?? {};
return deleteItem(itemId, requestOptions);
return deleteItem(itemId);
};
return { mutationFn, ...mutationOptions };
@ -672,7 +865,6 @@ export const useDeleteItem = <
{ itemId: string },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
@ -688,15 +880,12 @@ export const useDeleteItem = <
/**
* @summary 최저가 수집 요청(스텁)
*/
export const triggerLowestPrice = (
itemId: string,
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<ResLowestPriceTrigger>(
{ url: `/v1/item/${itemId}/lowest-price`, method: "POST", signal },
options,
);
export const triggerLowestPrice = (itemId: string, signal?: AbortSignal) => {
return customFetch<ResLowestPriceTrigger>({
url: `/v1/item/${itemId}/lowest-price`,
method: "POST",
signal,
});
};
export const getTriggerLowestPriceMutationOptions = <
@ -709,7 +898,6 @@ export const getTriggerLowestPriceMutationOptions = <
{ itemId: string },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof triggerLowestPrice>>,
TError,
@ -717,13 +905,13 @@ export const getTriggerLowestPriceMutationOptions = <
TContext
> => {
const mutationKey = ["triggerLowestPrice"];
const { mutation: mutationOptions, request: requestOptions } = options
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof triggerLowestPrice>>,
@ -731,7 +919,7 @@ export const getTriggerLowestPriceMutationOptions = <
> = (props) => {
const { itemId } = props ?? {};
return triggerLowestPrice(itemId, requestOptions);
return triggerLowestPrice(itemId);
};
return { mutationFn, ...mutationOptions };
@ -757,7 +945,6 @@ export const useTriggerLowestPrice = <
{ itemId: string },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
@ -773,15 +960,12 @@ export const useTriggerLowestPrice = <
/**
* @summary 최저가 수집 결과(스텁)
*/
export const getLowestPrice = (
itemId: string,
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<ResLowestPriceResult>(
{ url: `/v1/item/${itemId}/lowest-price`, method: "GET", signal },
options,
);
export const getLowestPrice = (itemId: string, signal?: AbortSignal) => {
return customFetch<ResLowestPriceResult>({
url: `/v1/item/${itemId}/lowest-price`,
method: "GET",
signal,
});
};
export const getGetLowestPriceQueryKey = (itemId?: string) => {
@ -797,16 +981,15 @@ export const getGetLowestPriceQueryOptions = <
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getLowestPrice>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetLowestPriceQueryKey(itemId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getLowestPrice>>> = ({
signal,
}) => getLowestPrice(itemId, requestOptions, signal);
}) => getLowestPrice(itemId, signal);
return {
queryKey,
@ -842,7 +1025,6 @@ export function useGetLowestPrice<
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
@ -865,7 +1047,6 @@ export function useGetLowestPrice<
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -880,7 +1061,6 @@ export function useGetLowestPrice<
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getLowestPrice>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -899,7 +1079,6 @@ export function useGetLowestPrice<
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getLowestPrice>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {

View File

@ -0,0 +1,30 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { CardDataUserId } from "./cardDataUserId";
import type { CardDataName } from "./cardDataName";
import type { CardDataNumber } from "./cardDataNumber";
import type { CardDataScript } from "./cardDataScript";
import type { CardDataEditScript } from "./cardDataEditScript";
import type { CardDataCondition } from "./cardDataCondition";
import type { CardDataMemo } from "./cardDataMemo";
import type { CardDataCreatedAt } from "./cardDataCreatedAt";
import type { CardDataUpdatedAt } from "./cardDataUpdatedAt";
export interface CardData {
nego_card_id: string;
user_id?: CardDataUserId;
is_wildcard?: boolean;
name?: CardDataName;
number?: CardDataNumber;
script?: CardDataScript;
edit_script?: CardDataEditScript;
status?: number;
condition?: CardDataCondition;
memo?: CardDataMemo;
created_at?: CardDataCreatedAt;
updated_at?: CardDataUpdatedAt;
}

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type CardDataCondition = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type CardDataCreatedAt = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type CardDataEditScript = unknown | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type CardDataMemo = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type CardDataName = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type CardDataNumber = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type CardDataScript = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type CardDataUpdatedAt = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type CardDataUserId = string | null;

View File

@ -5,7 +5,7 @@
* OpenAPI spec version: 0.1.0
*/
export interface CompanyBrief {
export interface CompanyData {
company_id?: string;
name?: string;
}

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export interface EnumOption {
value: number;
name: string;
label: string;
}

View File

@ -8,17 +8,29 @@
export * from "./asyncJob";
export * from "./bodyUploadItemsExcelV1ItemUploadExcelPost";
export * from "./bodyUploadSuppliersExcelV1SupplierUploadExcelPost";
export * from "./cardData";
export * from "./cardDataCondition";
export * from "./cardDataCreatedAt";
export * from "./cardDataEditScript";
export * from "./cardDataMemo";
export * from "./cardDataName";
export * from "./cardDataNumber";
export * from "./cardDataScript";
export * from "./cardDataUpdatedAt";
export * from "./cardDataUserId";
export * from "./chatMessageData";
export * from "./chatMessageDataCardId";
export * from "./chatMessageDataCardType";
export * from "./chatMessageDataCardUsedYn";
export * from "./chatMessageDataIndicatorValue";
export * from "./companyBrief";
export * from "./companyData";
export * from "./enumOption";
export * from "./errorInfo";
export * from "./errorInfoCode";
export * from "./errorInfoDesc";
export * from "./errorInfoSuccess";
export * from "./hTTPValidationError";
export * from "./itemCategory";
export * from "./itemData";
export * from "./itemDataCategory";
export * from "./itemDataCode";
@ -36,6 +48,7 @@ export * from "./itemDataQuantityUnit";
export * from "./itemDataSpec";
export * from "./itemDataUpdatedAt";
export * from "./itemDataVatYn";
export * from "./listCardsParams";
export * from "./listItemsParams";
export * from "./listQuotationsParams";
export * from "./listSuppliersParams";
@ -62,7 +75,15 @@ export * from "./quotationSettingData";
export * from "./quotationSettingDataCreatedAt";
export * from "./quotationSettingDataUpdatedAt";
export * from "./quotationSettingDataUserId";
export * from "./reqCheckCodes";
export * from "./reqCreateAccount";
export * from "./reqCreateCard";
export * from "./reqCreateCardCondition";
export * from "./reqCreateCardEditScript";
export * from "./reqCreateCardMemo";
export * from "./reqCreateCardName";
export * from "./reqCreateCardNumber";
export * from "./reqCreateCardScript";
export * from "./reqCreateItem";
export * from "./reqCreateItemCategory";
export * from "./reqCreateItemCode";
@ -91,6 +112,14 @@ export * from "./reqCreateSupplierManagerEmail";
export * from "./reqCreateSupplierManagerName";
export * from "./reqCreateSupplierPriority";
export * from "./reqLogin";
export * from "./reqUpdateCard";
export * from "./reqUpdateCardCondition";
export * from "./reqUpdateCardEditScript";
export * from "./reqUpdateCardMemo";
export * from "./reqUpdateCardName";
export * from "./reqUpdateCardNumber";
export * from "./reqUpdateCardScript";
export * from "./reqUpdateCardStatus";
export * from "./reqUpdateItem";
export * from "./reqUpdateItemCategory";
export * from "./reqUpdateItemCategoryType";
@ -120,12 +149,21 @@ export * from "./reqUpdateSupplierManagerEmail";
export * from "./reqUpdateSupplierManagerName";
export * from "./reqUpdateSupplierName";
export * from "./reqUpdateSupplierPriority";
export * from "./resCard";
export * from "./resCardCard";
export * from "./resCardList";
export * from "./resCardListMsg";
export * from "./resCardMsg";
export * from "./resCheckCodes";
export * from "./resCheckCodesMsg";
export * from "./resCreateAccount";
export * from "./resCreateAccountMsg";
export * from "./resCreateQuotation";
export * from "./resCreateQuotationAsyncJob";
export * from "./resCreateQuotationMsg";
export * from "./resCreateQuotationQuotation";
export * from "./resDeleteCard";
export * from "./resDeleteCardMsg";
export * from "./resDeleteItem";
export * from "./resDeleteItemMsg";
export * from "./resDeleteQuotation";
@ -134,10 +172,15 @@ export * from "./resDeleteQuotationSetting";
export * from "./resDeleteQuotationSettingMsg";
export * from "./resDeleteSupplier";
export * from "./resDeleteSupplierMsg";
export * from "./resEnums";
export * from "./resEnumsEnums";
export * from "./resEnumsMsg";
export * from "./resExcelUpload";
export * from "./resExcelUploadMsg";
export * from "./resExcelUploadReceivedFilename";
export * from "./resItem";
export * from "./resItemCategories";
export * from "./resItemCategoriesMsg";
export * from "./resItemItem";
export * from "./resItemList";
export * from "./resItemListMsg";

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export interface ItemCategory {
name: string;
category_type?: number;
}

View File

@ -5,4 +5,4 @@
* OpenAPI spec version: 0.1.0
*/
export type ItemDataDeliveryType = string | null;
export type ItemDataDeliveryType = number | null;

View File

@ -5,4 +5,4 @@
* OpenAPI spec version: 0.1.0
*/
export type ItemDataQuantityUnit = number | null;
export type ItemDataQuantityUnit = string | null;

View File

@ -0,0 +1,22 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ListCardsParams = {
/**
* 카드명/카드번호/스크립트 검색
*/
search?: string | null;
/**
* @minimum 1
*/
page?: number;
/**
* @minimum 1
* @maximum 100
*/
size?: number;
};

View File

@ -10,6 +10,10 @@ export type ListSuppliersParams = {
* 협력사명/코드/담당자명 검색
*/
search?: string | null;
/**
* 우선순위 필터(HIGH/MEDIUM/LOW)
*/
priority?: string | null;
/**
* @minimum 1
*/

View File

@ -0,0 +1,10 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export interface ReqCheckCodes {
codes?: string[];
}

View File

@ -0,0 +1,23 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { ReqCreateCardName } from "./reqCreateCardName";
import type { ReqCreateCardNumber } from "./reqCreateCardNumber";
import type { ReqCreateCardScript } from "./reqCreateCardScript";
import type { ReqCreateCardEditScript } from "./reqCreateCardEditScript";
import type { ReqCreateCardCondition } from "./reqCreateCardCondition";
import type { ReqCreateCardMemo } from "./reqCreateCardMemo";
export interface ReqCreateCard {
is_wildcard?: boolean;
name?: ReqCreateCardName;
number?: ReqCreateCardNumber;
script?: ReqCreateCardScript;
edit_script?: ReqCreateCardEditScript;
status?: number;
condition?: ReqCreateCardCondition;
memo?: ReqCreateCardMemo;
}

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqCreateCardCondition = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqCreateCardEditScript = unknown | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqCreateCardMemo = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqCreateCardName = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqCreateCardNumber = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqCreateCardScript = string | null;

View File

@ -5,4 +5,4 @@
* OpenAPI spec version: 0.1.0
*/
export type ReqCreateItemDeliveryType = string | null;
export type ReqCreateItemDeliveryType = number | null;

View File

@ -5,4 +5,4 @@
* OpenAPI spec version: 0.1.0
*/
export type ReqCreateItemQuantityUnit = number | null;
export type ReqCreateItemQuantityUnit = string | null;

View File

@ -0,0 +1,23 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { ReqUpdateCardName } from "./reqUpdateCardName";
import type { ReqUpdateCardNumber } from "./reqUpdateCardNumber";
import type { ReqUpdateCardScript } from "./reqUpdateCardScript";
import type { ReqUpdateCardEditScript } from "./reqUpdateCardEditScript";
import type { ReqUpdateCardStatus } from "./reqUpdateCardStatus";
import type { ReqUpdateCardCondition } from "./reqUpdateCardCondition";
import type { ReqUpdateCardMemo } from "./reqUpdateCardMemo";
export interface ReqUpdateCard {
name?: ReqUpdateCardName;
number?: ReqUpdateCardNumber;
script?: ReqUpdateCardScript;
edit_script?: ReqUpdateCardEditScript;
status?: ReqUpdateCardStatus;
condition?: ReqUpdateCardCondition;
memo?: ReqUpdateCardMemo;
}

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqUpdateCardCondition = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqUpdateCardEditScript = unknown | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqUpdateCardMemo = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqUpdateCardName = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqUpdateCardNumber = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqUpdateCardScript = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ReqUpdateCardStatus = number | null;

View File

@ -5,4 +5,4 @@
* OpenAPI spec version: 0.1.0
*/
export type ReqUpdateItemDeliveryType = string | null;
export type ReqUpdateItemDeliveryType = number | null;

View File

@ -5,4 +5,4 @@
* OpenAPI spec version: 0.1.0
*/
export type ReqUpdateItemQuantityUnit = number | null;
export type ReqUpdateItemQuantityUnit = string | null;

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { ErrorInfo } from "./errorInfo";
import type { ResCardMsg } from "./resCardMsg";
import type { ResCardCard } from "./resCardCard";
export interface ResCard {
result?: ErrorInfo;
msg?: ResCardMsg;
card?: ResCardCard;
}

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { CardData } from "./cardData";
export type ResCardCard = CardData | null;

View File

@ -0,0 +1,18 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { ErrorInfo } from "./errorInfo";
import type { ResCardListMsg } from "./resCardListMsg";
import type { CardData } from "./cardData";
export interface ResCardList {
result?: ErrorInfo;
msg?: ResCardListMsg;
cards?: CardData[];
total?: number;
page?: number;
size?: number;
}

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ResCardListMsg = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ResCardMsg = string | null;

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { ErrorInfo } from "./errorInfo";
import type { ResCheckCodesMsg } from "./resCheckCodesMsg";
export interface ResCheckCodes {
result?: ErrorInfo;
msg?: ResCheckCodesMsg;
existing?: string[];
}

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ResCheckCodesMsg = string | null;

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { ErrorInfo } from "./errorInfo";
import type { ResDeleteCardMsg } from "./resDeleteCardMsg";
export interface ResDeleteCard {
result?: ErrorInfo;
msg?: ResDeleteCardMsg;
}

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ResDeleteCardMsg = string | null;

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { ErrorInfo } from "./errorInfo";
import type { ResEnumsMsg } from "./resEnumsMsg";
import type { ResEnumsEnums } from "./resEnumsEnums";
export interface ResEnums {
result?: ErrorInfo;
msg?: ResEnumsMsg;
enums?: ResEnumsEnums;
}

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { EnumOption } from "./enumOption";
export type ResEnumsEnums = { [key: string]: EnumOption[] };

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ResEnumsMsg = string | null;

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { ErrorInfo } from "./errorInfo";
import type { ResItemCategoriesMsg } from "./resItemCategoriesMsg";
import type { ItemCategory } from "./itemCategory";
export interface ResItemCategories {
result?: ErrorInfo;
msg?: ResItemCategoriesMsg;
categories?: ItemCategory[];
}

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
export type ResItemCategoriesMsg = string | null;

View File

@ -20,5 +20,6 @@ export interface ResMe {
email?: ResMeEmail;
contact_number?: ResMeContactNumber;
role?: number;
role_label?: string;
company?: ResMeCompany;
}

View File

@ -4,6 +4,6 @@
* Negodata Api Server
* OpenAPI spec version: 0.1.0
*/
import type { CompanyBrief } from "./companyBrief";
import type { CompanyData } from "./companyData";
export type ResMeCompany = CompanyBrief | null;
export type ResMeCompany = CompanyData | null;

View File

@ -31,19 +31,15 @@ import type {
import { customFetch } from "../../mutator/custom-fetch";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* @summary 견적 설정 목록
*/
export const listSettings = (
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<ResQuotationSettingList>(
{ url: `/v1/quotation-setting/list`, method: "GET", signal },
options,
);
export const listSettings = (signal?: AbortSignal) => {
return customFetch<ResQuotationSettingList>({
url: `/v1/quotation-setting/list`,
method: "GET",
signal,
});
};
export const getListSettingsQueryKey = () => {
@ -57,15 +53,14 @@ export const getListSettingsQueryOptions = <
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listSettings>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListSettingsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listSettings>>> = ({
signal,
}) => listSettings(requestOptions, signal);
}) => listSettings(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listSettings>>,
@ -95,7 +90,6 @@ export function useListSettings<
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
@ -117,7 +111,6 @@ export function useListSettings<
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -131,7 +124,6 @@ export function useListSettings<
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listSettings>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -149,7 +141,6 @@ export function useListSettings<
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof listSettings>>, TError, TData>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
@ -172,19 +163,15 @@ export function useListSettings<
*/
export const createSetting = (
reqCreateQuotationSetting: ReqCreateQuotationSetting,
options?: SecondParameter<typeof customFetch>,
signal?: AbortSignal,
) => {
return customFetch<ResQuotationSetting>(
{
url: `/v1/quotation-setting/create`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: reqCreateQuotationSetting,
signal,
},
options,
);
return customFetch<ResQuotationSetting>({
url: `/v1/quotation-setting/create`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: reqCreateQuotationSetting,
signal,
});
};
export const getCreateSettingMutationOptions = <
@ -197,7 +184,6 @@ export const getCreateSettingMutationOptions = <
{ data: ReqCreateQuotationSetting },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createSetting>>,
TError,
@ -205,13 +191,13 @@ export const getCreateSettingMutationOptions = <
TContext
> => {
const mutationKey = ["createSetting"];
const { mutation: mutationOptions, request: requestOptions } = options
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createSetting>>,
@ -219,7 +205,7 @@ export const getCreateSettingMutationOptions = <
> = (props) => {
const { data } = props ?? {};
return createSetting(data, requestOptions);
return createSetting(data);
};
return { mutationFn, ...mutationOptions };
@ -245,7 +231,6 @@ export const useCreateSetting = <
{ data: ReqCreateQuotationSetting },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
@ -264,17 +249,13 @@ export const useCreateSetting = <
export const updateSetting = (
qtSettingId: string,
reqUpdateQuotationSetting: ReqUpdateQuotationSetting,
options?: SecondParameter<typeof customFetch>,
) => {
return customFetch<ResQuotationSetting>(
{
url: `/v1/quotation-setting/update/${qtSettingId}`,
method: "PATCH",
headers: { "Content-Type": "application/json" },
data: reqUpdateQuotationSetting,
},
options,
);
return customFetch<ResQuotationSetting>({
url: `/v1/quotation-setting/update/${qtSettingId}`,
method: "PATCH",
headers: { "Content-Type": "application/json" },
data: reqUpdateQuotationSetting,
});
};
export const getUpdateSettingMutationOptions = <
@ -287,7 +268,6 @@ export const getUpdateSettingMutationOptions = <
{ qtSettingId: string; data: ReqUpdateQuotationSetting },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateSetting>>,
TError,
@ -295,13 +275,13 @@ export const getUpdateSettingMutationOptions = <
TContext
> => {
const mutationKey = ["updateSetting"];
const { mutation: mutationOptions, request: requestOptions } = options
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateSetting>>,
@ -309,7 +289,7 @@ export const getUpdateSettingMutationOptions = <
> = (props) => {
const { qtSettingId, data } = props ?? {};
return updateSetting(qtSettingId, data, requestOptions);
return updateSetting(qtSettingId, data);
};
return { mutationFn, ...mutationOptions };
@ -335,7 +315,6 @@ export const useUpdateSetting = <
{ qtSettingId: string; data: ReqUpdateQuotationSetting },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
@ -351,14 +330,11 @@ export const useUpdateSetting = <
/**
* @summary 견적 설정 삭제
*/
export const deleteSetting = (
qtSettingId: string,
options?: SecondParameter<typeof customFetch>,
) => {
return customFetch<ResDeleteQuotationSetting>(
{ url: `/v1/quotation-setting/delete/${qtSettingId}`, method: "DELETE" },
options,
);
export const deleteSetting = (qtSettingId: string) => {
return customFetch<ResDeleteQuotationSetting>({
url: `/v1/quotation-setting/delete/${qtSettingId}`,
method: "DELETE",
});
};
export const getDeleteSettingMutationOptions = <
@ -371,7 +347,6 @@ export const getDeleteSettingMutationOptions = <
{ qtSettingId: string },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteSetting>>,
TError,
@ -379,13 +354,13 @@ export const getDeleteSettingMutationOptions = <
TContext
> => {
const mutationKey = ["deleteSetting"];
const { mutation: mutationOptions, request: requestOptions } = options
const { mutation: mutationOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteSetting>>,
@ -393,7 +368,7 @@ export const getDeleteSettingMutationOptions = <
> = (props) => {
const { qtSettingId } = props ?? {};
return deleteSetting(qtSettingId, requestOptions);
return deleteSetting(qtSettingId);
};
return { mutationFn, ...mutationOptions };
@ -419,7 +394,6 @@ export const useDeleteSetting = <
{ qtSettingId: string },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<

Some files were not shown because too many files have changed in this diff Show More